@smoothbricks/cli 0.11.18 → 0.11.20

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 (96) hide show
  1. package/README.md +91 -11
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +28 -4
  4. package/dist/github-ci/index.d.ts.map +1 -1
  5. package/dist/github-ci/index.js +6 -20
  6. package/dist/lib/deploy-tags.d.ts +17 -3
  7. package/dist/lib/deploy-tags.d.ts.map +1 -1
  8. package/dist/lib/deploy-tags.js +23 -4
  9. package/dist/lib/json.d.ts +22 -1
  10. package/dist/lib/json.d.ts.map +1 -1
  11. package/dist/lib/json.js +15 -3
  12. package/dist/monorepo/cargo-policy.d.ts +17 -0
  13. package/dist/monorepo/cargo-policy.d.ts.map +1 -1
  14. package/dist/monorepo/cargo-policy.js +73 -1
  15. package/dist/monorepo/index.d.ts.map +1 -1
  16. package/dist/monorepo/index.js +4 -2
  17. package/dist/monorepo/managed-files.d.ts +5 -2
  18. package/dist/monorepo/managed-files.d.ts.map +1 -1
  19. package/dist/monorepo/managed-files.js +35 -21
  20. package/dist/monorepo/packs/index.d.ts.map +1 -1
  21. package/dist/monorepo/packs/index.js +6 -1
  22. package/dist/nx/index.d.ts +6 -0
  23. package/dist/nx/index.d.ts.map +1 -1
  24. package/dist/nx/index.js +14 -0
  25. package/dist/secrets/commands.d.ts +9 -5
  26. package/dist/secrets/commands.d.ts.map +1 -1
  27. package/dist/secrets/commands.js +122 -49
  28. package/dist/secrets/index.d.ts +42 -56
  29. package/dist/secrets/index.d.ts.map +1 -1
  30. package/dist/secrets/index.js +52 -79
  31. package/dist/secrets/resolver.d.ts +59 -0
  32. package/dist/secrets/resolver.d.ts.map +1 -0
  33. package/dist/secrets/resolver.js +100 -0
  34. package/dist/secrets/run.d.ts +23 -0
  35. package/dist/secrets/run.d.ts.map +1 -0
  36. package/dist/secrets/run.js +130 -0
  37. package/dist/secrets/status.d.ts +177 -0
  38. package/dist/secrets/status.d.ts.map +1 -0
  39. package/dist/secrets/status.js +724 -0
  40. package/dist/wrangler/deploy-stage.d.ts +1 -1
  41. package/dist/wrangler/deploy-stage.d.ts.map +1 -1
  42. package/dist/wrangler/deploy-stage.js +22 -20
  43. package/dist/wrangler/deployed-version.d.ts.map +1 -1
  44. package/dist/wrangler/deployed-version.js +7 -12
  45. package/dist/wrangler/flat-config.d.ts +1 -4
  46. package/dist/wrangler/flat-config.d.ts.map +1 -1
  47. package/dist/wrangler/flat-config.js +98 -44
  48. package/dist/wrangler/prepare-env.d.ts +4 -3
  49. package/dist/wrangler/prepare-env.d.ts.map +1 -1
  50. package/dist/wrangler/prepare-env.js +7 -5
  51. package/dist/wrangler/source-config.d.ts +24 -0
  52. package/dist/wrangler/source-config.d.ts.map +1 -0
  53. package/dist/wrangler/source-config.js +110 -0
  54. package/dist/wrangler/stage.d.ts +55 -23
  55. package/dist/wrangler/stage.d.ts.map +1 -1
  56. package/dist/wrangler/stage.js +81 -162
  57. package/managed/raw/tooling/direnv/devenv.smoo.nix +19 -3
  58. package/managed/raw/tooling/direnv/secret-references.ts +280 -76
  59. package/managed/raw/tooling/direnv/setup-environment.ts +58 -2
  60. package/managed/raw/tsconfig.lib.json +28 -0
  61. package/package.json +7 -2
  62. package/src/cli.ts +34 -5
  63. package/src/github-ci/index.test.ts +6 -7
  64. package/src/github-ci/index.ts +11 -19
  65. package/src/lib/deploy-tags.ts +22 -4
  66. package/src/lib/json.ts +23 -1
  67. package/src/monorepo/cargo-policy.test.ts +91 -1
  68. package/src/monorepo/cargo-policy.ts +87 -1
  69. package/src/monorepo/index.ts +8 -2
  70. package/src/monorepo/managed-files.test.ts +69 -37
  71. package/src/monorepo/managed-files.ts +38 -21
  72. package/src/monorepo/packs/index.ts +10 -1
  73. package/src/monorepo/secret-references.test.ts +412 -6
  74. package/src/monorepo/setup-environment.test.ts +181 -0
  75. package/src/nx/index.test.ts +8 -0
  76. package/src/nx/index.ts +20 -0
  77. package/src/secrets/commands.test.ts +186 -4
  78. package/src/secrets/commands.ts +142 -51
  79. package/src/secrets/index.test.ts +4 -10
  80. package/src/secrets/index.ts +54 -115
  81. package/src/secrets/resolver.ts +130 -0
  82. package/src/secrets/run.test.ts +359 -0
  83. package/src/secrets/run.ts +148 -0
  84. package/src/secrets/status.test.ts +164 -0
  85. package/src/secrets/status.ts +297 -0
  86. package/src/wrangler/deploy-stage.test.ts +95 -2
  87. package/src/wrangler/deploy-stage.ts +26 -23
  88. package/src/wrangler/deployed-version.ts +7 -11
  89. package/src/wrangler/flat-config.test.ts +9 -4
  90. package/src/wrangler/flat-config.ts +1 -20
  91. package/src/wrangler/format-parity.test.ts +433 -0
  92. package/src/wrangler/prepare-env.ts +7 -5
  93. package/src/wrangler/source-config.test.ts +102 -0
  94. package/src/wrangler/source-config.ts +110 -0
  95. package/src/wrangler/stage.test.ts +61 -32
  96. package/src/wrangler/stage.ts +124 -173
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Reconciles the three places a repository's secrets are described, all of
3
- * which smoo already owns:
2
+ * Reads the three places a repository's secrets are described, all of which
3
+ * smoo already owns:
4
4
  *
