@spfn/core 0.3.0-beta.4 → 0.3.0-beta.6

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.
Files changed (45) hide show
  1. package/README.md +183 -4
  2. package/dist/authz/index.js +1 -381
  3. package/dist/authz/index.js.map +1 -1
  4. package/dist/db/index.d.ts +173 -27
  5. package/dist/db/index.js +192 -57
  6. package/dist/db/index.js.map +1 -1
  7. package/dist/env/loader.js +24 -1
  8. package/dist/env/loader.js.map +1 -1
  9. package/dist/errors/index.js +1 -381
  10. package/dist/errors/index.js.map +1 -1
  11. package/dist/logger/index.js +0 -12
  12. package/dist/logger/index.js.map +1 -1
  13. package/dist/middleware/index.js +6 -387
  14. package/dist/middleware/index.js.map +1 -1
  15. package/dist/nextjs/index.d.ts +18 -1
  16. package/dist/nextjs/index.js +40 -1
  17. package/dist/nextjs/index.js.map +1 -1
  18. package/dist/nextjs/server.d.ts +34 -1
  19. package/dist/nextjs/server.js +14 -0
  20. package/dist/nextjs/server.js.map +1 -1
  21. package/dist/ops/index.d.ts +61 -6
  22. package/dist/ops/index.js +330 -30
  23. package/dist/ops/index.js.map +1 -1
  24. package/dist/server/index.js +24 -1
  25. package/dist/server/index.js.map +1 -1
  26. package/docs/file-upload.md +195 -333
  27. package/package.json +6 -5
  28. package/src/cache/README.md +330 -0
  29. package/src/codegen/README.md +516 -0
  30. package/src/config/README.md +326 -0
  31. package/src/contract/README.md +326 -0
  32. package/src/db/README.md +589 -0
  33. package/src/db/manager/README.md +500 -0
  34. package/src/db/schema/README.md +344 -0
  35. package/src/db/transaction/README.md +822 -0
  36. package/src/env/README.md +651 -0
  37. package/src/errors/README.md +429 -0
  38. package/src/event/README.md +736 -0
  39. package/src/job/README.md +514 -0
  40. package/src/logger/README.md +321 -0
  41. package/src/middleware/README.md +634 -0
  42. package/src/nextjs/README.md +608 -0
  43. package/src/route/README.md +738 -0
  44. package/src/security/README.md +100 -0
  45. package/src/server/README.md +704 -0
