@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.
- package/README.md +1 -1
- package/dist/app-config/index.d.ts +39 -1
- package/dist/app-config/index.js.map +1 -1
- package/dist/config/index.js +3 -3
- package/dist/config/index.js.map +1 -1
- package/dist/env/index.d.ts +6 -442
- package/dist/env/index.js +13 -16
- package/dist/env/index.js.map +1 -1
- package/dist/registry-DZHXQ134.d.ts +472 -0
- package/dist/server/index.d.ts +51 -0
- package/dist/server/index.js +71 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config/README.md +20 -13
- package/src/env/README.md +135 -1
|
@@ -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 };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
*
|