@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.
- package/README.md +91 -11
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +28 -4
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +6 -20
- package/dist/lib/deploy-tags.d.ts +17 -3
- package/dist/lib/deploy-tags.d.ts.map +1 -1
- package/dist/lib/deploy-tags.js +23 -4
- package/dist/lib/json.d.ts +22 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +15 -3
- package/dist/monorepo/cargo-policy.d.ts +17 -0
- package/dist/monorepo/cargo-policy.d.ts.map +1 -1
- package/dist/monorepo/cargo-policy.js +73 -1
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +4 -2
- package/dist/monorepo/managed-files.d.ts +5 -2
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +35 -21
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +6 -1
- package/dist/nx/index.d.ts +6 -0
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +14 -0
- package/dist/secrets/commands.d.ts +9 -5
- package/dist/secrets/commands.d.ts.map +1 -1
- package/dist/secrets/commands.js +122 -49
- package/dist/secrets/index.d.ts +42 -56
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +52 -79
- package/dist/secrets/resolver.d.ts +59 -0
- package/dist/secrets/resolver.d.ts.map +1 -0
- package/dist/secrets/resolver.js +100 -0
- package/dist/secrets/run.d.ts +23 -0
- package/dist/secrets/run.d.ts.map +1 -0
- package/dist/secrets/run.js +130 -0
- package/dist/secrets/status.d.ts +177 -0
- package/dist/secrets/status.d.ts.map +1 -0
- package/dist/secrets/status.js +724 -0
- package/dist/wrangler/deploy-stage.d.ts +1 -1
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +22 -20
- package/dist/wrangler/deployed-version.d.ts.map +1 -1
- package/dist/wrangler/deployed-version.js +7 -12
- package/dist/wrangler/flat-config.d.ts +1 -4
- package/dist/wrangler/flat-config.d.ts.map +1 -1
- package/dist/wrangler/flat-config.js +98 -44
- package/dist/wrangler/prepare-env.d.ts +4 -3
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +7 -5
- package/dist/wrangler/source-config.d.ts +24 -0
- package/dist/wrangler/source-config.d.ts.map +1 -0
- package/dist/wrangler/source-config.js +110 -0
- package/dist/wrangler/stage.d.ts +55 -23
- package/dist/wrangler/stage.d.ts.map +1 -1
- package/dist/wrangler/stage.js +81 -162
- package/managed/raw/tooling/direnv/devenv.smoo.nix +19 -3
- package/managed/raw/tooling/direnv/secret-references.ts +280 -76
- package/managed/raw/tooling/direnv/setup-environment.ts +58 -2
- package/managed/raw/tsconfig.lib.json +28 -0
- package/package.json +7 -2
- package/src/cli.ts +34 -5
- package/src/github-ci/index.test.ts +6 -7
- package/src/github-ci/index.ts +11 -19
- package/src/lib/deploy-tags.ts +22 -4
- package/src/lib/json.ts +23 -1
- package/src/monorepo/cargo-policy.test.ts +91 -1
- package/src/monorepo/cargo-policy.ts +87 -1
- package/src/monorepo/index.ts +8 -2
- package/src/monorepo/managed-files.test.ts +69 -37
- package/src/monorepo/managed-files.ts +38 -21
- package/src/monorepo/packs/index.ts +10 -1
- package/src/monorepo/secret-references.test.ts +412 -6
- package/src/monorepo/setup-environment.test.ts +181 -0
- package/src/nx/index.test.ts +8 -0
- package/src/nx/index.ts +20 -0
- package/src/secrets/commands.test.ts +186 -4
- package/src/secrets/commands.ts +142 -51
- package/src/secrets/index.test.ts +4 -10
- package/src/secrets/index.ts +54 -115
- package/src/secrets/resolver.ts +130 -0
- package/src/secrets/run.test.ts +359 -0
- package/src/secrets/run.ts +148 -0
- package/src/secrets/status.test.ts +164 -0
- package/src/secrets/status.ts +297 -0
- package/src/wrangler/deploy-stage.test.ts +95 -2
- package/src/wrangler/deploy-stage.ts +26 -23
- package/src/wrangler/deployed-version.ts +7 -11
- package/src/wrangler/flat-config.test.ts +9 -4
- package/src/wrangler/flat-config.ts +1 -20
- package/src/wrangler/format-parity.test.ts +433 -0
- package/src/wrangler/prepare-env.ts +7 -5
- package/src/wrangler/source-config.test.ts +102 -0
- package/src/wrangler/source-config.ts +110 -0
- package/src/wrangler/stage.test.ts +61 -32
- package/src/wrangler/stage.ts +124 -173
package/dist/secrets/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
/**
|
|
91
|
-
|
|
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
|
|
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"}
|
package/dist/secrets/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
+
}
|