env.ts37 lines · main
1import { z } from 'zod';
2
3import { ValidationError } from './errors.js';
4
5/**
6 * Load and validate `BRIVEN_*` env vars at process boot.
7 *
8 * Per CLAUDE.md §4.1, every env var owned by briven must carry the `BRIVEN_`
9 * prefix. This loader enforces that invariant — if a required var is missing
10 * or unprefixed, the process fails loudly at startup rather than later.
11 */
12export function loadEnv<T extends z.ZodObject<z.ZodRawShape>>(schema: T): z.infer<T> {
13 const keys = Object.keys(schema.shape);
14 const unprefixed = keys.filter((k) => !k.startsWith('BRIVEN_'));
15 if (unprefixed.length > 0) {
16 throw new ValidationError(
17 `env vars must carry the BRIVEN_ prefix, got: ${unprefixed.join(', ')}`,
18 );
19 }
20
21 // Treat empty-string env vars as unset. Docker Compose interpolation like
22 // `${BRIVEN_FOO:-}` injects "" for an unset var; an empty string would
23 // otherwise fail `.url()`/`.min()` on `.optional()` fields, which are meant
24 // to accept "absent". Strip empties so optional vars resolve to undefined.
25 const cleanedEnv: Record<string, string> = {};
26 for (const [k, v] of Object.entries(process.env)) {
27 if (v !== undefined && v !== '') cleanedEnv[k] = v;
28 }
29
30 const parsed = schema.safeParse(cleanedEnv);
31 if (!parsed.success) {
32 const missing = parsed.error.issues.map((i) => i.path.join('.')).join(', ');
33 throw new ValidationError(`missing or invalid env vars: ${missing}`);
34 }
35
36 return parsed.data;
37}