Some checks failed
Deploy to Production / deploy (push) Failing after 1m8s
GitHub: /v1/auth/github + /callback — authorization-code flow, fetches the verified primary email via /user/emails, reuses upsertOAuthLogin. SMS: phone is now a first-class login identity. - schema: users.email nullable, users.phone added, new sms_codes table. - @bmm/auth: issueSmsCode / consumeSmsCode — 6-digit code, hashed at rest, 10-min TTL, per-phone rate limit, 5-attempt cap, get-or-create user by phone. - apps/api: /v1/auth/sms/request + /verify, Twilio REST send (no SDK), per-IP throttle. /v1/auth/providers now reports google/github/sms. - login UI: Google + GitHub buttons, Email|Phone toggle, two-step SMS (number -> 6-digit code with one-time-code autofill). SMS link was rejected in favour of an OTP code — carrier link-scanners consume magic-link tokens before the user taps them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
62 lines
2.5 KiB
TypeScript
62 lines
2.5 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
const Env = z.object({
|
|
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
|
|
DATABASE_URL: z.string(),
|
|
REDIS_URL: z.string().default('redis://localhost:6379'),
|
|
PORT: z.coerce.number().default(4000),
|
|
NEXT_PUBLIC_APP_URL: z.string().default('http://localhost:3001'),
|
|
OAUTH_KEY_DIR: z.string().default('./keys'),
|
|
ANTHROPIC_API_KEY: z.string().optional(),
|
|
SECRETS_ENCRYPTION_KEY: z
|
|
.string()
|
|
.min(64, '32 bytes hex required')
|
|
.default('0000000000000000000000000000000000000000000000000000000000000000'),
|
|
CONTROL_PLANE_PUBLIC_URL: z.string().default('http://localhost:4000'),
|
|
ADMIN_EMAIL: z.string().email().optional(),
|
|
ADMIN_PASSWORD: z.string().min(8).optional(),
|
|
ADMIN_NAME: z.string().optional(),
|
|
GOOGLE_OAUTH_ID: z.string().optional(),
|
|
GOOGLE_OAUTH_SECRET: z.string().optional(),
|
|
GITHUB_OAUTH_ID: z.string().optional(),
|
|
GITHUB_OAUTH_SECRET: z.string().optional(),
|
|
TWILIO_ACCOUNT_SID: z.string().optional(),
|
|
TWILIO_AUTH_TOKEN: z.string().optional(),
|
|
TWILIO_SMS_FROM: z.string().optional(),
|
|
});
|
|
|
|
export const config = Env.parse({
|
|
NODE_ENV: process.env.NODE_ENV,
|
|
DATABASE_URL: process.env.DATABASE_URL,
|
|
REDIS_URL: process.env.REDIS_URL,
|
|
PORT: process.env.PORT,
|
|
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
|
OAUTH_KEY_DIR: process.env.OAUTH_KEY_DIR,
|
|
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
|
|
SECRETS_ENCRYPTION_KEY: process.env.SECRETS_ENCRYPTION_KEY,
|
|
CONTROL_PLANE_PUBLIC_URL: process.env.CONTROL_PLANE_PUBLIC_URL,
|
|
ADMIN_EMAIL: process.env.ADMIN_EMAIL,
|
|
ADMIN_PASSWORD: process.env.ADMIN_PASSWORD,
|
|
ADMIN_NAME: process.env.ADMIN_NAME,
|
|
GOOGLE_OAUTH_ID: process.env.GOOGLE_OAUTH_ID,
|
|
GOOGLE_OAUTH_SECRET: process.env.GOOGLE_OAUTH_SECRET,
|
|
GITHUB_OAUTH_ID: process.env.GITHUB_OAUTH_ID,
|
|
GITHUB_OAUTH_SECRET: process.env.GITHUB_OAUTH_SECRET,
|
|
TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID,
|
|
TWILIO_AUTH_TOKEN: process.env.TWILIO_AUTH_TOKEN,
|
|
TWILIO_SMS_FROM: process.env.TWILIO_SMS_FROM,
|
|
});
|
|
|
|
// INFRA-001: refuse to boot in production with the placeholder encryption key.
|
|
// The zero-key passes the min(64) length check but would render every stored
|
|
// secret effectively plaintext.
|
|
const ZERO_KEY = '0'.repeat(64);
|
|
if (config.NODE_ENV === 'production' && config.SECRETS_ENCRYPTION_KEY === ZERO_KEY) {
|
|
throw new Error(
|
|
'SECRETS_ENCRYPTION_KEY is the all-zero placeholder. Set a real 32-byte hex key ' +
|
|
'(openssl rand -hex 32) before running in production.',
|
|
);
|
|
}
|
|
|
|
export type Config = z.infer<typeof Env>;
|