Skip to content

Commit 46aeb27

Browse files
committed
refactor: simplify CODEBUFF_API_KEY protection with ciOnlyEnvVars
- Remove CODEBUFF_API_KEY from typed serverEnvSchema entirely - Add ciOnlyEnvVars array for CI injection only - Remove webEnv/webEnvSchema (no longer needed) - Revert web files from webEnv back to env - env.CODEBUFF_API_KEY is now a TypeScript error everywhere - Keep ESLint rule for process.env.CODEBUFF_API_KEY in web
1 parent 2822177 commit 46aeb27

File tree

21 files changed

+59
-81
lines changed

21 files changed

+59
-81
lines changed

packages/internal/src/env-schema.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ import { clientEnvSchema, clientProcessEnv } from '@codebuff/common/env-schema'
22
import z from 'zod/v4'
33

44
export const serverEnvSchema = clientEnvSchema.extend({
5-
// Codebuff API key - used by CLI/SDK for authentication
6-
// NOTE: Web should NOT use this directly - users must provide their own key
7-
// See web/.eslintrc.cjs for enforcement
8-
CODEBUFF_API_KEY: z.string().optional(),
95
// LLM API keys
106
OPEN_ROUTER_API_KEY: z.string().min(1),
117
OPENAI_API_KEY: z.string().min(1),
@@ -36,17 +32,15 @@ export type ServerInput = {
3632
}
3733
export type ServerEnv = z.infer<typeof serverEnvSchema>
3834

39-
// Web-specific schema that omits CODEBUFF_API_KEY
40-
// Web should never use this key - users must provide their own via Authorization header
41-
export const webEnvSchema = serverEnvSchema.omit({ CODEBUFF_API_KEY: true })
42-
export type WebEnv = z.infer<typeof webEnvSchema>
35+
// CI-only env vars that are NOT in the typed schema
36+
// These are injected for SDK tests but should never be accessed via env.* in code
37+
export const ciOnlyEnvVars = ['CODEBUFF_API_KEY'] as const
38+
export type CiOnlyEnvVar = (typeof ciOnlyEnvVars)[number]
4339

4440
// Bun will inject all these values, so we need to reference them individually (no for-loops)
4541
export const serverProcessEnv: ServerInput = {
4642
...clientProcessEnv,
4743

48-
// Codebuff API key
49-
CODEBUFF_API_KEY: process.env.CODEBUFF_API_KEY,
5044
// LLM API keys
5145
OPEN_ROUTER_API_KEY: process.env.OPEN_ROUTER_API_KEY,
5246
OPENAI_API_KEY: process.env.OPENAI_API_KEY,

packages/internal/src/env.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { serverEnvSchema, serverProcessEnv, webEnvSchema } from './env-schema'
1+
import { serverEnvSchema, serverProcessEnv } from './env-schema'
22

33
// Provide safe defaults for local/test runs to avoid schema failures
44
const ensureEnvDefault = (key: string, value: string) => {
@@ -29,9 +29,4 @@ if (process.env.NEXT_PUBLIC_CB_ENVIRONMENT !== 'prod') {
2929
console.log('Using environment:', process.env.NEXT_PUBLIC_CB_ENVIRONMENT)
3030
}
3131

32-
// Full env for SDK/CLI - includes CODEBUFF_API_KEY
3332
export const env = serverEnvSchema.parse(serverProcessEnv)
34-
35-
// Web-specific env - CODEBUFF_API_KEY does NOT exist on this type
36-
// Use this in web package to get type-level protection against using CODEBUFF_API_KEY
37-
export const webEnv = webEnvSchema.parse(serverProcessEnv)

scripts/generate-ci-env.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import path from 'path'
88
import { fileURLToPath } from 'url'
99

1010
import { CLIENT_ENV_PREFIX, clientEnvVars } from '@codebuff/common/env-schema'
11-
import { serverEnvVars } from '@codebuff/internal/env-schema'
11+
import { ciOnlyEnvVars, serverEnvVars } from '@codebuff/internal/env-schema'
1212

1313
const __filename = fileURLToPath(import.meta.url)
1414
const __dirname = path.dirname(__filename)
@@ -49,8 +49,10 @@ function parseArgs() {
4949

5050
function generateGitHubEnv() {
5151
const { prefix, scope } = parseArgs()
52+
// CI needs both serverEnvVars (typed schema) and ciOnlyEnvVars (for SDK tests)
53+
const allVars = [...serverEnvVars, ...ciOnlyEnvVars]
5254
const varsByScope = {
53-
all: serverEnvVars,
55+
all: allVars,
5456
client: clientEnvVars,
5557
}
5658

web/.eslintrc.cjs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,27 +22,14 @@ module.exports = {
2222
'@typescript-eslint/no-explicit-any': 'off',
2323
'@typescript-eslint/no-unused-vars': 'off',
2424
'react/no-unescaped-entities': 'off',
25-
// Prevent using CODEBUFF_API_KEY in web - users must provide their own API key
25+
// Prevent using process.env.CODEBUFF_API_KEY in web - users must provide their own API key
2626
// This prevents accidentally using Codebuff's credits for user operations
27+
// Note: env.CODEBUFF_API_KEY is already a TypeScript error (not in schema)
2728
'no-restricted-syntax': [
2829
'error',
2930
{
30-
selector: "MemberExpression[property.name='CODEBUFF_API_KEY']",
31-
message: 'CODEBUFF_API_KEY is not allowed in web package. Users must provide their own API key via Authorization header.',
32-
},
33-
],
34-
// Enforce using webEnv instead of env in web package
35-
// webEnv omits CODEBUFF_API_KEY for type-level protection
36-
'no-restricted-imports': [
37-
'error',
38-
{
39-
paths: [
40-
{
41-
name: '@codebuff/internal/env',
42-
importNames: ['env'],
43-
message: "Use 'webEnv' instead of 'env' in web package. webEnv omits CODEBUFF_API_KEY for security.",
44-
},
45-
],
31+
selector: "MemberExpression[object.object.name='process'][object.property.name='env'][property.name='CODEBUFF_API_KEY']",
32+
message: 'process.env.CODEBUFF_API_KEY is not allowed in web package. Users must provide their own API key via Authorization header.',
4633
},
4734
],
4835
},

web/scripts/discord/register-commands.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { webEnv } from '@codebuff/internal/env'
1+
import { env } from '@codebuff/internal/env'
22
import { REST, Routes, SlashCommandBuilder } from 'discord.js'
33

44
import { logger } from '@/util/logger'
@@ -15,13 +15,13 @@ const commands = [
1515
),
1616
]
1717

18-
const rest = new REST().setToken(webEnv.DISCORD_BOT_TOKEN)
18+
const rest = new REST().setToken(env.DISCORD_BOT_TOKEN)
1919

2020
async function main() {
2121
try {
2222
logger.info('Started refreshing application (/) commands.')
2323

24-
await rest.put(Routes.applicationCommands(webEnv.DISCORD_APPLICATION_ID), {
24+
await rest.put(Routes.applicationCommands(env.DISCORD_APPLICATION_ID), {
2525
body: commands,
2626
})
2727

web/src/app/api/auth/[...nextauth]/auth-options.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { generateCompactId } from '@codebuff/common/util/string'
88
import { loops } from '@codebuff/internal'
99
import db from '@codebuff/internal/db'
1010
import * as schema from '@codebuff/internal/db/schema'
11-
import { webEnv } from '@codebuff/internal/env'
11+
import { env } from '@codebuff/internal/env'
1212
import { stripeServer } from '@codebuff/internal/util/stripe'
1313
import { logSyncFailure } from '@codebuff/internal/util/sync-failure'
1414
import { eq } from 'drizzle-orm'
@@ -46,14 +46,14 @@ async function createAndLinkStripeCustomer(params: {
4646
// Create subscription with the usage price
4747
await stripeServer.subscriptions.create({
4848
customer: customer.id,
49-
items: [{ price: webEnv.STRIPE_USAGE_PRICE_ID }],
49+
items: [{ price: env.STRIPE_USAGE_PRICE_ID }],
5050
})
5151

5252
await db
5353
.update(schema.user)
5454
.set({
5555
stripe_customer_id: customer.id,
56-
stripe_price_id: webEnv.STRIPE_USAGE_PRICE_ID,
56+
stripe_price_id: env.STRIPE_USAGE_PRICE_ID,
5757
})
5858
.where(eq(schema.user.id, userId))
5959

@@ -137,8 +137,8 @@ export const authOptions: NextAuthOptions = {
137137
}) as Adapter,
138138
providers: [
139139
GitHubProvider({
140-
clientId: webEnv.CODEBUFF_GITHUB_ID,
141-
clientSecret: webEnv.CODEBUFF_GITHUB_SECRET,
140+
clientId: env.CODEBUFF_GITHUB_ID,
141+
clientSecret: env.CODEBUFF_GITHUB_SECRET,
142142
}),
143143
],
144144
session: {

web/src/app/api/auth/cli/code/route.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { genAuthCode } from '@codebuff/common/util/credentials'
22
import db from '@codebuff/internal/db'
33
import * as schema from '@codebuff/internal/db/schema'
4-
import { webEnv } from '@codebuff/internal/env'
4+
import { env } from '@codebuff/internal/env'
55
import { and, eq, gt } from 'drizzle-orm'
66
import { NextResponse } from 'next/server'
77
import { z } from 'zod/v4'
@@ -25,7 +25,7 @@ export async function POST(req: Request) {
2525
const fingerprintHash = genAuthCode(
2626
fingerprintId,
2727
expiresAt.toString(),
28-
webEnv.NEXTAUTH_SECRET,
28+
env.NEXTAUTH_SECRET,
2929
)
3030

3131
// Check if this fingerprint has any active sessions
@@ -56,7 +56,7 @@ export async function POST(req: Request) {
5656
}
5757

5858
// Generate login URL without modifying the fingerprint record
59-
const loginUrl = `${webEnv.NEXT_PUBLIC_CODEBUFF_APP_URL}/login?auth_code=${fingerprintId}.${expiresAt}.${fingerprintHash}${
59+
const loginUrl = `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/login?auth_code=${fingerprintId}.${expiresAt}.${fingerprintHash}${
6060
referralCode ? `&referral_code=${referralCode}` : ''
6161
}`
6262

web/src/app/api/auth/cli/status/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { genAuthCode } from '@codebuff/common/util/credentials'
22
import db from '@codebuff/internal/db'
33
import * as schema from '@codebuff/internal/db/schema'
4-
import { webEnv } from '@codebuff/internal/env'
4+
import { env } from '@codebuff/internal/env'
55
import { and, eq, gt, or, isNull } from 'drizzle-orm'
66
import { NextResponse } from 'next/server'
77
import { z } from 'zod/v4'
@@ -45,7 +45,7 @@ export async function GET(req: Request) {
4545
const expectedHash = genAuthCode(
4646
fingerprintId,
4747
expiresAt.toString(),
48-
webEnv.NEXTAUTH_SECRET,
48+
env.NEXTAUTH_SECRET,
4949
)
5050
if (fingerprintHash !== expectedHash) {
5151
logger.info(

web/src/app/api/orgs/[orgId]/billing/setup/route.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { pluralize } from '@codebuff/common/util/string'
22
import db from '@codebuff/internal/db'
33
import * as schema from '@codebuff/internal/db/schema'
4-
import { webEnv } from '@codebuff/internal/env'
4+
import { env } from '@codebuff/internal/env'
55
import { stripeServer } from '@codebuff/internal/util/stripe'
66
import { eq, and, sql } from 'drizzle-orm'
77
import { NextResponse } from 'next/server'
@@ -190,13 +190,13 @@ export async function POST(req: NextRequest, { params }: RouteParams) {
190190
mode: 'subscription',
191191
line_items: [
192192
{
193-
price: webEnv.STRIPE_TEAM_FEE_PRICE_ID,
193+
price: env.STRIPE_TEAM_FEE_PRICE_ID,
194194
quantity: seatCount,
195195
},
196196
],
197197
allow_promotion_codes: true,
198-
success_url: `${webEnv.NEXT_PUBLIC_CODEBUFF_APP_URL}/orgs/${organization.slug}/billing/purchase?subscription_success=true`,
199-
cancel_url: `${webEnv.NEXT_PUBLIC_CODEBUFF_APP_URL}/orgs/${organization.slug}?subscription_canceled=true`,
198+
success_url: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/orgs/${organization.slug}/billing/purchase?subscription_success=true`,
199+
cancel_url: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/orgs/${organization.slug}?subscription_canceled=true`,
200200
metadata: {
201201
organization_id: orgId,
202202
type: 'subscription_setup',

web/src/app/api/orgs/[orgId]/billing/status/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import db from '@codebuff/internal/db'
22
import * as schema from '@codebuff/internal/db/schema'
3-
import { webEnv } from '@codebuff/internal/env'
3+
import { env } from '@codebuff/internal/env'
44
import { stripeServer } from '@codebuff/internal/util/stripe'
55
import { eq, and, sql } from 'drizzle-orm'
66
import { NextResponse } from 'next/server'
@@ -76,7 +76,7 @@ export async function GET(req: NextRequest, { params }: RouteParams) {
7676
// Create billing portal session
7777
const portalSession = await stripeServer.billingPortal.sessions.create({
7878
customer: organization.stripe_customer_id,
79-
return_url: `${webEnv.NEXT_PUBLIC_CODEBUFF_APP_URL}/orgs/${organization.slug}/settings`,
79+
return_url: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/orgs/${organization.slug}/settings`,
8080
})
8181
billingPortalUrl = portalSession.url
8282

0 commit comments

Comments
 (0)