@spfn/core 0.3.0-beta.15 → 0.3.0-beta.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,472 @@
1
+ /**
2
+ * Environment Variable Schema Definition System
3
+ *
4
+ * 환경변수에 메타데이터를 정의하여 중앙 관리, 문서화, 검증을 지원합니다.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const schema = defineEnvSchema({
9
+ * DATABASE_URL: envUrl({
10
+ * description: 'Database connection',
11
+ * required: true,
12
+ * validator: parsePostgresUrl,
13
+ * sensitive: true,
14
+ * })
15
+ * });
16
+ * ```
17
+ *
18
+ * @module env/schema
19
+ */
20
+ /**
21
+ * 환경변수 스키마 정의
22
+ */
23
+ interface EnvVarSchema<T = string> {
24
+ /** 환경변수 키 */
25
+ key: string;
26
+ /** 설명 (목적, 사용처) */
27
+ description: string;
28
+ /** 타입 */
29
+ type: 'string' | 'number' | 'boolean' | 'url' | 'enum' | 'json';
30
+ /** 필수 여부 */
31
+ required?: boolean;
32
+ /** 기본값 */
33
+ default?: T;
34
+ /** 검증/변환 함수 */
35
+ validator?: (value: string) => T;
36
+ /** Fallback 환경변수 키들 (backward compatibility) */
37
+ fallbackKeys?: string[];
38
+ /** 최소 길이 (문자열 타입) */
39
+ minLength?: number;
40
+ /** 민감정보 여부 (로깅 시 마스킹) */
41
+ sensitive?: boolean;
42
+ /**
43
+ * 자동 생성 방식 — 값이 미지정이면 프로비저닝(secrets 관리 CLI)이 이 방식으로 생성한다.
44
+ * 외부에서 받아야 하는 시크릿(API 키 등)은 generate를 비워 사용자 입력으로 둔다.
45
+ */
46
+ generate?: 'hex32' | 'hex64' | 'uuid' | 'base64url32';
47
+ /** 예시 값들 (타입과 일치해야 함) */
48
+ examples?: T[];
49
+ /**
50
+ * Next.js 프로세스에서 사용 여부
51
+ *
52
+ * - true: .env.local에 존재해야 함 (Next.js 서버 컴포넌트에서 접근 가능)
53
+ * - false: .env.server에만 존재해야 함 (SPFN 서버에서만 접근)
54
+ *
55
+ * @default NEXT_PUBLIC_* 이면 true, 아니면 false
56
+ */
57
+ nextjs?: boolean;
58
+ /**
59
+ * Which layer of a deployment supplies the value.
60
+ *
61
+ * - `'environment'`: one value shared by every instance of an environment
62
+ * (a third-party API key) — kept in `secrets/<env>.enc.json`.
63
+ * - `'instance'`: a value that differs per instance — kept in
64
+ * `secrets/<env>.<instance>.enc.json` or computed by the deploy script
65
+ * and passed to `spfn secret export --with <file>`.
66
+ *
67
+ * `spfn secret export` fails when a value arrives from the wrong layer.
68
+ *
69
+ * @default 'environment'
70
+ */
71
+ layer?: EnvLayer;
72
+ /**
73
+ * Files that read this variable under a computed key, as paths relative to
74
+ * the project root — e.g. `['src/server/config/providers.ts']`.
75
+ *
76
+ * `spfn env audit` counts a name as read when it appears as a property
77
+ * access or a string literal; a name only ever reached through
78
+ * `env[name]` needs this instead. The name passes when every listed file
79
+ * exists and is scanned. The runtime ignores `readBy`.
80
+ */
81
+ readBy?: string[];
82
+ }
83
+ /**
84
+ * The layer of a deployment an env value comes from — see {@link EnvVarSchema.layer}.
85
+ */
86
+ type EnvLayer = 'environment' | 'instance';
87
+ /**
88
+ * 스키마 컬렉션 타입
89
+ */
90
+ type EnvSchemaCollection = Record<string, EnvVarSchema<any>>;
91
+ /**
92
+ * Helper type: Check if field has default value
93
+ */
94
+ type HasDefault<T> = T extends {
95
+ default: any;
96
+ } ? true : false;
97
+ /**
98
+ * Helper type: Check if field is explicitly required
99
+ */
100
+ type IsRequired<T> = T extends {
101
+ required: true;
102
+ } ? true : false;
103
+ /**
104
+ * Helper type: Check if field should be required (has default OR required: true)
105
+ */
106
+ type ShouldBeRequired<T> = HasDefault<T> extends true ? true : IsRequired<T>;
107
+ /**
108
+ * 스키마로부터 타입 추출
109
+ *
110
+ * required: true 또는 default가 있는 필드 → 필수
111
+ * required: false 또는 미지정 → optional (| undefined)
112
+ */
113
+ type InferEnvType<T extends EnvSchemaCollection> = {
114
+ [K in keyof T as ShouldBeRequired<T[K]> extends true ? K : never]: T[K] extends EnvVarSchema<infer U> ? U : string;
115
+ } & {
116
+ [K in keyof T as ShouldBeRequired<T[K]> extends true ? never : K]?: T[K] extends EnvVarSchema<infer U> ? U | undefined : string | undefined;
117
+ };
118
+ /**
119
+ * 스키마 정의 헬퍼 (타입 추론 지원)
120
+ *
121
+ * Automatically fills in the `key` property from object keys. Every other
122
+ * field passes through as written — an entry without `layer` is an
123
+ * `'environment'` value.
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const schema = defineEnvSchema({
128
+ * DATABASE_URL: envString({ description: 'Database URL', required: true })
129
+ * });
130
+ * // Automatically adds key: 'DATABASE_URL'
131
+ * ```
132
+ */
133
+ declare function defineEnvSchema<T extends Record<string, any>>(schema: T): {
134
+ [K in keyof T]: T[K] & {
135
+ key: K;
136
+ };
137
+ };
138
+ /**
139
+ * 문자열 스키마 헬퍼
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * const schema = {
144
+ * API_KEY: {
145
+ * ...envString({
146
+ * description: 'API authentication key',
147
+ * required: true,
148
+ * sensitive: true,
149
+ * }),
150
+ * key: 'API_KEY',
151
+ * }
152
+ * };
153
+ * ```
154
+ */
155
+ declare function envString<T extends Omit<EnvVarSchema, 'key' | 'type'>>(options: T): T & {
156
+ type: 'string';
157
+ validator: (value: string) => string;
158
+ };
159
+ /**
160
+ * 시크릿 스키마 헬퍼 — sensitive:true 자동.
161
+ *
162
+ * generate 지정 시 프로비저닝(secrets CLI)이 값을 자동 생성, 미지정이면 외부 입력(사용자가 붙여넣음).
163
+ * 이로써 env.config.ts가 "설정(envString) vs 외부 시크릿(envSecret) vs 자동 시크릿(envSecret+generate)"을
164
+ * 선언적으로 가른다 — CLI·프레임워크·프로비저닝이 한 원천을 읽는다.
165
+ *
166
+ * @example
167
+ * ```typescript
168
+ * SPFN_AUTH_SESSION_SECRET: envSecret({ description: '세션 서명 키', generate: 'base64url32' }),
169
+ * OPENAI_API_KEY: envSecret({ description: 'OpenAI API 키' }), // generate 없음 → 외부 입력
170
+ * ```
171
+ */
172
+ declare function envSecret<T extends Omit<EnvVarSchema, 'key' | 'type' | 'sensitive'>>(options: T): T & {
173
+ type: 'string';
174
+ sensitive: true;
175
+ validator: (value: string) => string;
176
+ };
177
+ /**
178
+ * 숫자 스키마 헬퍼
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * const schema = {
183
+ * PORT: {
184
+ * ...envNumber({
185
+ * description: 'Server port',
186
+ * default: 3000,
187
+ * validator: createNumberParser({ min: 1, max: 65535 }),
188
+ * }),
189
+ * key: 'PORT',
190
+ * }
191
+ * };
192
+ * ```
193
+ */
194
+ declare function envNumber<T extends Omit<EnvVarSchema<number>, 'key' | 'type'>>(options: T): T & {
195
+ type: 'number';
196
+ validator: (value: string) => number;
197
+ };
198
+ /**
199
+ * Boolean 스키마 헬퍼
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * const schema = {
204
+ * DEBUG: {
205
+ * ...envBoolean({
206
+ * description: 'Enable debug mode',
207
+ * default: false,
208
+ * }),
209
+ * key: 'DEBUG',
210
+ * }
211
+ * };
212
+ * ```
213
+ */
214
+ declare function envBoolean<T extends Omit<EnvVarSchema<boolean>, 'key' | 'type'>>(options: T): T & {
215
+ type: 'boolean';
216
+ validator: (value: string) => boolean;
217
+ };
218
+ /**
219
+ * URL 스키마 헬퍼
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * const schema = {
224
+ * DATABASE_URL: {
225
+ * ...envUrl({
226
+ * description: 'Database connection URL',
227
+ * required: true,
228
+ * validator: parsePostgresUrl,
229
+ * }),
230
+ * key: 'DATABASE_URL',
231
+ * }
232
+ * };
233
+ * ```
234
+ */
235
+ declare function envUrl<T extends Omit<EnvVarSchema, 'key' | 'type'>>(options: T): T & {
236
+ type: 'url';
237
+ };
238
+ /**
239
+ * Enum 스키마 헬퍼
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * const schema = {
244
+ * LOG_LEVEL: {
245
+ * ...envEnum(['debug', 'info', 'warn', 'error'] as const, {
246
+ * description: 'Logging level',
247
+ * default: 'info',
248
+ * }),
249
+ * key: 'LOG_LEVEL',
250
+ * }
251
+ * };
252
+ * ```
253
+ */
254
+ declare function envEnum<T extends string, O extends Omit<EnvVarSchema<T>, 'key' | 'type' | 'validator'>>(allowed: readonly T[], options: O): O & {
255
+ type: 'enum';
256
+ validator: (val: string) => T;
257
+ };
258
+ /**
259
+ * JSON 스키마 헬퍼
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * const schema = {
264
+ * CONFIG_JSON: {
265
+ * ...envJson<{ host: string; port: number }>({
266
+ * description: 'JSON configuration object',
267
+ * required: true,
268
+ * }),
269
+ * key: 'CONFIG_JSON',
270
+ * }
271
+ * };
272
+ * ```
273
+ */
274
+ declare function envJson<T = any, O extends Omit<EnvVarSchema<T>, 'key' | 'type' | 'validator'> = Omit<EnvVarSchema<T>, 'key' | 'type' | 'validator'>>(options: O): O & {
275
+ type: 'json';
276
+ validator: (val: string) => T;
277
+ };
278
+ /**
279
+ * 환경변수가 클라이언트에서 접근 가능한지 확인
280
+ * (NEXT_PUBLIC_ 접두사로 판단)
281
+ *
282
+ * @param key - 환경변수 키
283
+ * @returns 클라이언트에서 접근 가능하면 true
284
+ *
285
+ * @example
286
+ * ```typescript
287
+ * isClientAccessible('NEXT_PUBLIC_API_URL'); // true
288
+ * isClientAccessible('DATABASE_URL'); // false
289
+ * ```
290
+ */
291
+ declare function isClientAccessible(key: string): boolean;
292
+ /**
293
+ * 환경변수가 서버 전용인지 확인
294
+ * (NEXT_PUBLIC_ 접두사가 없으면 서버 전용)
295
+ *
296
+ * @param key - 환경변수 키
297
+ * @returns 서버 전용이면 true
298
+ *
299
+ * @example
300
+ * ```typescript
301
+ * isServerOnly('DATABASE_URL'); // true
302
+ * isServerOnly('NEXT_PUBLIC_API_URL'); // false
303
+ * ```
304
+ */
305
+ declare function isServerOnly(key: string): boolean;
306
+ /**
307
+ * 스키마의 nextjs 옵션 값 결정
308
+ *
309
+ * 명시적으로 지정되지 않은 경우:
310
+ * - NEXT_PUBLIC_* → true
311
+ * - 그 외 → false
312
+ *
313
+ * @param schema - 환경변수 스키마
314
+ * @returns Next.js 프로세스에서 사용 가능 여부
315
+ */
316
+ declare function isNextjsAccessible(schema: EnvVarSchema): boolean;
317
+ /**
318
+ * 스키마가 SPFN 서버 전용인지 확인
319
+ *
320
+ * @param schema - 환경변수 스키마
321
+ * @returns SPFN 서버에서만 사용되면 true
322
+ */
323
+ declare function isSpfnServerOnly(schema: EnvVarSchema): boolean;
324
+
325
+ /**
326
+ * Environment Variable Registry
327
+ *
328
+ * 환경변수 스키마를 등록하고 타입 안전하게 접근할 수 있는 레지스트리
329
+ *
330
+ * @example
331
+ * ```typescript
332
+ * const schema = defineEnvSchema({
333
+ * DATABASE_URL: envString({ description: 'Database URL', required: true })
334
+ * });
335
+ *
336
+ * const registry = createEnvRegistry(schema);
337
+ * const env = registry.validate(); // 검증 + env 반환
338
+ * console.log(env.DATABASE_URL);
339
+ * ```
340
+ *
341
+ * @module env/registry
342
+ */
343
+
344
+ /**
345
+ * 환경변수 레지스트리
346
+ *
347
+ * 스키마 기반 환경변수 관리 및 검증
348
+ */
349
+ declare class EnvRegistry<T extends EnvSchemaCollection = EnvSchemaCollection> {
350
+ private schemas;
351
+ private hasValidated;
352
+ constructor(schemas?: T);
353
+ /**
354
+ * 스키마 등록
355
+ */
356
+ register(schema: EnvVarSchema): void;
357
+ /**
358
+ * 여러 스키마 등록
359
+ */
360
+ registerMultiple(schemas: EnvSchemaCollection): void;
361
+ /**
362
+ * 검증 상태 리셋 (테스트용)
363
+ */
364
+ reset(): void;
365
+ /**
366
+ * 환경변수 원시값 가져오기 (fallback 지원)
367
+ */
368
+ private getRawValue;
369
+ /**
370
+ * 값에 validator 적용
371
+ */
372
+ private applyValidator;
373
+ /**
374
+ * 스키마 검증 수행 (값 읽기 없이)
375
+ *
376
+ * @internal
377
+ */
378
+ private validateSchemas;
379
+ /**
380
+ * SKIP_ENV_VALIDATION 환경변수 확인
381
+ */
382
+ private shouldSkipValidation;
383
+ /**
384
+ * 실제 접근 시점에 환경변수 값 가져오기 및 검증
385
+ *
386
+ * @internal
387
+ */
388
+ private getAndValidate;
389
+ /**
390
+ * 모든 환경변수를 명시적으로 검증 (SKIP_ENV_VALIDATION 무시)
391
+ *
392
+ * CLI에서 사용하기 위한 메서드로, 모든 required 환경변수를 강제 검증합니다.
393
+ *
394
+ * @returns 검증 결과 (errors, warnings)
395
+ */
396
+ validateAll(): {
397
+ errors: Array<{
398
+ key: string;
399
+ message: string;
400
+ }>;
401
+ warnings: Array<{
402
+ key: string;
403
+ message: string;
404
+ }>;
405
+ };
406
+ /**
407
+ * 환경변수 검증 및 타입 안전한 env 객체 반환
408
+ *
409
+ * Proxy 기반으로 구현되어 실제 환경변수 접근 시점에 값을 읽고 검증합니다.
410
+ * 이를 통해 dotenv 로딩 타이밍과 무관하게 최신 환경변수 값을 가져올 수 있습니다.
411
+ *
412
+ * @returns 검증된 환경변수 객체 (Proxy)
413
+ * @throws {Error} 필수 변수 누락 또는 검증 실패 시
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * const registry = createEnvRegistry(schema);
418
+ * const env = registry.validate(); // 스키마만 검증
419
+ * // ... dotenv 로딩 ...
420
+ * console.log(env.DATABASE_URL); // 이 시점에 실제 값 읽기
421
+ * ```
422
+ */
423
+ validate(): InferEnvType<T>;
424
+ }
425
+ /**
426
+ * 환경변수 검증 결과
427
+ */
428
+ interface EnvValidationResult {
429
+ valid: boolean;
430
+ errors: Array<{
431
+ key: string;
432
+ message: string;
433
+ }>;
434
+ warnings: Array<{
435
+ key: string;
436
+ message: string;
437
+ }>;
438
+ }
439
+ /**
440
+ * 레지스트리 생성 헬퍼
441
+ *
442
+ * @example
443
+ * ```typescript
444
+ * const schema = defineEnvSchema({
445
+ * DATABASE_URL: envString({ description: 'Database URL', required: true })
446
+ * });
447
+ *
448
+ * const registry = createEnvRegistry(schema);
449
+ * const env = registry.validate();
450
+ * ```
451
+ */
452
+ declare function createEnvRegistry<T extends EnvSchemaCollection>(schemas: T): EnvRegistry<T>;
453
+ /**
454
+ * 모든 환경변수를 명시적으로 검증 (SKIP_ENV_VALIDATION 무시)
455
+ *
456
+ * CLI `spfn env validate` 명령어에서 사용
457
+ *
458
+ * @param registries - 검증할 레지스트리 배열
459
+ * @returns 검증 결과
460
+ *
461
+ * @example
462
+ * ```typescript
463
+ * const result = validateAllEnv([coreRegistry, authRegistry]);
464
+ * if (!result.valid) {
465
+ * console.error('Missing env vars:', result.errors);
466
+ * process.exit(1);
467
+ * }
468
+ * ```
469
+ */
470
+ declare function validateAllEnv(registries: EnvRegistry<any>[]): EnvValidationResult;
471
+
472
+ export { type EnvLayer as E, type InferEnvType as I, EnvRegistry as a, type EnvSchemaCollection as b, type EnvValidationResult as c, type EnvVarSchema as d, createEnvRegistry as e, defineEnvSchema as f, envBoolean as g, envEnum as h, envJson as i, envNumber as j, envSecret as k, envString as l, envUrl as m, isClientAccessible as n, isNextjsAccessible as o, isServerOnly as p, isSpfnServerOnly as q, validateAllEnv as v };
@@ -13,6 +13,7 @@ import { DatabaseProvider, MigrationStatus, MigrationStatusDb } from '@spfn/core
13
13
  import * as _sinclair_typebox from '@sinclair/typebox';
