spfn 0.3.0-beta.13 → 0.3.0-beta.14

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 (3) hide show
  1. package/README.md +12 -8
  2. package/dist/index.js +15 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -495,7 +495,7 @@ reads plain `process.env`.
495
495
  | `secret rotate [key]` | Rotate values; external secrets are flagged for manual reissue (`-a/--all`) |
496
496
  | `secret keygen` | Generate an age key pair for the SOPS no-cloud backend |
497
497
  | `secret recipients <add\|remove\|list> [age1…]` | Manage `.sops.yaml` recipients + re-encrypt |
498
- | `secret check` | Static lint — flag plaintext secret leaks; with a deployed `--env`, names in the wrong layer's file |
498
+ | `secret check` | Static lint — flag plaintext secret leaks; with a deployed `--env`, names outside their declared layer or in both files |
499
499
  | `secret export` | Decrypt and merge one deployment's layers into a 0600 dotenv file (prints names only) |
500
500
 
501
501
  Options: `-e, --env <env>` (`local` default; also `development`/`staging`/`production`),
@@ -508,11 +508,15 @@ whole of stdin as the value — one trailing newline dropped, inner newlines kep
508
508
  local and deployed targets alike. It never prompts, and refuses a terminal on stdin.
509
509
  Shaped values such as key pairs come from the app's own command, piped in.
510
510
 
511
- **Layers.** A schema entry's `layer` (`'environment'` default, or `'instance'`) says where a
512
- deployment's value comes from. Without `--instance`, `set`/`generate`/`rotate`/`list` on a
513
- deployed environment work on the `environment` names in `secrets/<env>.enc.json`; with
514
- `--instance blue`, on the `instance` names in `secrets/<env>.blue.enc.json`. A named key of
515
- the other layer is refused.
511
+ **Layers.** A schema entry's `layer` (`'environment'` or `'instance'`) pins where a
512
+ deployment's value must live; unset, the value may come from any layer. Without
513
+ `--instance`, `set`/`generate`/`rotate`/`list` on a deployed environment work on
514
+ `secrets/<env>.enc.json`; with `--instance blue`, on `secrets/<env>.blue.enc.json`. A named
515
+ key declared with the other layer is refused; a key without `layer` is accepted by either.
516
+ `--all` (and `list`) takes the names declared with the target's layer, and names without
517
+ `layer` on the environment target only — so `--all --instance blue` never fills an
518
+ instance file with names every instance shares. To give each instance its own value of
519
+ such a name, set it by name with `--instance`.
516
520
 
517
521
  **Export.**
518
522
 
@@ -525,8 +529,8 @@ Decrypts `secrets/<env>.enc.json` and — with `--instance`, when it exists —
525
529
  instance values such as port and URLs), and writes the merge to `--out`. Only names in the
526
530
  whole-app list are written; an unlisted name in an encrypted file is a warning by name.
527
531
  The export fails, writing nothing, when a required name is in no layer, a name is in two