@@ -0,0 +1,651 @@
1
+ # @spfn/core/env — Environment Variable Management
2
+
3
+ Type-safe, schema-based environment variable validation, parsing, and security-focused
4
+ file separation for Next.js + SPFN server.
5
+
6
+ ## Import paths
7
+
8
+ There are **two** entry points. Picking the wrong one breaks the build.
9
+
10
+ ```typescript
11
+ // Schema, registry, parsers, type guards — isomorphic (no node:fs)
12
+ import { defineEnvSchema, createEnvRegistry, envString /* ... */ } from '@spfn/core/env';
13
+
14
+ // File loader — SERVER ONLY (uses node:fs). Never import in client/edge code.
15
+ import { loadEnv, loadEnvOnce } from '@spfn/core/env/loader';
16
+ ```
17
+
18
+ `loadEnv` is **not** re-exported from `@spfn/core/env`. Import it from `@spfn/core/env/loader`.
19
+
20
+ ---
21
+
22
+ ## Public API (complete)
23
+
24
+ From `@spfn/core/env`:
25
+
26
+ - Schema helpers: `defineEnvSchema`, `envString`, `envNumber`, `envBoolean`, `envUrl`,
27
+ `envEnum`, `envJson`
28
+ - Registry: `createEnvRegistry`, `EnvRegistry` (class), `validateAllEnv`
29
+ - Type guards: `isClientAccessible`, `isServerOnly`, `isNextjsAccessible`, `isSpfnServerOnly`
30
+ - Parsers: `parseString`, `createStringParser`, `parseBoolean`, `parseNumber`,
31
+ `createNumberParser`, `parseInteger`, `parseDecimal`, `parseUrl`, `createUrlParser`,
32
+ `parsePostgresUrl`, `parseRedisUrl`, `parseEnum`, `createEnumParser`, `parseJson`,
33
+ `createJsonParser`, `parseArray`, `createArrayParser`, `createSecureSecretParser`,
34
+ `createPasswordParser`
35
+ - Parser composition: `chain`, `withFallback`, `optional`
36
+ - Types: `Parser<T>`, `EnvVarSchema`, `EnvSchemaCollection`, `InferEnvType`,
37
+ `EnvValidationResult`, `NodeEnv`, `LogLevel`
38
+
39
+ From `@spfn/core/env/loader`:
40
+
41
+ - `loadEnv`, `loadEnvOnce`, `resetEnvLoadState`
42
+ - Types: `LoadEnvOptions`, `LoadEnvResult`
43
+
44
+ > There is **no** `getEnvVar` / `requireEnvVar` / `hasEnvVar` / `getEnvVars` /
45
+ > `loadEnvironment` function, and **no** `env.get()` / `env.require()` /
46
+ > `getByCategory()` / `generateMarkdownDocs()` etc. Those belong to a removed API — do
47
+ > not use them. The current model is: define a schema, build a registry, call
48
+ > `.validate()`, read properties off the returned proxy.
49
+
50
+ ---
51
+
52
+ ## Quick Start
53
+
54
+ ```typescript
55
+ // src/config/env.ts
56
+ import {
57
+ defineEnvSchema,
58
+ envString,
59
+ envNumber,
60
+ envBoolean,
61
+ envEnum,
62
+ createEnvRegistry,
63
+ parsePostgresUrl,
64
+ } from '@spfn/core/env';
65
+
66
+ const schema = defineEnvSchema({
67
+ DATABASE_URL: envString({
68
+ description: 'PostgreSQL connection URL',
69
+ required: true,
70
+ sensitive: true,
71
+ validator: parsePostgresUrl,
72
+ }),
73
+ PORT: envNumber({
74
+ description: 'Server port',
75
+ default: 3000,
76
+ }),
77
+ DEBUG: envBoolean({
78
+ description: 'Enable debug mode',
79
+ default: false,
80
+ }),
81
+ LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error'] as const, {
82
+ description: 'Logging level',
83
+ default: 'info',
84
+ }),
85
+ });
86
+
87
+ const registry = createEnvRegistry(schema);
88
+ export const env = registry.validate();
89
+ export type Env = typeof env;
90
+
91
+ // Full type safety:
92
+ env.DATABASE_URL; // string (required)
93
+ env.PORT; // number (default: 3000)
94
+ env.DEBUG; // boolean (default: false)
95
+ env.LOG_LEVEL; // 'debug' | 'info' | 'warn' | 'error'
96
+ ```
97
+
98
+ `defineEnvSchema` auto-fills the `key` field from each object key. You can write the
99
+ schema object inline without it, but then you must set `key` manually on every entry —
100
+ prefer `defineEnvSchema`.
101
+
102
+ ---
103
+
104
+ ## Schema Definition
105
+
106
+ ### Type helpers
107
+
108
+ | Helper | Result type | Default validator |
109
+ |--------|-------------|-------------------|
110
+ | `envString(options)` | `string` | identity (no-op) |
111
+ | `envNumber(options)` | `number` | `parseNumber` |
112
+ | `envBoolean(options)` | `boolean` | `parseBoolean` |
113
+ | `envUrl(options)` | `string` | none (set `validator` yourself, e.g. `parsePostgresUrl`) |
114
+ | `envEnum(allowed, options)` | union of `allowed` | built-in membership check |
115
+ | `envJson<T>(options)` | `T` | `parseJson` |
116
+
117
+ ```typescript
118
+ API_KEY: envString({
119
+ description: 'API authentication key',
120
+ required: true,
121
+ sensitive: true,
122
+ minLength: 32,
123
+ }),
124
+
125
+ PORT: envNumber({
126
+ description: 'Server port',
127
+ default: 3000,
128
+ validator: createNumberParser({ min: 1, max: 65535, integer: true }),
129
+ }),
130
+
131
+ // envUrl has NO default validator — add one for protocol enforcement:
132
+ API_URL: envUrl({
133
+ description: 'API endpoint URL',
134
+ required: true,
135
+ validator: createUrlParser('https'),
136
+ }),
137
+
138
+ LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error'] as const, {
139
+ description: 'Logging level',
140
+ default: 'info',
141
+ }),
142
+
143
+ CONFIG: envJson<{ host: string; port: number }>({
144
+ description: 'JSON configuration',
145
+ required: true,
146
+ }),
147
+ ```
148
+
149
+ ### `EnvVarSchema` options
150
+
151
+ | Option | Type | Description |
152
+ |--------|------|-------------|
153
+ | `description` | `string` | Purpose / usage (used in CLI validate output) |
154
+ | `type` | `'string' \| 'number' \| 'boolean' \| 'url' \| 'enum' \| 'json'` | Set by the helper; don't pass manually |
155
+ | `required` | `boolean` | Throw at access time if unset and no default |
156
+ | `default` | `T` | Value returned when the variable is unset |
157
+ | `validator` | `(value: string) => T` | Parse/validate. Throwing fails validation |
158
+ | `fallbackKeys` | `string[]` | Legacy keys read (in order) when the primary key is unset |
159
+ | `minLength` | `number` | Minimum raw string length (checked before `validator`) |
160
+ | `sensitive` | `boolean` | Marks a secret; triggers a warning if `NEXT_PUBLIC_*` |
161
+ | `examples` | `T[]` | Example values (metadata only) |
162
+ | `nextjs` | `boolean` | File-separation hint (see below). Defaults to `true` for `NEXT_PUBLIC_*`, else `false` |
163
+
164
+ > There is **no** `category` option. Docs that show `category: '...'` are stale — passing
165
+ > it is harmless (excess property) but it does nothing.
166
+
167
+ ### Type inference (`InferEnvType`)
168
+
169
+ ```typescript
170
+ import type { InferEnvType } from '@spfn/core/env';
171
+
172
+ const schema = defineEnvSchema({
173
+ DATABASE_URL: envString({ description: 'DB URL', required: true }),
174
+ PORT: envNumber({ description: 'Port', default: 3000 }),
175
+ DEBUG: envBoolean({ description: 'Debug' }),
176
+ });
177
+
178
+ type Env = InferEnvType<typeof schema>;
179
+ // {
180
+ // DATABASE_URL: string; // required: true → required
181
+ // PORT: number; // has default → required
182
+ // DEBUG?: boolean | undefined; // neither → optional
183
+ // }
184
+ ```
185
+
186
+ Rule: `required: true` **or** a `default` present ⇒ required field. Otherwise optional
187
+ (`| undefined`). `registry.validate()` returns `InferEnvType<typeof schema>`.
188
+
189
+ ---
190
+
191
+ ## Registry
192
+
193
+ ```typescript
194
+ import { createEnvRegistry } from '@spfn/core/env';
195
+
196
+ const registry = createEnvRegistry(schema); // or: new EnvRegistry(schema)
197
+ const env = registry.validate();
198
+ ```
199
+
200
+ ### Lazy validation (timing matters)
201
+
202
+ `registry.validate()` performs **only schema-level checks** (e.g. sensitive + client-
203
+ accessible warnings) and returns a **Proxy**. Each individual variable is read from
204
+ `process.env`, defaulted, and run through its `validator` **at the moment you access the
205
+ property** — not when `validate()` is called.
206
+
207
+ ```typescript
208
+ const env = registry.validate(); // does NOT read process.env values yet
209
+ loadEnv(); // dotenv files populate process.env
210
+ env.DATABASE_URL; // value is read + validated HERE
211
+ ```
212
+
213
+ Consequences:
214
+
215
+ - You can call `validate()` before `loadEnv()` — values are picked up lazily. This is the
216
+ intended order for the SPFN server entry point.
217
+ - A missing `required` variable does **not** throw at `validate()` time; it throws on
218
+ first access (`Error('Environment validation failed')`, with the offending key logged).
219
+ - Accessing an unknown key (not in the schema) returns `undefined`.
220
+
221
+ ### Skipping validation
222
+
223
+ Set `SKIP_ENV_VALIDATION=true` (or `=1`) to make the proxy skip the `required` check —
224
+ missing required vars return their `default` (or `undefined`) instead of throwing. Useful
225
+ for build steps that don't have secrets. `validator`/`minLength` checks still apply when a
226
+ value is present. **`EnvRegistry.validateAll()` ignores this flag** and always reports
227
+ missing requireds.
228
+
229
+ ### `validateAll()` / `validateAllEnv()` (eager, for CLI)
230
+
231
+ For an explicit up-front check of *all* variables (used by `spfn env validate`):
232
+
233
+ ```typescript
234
+ import { validateAllEnv } from '@spfn/core/env';
235
+
236
+ const result = validateAllEnv([coreRegistry, authRegistry]); // EnvValidationResult
237
+ if (!result.valid) {
238
+ for (const e of result.errors) {
239
+ console.error(`${e.key}: ${e.message}`);
240
+ }
241
+ process.exit(1);
242
+ }
243
+ for (const w of result.warnings) {
244
+ console.warn(`${w.key}: ${w.message}`);
245
+ }
246
+ ```
247
+
248
+ `EnvValidationResult` is `{ valid: boolean; errors: {key,message}[]; warnings: {key,message}[] }`.
249
+ A single registry exposes the same via `registry.validateAll()` returning just
250
+ `{ errors, warnings }`.
251
+
252
+ ### Fallback keys
253
+
254
+ ```typescript
255
+ DATABASE_URL: envString({
256
+ description: 'Database URL',
257
+ required: true,
258
+ fallbackKeys: ['DB_URL', 'POSTGRES_URL'], // tried in order if DATABASE_URL is unset
259
+ }),
260
+ ```
261
+
262
+ ### Other `EnvRegistry` methods
263
+
264
+ `register(schema)`, `registerMultiple(schemas)` — add schemas after construction.
265
+ `reset()` — clears the internal "already validated" flag (test helper).
266
+
267
+ ---
268
+
269
+ ## Environment File Loading
270
+
271
+ `loadEnv` is server-only (`@spfn/core/env/loader`). It parses `.env*` files, merges them,
272
+ and writes into `process.env`.
273
+
274
+ ```typescript
275
+ import { loadEnv } from '@spfn/core/env/loader';
276
+ import { createEnvRegistry } from '@spfn/core/env';
277
+ import { envSchema } from './env.schema';
278
+
279
+ loadEnv(); // server entry point
280
+ const env = createEnvRegistry(envSchema).validate();
281
+ ```
282
+
283
+ ### Loading priority (5 tiers, low → high; later overrides earlier)
284
+
285
+ `NODE_ENV` (default `'local'`) selects which files apply:
286
+
287
+ 1. `.env` — common defaults (committed)
288
+ 2. `.env.{NODE_ENV}` — per-environment override (committed)
289
+ 3. `.env.local` — local override (gitignored; **skipped when `NODE_ENV=test`**)
290
+ 4. `.env.{NODE_ENV}.local` — per-environment secrets (gitignored)
291
+ 5. `.env.server` — SPFN-server-only secrets (gitignored; **never read by Next.js**)
292
+
293
+ Within `loadEnv`, **keys already present in `process.env` are not overwritten** (so
294
+ platform-injected vars win) unless `override: true`.
295
+
296
+ ### `LoadEnvOptions`
297
+
298
+ ```typescript
299
+ loadEnv({
300
+ cwd: '/path/to/project', // project root (default: process.cwd())
301
+ nodeEnv: 'production', // selects .env.{nodeEnv} (default: process.env.NODE_ENV || 'local')
302
+ server: true, // include .env.server (default: true; set false for Next.js client builds)
303
+ debug: true, // log loaded files (default: false)
304
+ override: false, // overwrite existing process.env keys (default: false)
305
+ });
306
+ ```
307
+
308
+ `loadEnv` returns `LoadEnvResult` = `{ loadedFiles: string[]; loadedKeys: string[] }`.
309
+
310
+ ### `loadEnvOnce()` / `resetEnvLoadState()`
311
+
312
+ `loadEnvOnce(options?)` runs `loadEnv` once per process; subsequent calls return an empty
313
+ result. `resetEnvLoadState()` clears that latch (test helper).
314
+
315
+ ### Next.js does its own loading
316
+
317
+ Next.js loads `.env` / `.env.local` / `.env.{NODE_ENV}` itself. In Next.js code (server
318
+ components, route handlers) **do not call `loadEnv()`** — just build the registry and read
319
+ values. `.env.server` is intentionally invisible to Next.js (that is the whole point of
320
+ the separation below).
321
+
322
+ ---
323
+
324
+ ## Security Separation (Next.js + SPFN)
325
+
326
+ SPFN runs as a **separate process** from Next.js. Next.js server components can read every
327
+ variable in `.env` / `.env.local`; a server-side data-exfiltration bug (e.g. a
328
+ react2shell-style vulnerability) would expose all of them. Keep server-only secrets out of
329
+ files Next.js reads.
330
+
331
+ ### File layout
332
+
333
+ ```
334
+ project/
335
+ ├── .env # common defaults (committed)
336
+ ├── .env.production # production non-secret overrides (committed)
337
+ ├── .env.local # Next.js local overrides (gitignored)
338
+ ├── .env.production.local # production secrets (gitignored)
339
+ └── .env.server # SPFN-server-only secrets (gitignored)
340
+ ```
341
+
342
+ Commit a `.env.server.example` template (placeholder values only) so teammates know which
343
+ keys to fill in.
344
+
345
+ ### Which file for what?
346
+
347
+ | Variable | File | Why |
348
+ |----------|------|-----|
349
+ | `NEXT_PUBLIC_*` | `.env.local` | Client-exposed; browser-safe |
350
+ | `SPFN_API_URL` (per-env) | `.env` / `.env.production` | Used by Next.js too; non-secret |
351
+ | `SPFN_APP_URL` | `.env.local` | Next.js local setting |
352
+ | `DB_POOL_MAX` | `.env.server` | Server-only, non-secret |
353
+ | `DATABASE_URL` | `.env.server` | Server-only, **secret** |
354
+ | `SESSION_SECRET` | `.env.server` | Server-only, **secret** |
355
+
356
+ Rule of thumb: anything Next.js must read goes in `.env` / `.env.local`; everything
357
+ server-only (especially secrets) goes in `.env.server`.
358
+
359
+ ### Schema `nextjs` flag
360
+
361
+ `nextjs` documents which process a variable belongs to. Defaults: `true` for
362
+ `NEXT_PUBLIC_*`, `false` otherwise. The type guards below consume it.
363
+
364
+ ```typescript
365
+ DATABASE_URL: envString({
366
+ description: 'PostgreSQL connection URL',
367
+ required: true,
368
+ sensitive: true,
369
+ nextjs: false, // SPFN-server-only → belongs in .env.server
370
+ }),
371
+
372
+ SPFN_API_URL: envString({
373
+ description: 'Backend API URL',
374
+ required: true,
375
+ nextjs: true, // Next.js also reads it → .env / .env.local
376
+ }),
377
+
378
+ NEXT_PUBLIC_WS_URL: envString({
379
+ description: 'WebSocket URL',
380
+ // nextjs defaults to true (NEXT_PUBLIC_ prefix)
381
+ }),
382
+ ```
383
+
384
+ ### Type guards
385
+
386
+ ```typescript
387
+ import {
388
+ isClientAccessible, isServerOnly,
389
+ isNextjsAccessible, isSpfnServerOnly,
390
+ } from '@spfn/core/env';
391
+
392
+ isClientAccessible('NEXT_PUBLIC_API_URL'); // true — string key, NEXT_PUBLIC_ prefix
393
+ isClientAccessible('DATABASE_URL'); // false
394
+ isServerOnly('DATABASE_URL'); // true — inverse of isClientAccessible
395
+
396
+ // These take a SCHEMA object (not a string) and honor the `nextjs` flag:
397
+ isNextjsAccessible(schema.SPFN_API_URL); // true
398
+ isSpfnServerOnly(schema.DATABASE_URL); // true — inverse of isNextjsAccessible
399
+ ```
400
+
401
+ > `isClientAccessible` / `isServerOnly` take a **string key**.
402
+ > `isNextjsAccessible` / `isSpfnServerOnly` take an **`EnvVarSchema` object** and respect
403
+ > its `nextjs` flag. Don't mix them up.
404
+
405
+ ### Sensitive-on-client warning
406
+
407
+ `registry.validate()` (and `validateAll()`) emit a warning if a `sensitive` variable is
408
+ client-accessible — fix it by dropping the `NEXT_PUBLIC_` prefix, not by unmarking it:
409
+
410
+ ```typescript
411
+ // Wrong — secret exposed to the browser, triggers a warning:
412
+ NEXT_PUBLIC_API_SECRET: envString({ description: 'secret', sensitive: true }),
413
+
414
+ // Right:
415
+ API_SECRET: envString({ description: 'secret', sensitive: true }),
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Parsers
421
+
422
+ All parsers are `(value: string) => T` and **throw** on invalid input. Use them as a
423
+ schema `validator`, or standalone.
424
+
425
+ ### String
426
+
427
+ ```typescript
428
+ import { parseString, createStringParser } from '@spfn/core/env';
429
+
430
+ parseString(' hello '); // 'hello' (trims; throws if empty)
431
+
432
+ createStringParser({ minLength: 32, maxLength: 128, pattern: /^[A-Za-z0-9_-]+$/, trim: true });
433
+ ```
434
+
435
+ ### Boolean
436
+
437
+ ```typescript
438
+ import { parseBoolean } from '@spfn/core/env';
439
+
440
+ parseBoolean('true' /* | '1' | 'yes' */); // true (case-insensitive)
441
+ parseBoolean('false' /* | '0' | 'no' */); // false
442
+ // anything else → throws
443
+ ```
444
+
445
+ ### Number
446
+
447
+ ```typescript
448
+ import {
449
+ parseNumber, createNumberParser, parseInteger, parseDecimal,
450
+ } from '@spfn/core/env';
451
+
452
+ parseNumber('42'); // 42
453
+ parseNumber('42', { min: 1, max: 100, integer: true });
454
+ createNumberParser({ min: 1, max: 65535, integer: true });
455
+ parseInteger('42', { min: 1, max: 100 }); // integer-constrained
456
+ parseDecimal('0.75', { min: 0, max: 1 }); // float-constrained
457
+ ```
458
+
459
+ > The float helper is named **`parseDecimal`**. There is no `parseFloat` export.
460
+
461
+ ### URL
462
+
463
+ ```typescript
464
+ import {
465
+ parseUrl, createUrlParser, parsePostgresUrl, parseRedisUrl,
466
+ } from '@spfn/core/env';
467
+
468
+ parseUrl('https://api.example.com'); // any protocol
469
+ parseUrl('https://x', { protocol: 'https' }); // enforce protocol
470
+ createUrlParser('https'); // reusable
471
+ parsePostgresUrl('postgres://u:p@host:5432/db'); // postgres:// or postgresql://
472
+ parseRedisUrl('redis://localhost:6379'); // redis:// or rediss://
473
+ ```
474
+
475
+ ### Enum
476
+
477
+ ```typescript
478
+ import { parseEnum, createEnumParser } from '@spfn/core/env';
479
+
480
+ parseEnum('info', ['debug', 'info', 'warn', 'error']);
481
+ createEnumParser(['debug', 'info', 'warn', 'error'], true /* caseInsensitive */);
482
+ ```
483
+
484
+ > For schema fields prefer the `envEnum(allowed, options)` helper — it produces a precise
485
+ > string-literal union type. `parseEnum`/`createEnumParser` return a plain `string`.
486
+
487
+ ### JSON
488
+
489
+ ```typescript
490
+ import { parseJson, createJsonParser } from '@spfn/core/env';
491
+
492
+ parseJson('{"host":"localhost","port":3000}');
493
+ createJsonParser<{ host: string; port: number }>();
494
+ ```
495
+
496
+ ### Array
497
+
498
+ ```typescript
499
+ import { parseArray, createArrayParser, createNumberParser } from '@spfn/core/env';
500
+
501
+ parseArray('a,b,c'); // ['a','b','c']
502
+ parseArray('a|b|c', { separator: '|' }); // ['a','b','c'] (empty string → [])
503
+
504
+ createArrayParser(createNumberParser({ min: 1, max: 65535, integer: true }))('3000,4000');
505
+ // → [3000, 4000]
506
+ ```
507
+
508
+ ### Secrets & passwords
509
+
510
+ ```typescript
511
+ import { createSecureSecretParser, createPasswordParser } from '@spfn/core/env';
512
+
513
+ // Entropy-based secret check (Shannon entropy in bits/char)
514
+ createSecureSecretParser({ minLength: 32, minUniqueChars: 16, minEntropy: 3.5 });
515
+ // reference: random lowercase ~4.7 · alphanumeric ~5.2 · printable ASCII ~6.6 · "aaaa" ~0
516
+
517
+ createPasswordParser({
518
+ minLength: 12,
519
+ requireUppercase: true,
520
+ requireLowercase: true,
521
+ requireNumber: true,
522
+ requireSpecial: true,
523
+ });
524
+ ```
525
+
526
+ ### Composition
527
+
528
+ ```typescript
529
+ import { chain, withFallback, optional, parseString, createStringParser, parseRedisUrl, parseJson } from '@spfn/core/env';
530
+
531
+ chain(parseString, createStringParser({ minLength: 32 })); // run sequentially
532
+ withFallback(parseJson, { host: 'localhost' }); // return fallback if parser throws
533
+ optional(parseRedisUrl); // '' → undefined, else parse
534
+ ```
535
+
536
+ ---
537
+
538
+ ## Pitfalls & anti-patterns
539
+
540
+ - **Don't import `loadEnv` from `@spfn/core/env`.** It lives in `@spfn/core/env/loader`
541
+ (server-only, uses `node:fs`). Importing the loader into client/edge code breaks bundling.
542
+ - **`.env.server.local` does not exist.** The loader's 5 tiers are `.env`,
543
+ `.env.{NODE_ENV}`, `.env.local`, `.env.{NODE_ENV}.local`, `.env.server`. Do not invent a
544
+ `.env.server.local` file — it is never loaded.
545
+ - **`.env.server` is gitignored and Next.js-invisible by design.** Commit
546
+ `.env.server.example` as the template; never put secrets in `.env`/`.env.local`/committed
547
+ files.
548
+ - **Don't call `loadEnv()` inside Next.js.** Next.js already loads `.env*` (minus
549
+ `.env.server`). Calling `loadEnv` there is redundant and, with `server: true`, would pull
550
+ server secrets into the Next.js process — defeating the separation.
551
+ - **`validate()` is lazy.** It does not throw for missing required vars; the throw happens
552
+ on first property access. For an eager all-at-once check (CI/startup gate) use
553
+ `validateAllEnv([...])` / `registry.validateAll()`.
554
+ - **`envUrl` has no built-in validator.** Without `validator`, it only enforces presence —
555
+ add `parsePostgresUrl` / `createUrlParser('https')` etc. for actual URL validation.
556
+ - **`sensitive: true` + `NEXT_PUBLIC_` is wrong.** It only emits a warning, not an error —
557
+ the secret is still exposed. Remove the prefix.
558
+ - **`SKIP_ENV_VALIDATION` only skips the `required` check** (and only in the lazy proxy);
559
+ present values are still validated, and `validateAll()` ignores the flag entirely.
560
+ - **Removed API.** `getEnvVar`, `requireEnvVar`, `hasEnvVar`, `getEnvVars`,
561
+ `loadEnvironment`, `namespace`/`useFolderStructure`/`customPaths`/`useCache` loader
562
+ options, `env.get()`/`env.require()`, `getByCategory()`/`getAllSchemas()`,
563
+ `generateMarkdownDocs`/`generateEnvExample`/`generateJsonDocs`, and the `category` schema
564
+ option **do not exist**. Some validator JSDoc still shows `getEnvVar(...)` in examples —
565
+ that is stale; use the schema-`validator` pattern instead.
566
+
567
+ ---
568
+
569
+ ## Complete example
570
+
571
+ ```typescript
572
+ // src/config/env.ts
573
+ import {
574
+ defineEnvSchema,
575
+ envString, envNumber, envBoolean, envEnum, envUrl,
576
+ createEnvRegistry,
577
+ parsePostgresUrl, createNumberParser, createSecureSecretParser, createUrlParser,
578
+ } from '@spfn/core/env';
579
+
580
+ const schema = defineEnvSchema({
581
+ DATABASE_URL: envString({
582
+ description: 'PostgreSQL connection URL',
583
+ required: true,
584
+ sensitive: true,
585
+ nextjs: false,
586
+ validator: parsePostgresUrl,
587
+ }),
588
+ PORT: envNumber({
589
+ description: 'Server port',
590
+ default: 3000,
591
+ validator: createNumberParser({ min: 1, max: 65535, integer: true }),
592
+ }),
593
+ HOST: envString({ description: 'Server host', default: '0.0.0.0' }),
594
+ SESSION_SECRET: envString({
595
+ description: 'Session encryption secret',
596
+ required: true,
597
+ sensitive: true,
598
+ nextjs: false,
599
+ validator: createSecureSecretParser({ minLength: 32 }),
600
+ }),
601
+ NODE_ENV: envEnum(['local', 'development', 'staging', 'production', 'test'] as const, {
602
+ description: 'Node environment',
603
+ default: 'local',
604
+ }),
605
+ LOG_LEVEL: envEnum(['debug', 'info', 'warn', 'error', 'fatal'] as const, {
606
+ description: 'Log level',
607
+ default: 'info',
608
+ }),
609
+ REDIS_URL: envUrl({
610
+ description: 'Redis connection URL (optional)',
611
+ required: false,
612
+ validator: createUrlParser('any'),
613
+ }),
614
+ DEBUG: envBoolean({ description: 'Enable debug mode', default: false }),
615
+ });
616
+
617
+ const registry = createEnvRegistry(schema);
618
+ export const env = registry.validate();
619
+ export type Env = typeof env;
620
+ ```
621
+
622
+ ```typescript
623
+ // SPFN server entry point
624
+ import { loadEnv } from '@spfn/core/env/loader';
625
+ import { env } from '@/config/env';
626
+
627
+ loadEnv(); // populate process.env from .env* (server reads .env.server too)
628
+ console.log(env.PORT); // validated lazily on access
629
+ ```
630
+
631
+ ```typescript
632
+ // Next.js — no loadEnv()
633
+ import { env } from '@/config/env';
634
+ const dbUrl = env.DATABASE_URL; // (only resolves if DATABASE_URL is in the Next.js process)
635
+ ```
636
+
637
+ ---
638
+
639
+ ## Types reference
640
+
641
+ ```typescript
642
+ type NodeEnv = 'local' | 'development' | 'staging' | 'production' | 'test';
643
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'; // re-exported from logger
644
+ type Parser<T> = (value: string) => T;
645
+ ```
646
+
647
+ ## Related
648
+
649
+ - [@spfn/core/config](../config/README.md) — application configuration
650
+ - [@spfn/core/logger](../logger/README.md) — logging infrastructure
651
+ - [@spfn/core](../../README.md) — main package documentation