14
14
  import { Static } from '@sinclair/typebox';
15
15
  import { R as RouteDef } from '../route-builder-2ani2jEI.js';
16
+ import { a as EnvRegistry } from '../registry-DZHXQ134.js';
16
17
  import 'pg-boss';
17
18
  import '../define-middleware-CVKgqo8S.js';
18
19
  import 'hono/utils/http-status';
@@ -553,6 +554,35 @@ interface ServerConfig {
553
554
  */
554
555
  allowPending?: boolean;
555
556
  };
557
+ /**
558
+ * Environment boot check
559
+ *
560
+ * Before serving, the server validates every variable of `@spfn/core`'s
561
+ * registry and of the registries listed here: a required variable must be
562
+ * set, and a set variable must pass its `validator`. On failure it logs
563
+ * each key with the reason — never a value — and refuses to start.
564
+ * `SKIP_ENV_VALIDATION` does not skip this check.
565
+ *
566
+ * @example
567
+ * ```typescript
568
+ * // src/server/config/env.config.ts
569
+ * export const envSchema = defineEnvSchema({ ... });
570
+ * export const envRegistry = createEnvRegistry(envSchema);
571
+ * export const env = envRegistry.validate();
572
+ *
573
+ * // src/server/server.config.ts
574
+ * export default defineServerConfig()
575
+ * .env({ registries: [envRegistry] })
576
+ * .build();
577
+ * ```
578
+ */
579
+ env?: {
580
+ /**
581
+ * Registries checked alongside `@spfn/core`'s: the app's own, and any
582
+ * package registry the app wants held to the same gate.
583
+ */
584
+ registries?: EnvRegistry<any>[];
585
+ };
556
586
  /**
557
587
  * Infrastructure initialization control
558
588
  * Controls automatic initialization of database and Redis
@@ -818,6 +848,8 @@ declare const CORE_TIME_PATH = "/_core/time";
818
848
  * Start SPFN server
819
849
  *
820
850
  * Automatically loads server.config.ts if exists
851
+ * Validates the environment (core's registry + `config.env.registries`) and
852
+ * exits with code 1 when a variable is missing or invalid
821
853
  * Automatically initializes Database and Redis from environment
822
854
  * Sets up graceful shutdown handlers for SIGTERM and SIGINT
823
855
  *
@@ -961,6 +993,9 @@ declare function getShutdownManager(): ShutdownManager;
961
993
  * - does NOT run seed/RBAC provisioning per cold start — that moves to a
962
994
  * deploy-time step, see {@link provisionInfrastructure}.
963
995
  *
996
+ * Like {@link startServer}, it runs the environment boot check before building
997
+ * the app; a failure rejects the returned promise instead of exiting.
998
+ *
964
999
  * `startServer()` / `spfn start` (the always-on path) is unchanged by this module.
965
1000
  */
