@spfn/core 0.2.0-beta.6 → 0.2.0-beta.9

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/docs/env.md ADDED
@@ -0,0 +1,477 @@
1
+ # Environment
2
+
3
+ Type-safe environment variable management with schema-based validation.
4
+
5
+ ## Quick Start
6
+
7
+ ```typescript
8
+ // src/config/env.ts
9
+ import {
10
+ defineEnvSchema,
11
+ envString,
12
+ envNumber,
13
+ envBoolean,
14
+ envEnum,
15
+ createEnvRegistry,
16
+ parsePostgresUrl,
17
+ } from '@spfn/core/env';
18
+
19
+ // 1. Define schema
20
+ const schema = defineEnvSchema({
21
+ DATABASE_URL: envString({
22
+ description: 'PostgreSQL connection URL',
23
+ required: true,
24
+ sensitive: true,
25
+ validator: parsePostgresUrl,
26
+ }),
27
+ PORT: envNumber({
28
+ description: 'Server port',
29
+ default: 3000,
30
+ }),
31
+ DEBUG: envBoolean({
32
+ description: 'Enable debug mode',
33
+ default: false,
34
+ }),
35
+ LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error'] as const, {
36
+ description: 'Logging level',
37
+ default: 'info',
38
+ }),
39
+ });
40
+
41
+ // 2. Create registry and validate
42
+ const registry = createEnvRegistry(schema);
43
+ export const env = registry.validate();
44
+
45
+ // 3. Use with full type safety
46
+ env.DATABASE_URL // string (required)
47
+ env.PORT // number (default: 3000)
48
+ env.DEBUG // boolean (default: false)
49
+ env.LOG_LEVEL // 'debug' | 'info' | 'warn' | 'error'
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Schema Definition
55
+
56
+ ### `defineEnvSchema(schema)`
57
+
58
+ Define environment variable schema with auto-filled keys.
59
+
60
+ ```typescript
61
+ const schema = defineEnvSchema({
62
+ API_KEY: envString({ description: 'API key', required: true }),
63
+ // Automatically adds key: 'API_KEY'
64
+ });
65
+ ```
66
+
67
+ ### Schema Type Helpers
68
+
69
+ #### `envString(options)`
70
+
71
+ ```typescript
72
+ API_KEY: envString({
73
+ description: 'API authentication key',
74
+ required: true,
75
+ sensitive: true,
76
+ minLength: 32,
77
+ })
78
+ ```
79
+
80
+ #### `envNumber(options)`
81
+
82
+ ```typescript
83
+ PORT: envNumber({
84
+ description: 'Server port',
85
+ default: 3000,
86
+ validator: createNumberParser({ min: 1, max: 65535 }),
87
+ })
88
+ ```
89
+
90
+ #### `envBoolean(options)`
91
+
92
+ ```typescript
93
+ DEBUG: envBoolean({
94
+ description: 'Enable debug mode',
95
+ default: false,
96
+ })
97
+ // Parses: 'true', '1', 'yes' → true
98
+ // Parses: 'false', '0', 'no' → false
99
+ ```
100
+
101
+ #### `envUrl(options)`
102
+
103
+ ```typescript
104
+ API_URL: envUrl({
105
+ description: 'API endpoint URL',
106
+ required: true,
107
+ })
108
+ ```
109
+
110
+ #### `envEnum(allowed, options)`
111
+
112
+ ```typescript
113
+ LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error'] as const, {
114
+ description: 'Logging level',
115
+ default: 'info',
116
+ })
117
+ ```
118
+
119
+ #### `envJson<T>(options)`
120
+
121
+ ```typescript
122
+ CONFIG: envJson<{ host: string; port: number }>({
123
+ description: 'JSON configuration',
124
+ required: true,
125
+ })
126
+ ```
127
+
128
+ ### Schema Options
129
+
130
+ | Option | Type | Description |
131
+ |--------|------|-------------|
132
+ | `description` | `string` | Variable description |
133
+ | `required` | `boolean` | Whether variable is required |
134
+ | `default` | `T` | Default value if not set |
135
+ | `validator` | `(value: string) => T` | Custom validation/transform |
136
+ | `fallbackKeys` | `string[]` | Fallback environment variable keys |
137
+ | `minLength` | `number` | Minimum string length |
138
+ | `sensitive` | `boolean` | Mark as sensitive (masked in logs) |
139
+
140
+ ---
141
+
142
+ ## EnvRegistry
143
+
144
+ ### Creating Registry
145
+
146
+ ```typescript
147
+ import { createEnvRegistry } from '@spfn/core/env';
148
+
149
+ const registry = createEnvRegistry(schema);
150
+ const env = registry.validate();
151
+ ```
152
+
153
+ ### Lazy Validation
154
+
155
+ Values are validated when accessed (Proxy-based):
156
+
157
+ ```typescript
158
+ const env = registry.validate();
159
+
160
+ // Later, when accessed:
161
+ console.log(env.DATABASE_URL); // Validates at this point
162
+ ```
163
+
164
+ ### Fallback Keys
165
+
166
+ Support legacy environment variable names:
167
+
168
+ ```typescript
169
+ DATABASE_URL: envString({
170
+ description: 'Database URL',
171
+ required: true,
172
+ fallbackKeys: ['DB_URL', 'POSTGRES_URL'],
173
+ })
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Parsers
179
+
180
+ ### String Parsers
181
+
182
+ ```typescript
183
+ import { parseString, createStringParser } from '@spfn/core/env';
184
+
185
+ parseString(' hello '); // 'hello'
186
+
187
+ const apiKeyParser = createStringParser({
188
+ minLength: 32,
189
+ maxLength: 128,
190
+ pattern: /^[A-Za-z0-9_-]+$/,
191
+ });
192
+ ```
193
+
194
+ ### Number Parsers
195
+
196
+ ```typescript
197
+ import { parseNumber, createNumberParser, parseInteger } from '@spfn/core/env';
198
+
199
+ parseNumber('42'); // 42
200
+
201
+ const portParser = createNumberParser({
202
+ min: 1,
203
+ max: 65535,
204
+ integer: true,
205
+ });
206
+ ```
207
+
208
+ ### URL Parsers
209
+
210
+ ```typescript
211
+ import { parseUrl, parsePostgresUrl, parseRedisUrl } from '@spfn/core/env';
212
+
213
+ parseUrl('https://api.example.com');
214
+ parsePostgresUrl('postgres://user:pass@localhost:5432/db');
215
+ parseRedisUrl('redis://localhost:6379');
216
+ ```
217
+
218
+ ### Enum Parser
219
+
220
+ ```typescript
221
+ import { parseEnum, createEnumParser } from '@spfn/core/env';
222
+
223
+ parseEnum('info', ['debug', 'info', 'warn', 'error']);
224
+
225
+ const logLevelParser = createEnumParser(
226
+ ['debug', 'info', 'warn', 'error'],
227
+ true // case-insensitive
228
+ );
229
+ ```
230
+
231
+ ### Array Parser
232
+
233
+ ```typescript
234
+ import { parseArray, createArrayParser } from '@spfn/core/env';
235
+
236
+ parseArray('a,b,c'); // ['a', 'b', 'c']
237
+ parseArray('a|b|c', { separator: '|' }); // ['a', 'b', 'c']
238
+
239
+ const portsParser = createArrayParser(
240
+ createNumberParser({ min: 1, max: 65535, integer: true })
241
+ );
242
+ portsParser('3000,4000,5000'); // [3000, 4000, 5000]
243
+ ```
244
+
245
+ ### Security Parsers
246
+
247
+ ```typescript
248
+ import { createSecureSecretParser, createPasswordParser } from '@spfn/core/env';
249
+
250
+ // Entropy-based secret validation
251
+ const secretParser = createSecureSecretParser({
252
+ minLength: 32,
253
+ minUniqueChars: 16,
254
+ minEntropy: 3.5,
255
+ });
256
+
257
+ // Password strength validation
258
+ const passwordParser = createPasswordParser({
259
+ minLength: 12,
260
+ requireUppercase: true,
261
+ requireLowercase: true,
262
+ requireNumber: true,
263
+ requireSpecial: true,
264
+ });
265
+ ```
266
+
267
+ ### Parser Composition
268
+
269
+ ```typescript
270
+ import { chain, withFallback, optional } from '@spfn/core/env';
271
+
272
+ // Chain parsers
273
+ const apiKeyParser = chain(
274
+ parseString,
275
+ createStringParser({ minLength: 32 })
276
+ );
277
+
278
+ // Fallback value
279
+ const configParser = withFallback(parseJson, { host: 'localhost' });
280
+
281
+ // Optional (returns undefined for empty)
282
+ const optionalRedisParser = optional(parseRedisUrl);
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Environment File Loading
288
+
289
+ ### SPFN Server
290
+
291
+ ```typescript
292
+ import { loadEnv } from '@spfn/core/env/loader';
293
+
294
+ loadEnv(); // Loads .env, .env.local, .env.server, .env.server.local
295
+ ```
296
+
297
+ ### Loading Priority
298
+
299
+ 1. `.env` - 기본값
300
+ 2. `.env.local` - 로컬 오버라이드
301
+ 3. `.env.server` - 서버 전용 기본값
302
+ 4. `.env.server.local` - 서버 전용 민감정보
303
+
304
+ ### Options
305
+
306
+ ```typescript
307
+ loadEnv({
308
+ cwd: '/path/to/project',
309
+ debug: true,
310
+ override: false,
311
+ });
312
+
313
+ // Load once (prevent duplicate calls)
314
+ import { loadEnvOnce } from '@spfn/core/env/loader';
315
+ loadEnvOnce();
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Security Separation (Next.js + SPFN)
321
+
322
+ ### File Structure
323
+
324
+ ```
325
+ project/
326
+ ├── .env # 기본값 (커밋 O)
327
+ ├── .env.local # Next.js용 (커밋 X)
328
+ ├── .env.server # SPFN 전용 기본값 (커밋 O)
329
+ └── .env.server.local # SPFN 전용 민감정보 (커밋 X)
330
+ ```
331
+
332
+ ### Which File for What?
333
+
334
+ | 환경변수 | 파일 | 이유 |
335
+ |----------|------|------|
336
+ | `NEXT_PUBLIC_*` | `.env.local` | 브라우저 노출 OK |
337
+ | `SPFN_API_URL` | `.env.local` | Next.js에서 사용 |
338
+ | `DATABASE_URL` | `.env.server.local` | SPFN 전용, 민감정보 |
339
+ | `SESSION_SECRET` | `.env.server.local` | SPFN 전용, 민감정보 |
340
+
341
+ ### Schema with `nextjs` Option
342
+
343
+ ```typescript
344
+ DATABASE_URL: envString({
345
+ description: 'PostgreSQL connection URL',
346
+ required: true,
347
+ sensitive: true,
348
+ nextjs: false, // SPFN 서버에서만 사용
349
+ }),
350
+
351
+ SPFN_API_URL: envString({
352
+ description: 'Backend API URL',
353
+ required: true,
354
+ nextjs: true, // Next.js에서도 사용
355
+ }),
356
+ ```
357
+
358
+ ---
359
+
360
+ ## Type Inference
361
+
362
+ ```typescript
363
+ import type { InferEnvType } from '@spfn/core/env';
364
+
365
+ const schema = defineEnvSchema({
366
+ DATABASE_URL: envString({ required: true }),
367
+ PORT: envNumber({ default: 3000 }),
368
+ DEBUG: envBoolean({}),
369
+ });
370
+
371
+ type Env = InferEnvType<typeof schema>;
372
+ // {
373
+ // DATABASE_URL: string; // required
374
+ // PORT: number; // has default
375
+ // DEBUG?: boolean | undefined; // optional
376
+ // }
377
+ ```
378
+
379
+ ---
380
+
381
+ ## Complete Example
382
+
383
+ ```typescript
384
+ // src/config/env.ts
385
+ import {
386
+ defineEnvSchema,
387
+ envString,
388
+ envNumber,
389
+ envBoolean,
390
+ envEnum,
391
+ createEnvRegistry,
392
+ parsePostgresUrl,
393
+ createSecureSecretParser,
394
+ createNumberParser,
395
+ } from '@spfn/core/env';
396
+
397
+ const schema = defineEnvSchema({
398
+ // Database
399
+ DATABASE_URL: envString({
400
+ description: 'PostgreSQL connection URL',
401
+ required: true,
402
+ sensitive: true,
403
+ validator: parsePostgresUrl,
404
+ }),
405
+
406
+ // Server
407
+ PORT: envNumber({
408
+ description: 'Server port',
409
+ default: 3000,
410
+ validator: createNumberParser({ min: 1, max: 65535, integer: true }),
411
+ }),
412
+
413
+ // Security
414
+ SESSION_SECRET: envString({
415
+ description: 'Session encryption secret',
416
+ required: true,
417
+ sensitive: true,
418
+ validator: createSecureSecretParser({ minLength: 32 }),
419
+ }),
420
+
421
+ // Environment
422
+ NODE_ENV: envEnum(['development', 'staging', 'production', 'test'] as const, {
423
+ description: 'Node environment',
424
+ default: 'development',
425
+ }),
426
+
427
+ // Logging
428
+ LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error'] as const, {
429
+ description: 'Log level',
430
+ default: 'info',
431
+ }),
432
+
433
+ // Optional
434
+ REDIS_URL: envString({
435
+ description: 'Redis connection URL',
436
+ required: false,
437
+ }),
438
+
439
+ DEBUG: envBoolean({
440
+ description: 'Enable debug mode',
441
+ default: false,
442
+ }),
443
+ });
444
+
445
+ const registry = createEnvRegistry(schema);
446
+ export const env = registry.validate();
447
+ export type Env = typeof env;
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Best Practices
453
+
454
+ ```typescript
455
+ // 1. Centralize in single file
456
+ // src/config/env.ts
457
+
458
+ // 2. Use descriptive descriptions
459
+ DATABASE_URL: envString({
460
+ description: 'PostgreSQL connection URL for primary database',
461
+ })
462
+
463
+ // 3. Mark sensitive variables
464
+ API_SECRET: envString({
465
+ sensitive: true,
466
+ })
467
+
468
+ // 4. Provide fallback keys for migrations
469
+ DATABASE_URL: envString({
470
+ fallbackKeys: ['DB_URL', 'POSTGRES_URL'],
471
+ })
472
+
473
+ // 5. Use strong validators for secrets
474
+ SESSION_SECRET: envString({
475
+ validator: createSecureSecretParser({ minLength: 32, minEntropy: 3.5 }),
476
+ })
477
+ ```