5
5
  * 1. **Workers declare names.** `.dev.vars.example` per wrangler project — the
6
- * file `wrangler types --env-file` reads and `prepare-env` prompts from.
6
+ * file `wrangler types --env-file` reads and `prepare-env` prompts from
7
+ * and `smoo.wrangler.secretStages` beside it says which stages each name
8
+ * belongs to.
7
9
  * 2. **Workflows declare where values come from.** `smoo.github.deploySecrets`
8
10
  * and `e2eSecrets` map an env name to a repository secret, and the managed
9
11
  * workflows render exactly those into the job environment.
@@ -17,58 +19,12 @@
17
19
  * `STRIPE_PUBLISHABLE_KEY`, `ci.yml` passes `secrets.STRIPE_PUBLISHABLE_KEY`,
18
20
  * the repository holds no such secret, and the empty value surfaces as the
19
21
  * payment library's own `publishable_key_mismatch` refusal.
22
+ *
23
+ * The join itself is ./status.ts: pure, and the owner of the document type
24
+ * these readings are projected into.
20
25
  */
21
- /** Where an env name is described, which repository secret carries it, and whether that exists. */
22
- export interface SecretRow {
23
- name: string;
24
- /** The repository secret this env name reads from, by convention or declaration. */
25
- repositorySecret: string;
26
- /** Wrangler projects whose `.dev.vars.example` declares it. */
27
- declaredByWorkers: string[];
28
- /** True when a managed workflow renders `secrets.<name>` into a job. */
29
- suppliedByWorkflow: boolean;
30
- /** True when `smoo.secrets` can fetch it for a developer shell. */
31
- fetchableLocally: boolean;
32
- /** True when the repository holds a secret of this name. */
33
- onRepository: boolean;
34
- /**
35
- * GitHub Environments holding their own value for this name. A job bound to
36
- * an environment reads that value in preference to the repository's, which
37
- * is how one name carries test credentials on a preview stage and live ones
38
- * in production.
39
- */
40
- heldByEnvironment: string[];
41
- }
42
- export interface SecretSources {
43
- /** Worker label -> env names it declares. */
44
- workerSecrets: Record<string, readonly string[]>;
45
- /** Env names a managed workflow passes into a job. */
46
- workflowSecrets: readonly string[];
47
- /** Env name -> repository secret, by convention with declared exceptions. */
48
- secretNames: Readonly<Record<string, string>>;
49
- /** Env names `smoo.secrets` can fetch locally. */
50
- localCommands: readonly string[];
51
- /** Repository secret names GitHub currently holds. */
52
- repositorySecrets: readonly string[];
53
- /** Environment name -> secret names that environment holds. */
54
- environmentSecrets?: Readonly<Record<string, readonly string[]>>;
55
- }
56
- /**
57
- * One row per name known to any source, sorted, so a reader sees the whole
58
- * picture rather than one source's view of it.
59
- */
60
- export declare function reconcileSecrets(sources: SecretSources): SecretRow[];
61
- /**
62
- * The rows a CI deploy cannot satisfy: a Worker declares the name, a workflow
63
- * promises to pass it, and the repository has no value to pass. Reported
64
- * separately from "declared but not wired into any workflow", because the
65
- * remedies differ — set a secret, versus declare it in `smoo.github`.
66
- */
67
- export declare function unsatisfiedSecrets(rows: readonly SecretRow[], boundEnvironments?: readonly string[]): SecretRow[];
68
- /** Bound environments that lack their own value and cannot fall back to the repository. */
69
- export declare function environmentsMissing(row: SecretRow, boundEnvironments: readonly string[]): string[];
70
- /** Worker-declared names no managed workflow passes: a CI deploy will run without them. */
71
- export declare function unwiredSecrets(rows: readonly SecretRow[]): SecretRow[];
26
+ import { type SecretStageMap } from '../wrangler/stage-secrets.js';
27
+ import { type GroupedSecret } from './resolver.js';
72
28
  /** Env names the managed workflows pass into a job, from the declarations that render them. */