528
- layers, an `instance` name is in the environment file, an `environment` name is in an
529
- instance layer, a value fails its validator (the error names the variable and the
532
+ layers, a name declared `instance` is in the environment file, a name declared
533
+ `environment` is in an instance layer, a value fails its validator (the error names the variable and the
530
534
  reason, never the value), or decryption fails. `--out` is required and its directory must
531
535
  exist; relative paths resolve from the working directory. The file is created 0600 beside
532
536
  the destination and renamed over it, so a failure leaves the previous file untouched
package/dist/index.js CHANGED
@@ -949,7 +949,7 @@ import { dirname as dirname2, join as join10 } from "path";
949
949
  import { fileURLToPath as fileURLToPath2 } from "url";
950
950
  function getCliVersion() {
951
951
  if (true) {
952
- return "0.3.0-beta.13";
952
+ return "0.3.0-beta.14";
953
953
  }
954
954
  return fromPackageJson ?? (fromPackageJson = readPackageVersion());
955
955
  }
@@ -5375,8 +5375,8 @@ function secretEntries(schema) {
5375
5375
  function generatableSecrets(schema) {
5376
5376
  return secretEntries(schema).filter((entry) => !!entry.generate);
5377
5377
  }
5378
- function layerOf(entry) {
5379
- return entry.layer ?? "environment";
5378
+ function allowsLayer(entry, layer) {
5379
+ return entry.layer === void 0 || entry.layer === layer;
5380
5380
  }
5381
5381
 
5382
5382
  // src/utils/env-list.ts
@@ -5432,7 +5432,7 @@ function describeEnvList(options) {
5432
5432
  return options.package ?? "whole app";
5433
5433
  }
5434
5434
  function describeRules(entry) {
5435
- return `"${entry.type}" (${entry.required ? "required" : "optional"}${entry.sensitive ? ", sensitive" : ""}${layerOf(entry) === "instance" ? ", instance layer" : ""})`;
5435
+ return `"${entry.type}" (${entry.required ? "required" : "optional"}${entry.sensitive ? ", sensitive" : ""}${entry.layer ? `, ${entry.layer} layer` : ""})`;
5436
5436
  }
5437
5437
  async function loadAppSchemas(cwd) {
5438
5438
  const paths = (await loadAppConfig3(cwd)).env?.schemas ?? [];
@@ -7514,15 +7514,15 @@ function targetLayer(target) {
7514
7514
  }
7515
7515
  function entriesForTarget(entries, target) {
7516
7516
  const layer = targetLayer(target);
7517
- return layer ? entries.filter((entry) => layerOf(entry) === layer) : entries;
7517
+ return layer ? entries.filter((entry) => (entry.layer ?? "environment") === layer) : entries;
7518
7518
  }
7519
7519
  function assertTargetLayer(entry, target) {
7520
7520
  const layer = targetLayer(target);
7521
- if (!entry || !layer || layerOf(entry) === layer) {
7521
+ if (!entry || !layer || allowsLayer(entry, layer)) {
7522
7522
  return;
7523
7523
  }
7524
7524
  const hint = layer === "environment" ? "pass --instance <name>" : "drop --instance";
7525
- logger.error(`${entry.key} is declared with layer "${layerOf(entry)}" \u2014 ${hint}.`);
7525
+ logger.error(`${entry.key} is declared with layer "${entry.layer}" \u2014 ${hint}.`);
7526
7526
  process.exit(1);
7527
7527
  }
7528
7528
 
@@ -7946,6 +7946,7 @@ async function secretCheck(options) {
7946
7946
  }
7947
7947
  function checkPlacement(cwd, schema, target, issues, warnings) {
7948
7948
  const files = [{ file: getSopsFile(cwd, target.env), layer: "environment" }];
7949
+ const seenIn = /* @__PURE__ */ new Map();
7949
7950
  if (target.instance) {
7950
7951
  files.push({ file: getSopsFile(cwd, target.env, target.instance), layer: "instance" });
7951
7952
  }
@@ -7954,9 +7955,12 @@ function checkPlacement(cwd, schema, target, issues, warnings) {
7954
7955
  const entry = Object.hasOwn(schema, name) ? schema[name] : void 0;
7955
7956
  if (!entry) {
7956
7957
  warnings.push(`${chalk43.cyan(name)} in ${chalk43.yellow(file.relFile)} is not in the env list \u2014 export skips it.`);
7957
- } else if (layerOf(entry) !== layer) {
7958
- issues.push(`${chalk43.cyan(name)} is declared with layer "${layerOf(entry)}" but is in ${chalk43.yellow(file.relFile)}.`);
7958
+ } else if (!allowsLayer(entry, layer)) {
7959
+ issues.push(`${chalk43.cyan(name)} is declared with layer "${entry.layer}" but is in ${chalk43.yellow(file.relFile)}.`);
7960
+ } else if (seenIn.has(name)) {
7961
+ issues.push(`${chalk43.cyan(name)} is in both ${chalk43.yellow(seenIn.get(name))} and ${chalk43.yellow(file.relFile)}.`);
7959
7962
  }
7963
+ seenIn.set(name, file.relFile);
7960
7964
  }
7961
7965
  }
7962
7966
  }
@@ -8023,8 +8027,8 @@ function placementError(entry, found) {
8023
8027
  if (found.length > 1) {
8024
8028
  return `in more than one layer: ${found.map((source) => source.label).join(" and ")}`;
8025
8029
  }
8026
- if (found[0].kind !== layerOf(entry)) {
8027
- return `declared with layer "${layerOf(entry)}", but found in ${found[0].label}`;
8030
+ if (!allowsLayer(entry, found[0].kind)) {
8031
+ return `declared with layer "${entry.layer}", but found in ${found[0].label}`;
8028
8032
  }
8029
8033
  return void 0;
8030
8034
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spfn",
3
- "version": "0.3.0-beta.13",
3
+ "version": "0.3.0-beta.14",
4
4
  "description": "Scaffold a full-stack TypeScript backend onto a Next.js app built with an AI coding agent: auth, database, typed routes and codegen, one fixed vertical slice per feature",
5
5
  "type": "module",
6
6
  "bin": {