966
1001
 
@@ -1284,6 +1319,22 @@ declare class ServerConfigBuilder {
1284
1319
  * ```
1285
1320
  */
1286
1321
  migrations(migrations: ServerConfig['migrations']): this;
1322
+ /**
1323
+ * Hand the app's env registries to the boot check
1324
+ *
1325
+ * `@spfn/core`'s own registry is always checked; the registries given here
1326
+ * are checked with it, before the server listens.
1327
+ *
1328
+ * @example
1329
+ * ```typescript
1330
+ * import { envRegistry } from '@/server/config/env.config';
1331
+ *
1332
+ * export default defineServerConfig()
1333
+ * .env({ registries: [envRegistry] })
1334
+ * .build();
1335
+ * ```
1336
+ */
1337
+ env(env: ServerConfig['env']): this;
1287
1338
  /**
1288
1339
  * Register workflow router for workflow orchestration
1289
1340
  *
@@ -1,4 +1,4 @@
1
- import { env } from '@spfn/core/config';
1
+ import { env, registry } from '@spfn/core/config';
2
2
  import { existsSync, readFileSync } from 'fs';
3
3
  import { resolve, join } from 'path';
4
4
  import { parse } from 'dotenv';
@@ -2880,6 +2880,50 @@ function readPort(value) {
2880
2880
  const port = Number(value);
2881
2881
  return Number.isInteger(port) && port > 0 && port < 65536 ? port : void 0;
2882
2882
  }
2883
+ logger.child("@spfn/core:env-registry");
2884
+ function validateAllEnv(registries) {
2885
+ const errors = [];
2886
+ const warnings = [];
2887
+ for (const registry of registries) {
2888
+ const result = registry.validateAll();
2889
+ errors.push(...result.errors);
2890
+ warnings.push(...result.warnings);
2891
+ }
2892
+ return {
2893
+ valid: errors.length === 0,
2894
+ errors,
2895
+ warnings
2896
+ };
2897
+ }
2898
+
2899
+ // src/server/env-boot-check.ts
2900
+ function runEnvBootCheck(config) {
2901
+ const result = validateAllEnv([registry, ...config.env?.registries ?? []]);
2902
+ const errors = uniqueByMessage(result.errors);
2903
+ const warnings = uniqueByMessage(result.warnings);
2904
+ for (const warning of warnings) {
2905
+ serverLogger.warn(`Environment: ${warning.message}`);
2906
+ }
2907
+ if (errors.length > 0) {
2908
+ serverLogger.error(
2909
+ `Environment validation failed (${errors.length}):
2910
+ ` + errors.map((error) => ` - ${error.message}`).join("\n")
2911
+ );
2912
+ }
2913
+ return { valid: errors.length === 0, errors, warnings };
2914
+ }
2915
+ function uniqueByMessage(entries) {
2916
+ const seen = /* @__PURE__ */ new Set();
2917
+ return entries.filter((entry) => {
2918
+ const id = `${entry.key}
2919
+ ${entry.message}`;
2920
+ if (seen.has(id)) {
2921
+ return false;
2922
+ }
2923
+ seen.add(id);
2924
+ return true;
2925
+ });
2926
+ }
2883
2927
 
2884
2928
  // src/server/validation.ts
2885
2929
  function validateServerConfig(config) {
@@ -2938,6 +2982,10 @@ var processHandlersRegistered = false;
2938
2982
  async function startServer(config) {
2939
2983
  loadEnv();
2940
2984
  const finalConfig = await loadAndMergeConfig(config);
2985
+ if (!runEnvBootCheck(finalConfig).valid) {
2986
+ serverLogger.error("Refusing to start: fix the environment variables listed above.");
2987
+ process.exit(1);
2988
+ }
2941
2989
  const { host, port, debug } = finalConfig;
2942
2990
  validateServerConfig(finalConfig);
2943
2991
  if (!host || !port) {
@@ -3428,6 +3476,9 @@ function createServerlessApp(config) {
3428
3476
  }
3429
3477
  async function buildServerlessApp(rawConfig) {
3430
3478
  loadEnv();
3479
+ if (!runEnvBootCheck(rawConfig ?? {}).valid) {
3480
+ throw new Error("Environment validation failed \u2014 the missing or invalid keys are logged above.");
3481
+ }
3431
3482
  const config = withServerlessDefaults(rawConfig);
3432
3483
  warnIfJobsConfigured(config);
3433
3484
  await initInfrastructure(config);
@@ -3756,6 +3807,25 @@ var ServerConfigBuilder = class {
3756
3807
  this.config.migrations = migrations;
3757
3808
  return this;
3758
3809
  }
3810
+ /**
3811
+ * Hand the app's env registries to the boot check
3812
+ *
3813
+ * `@spfn/core`'s own registry is always checked; the registries given here
3814
+ * are checked with it, before the server listens.
3815
+ *
3816
+ * @example
3817
+ * ```typescript
3818
+ * import { envRegistry } from '@/server/config/env.config';
3819
+ *
3820
+ * export default defineServerConfig()
3821
+ * .env({ registries: [envRegistry] })
3822
+ * .build();
3823
+ * ```
3824
+ */
3825
+ env(env6) {
3826
+ this.config.env = env6;
3827
+ return this;
3828
+ }
3759
3829
  /**
3760
3830
  * Register workflow router for workflow orchestration
3761
3831
  *