73
29
  export declare function workflowSecretNames(root: string): string[];
74
30
  /**
@@ -87,8 +43,38 @@ export declare function workflowEnvironments(root: string): string[];
87
43
  export declare function secretNameMapping(root: string, envNames: readonly string[]): Record<string, string>;
88
44
  /** Every wrangler project's declared secret names, keyed by the project directory. */
89
45
  export declare function workerSecretNames(root: string, workspaceDirs: readonly string[]): Record<string, string[]>;
90
- /** Names `smoo.secrets` declares a fetch command for. */
91
- export declare function localSecretCommandNames(root: string): string[];
46
+ /**
47
+ * Every wrangler project's `smoo.wrangler.secretStages`, keyed by the project
48
+ * directory — the same keys `workerSecretNames` uses, so a name's scopes and
49
+ * its declarations are joined on one label.
50
+ *
51
+ * A malformed block is a refusal rather than a throw: it is the same class of
52
+ * problem as a repository `gh` will not talk to, and `smoo secrets status`
53
+ * reports those instead of dying with a stack. Silently reading it as "no
54
+ * scopes" is the one thing this must not do — that is the direction where
55
+ * every secret quietly reaches every stage.
56
+ */
57
+ export declare function workerSecretStages(root: string, workspaceDirs: readonly string[]): {
58
+ ok: true;
59
+ byWorker: Record<string, SecretStageMap>;
60
+ } | {
61
+ ok: false;
62
+ reason: string;
63
+ };
64
+ /**
65
+ * Every `smoo.secrets` entry with the group that resolves it, sorted by name,
66
+ * or a refusal naming the declaration that could not be read. The groups come
67
+ * from ./resolver.ts — the same file shell entry routes with — so a status
68
+ * table and a `smoo secrets run` cannot disagree about which group resolves
69
+ * a name.
70
+ */
71
+ export declare function localSecretGroups(root: string): Promise<{
72
+ ok: true;
73
+ secrets: GroupedSecret[];
74
+ } | {
75
+ ok: false;
76
+ reason: string;
77
+ }>;
92
78
  /**
93
79
  * Resolve one declared secret through its command. The value is returned, never
94
80
  * logged: callers hand it to `gh secret set` over stdin.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,gBAAgB,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,wEAAwE;IACxE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mEAAmE;IACnE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;OAKG;IACH,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IACjD,sDAAsD;IACtD,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,6EAA6E;IAC7E,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,kDAAkD;IAClD,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,sDAAsD;IACtD,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,+DAA+D;IAC/D,kBAAkB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC,CAAC;CAClE;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,EAAE,CAoCpE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,EAAE,iBAAiB,GAAE,SAAS,MAAM,EAAO,GAAG,SAAS,EAAE,CAUrH;AAED,2FAA2F;AAC3F,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,EAAE,iBAAiB,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAGlG;AAED,2FAA2F;AAC3F,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,EAAE,CAEtE;AAED,+FAA+F;AAC/F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAkB1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAW3D;AAoDD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMnG;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAS1G;AAED,yDAAyD;AACzD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAG9D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAqB7D"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACvF,OAAO,EAAE,KAAK,aAAa,EAAoB,MAAM,eAAe,CAAC;AAErE,+FAA+F;AAC/F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAkB1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAW3D;AAoDD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMnG;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAS1G;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,SAAS,MAAM,EAAE,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAYxF;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAOjF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAqB7D"}
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Reconciles the three places a repository's secrets are described, all of
3
- * which smoo already owns:
2
+ * Reads the three places a repository's secrets are described, all of which
3
+ * smoo already owns:
4
4
  *
5
5
  * 1. **Workers declare names.** `.dev.vars.example` per wrangler project — the
6
- * file `wrangler types --env-file` reads and `prepare-env` prompts from.
6
+ * file `wrangler types --env-file` reads and `prepare-env` prompts from
7
+ * and `smoo.wrangler.secretStages` beside it says which stages each name
8
+ * belongs to.
7
9
  * 2. **Workflows declare where values come from.** `smoo.github.deploySecrets`
8
10
  * and `e2eSecrets` map an env name to a repository secret, and the managed
9
11
  * workflows render exactly those into the job environment.
@@ -17,6 +19,9 @@
17
19
  * `STRIPE_PUBLISHABLE_KEY`, `ci.yml` passes `secrets.STRIPE_PUBLISHABLE_KEY`,
18
20
  * the repository holds no such secret, and the empty value surfaces as the
19
21
  * payment library's own `publishable_key_mismatch` refusal.
22
+ *
23
+ * The join itself is ./status.ts: pure, and the owner of the document type
24
+ * these readings are projected into.
20
25
  */
