@spfn/core 0.3.0-beta.15 → 0.3.0-beta.17
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-C2KQCh0m.d.ts +473 -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 +2 -2
- package/src/config/README.md +20 -13
- package/src/env/README.md +137 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfn/core",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.17",
|
|
4
4
|
"description": "Full-stack TypeScript backend for Next.js: file-based typed routes, Drizzle entities and repositories, PostgreSQL transactions and a generated end-to-end client, in one fixed vertical slice per feature",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -209,7 +209,7 @@
|
|
|
209
209
|
"pg-boss": "^11.1.2",
|
|
210
210
|
"postgres": "^3.4.0",
|
|
211
211
|
"typescript": "^5.3.3",
|
|
212
|
-
"undici": "^7.29.
|
|
212
|
+
"undici": "^7.29.1",
|
|
213
213
|
"zod": "^4.4.3"
|
|
214
214
|
},
|
|
215
215
|
"optionalDependencies": {
|
package/src/config/README.md
CHANGED
|
@@ -81,13 +81,18 @@ env.SPFN_API_URL; // read + validated HERE; throws if missing (required) or inv
|
|
|
81
81
|
|
|
82
82
|
Consequences:
|
|
83
83
|
|
|
84
|
-
- A missing **required** variable (`SPFN_API_URL
|
|
84
|
+
- A missing **required** variable (`SPFN_API_URL`) does not
|
|
85
85
|
throw on import — it throws on first access (`Error('Environment validation failed')`).
|
|
86
86
|
- You may import `env` before `loadEnv()` runs; values are picked up lazily.
|
|
87
87
|
- For an **eager** all-at-once check (CI / startup gate), call `registry.validateAll()`
|
|
88
88
|
(returns `{ errors, warnings }`) or pass `registry` to `validateAllEnv([...])` from
|
|
89
89
|
`@spfn/core/env`. `SKIP_ENV_VALIDATION=true` skips only the lazy `required` check, not
|
|
90
90
|
these eager checks.
|
|
91
|
+
- **The SPFN server runs that eager check at boot.** `startServer` validates this
|
|
92
|
+
registry — plus any registries the app hands over with
|
|
93
|
+
`defineServerConfig().env({ registries })` — before it listens, and exits with code 1
|
|
94
|
+
listing each failing key (never a value). See
|
|
95
|
+
[Boot-time check](../env/README.md#boot-time-check-the-server).
|
|
91
96
|
|
|
92
97
|
See [@spfn/core/env](../env/README.md) for the full registry / proxy / `SKIP_ENV_VALIDATION`
|
|
93
98
|
semantics — this module inherits all of it unchanged.
|
|
@@ -221,24 +226,25 @@ validator **throw on access** if the value is present but malformed.
|
|
|
221
226
|
| Variable | Type | Required | Validator | Notes |
|
|
222
227
|
|----------|------|----------|-----------|-------|
|
|
223
228
|
| `SPFN_API_URL` | `string` (`envUrl`) | **Yes** | URL | `nextjs: true`; Next.js → backend |
|
|
224
|
-
| `NEXT_PUBLIC_SPFN_API_URL` | `string` (`envUrl`) |
|
|
225
|
-
| `SPFN_APP_URL` | `string` (`envUrl`) | No | URL | `nextjs: true`; SPFN server → Next.js |
|
|
229
|
+
| `NEXT_PUBLIC_SPFN_API_URL` | `string` (`envUrl`) | No | URL | `nextjs: true`; client-exposed, inlined by Next.js at build; the server uses `SPFN_API_URL` |
|
|
230
|
+
| `SPFN_APP_URL` | `string` (`envUrl`) | No (no default) | URL | `nextjs: true`; SPFN server → Next.js |
|
|
226
231
|
| `RPC_PROXY_TIMEOUT` | `number` | No (default `120000`) | — | `nextjs: true`; keep < `FETCH_HEADERS_TIMEOUT` |
|
|
227
232
|
|
|
228
|
-
> `SPFN_API_URL`
|
|
229
|
-
>
|
|
233
|
+
> `SPFN_API_URL` is **required** — a missing or non-URL value throws on first access.
|
|
234
|
+
> `NEXT_PUBLIC_SPFN_API_URL` is optional to the server, which falls back to `SPFN_API_URL`;
|
|
235
|
+
> set it wherever browser code needs the API URL. These three URLs are declared by
|
|
236
|
+
> `@spfn/core` only — packages read them from `@spfn/core/config` rather than declaring them again.
|
|
230
237
|
|
|
231
238
|
---
|
|
232
239
|
|
|
233
240
|
## Pitfalls & anti-patterns
|
|
234
241
|
|
|
235
242
|
- **Don't expect validation on import.** Importing `@spfn/core/config` validates nothing;
|
|
236
|
-
the proxy validates each variable on property access. A missing `SPFN_API_URL`
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
-
|
|
240
|
-
|
|
241
|
-
so a non-URL string fails too.
|
|
243
|
+
the proxy validates each variable on property access. A missing `SPFN_API_URL` throws
|
|
244
|
+
when you *read* it, not when you import `env`. The SPFN server does check it eagerly at
|
|
245
|
+
boot (`startServer` / `createServerlessApp`), so a server process needs it set.
|
|
246
|
+
- **The three URLs are validated as URLs (`envUrl`).** A non-URL string in `SPFN_API_URL`,
|
|
247
|
+
`NEXT_PUBLIC_SPFN_API_URL` or `SPFN_APP_URL` fails on access and at boot.
|
|
242
248
|
- **Don't call `loadEnv()` inside Next.js.** Next.js already loads `.env*`. Calling it there
|
|
243
249
|
(with `server: true`) would pull `.env.server` secrets into the Next.js process. In the
|
|
244
250
|
**SPFN server** entry point you *do* call `loadEnv()` before touching `env`.
|
|
@@ -265,7 +271,8 @@ import { env, registry } from '@spfn/core/config';
|
|
|
265
271
|
|
|
266
272
|
loadEnv(); // populate process.env from .env* (server includes .env.server)
|
|
267
273
|
|
|
268
|
-
//
|
|
274
|
+
// Eager gate for a custom entry point — startServer() already runs this check
|
|
275
|
+
// (see "Boot-time check" in the env README), so an SPFN server needs none of it.
|
|
269
276
|
const { errors, warnings } = registry.validateAll();
|
|
270
277
|
for (const w of warnings) console.warn(`${w.key}: ${w.message}`);
|
|
271
278
|
if (errors.length)
|
|
@@ -296,7 +303,7 @@ const apiUrl = env.SPFN_API_URL; // required URL — throws if unset/in
|
|
|
296
303
|
// Next.js — no loadEnv()
|
|
297
304
|
import { env } from '@spfn/core/config';
|
|
298
305
|
|
|
299
|
-
const apiUrl = env.NEXT_PUBLIC_SPFN_API_URL; //
|
|
306
|
+
const apiUrl = env.NEXT_PUBLIC_SPFN_API_URL; // optional URL, client-exposed
|
|
300
307
|
```
|
|
301
308
|
|
|
302
309
|
```typescript
|
package/src/env/README.md
CHANGED
|
@@ -160,6 +160,8 @@ CONFIG: envJson<{ host: string; port: number }>({
|
|
|
160
160
|
| `sensitive` | `boolean` | Marks a secret; triggers a warning if `NEXT_PUBLIC_*` |
|
|
161
161
|
| `examples` | `T[]` | Example values (metadata only) |
|
|
162
162
|
| `nextjs` | `boolean` | File-separation hint (see below). Defaults to `true` for `NEXT_PUBLIC_*`, else `false` |
|
|
163
|
+
| `layer` | `'environment' \| 'instance'` | Which deployment layer must supply the value (see below). Unset: may come from any layer |
|
|
164
|
+
| `readBy` | `string[]` | Files that read the name under a computed key, relative to the project root — for `spfn env audit` (see below) |
|
|
163
165
|
|
|
164
166
|
> There is **no** `category` option. Docs that show `category: '...'` are stale — passing
|
|
165
167
|
> it is harmless (excess property) but it does nothing.
|
|
@@ -249,6 +251,137 @@ for (const w of result.warnings) {
|
|
|
249
251
|
A single registry exposes the same via `registry.validateAll()` returning just
|
|
250
252
|
`{ errors, warnings }`.
|
|
251
253
|
|
|
254
|
+
Messages carry the key and the rule, never the value. For a `sensitive` key the validator's
|
|
255
|
+
own message is replaced by `(value hidden — sensitive key)`, because a validator may echo its
|
|
256
|
+
input; core's parsers describe the expected shape instead of quoting the input. An app
|
|
257
|
+
validator on a non-sensitive key should do the same.
|
|
258
|
+
|
|
259
|
+
### Boot-time check (the server)
|
|
260
|
+
|
|
261
|
+
`startServer()` runs `validateAllEnv` right after loading the env files and the app's
|
|
262
|
+
`server.config`, before it initializes infrastructure or listens. The list is `@spfn/core`'s
|
|
263
|
+
registry plus the registries the app hands over in `server.config`:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// src/server/config/env.config.ts
|
|
267
|
+
export const envSchema = defineEnvSchema({ /* ... */ });
|
|
268
|
+
export const envRegistry = createEnvRegistry(envSchema);
|
|
269
|
+
export const env = envRegistry.validate();
|
|
270
|
+
|
|
271
|
+
// src/server/server.config.ts
|
|
272
|
+
import { envRegistry } from '@/server/config/env.config';
|
|
273
|
+
|
|
274
|
+
export default defineServerConfig()
|
|
275
|
+
.env({ registries: [envRegistry] })
|
|
276
|
+
.build();
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
On failure it logs each key with its reason (never a value) and exits with code 1. A
|
|
280
|
+
package's variables are checked only when the app passes a registry for them —
|
|
281
|
+
`createEnvRegistry(envSchema)` over the schema the package exports from `./config`.
|
|
282
|
+
|
|
283
|
+
- `SKIP_ENV_VALIDATION` does not apply: it relaxes the lazy proxy for build steps, and a
|
|
284
|
+
server about to serve is not a build step.
|
|
285
|
+
- `createServerlessApp()` runs the same check and rejects its promise instead of exiting.
|
|
286
|
+
- `createServer()` (the app factory both of them use) and `provisionInfrastructure()` do not
|
|
287
|
+
run it.
|
|
288
|
+
- `@spfn/core` itself requires `SPFN_API_URL`, so the server process needs it set.
|
|
289
|
+
`NEXT_PUBLIC_SPFN_API_URL` is optional there: Next.js inlines it at build time and the
|
|
290
|
+
server falls back to `SPFN_API_URL`.
|
|
291
|
+
|
|
292
|
+
### The app's schema in the CLI (`spfn.config.js` `env.schemas`)
|
|
293
|
+
|
|
294
|
+
`spfn env` and `spfn secret` find a package's schema at `<package>/config`; an app's own
|
|
295
|
+
schema can live anywhere, so `spfn.config.js` names it:
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
// spfn.config.js
|
|
299
|
+
export default {
|
|
300
|
+
env: {
|
|
301
|
+
schemas: ['src/server/config/env.config.ts'], // modules exporting `envSchema`
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Paths are relative to the project root; TypeScript and tsconfig path aliases are supported.
|
|
307
|
+
Without `-p <package>`, the CLI reads these modules plus every installed `@spfn/*` package
|
|
308
|
+
whose `./config` exports `envSchema`, and groups its output by source. A key two sources
|
|
309
|
+
declare identically is listed once; two declarations that differ in type, `required` or
|
|
310
|
+
`sensitive` are an error naming the key and both sources — one variable has one owner, and
|
|
311
|
+
other packages read it from the owner's `env` instead of declaring it again.
|
|
312
|
+
|
|
313
|
+
### Auditing the code against the list (`env.audit`, `readBy`)
|
|
314
|
+
|
|
315
|
+
`spfn env audit` keeps the list and the code in step, and exits 1 on any finding. It prints
|
|
316
|
+
`file:line NAME reason` — names only, never values — for:
|
|
317
|
+
|
|
318
|
+
- **Direct reads.** `process.env.X`, `process.env['X']`, `process.env[expr]` and
|
|
319
|
+
`const { X } = process.env` (also through `process?.env` and `globalThis.process.env`).
|
|
320
|
+
Allowed in the modules named in `env.schemas`, in root-level `*.config.{js,cjs,mjs,ts}`
|
|
321
|
+
files and in files matching `env.audit.ignore`. `NODE_ENV` is allowed everywhere.
|
|
322
|
+
- **Unused declarations.** A name in the app's own schemas that no scanned file outside the
|
|
323
|
+
schema modules reads — as a property (`env.X`), a destructured key (`const { X } = env`)
|
|
324
|
+
or a string literal `'X'`. Names an installed `@spfn/*` package also declares are read
|
|
325
|
+
inside that package and are not checked. Without `env.schemas` this check is skipped
|
|
326
|
+
with a notice.
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
// spfn.config.js
|
|
330
|
+
export default {
|
|
331
|
+
env: {
|
|
332
|
+
schemas: ['src/server/config/env.config.ts'],
|
|
333
|
+
audit: {
|
|
334
|
+
include: ['src'], // default; directories or globs from the project root
|
|
335
|
+
ignore: ['src/**/*.test.ts'], // may read process.env; the names they read do not count
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`node_modules`, build output (`dist`, `build`, `out`, `.next`, …), `*.d.ts` files and
|
|
342
|
+
symbolic links are never scanned. Sources are parsed with TypeScript, so a mention in a
|
|
343
|
+
comment or a string is not a read.
|
|
344
|
+
|
|
345
|
+
A name the code only reaches under a computed key — ``env[`${provider}_API_KEY`]`` — is
|
|
346
|
+
invisible to the scan. List the files that read it in `readBy`:
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
const schema = defineEnvSchema({
|
|
350
|
+
PAYMENT_API_KEY: envSecret({ description: 'Payment provider key', readBy: ['src/server/providers.ts'] }),
|
|
351
|
+
});
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
The name passes when every listed file exists and is scanned; a listed file that does not
|
|
355
|
+
exist, or that the audit does not scan, is a finding. The runtime ignores `readBy`.
|
|
356
|
+
|
|
357
|
+
A name that appears only in a log message string counts as read — the string-literal rule
|
|
358
|
+
cannot tell a message from a lookup key.
|
|
359
|
+
|
|
360
|
+
### Deployment layers (`layer`)
|
|
361
|
+
|
|
362
|
+
A deployed environment can run more than one instance — `staging` as `blue` and `green`,
|
|
363
|
+
each with its own port, URLs and database. `layer` pins where a variable's value must come
|
|
364
|
+
from, so `spfn secret export` can assemble one instance's env file and refuse a value that
|
|
365
|
+
arrives from the wrong place. Unset, the value may come from any layer — a package schema
|
|
366
|
+
leaves it unset, so an app can keep a value such as a session secret shared or give each
|
|
367
|
+
instance its own:
|
|
368
|
+
|
|
369
|
+
| Layer | Holds | Source |
|
|
370
|
+
|-------|-------|--------|
|
|
371
|
+
| `environment` | Values every instance of the environment shares — third-party API keys | `secrets/<env>.enc.json` |
|
|
372
|
+
| `instance` | Values of one instance: its own secrets, or plain values the deploy script computes (port, URLs, database name) | `secrets/<env>.<instance>.enc.json`, or a `--with <file>` dotenv file |
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
const schema = defineEnvSchema({
|
|
376
|
+
PAYMENT_API_KEY: envSecret({ description: 'Payment provider key', required: true, layer: 'environment' }),
|
|
377
|
+
DB_NAME: envString({ description: 'Database of this instance', required: true, layer: 'instance' }),
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
A name declared `environment` found in an instance layer, a name declared `instance` found
|
|
382
|
+
in the environment file, or any name found in two layers fails the export. The runtime ignores
|
|
383
|
+
`layer`; it only steers the CLI.
|
|
384
|
+
|
|
252
385
|
### Fallback keys
|
|
253
386
|
|
|
254
387
|
```typescript
|
|
@@ -556,7 +689,10 @@ optional(parseRedisUrl); // '' → undefined,
|
|
|
556
689
|
- **`sensitive: true` + `NEXT_PUBLIC_` is wrong.** It only emits a warning, not an error —
|
|
557
690
|
the secret is still exposed. Remove the prefix.
|
|
558
691
|
- **`SKIP_ENV_VALIDATION` only skips the `required` check** (and only in the lazy proxy);
|
|
559
|
-
present values are still validated, and `validateAll()`
|
|
692
|
+
present values are still validated, and `validateAll()` — and with it the server's boot
|
|
693
|
+
check — ignores the flag entirely.
|
|
694
|
+
- **A validator message must not contain the value.** Validation errors are logged. Throw
|
|
695
|
+
`'Must be a valid number'`, not `` `got: ${value}` ``.
|
|
560
696
|
- **Removed API.** `getEnvVar`, `requireEnvVar`, `hasEnvVar`, `getEnvVars`,
|
|
561
697
|
`loadEnvironment`, `namespace`/`useFolderStructure`/`customPaths`/`useCache` loader
|
|
562
698
|
options, `env.get()`/`env.require()`, `getByCategory()`/`getAllSchemas()`,
|