spfn 0.2.0-beta.52 → 0.2.0-beta.54

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 CHANGED
@@ -27,7 +27,7 @@ Requirements: Node.js 18.18+, Next.js 15+ (App Router, `src/` dir), PostgreSQL (
27
27
  npx spfn@beta create my-app
28
28
  cd my-app
29
29
  docker compose up -d # Postgres + Redis
30
- cp .env.local.example .env.local
30
+ # .env.local & .env.server are generated — put server secrets in .env.server
31
31
  pnpm spfn:dev # Next.js :3790 + SPFN API :8790
32
32
 
33
33
  # Add SPFN to an existing Next.js project
@@ -212,6 +212,45 @@ Options: `-l, --list`, `-b, --bytes <n>` (1–128), `-e, --env <name>`, `-c, --c
212
212
  The command prints the value to stdout for you to paste into an env file — it does **not**
213
213
  write any file.
214
214
 
215
+ ### `spfn secret`
216
+
217
+ Unified secret management: local secrets live in the OS keychain, deployed secrets in
218
+ encrypted SOPS files. The runtime never sees a reference — `spfn dev` injects local
219
+ values into the server process and GitOps injects them in production, so the app always
220
+ reads plain `process.env`.
221
+
222
+ | Subcommand | Description |
223
+ |------------|-------------|
224
+ | `secret set [key]` | Store a value (masked prompt). `--env local` → keychain; other envs → SOPS |
225
+ | `secret list` | List declared secrets and their status per env (never prints values) |
226
+ | `secret generate [key]` | Mint values for schema secrets with a `generate` strategy (`-a/--all`) |
227
+ | `secret rotate [key]` | Rotate values; external secrets are flagged for manual reissue (`-a/--all`) |
228
+ | `secret keygen` | Generate an age key pair for the SOPS no-cloud backend |
229
+ | `secret recipients <add\|remove\|list> [age1…]` | Manage `.sops.yaml` recipients + re-encrypt |
230
+ | `secret check` | Static lint — flag plaintext secret leaks |
231
+
232
+ Options: `-e, --env <env>` (`local` default; also `development`/`staging`/`production`),
233
+ `-p, --package <pkg>` (schema source, default `@spfn/core`).
234
+
235
+ **Local (keychain).** `spfn secret set DB_URL` stores the value in the OS keychain
236
+ (macOS `security`, Windows Credential Manager via optional `@napi-rs/keyring`, Linux
237
+ libsecret) and writes a `secret:keychain:spfn_DB_URL` reference into `.env.server`. The
238
+ reference is not sensitive; the real value never lands in the repo. `spfn dev` resolves
239
+ and injects it. Note: injection happens only when the server is started via `spfn dev` —
240
+ running the app another way (a bare `node`, tests) would see the raw reference, so use
241
+ `spfn dev` locally (a runtime resolver for other runners is planned).
242
+
243
+ **Deployed (SOPS).** `spfn secret set DB_URL --env production` writes the value into
244
+ `secrets/production.enc.json`, encrypted by SOPS. The backend (age / GCP KMS / AWS KMS)
245
+ is chosen by `.sops.yaml` creation rules — KMS needs no local key file (IAM + cloud
246
+ auth), age is the no-cloud fallback (`secret keygen` + `secret recipients add`). Commit
247
+ the encrypted file; your GitOps step decrypts it into env at deploy time. `sops`/`age`
248
+ are needed only for the deployed envs, never for local keychain use.
249
+
250
+ Schema-driven: a secret declared with `envSecret({ generate: 'base64url32' })` can be
251
+ minted/rotated automatically (`secret generate`/`rotate`); one without `generate` is an
252
+ external value you paste in (`secret set`).
253
+
215
254
  ### `spfn setup icons`
216
255
 
217
256
  Install and configure SVGR for SVG-as-component imports (Next.js only).
@@ -241,9 +280,9 @@ spfn.config.js # deployment config (subdomain/region/domai
241
280
  docker-compose.yml # Postgres + Redis (dev)
242
281
  docker-compose.production.yml
243
282
  Dockerfile, .dockerignore
244
- .env.example # committed, shared non-secret defaults
245
- .env.local.example # gitignored target template (Next.js local overrides)
246
- .env.server.example # copy to .env.server (gitignored, server secrets)
283
+ .env.example # committed reference — every key, placeholder values
284
+ .env.local # generated, gitignored (Next.js-facing URLs)
285
+ .env.server # generated, gitignored (server secrets: DB, cache)
247
286
  ```
248
287
 
249
288
  `init` also patches `package.json` (scripts: `spfn:dev`, `spfn:server`, `spfn:next`,
@@ -328,8 +367,8 @@ type ships from `spfn` (`@type {import('spfn').SpfnConfig}`).
328
367
 
329
368
  - **`.env.server` is gitignored and server-only.** Put DB/secret values there, not in
330
369
  `.env` (committed) and not in `.env.local` (that's Next.js's local file). There is no
331
- `.env.server.local`. Copy `.env.server.example` `.env.server`. Load order is the
332
- standard dotenv chain ending with `.env.server`.
370
+ `.env.server.local`. `spfn init` generates `.env.server`; put DB/secret values there.
371
+ Load order is the standard dotenv chain ending with `.env.server`.
333
372
  - **Never commit secrets in `spfn.config.js`.** It's checked into Git; its `env` block is
334
373
  for non-sensitive values only. Use CI/CD secret management for credentials.
335
374
  - **`spfn dev` does not hot-reload by default.** Add `--watch` to restart on `src/server`
package/bin/spfn.js CHANGED
@@ -28,13 +28,20 @@ else
28
28
 
29
29
  async function tryRelaunchWithTsx()
30
30
  {
31
- // Verify tsx is resolvable
32
- await import('tsx/esm/api');
31
+ // Resolve tsx to an absolute URL from THIS package's location. A bare
32
+ // '--import tsx' is resolved by Node against the child's CWD, so running
33
+ // outside a project (no node_modules/tsx up the tree) crashes the child
34
+ // with ERR_MODULE_NOT_FOUND even though the guard import above succeeds
35
+ // (it resolves package-relative). Resolving here also doubles as the
36
+ // availability check — a throw falls back to running without tsx.
37
+ const { createRequire } = await import('module');
38
+ const { pathToFileURL } = await import('url');
39
+ const tsxUrl = pathToFileURL(createRequire(import.meta.url).resolve(TSX_MODULE)).href;
33
40
 
34
41
  const { spawn } = await import('child_process');
35
42
  const child = spawn(
36
43
  process.execPath,
37
- [TSX_FLAG, TSX_MODULE, ...process.execArgv, process.argv[1], ...process.argv.slice(2)],
44
+ [TSX_FLAG, tsxUrl, ...process.execArgv, process.argv[1], ...process.argv.slice(2)],
38
45
  { stdio: 'inherit' },
39
46
  );
40
47