21
26
  import { spawnSync } from 'node:child_process';
22
27
  import { existsSync, readdirSync, readFileSync } from 'node:fs';
@@ -24,78 +29,8 @@ import { join } from 'node:path';
24
29
  import { repositoryOwnerFromUrl, repositorySecretMapping } from '../lib/secret-names.js';
25
30
  import { readPackageJsonObject, repositoryInfo } from '../lib/workspace.js';
26
31
  import { parseDevVarsExample } from '../wrangler/prepare-env.js';
27
- /**
28
- * One row per name known to any source, sorted, so a reader sees the whole
29
- * picture rather than one source's view of it.
30
- */
31
- export function reconcileSecrets(sources) {
32
- const declaredByAnyWorker = Object.values(sources.workerSecrets).flatMap((names) => [...names]);
33
- const names = new Set([...declaredByAnyWorker, ...sources.workflowSecrets, ...sources.localCommands]);
34
- // A repository secret that already carries a known env name is that name's
35
- // row, not a row of its own: listing ACME_GITHUB_CLIENT_SECRET beside
36
- // GITHUB_CLIENT_SECRET would report one value as two secrets, one of them
37
- // permanently "declared by nothing".
38
- const carriesKnownEnvName = new Set(names.size > 0 ? [...names].map((name) => sources.secretNames[name] ?? name) : []);
39
- // A value only an environment holds is still a value: leaving it out of the
40
- // rows is how an operator ends up hunting for a secret that is already set.
41
- const addUndeclared = (secret) => {
42
- if (!carriesKnownEnvName.has(secret))
43
- names.add(secret);
44
- };
45
- for (const secret of sources.repositorySecrets)
46
- addUndeclared(secret);
47
- for (const held of Object.values(sources.environmentSecrets ?? {})) {
48
- for (const secret of held)
49
- addUndeclared(secret);
50
- }
51
- return [...names]
52
- .sort((left, right) => left.localeCompare(right))
53
- .map((name) => ({
54
- name,
55
- repositorySecret: sources.secretNames[name] ?? name,
56
- declaredByWorkers: Object.entries(sources.workerSecrets)
57
- .filter(([, declared]) => declared.includes(name))
58
- .map(([label]) => label)
59
- .sort((left, right) => left.localeCompare(right)),
60
- suppliedByWorkflow: sources.workflowSecrets.includes(name),
61
- fetchableLocally: sources.localCommands.includes(name),
62
- onRepository: sources.repositorySecrets.includes(sources.secretNames[name] ?? name),
63
- heldByEnvironment: Object.entries(sources.environmentSecrets ?? {})
64
- .filter(([, held]) => held.includes(sources.secretNames[name] ?? name))
65
- .map(([environment]) => environment)
66
- .sort((left, right) => left.localeCompare(right)),
67
- }));
68
- }
69
- /**
70
- * The rows a CI deploy cannot satisfy: a Worker declares the name, a workflow
71
- * promises to pass it, and the repository has no value to pass. Reported
72
- * separately from "declared but not wired into any workflow", because the
73
- * remedies differ — set a secret, versus declare it in `smoo.github`.
74
- */
75
- export function unsatisfiedSecrets(rows, boundEnvironments = []) {
76
- return rows.filter((row) => {
77
- if (!row.suppliedByWorkflow)
78
- return false;
79
- if (row.onRepository)
80
- return false;
81
- // With no environment bound, the repository is the only scope a job reads.
82
- // With environments bound, each one can carry the value instead - so the
83
- // name is satisfied only when every bound environment holds it.
84
- if (boundEnvironments.length === 0)
85
- return true;
86
- return !boundEnvironments.every((environment) => row.heldByEnvironment.includes(environment));
87
- });
88
- }
89
- /** Bound environments that lack their own value and cannot fall back to the repository. */
90
- export function environmentsMissing(row, boundEnvironments) {
91
- if (row.onRepository)
92
- return [];
93
- return boundEnvironments.filter((environment) => !row.heldByEnvironment.includes(environment));
94
- }
95
- /** Worker-declared names no managed workflow passes: a CI deploy will run without them. */
96
- export function unwiredSecrets(rows) {
97
- return rows.filter((row) => row.declaredByWorkers.length > 0 && !row.suppliedByWorkflow);
98
- }
32
+ import { readSecretStageMap } from '../wrangler/stage-secrets.js';
33
+ import { readSecretGroups } from './resolver.js';
99
34
  /** Env names the managed workflows pass into a job, from the declarations that render them. */
100
35
  export function workflowSecretNames(root) {
101
36
  const manifest = readPackageJsonObject(join(root, 'package.json'));
@@ -222,10 +157,48 @@ export function workerSecretNames(root, workspaceDirs) {
222
157
  }
223
158
  return byWorker;
224
159
  }
225
- /** Names `smoo.secrets` declares a fetch command for. */
226
- export function localSecretCommandNames(root) {
227
- const manifest = readPackageJsonObject(join(root, 'package.json'));
228
- return Object.keys(manifest?.smoo?.secrets ?? {}).sort((left, right) => left.localeCompare(right));
160
+ /**
161
+ * Every wrangler project's `smoo.wrangler.secretStages`, keyed by the project
162
+ * directory — the same keys `workerSecretNames` uses, so a name's scopes and
163
+ * its declarations are joined on one label.
164
+ *
165
+ * A malformed block is a refusal rather than a throw: it is the same class of
166
+ * problem as a repository `gh` will not talk to, and `smoo secrets status`
167
+ * reports those instead of dying with a stack. Silently reading it as "no
168
+ * scopes" is the one thing this must not do — that is the direction where
169
+ * every secret quietly reaches every stage.
170
+ */
171
+ export function workerSecretStages(root, workspaceDirs) {
172
+ const byWorker = {};
173
+ for (const dir of workspaceDirs) {
174
+ if (!existsSync(join(root, dir, '.dev.vars.example')))
175
+ continue;
176
+ try {
177
+ const scopes = readSecretStageMap(join(root, dir));
178
+ if (Object.keys(scopes).length > 0)
179
+ byWorker[dir] = scopes;
180
+ }
181
+ catch (error) {
182
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
183
+ }
184
+ }
185
+ return { ok: true, byWorker };
186
+ }
187
+ /**
188
+ * Every `smoo.secrets` entry with the group that resolves it, sorted by name,
189
+ * or a refusal naming the declaration that could not be read. The groups come
190
+ * from ./resolver.ts — the same file shell entry routes with — so a status
191
+ * table and a `smoo secrets run` cannot disagree about which group resolves
192
+ * a name.
193
+ */
194
+ export async function localSecretGroups(root) {
195
+ try {
196
+ const secrets = await readSecretGroups(root);
197
+ return { ok: true, secrets: secrets.sort((left, right) => left.name.localeCompare(right.name)) };
198
+ }
199
+ catch (error) {
200
+ return { ok: false, reason: error instanceof Error ? error.message : String(error) };
201
+ }
229
202
  }
230
203
  /**
231
204
  * Resolve one declared secret through its command. The value is returned, never
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The bootstrap secret resolver, as the CLI sees it.
3
+ *
4
+ * `managed/raw/tooling/direnv/secret-references.ts` holds the one
5
+ * implementation of THE RULE — which group resolves where, and what CI and a
6
+ * cowshed workspace refuse instead. A managed repository runs the copy smoo
7
+ * writes into its own `tooling/direnv/secret-references.ts` at shell entry;
8
+ * `smoo secrets run` runs THIS package's copy of the same file. One
9
+ * implementation, so the command and the shell cannot disagree about a group.
10
+ *
11
+ * It is loaded, not statically imported, and that is forced rather than
12
+ * chosen: the file is a raw bootstrap script that must load before any
13
+ * workspace package and before the Typia transform exists, so it lives
14
+ * outside `src` — outside this package's compiled `rootDir`, which a static
15
+ * import may not cross. A dynamic `import()` reaches it in both layouts
16
+ * (`src/secrets/` and `dist/secrets/` are each two directories below the
17
+ * package root), and typia validates what comes back: the same boundary
18
+ * ../monorepo/packed-package.ts puts around a foreign module. `require` is
19
+ * not the alternative — under a Bun loader plugin, which this package's own
20
+ * tests preload, every module is async and `require` of one throws.
21
+ */
22
+ /** A declared `smoo.secrets` entry with its group settled. */
23
+ export interface GroupedSecret {
24
+ name: string;
25
+ /** The group that resolves it: the declared one when stated, `derivedGroup` otherwise. */
26
+ group: string;
27
+ /**
28
+ * What the repository's own declarations imply: `registry` for a variable
29
+ * `.npmrc` interpolates, `nx-cache` for the declared cache token, `shell`
30
+ * for everything else. Differs from `group` exactly when an entry overrides
31
+ * the derivation.
32
+ */
33
+ derivedGroup: string;
34
+ }
35
+ /** A declared secret a request declined to resolve, and how to supply it. */
36
+ export interface DeferredSecret {
37
+ name: string;
38
+ group: string;
39
+ guidance: string;
40
+ }
41
+ /** What belongs in a child's environment, and what was deliberately left out of it. */
42
+ export interface SecretResolution {
43
+ values: Record<string, string>;
44
+ deferred: DeferredSecret[];
45
+ }
46
+ /**
47
+ * Every `smoo.secrets` entry of the repository at `root`, each with the group
48
+ * that resolves it. Rejects with the offending declaration named — never a
49
+ * value — when the manifest or `.npmrc` cannot be read.
50
+ */
51
+ export declare function readSecretGroups(root: string): Promise<GroupedSecret[]>;
52
+ /**
53
+ * Resolve exactly one group's secrets for one child process. Everything
54
+ * outside the group comes back deferred rather than resolved, so a run for
55
+ * one group never triggers another group's provider command. Rejects with the
56
+ * resolver's aggregated refusal, which names variables and groups only.
57
+ */
58
+ export declare function resolveSecretGroup(root: string, group: string): Promise<SecretResolution>;
59
+ //# sourceMappingURL=resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/secrets/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,uFAAuF;AACvF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAoDD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAO7E;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAO/F"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The bootstrap secret resolver, as the CLI sees it.
3
+ *
4
+ * `managed/raw/tooling/direnv/secret-references.ts` holds the one
5
+ * implementation of THE RULE — which group resolves where, and what CI and a
6
+ * cowshed workspace refuse instead. A managed repository runs the copy smoo
7
+ * writes into its own `tooling/direnv/secret-references.ts` at shell entry;
8
+ * `smoo secrets run` runs THIS package's copy of the same file. One
9
+ * implementation, so the command and the shell cannot disagree about a group.
10
+ *
11
+ * It is loaded, not statically imported, and that is forced rather than
12
+ * chosen: the file is a raw bootstrap script that must load before any
13
+ * workspace package and before the Typia transform exists, so it lives
14
+ * outside `src` — outside this package's compiled `rootDir`, which a static
15
+ * import may not cross. A dynamic `import()` reaches it in both layouts
16
+ * (`src/secrets/` and `dist/secrets/` are each two directories below the
17
+ * package root), and typia validates what comes back: the same boundary
18
+ * ../monorepo/packed-package.ts puts around a foreign module. `require` is
19
+ * not the alternative — under a Bun loader plugin, which this package's own
20
+ * tests preload, every module is async and `require` of one throws.
21
+ */
22
+ import { fileURLToPath, pathToFileURL } from 'node:url';
23
+ import typia from 'typia';
24
+ const isSecretReferencesModule = (() => {
25
+ const _io0 = input => true && true;
26
+ return input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
27
+ })();
28
+ const isGroupedSecrets = (() => {
29
+ const _io0 = input => "string" === typeof input.name && "string" === typeof input.group && "string" === typeof input.derivedGroup;
30
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
31
+ })();
32
+ const isSecretResolution = (() => {
33
+ const _io0 = input => "object" === typeof input.values && null !== input.values && false === Array.isArray(input.values) && _io1(input.values) && (Array.isArray(input.deferred) && input.deferred.every(elem => "object" === typeof elem && null !== elem && _io2(elem)));
34
+ const _io1 = input => Object.keys(input).every(key => {
35
+ const value = input[key];
36
+ if (undefined === value)
37
+ return true;
38
+ return "string" === typeof value;
39
+ });
40
+ const _io2 = input => "string" === typeof input.name && "string" === typeof input.group && "string" === typeof input.guidance;
41
+ return input => "object" === typeof input && null !== input && _io0(input);
42
+ })();
43
+ const RESOLVER_PATH = fileURLToPath(new URL('../../managed/raw/tooling/direnv/secret-references.ts', import.meta.url));
44
+ /**
45
+ * A loaded export, narrowed to the signature this file calls it with. typia
46
+ * validates data, not functions — it reports a module missing both of these
47
+ * as valid — so presence is checked here, and what each one RETURNS is
48
+ * validated below, where it is data again.
49
+ */
50
+ function isSecretGroupsReader(value) {
51
+ return typeof value === 'function';
52
+ }
53
+ function isSecretGroupResolver(value) {
54
+ return typeof value === 'function';
55
+ }
56
+ let loaded;
57
+ async function secretReferences() {
58
+ if (loaded !== undefined) {
59
+ return loaded;
60
+ }
61
+ // Dynamic by necessity: the target is a managed raw script outside this
62
+ // package's compiled rootDir, so no static import can name it.
63
+ const imported = await import(pathToFileURL(RESOLVER_PATH).href);
64
+ if (!isSecretReferencesModule(imported)) {
65
+ throw new Error(`${RESOLVER_PATH} did not load as a module`);
66
+ }
67
+ const { readSecretGroups: read, resolveSecretEnvironment: resolve } = imported;
68
+ if (!isSecretGroupsReader(read) || !isSecretGroupResolver(resolve)) {
69
+ throw new Error(`${RESOLVER_PATH} does not expose the expected secret resolver API`);
70
+ }
71
+ loaded = { readSecretGroups: read, resolveSecretEnvironment: resolve };
72
+ return loaded;
73
+ }
74
+ /**
75
+ * Every `smoo.secrets` entry of the repository at `root`, each with the group
76
+ * that resolves it. Rejects with the offending declaration named — never a
77
+ * value — when the manifest or `.npmrc` cannot be read.
78
+ */
79
+ export async function readSecretGroups(root) {
80
+ const references = await secretReferences();
81
+ const groups = references.readSecretGroups(root);
82
+ if (!isGroupedSecrets(groups)) {
83
+ throw new Error(`${RESOLVER_PATH} returned an unexpected group listing`);
84
+ }
85
+ return groups;
86
+ }
87
+ /**
88
+ * Resolve exactly one group's secrets for one child process. Everything
89
+ * outside the group comes back deferred rather than resolved, so a run for
90
+ * one group never triggers another group's provider command. Rejects with the
91
+ * resolver's aggregated refusal, which names variables and groups only.
92
+ */
93
+ export async function resolveSecretGroup(root, group) {
94
+ const references = await secretReferences();
95
+ const resolution = await references.resolveSecretEnvironment({ root, group });
96
+ if (!isSecretResolution(resolution)) {
97
+ throw new Error(`${RESOLVER_PATH} returned an unexpected resolution`);
98
+ }
99
+ return resolution;
100
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `smoo secrets run <group> <command...>` — the half of THE RULE that shell
3
+ * entry is not.
4
+ *
5
+ * The rule lives in tooling/direnv/secret-references.ts: shell entry resolves
6
+ * the `shell` group and nothing else, because a provider authorises per
7
+ * requesting process lineage and every direnv reload is a new one. Every
8
+ * other group belongs to the command that needs it, and this is that command.
9
+ * One deliberate `smoo secrets run registry bun install` instead of a
10
+ * credential prompt on every shell.
11
+ *
12
+ * Exactly one group is resolved. A repository that declares a registry
13
+ * credential and a cache token runs one provider command here, not two: a
14
+ * wrapper that resolved everything declared would fire provider commands for
15
+ * credentials the command never touches, which is the prompt this exists to
16
+ * remove.
17
+ *
18
+ * The value reaches the child the only way that leaves no trace: its
19
+ * environment. Never a file, never argv — argv is world-readable through
20
+ * `ps` — and never this process's stdout.
21
+ */
22
+ export declare function secretsRun(root: string, group: string | undefined, command: readonly string[]): Promise<number>;
23
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/secrets/run.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAiBH,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA0CrH"}
@@ -0,0 +1,130 @@
1
+ /**
2
+ * `smoo secrets run <group> <command...>` — the half of THE RULE that shell
3
+ * entry is not.
4
+ *
5
+ * The rule lives in tooling/direnv/secret-references.ts: shell entry resolves
6
+ * the `shell` group and nothing else, because a provider authorises per
7
+ * requesting process lineage and every direnv reload is a new one. Every
8
+ * other group belongs to the command that needs it, and this is that command.
9
+ * One deliberate `smoo secrets run registry bun install` instead of a
10
+ * credential prompt on every shell.
11
+ *
12
+ * Exactly one group is resolved. A repository that declares a registry
13
+ * credential and a cache token runs one provider command here, not two: a
14
+ * wrapper that resolved everything declared would fire provider commands for
15
+ * credentials the command never touches, which is the prompt this exists to
16
+ * remove.
17
+ *
18
+ * The value reaches the child the only way that leaves no trace: its
19
+ * environment. Never a file, never argv — argv is world-readable through
20
+ * `ps` — and never this process's stdout.
21
+ */
22
+ import { spawn } from 'node:child_process';
23
+ import { constants } from 'node:os';
24
+ import { readSecretGroups, resolveSecretGroup } from './resolver.js';
25
+ /**
26
+ * Nothing ran, and the listing is the answer: no group named, a group this
27
+ * repository does not declare, or a group with no command to run. Distinct
28
+ * from 1, which is a resolution that refused, and from any other code, which
29
+ * is the child's own.
30
+ */
31
+ const REFUSED = 2;
32
+ /** A command that could not be started at all, as a shell reports it. */
33
+ const NOT_EXECUTABLE = 127;
34
+ export async function secretsRun(root, group, command) {
35
+ let declared;
36
+ try {
37
+ declared = await readSecretGroups(root);
38
+ }
39
+ catch (error) {
40
+ console.error(error instanceof Error ? error.message : String(error));
41
+ return 1;
42
+ }
43
+ if (group === undefined) {
44
+ console.error('smoo secrets run <group> <command...> runs one command with one group of secrets in its environment.');
45
+ printGroups(declared);
46
+ return REFUSED;
47
+ }
48
+ const members = declared.filter((secret) => secret.group === group);
49
+ if (members.length === 0) {
50
+ console.error(`smoo secrets run: no declared secret is in group \`${group}\`.`);
51
+ printGroups(declared);
52
+ return REFUSED;
53
+ }
54
+ const [program, ...args] = command;
55
+ if (program === undefined) {
56
+ console.error(`smoo secrets run ${group} <command...>: name the command to run with ` +
57
+ `${members.map((secret) => secret.name).join(', ')} in its environment.`);
58
+ return REFUSED;
59
+ }
60
+ let values;
61
+ try {
62
+ ({ values } = await resolveSecretGroup(root, group));
63
+ }
64
+ catch (error) {
65
+ console.error(error instanceof Error ? error.message : String(error));
66
+ return 1;
67
+ }
68
+ return await runWithSecrets(program, args, values);
69
+ }
70
+ /**
71
+ * The child's exit status, reproduced: a code as itself, death by signal as
72
+ * 128+signum, and a program that could not be started as 127. It shares this
73
+ * process group, so terminal signals reach it directly; it is spawned rather
74
+ * than exec'd only because neither Bun nor Node exposes `execve`.
75
+ */
76
+ async function runWithSecrets(program, args, values) {
77
+ return await new Promise((resolve) => {
78
+ const child = spawn(program, [...args], { stdio: 'inherit', env: { ...process.env, ...values } });
79
+ child.on('error', (error) => {
80
+ console.error(`smoo secrets run: cannot run \`${program}\`: ${error.message}`);
81
+ resolve(NOT_EXECUTABLE);
82
+ });
83
+ child.on('close', (code, signal) => {
84
+ if (signal !== null) {
85
+ resolve(128 + constants.signals[signal]);
86
+ return;
87
+ }
88
+ resolve(code ?? 1);
89
+ });
90
+ });
91
+ }
92
+ /**
93
+ * The groups this repository declares and the secrets in each. Most people
94
+ * meet this command exactly once, at a 401, after reading a message that
95
+ * named it — so the refusal answers "which group?" from this checkout's own
96
+ * declarations rather than printing a usage line that sends them to the
97
+ * manifest to work it out.
98
+ */
99
+ function printGroups(declared) {
100
+ if (declared.length === 0) {
101
+ console.error('This repository declares no smoo.secrets, so there is no group to run.');
102
+ return;
103
+ }
104
+ const members = new Map();
105
+ for (const secret of declared) {
106
+ const named = members.get(secret.group);
107
+ if (named === undefined) {
108
+ members.set(secret.group, [secret.name]);
109
+ continue;
110
+ }
111
+ named.push(secret.name);
112
+ }
113
+ const groups = [...members.keys()].sort((left, right) => left.localeCompare(right));
114
+ const width = Math.max(...groups.map((group) => group.length));
115
+ console.error('');
116
+ console.error('Groups this repository declares:');
117
+ for (const group of groups) {
118
+ console.error(` ${group.padEnd(width)} ${(members.get(group) ?? []).join(', ')}`);
119
+ }
120
+ // An override is stated, never left for the next reader to discover by
121
+ // wondering why a `.npmrc` credential resolves at shell entry.
122
+ for (const secret of declared) {
123
+ if (secret.group === secret.derivedGroup)
124
+ continue;
125
+ console.error(` note: ${secret.name} declares group \`${secret.group}\`, overriding the \`${secret.derivedGroup}\` ` +
126
+ "this repository's declarations derive.");
127
+ }
128
+ console.error('');
129
+ console.error(` smoo secrets run ${groups[0] ?? '<group>'} <command...>`);
130
+ }