@smoothbricks/cli 0.11.17 → 0.11.19

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 (60) hide show
  1. package/README.md +45 -10
  2. package/dist/cli.js +10 -7
  3. package/dist/github-ci/index.d.ts.map +1 -1
  4. package/dist/github-ci/index.js +6 -20
  5. package/dist/lib/deploy-tags.d.ts +17 -3
  6. package/dist/lib/deploy-tags.d.ts.map +1 -1
  7. package/dist/lib/deploy-tags.js +23 -4
  8. package/dist/monorepo/ci-workflow.d.ts +56 -0
  9. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  10. package/dist/monorepo/ci-workflow.js +199 -2
  11. package/dist/monorepo/managed-files.d.ts +28 -2
  12. package/dist/monorepo/managed-files.d.ts.map +1 -1
  13. package/dist/monorepo/managed-files.js +74 -22
  14. package/dist/release/github-release.d.ts +10 -0
  15. package/dist/release/github-release.d.ts.map +1 -1
  16. package/dist/release/github-release.js +11 -5
  17. package/dist/release/index.d.ts.map +1 -1
  18. package/dist/release/index.js +4 -5
  19. package/dist/secrets/commands.d.ts +52 -13
  20. package/dist/secrets/commands.d.ts.map +1 -1
  21. package/dist/secrets/commands.js +220 -34
  22. package/dist/secrets/index.d.ts +21 -1
  23. package/dist/secrets/index.d.ts.map +1 -1
  24. package/dist/secrets/index.js +110 -4
  25. package/dist/secrets/repository.d.ts +60 -0
  26. package/dist/secrets/repository.d.ts.map +1 -0
  27. package/dist/secrets/repository.js +145 -0
  28. package/dist/wrangler/cloudflare.d.ts +7 -0
  29. package/dist/wrangler/cloudflare.d.ts.map +1 -1
  30. package/dist/wrangler/cloudflare.js +10 -0
  31. package/dist/wrangler/deploy-stage.d.ts.map +1 -1
  32. package/dist/wrangler/deploy-stage.js +50 -24
  33. package/dist/wrangler/stage-secrets.d.ts +57 -0
  34. package/dist/wrangler/stage-secrets.d.ts.map +1 -0
  35. package/dist/wrangler/stage-secrets.js +178 -0
  36. package/managed/raw/tooling/direnv/devenv.smoo.nix +9 -1
  37. package/managed/raw/tooling/git-hooks/pre-push.sh +30 -29
  38. package/package.json +2 -2
  39. package/src/cli.ts +33 -10
  40. package/src/github-ci/index.test.ts +6 -7
  41. package/src/github-ci/index.ts +11 -19
  42. package/src/lib/deploy-tags.ts +22 -4
  43. package/src/monorepo/__tests__/ci-workflow.test.ts +161 -0
  44. package/src/monorepo/ci-workflow.ts +277 -2
  45. package/src/monorepo/managed-files.test.ts +96 -37
  46. package/src/monorepo/managed-files.ts +94 -23
  47. package/src/monorepo/package-policy.test.ts +1 -1
  48. package/src/release/__tests__/github-release.test.ts +8 -4
  49. package/src/release/github-release.ts +13 -5
  50. package/src/release/index.ts +4 -4
  51. package/src/secrets/commands.test.ts +28 -0
  52. package/src/secrets/commands.ts +237 -35
  53. package/src/secrets/index.test.ts +118 -1
  54. package/src/secrets/index.ts +108 -4
  55. package/src/secrets/repository.test.ts +98 -0
  56. package/src/secrets/repository.ts +164 -0
  57. package/src/wrangler/cloudflare.ts +18 -0
  58. package/src/wrangler/deploy-stage.test.ts +258 -11
  59. package/src/wrangler/deploy-stage.ts +70 -38
  60. package/src/wrangler/stage-secrets.ts +146 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Which GitHub repository `smoo secrets` reads and writes.
3
+ *
4
+ * `gh` refuses on a checkout with more than one remote ("multiple remotes
5
+ * detected"), which is exactly the public/private mirror layout this CLI's own
6
+ * users run — so the repository has to be resolved here, from the checkout,
7
+ * before `gh` is spawned. The current branch's upstream decides it: a branch
8
+ * that pushes to the private mirror is a branch whose secrets live there. That
9
+ * is a fact the checkout already records, not a preference to configure.
10
+ */
11
+ import { spawnSync } from 'node:child_process';
12
+ /** `owner/name` from any git remote URL form, or null when it names no repository. */
13
+ export function repositorySlugFromUrl(url) {
14
+ const withoutProtocol = url
15
+ .trim()
16
+ .replace(/^[a-z+]+:\/\//i, '')
17
+ .replace(/^[^@/]+@/, '');
18
+ // A remainder starting with `/` or `.` carries no host, so it is a path on
19
+ // this machine - a valid git remote, never a GitHub repository.
20
+ if (withoutProtocol.startsWith('/') || withoutProtocol.startsWith('.'))
21
+ return null;
22
+ const path = withoutProtocol.replace(/^[^/:]+(?::\d+)?[/:]/, '');
23
+ const segments = path
24
+ .replace(/\.git$/i, '')
25
+ .split('/')
26
+ .filter((segment) => segment.length > 0);
27
+ if (segments.length < 2)
28
+ return null;
29
+ return `${segments[segments.length - 2]}/${segments[segments.length - 1]}`;
30
+ }
31
+ /**
32
+ * Pure resolution, so the precedence is testable without a checkout.
33
+ *
34
+ * A requested value is either a remote name or an `owner/name` slug; naming a
35
+ * remote is what an operator reaches for first (`-R private-repo`), and
36
+ * refusing it because it lacks a slash would be pedantry.
37
+ *
38
+ * With no request and no upstream - a branch pushed by explicit refspec, which
39
+ * is how the mirror layout is driven - the remote-tracking refs still record
40
+ * where this branch has been pushed. One remote carrying it is an answer; two
41
+ * is a genuine ambiguity worth refusing.
42
+ */
43
+ export function chooseRepository(inputs) {
44
+ const { remotes, upstreamRemote, requested } = inputs;
45
+ const remotesCarryingBranch = inputs.remotesCarryingBranch ?? [];
46
+ if (requested !== undefined && requested.length > 0) {
47
+ const url = remotes.get(requested);
48
+ if (url !== undefined) {
49
+ const slug = repositorySlugFromUrl(url);
50
+ if (slug === null)
51
+ return { ok: false, reason: `remote ${requested} (${url}) names no owner/name` };
52
+ return { ok: true, choice: { repo: slug, source: `remote ${requested}` } };
53
+ }
54
+ if (requested.includes('/'))
55
+ return { ok: true, choice: { repo: requested, source: 'requested' } };
56
+ return {
57
+ ok: false,
58
+ reason: `${requested} is neither a remote of this checkout nor an owner/name. ` +
59
+ `Remotes: ${[...remotes.keys()].join(', ') || 'none'}`,
60
+ };
61
+ }
62
+ if (upstreamRemote !== null) {
63
+ const url = remotes.get(upstreamRemote);
64
+ const slug = url === undefined ? null : repositorySlugFromUrl(url);
65
+ if (slug !== null) {
66
+ return { ok: true, choice: { repo: slug, source: `upstream of the current branch (${upstreamRemote})` } };
67
+ }
68
+ }
69
+ if (remotesCarryingBranch.length === 1) {
70
+ const name = remotesCarryingBranch[0] ?? '';
71
+ const url = remotes.get(name);
72
+ const slug = url === undefined ? null : repositorySlugFromUrl(url);
73
+ if (slug !== null) {
74
+ return { ok: true, choice: { repo: slug, source: `the only remote carrying this branch (${name})` } };
75
+ }
76
+ }
77
+ if (remotes.size === 1) {
78
+ const [name, url] = [...remotes][0];
79
+ const slug = repositorySlugFromUrl(url);
80
+ if (slug !== null)
81
+ return { ok: true, choice: { repo: slug, source: `the only remote (${name})` } };
82
+ }
83
+ const origin = remotes.get('origin');
84
+ const originSlug = origin === undefined ? null : repositorySlugFromUrl(origin);
85
+ if (originSlug !== null)
86
+ return { ok: true, choice: { repo: originSlug, source: 'remote origin' } };
87
+ const candidates = [...remotes].map(([name, url]) => `${name} -> ${repositorySlugFromUrl(url) ?? url}`).join(', ');
88
+ const branchClause = inputs.branch === undefined || inputs.branch === null
89
+ ? 'this checkout has no current branch'
90
+ : `branch ${inputs.branch} has no upstream and no remote carries it`;
91
+ return {
92
+ ok: false,
93
+ reason: `${branchClause}, and there is no origin, so the repository is ambiguous. ` +
94
+ `Pass -R with one of: ${candidates || 'no remotes at all'}`,
95
+ };
96
+ }
97
+ function git(root, args) {
98
+ const result = spawnSync('git', [...args], { cwd: root, encoding: 'utf8' });
99
+ return result.status === 0 ? result.stdout.trim() : null;
100
+ }
101
+ /** Remote name -> URL, in config order. */
102
+ export function readRemotes(root) {
103
+ const remotes = new Map();
104
+ const config = git(root, ['config', '--get-regexp', '^remote\\..*\\.url']);
105
+ if (config === null)
106
+ return remotes;
107
+ for (const line of config.split('\n')) {
108
+ const match = /^remote\.(.+)\.url\s+(.+)$/.exec(line.trim());
109
+ if (match !== null && match[1] !== undefined && match[2] !== undefined)
110
+ remotes.set(match[1], match[2]);
111
+ }
112
+ return remotes;
113
+ }
114
+ /** Remotes with a remote-tracking ref for the current branch: where it has been pushed. */
115
+ export function readRemotesCarryingBranch(root, remotes) {
116
+ const branch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
117
+ if (branch === null || branch === 'HEAD')
118
+ return [];
119
+ const carrying = [];
120
+ for (const name of remotes.keys()) {
121
+ if (git(root, ['rev-parse', '--verify', '--quiet', `refs/remotes/${name}/${branch}`]) !== null) {
122
+ carrying.push(name);
123
+ }
124
+ }
125
+ return carrying;
126
+ }
127
+ /** The remote the current branch tracks, or null when it tracks nothing. */
128
+ export function readUpstreamRemote(root) {
129
+ const branch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
130
+ if (branch === null || branch === 'HEAD')
131
+ return null;
132
+ return git(root, ['config', '--get', `branch.${branch}.remote`]);
133
+ }
134
+ /** The repository to operate on, resolved from the checkout. */
135
+ export function resolveRepository(root, requested) {
136
+ const remotes = readRemotes(root);
137
+ const branch = git(root, ['rev-parse', '--abbrev-ref', 'HEAD']);
138
+ return chooseRepository({
139
+ remotes,
140
+ upstreamRemote: readUpstreamRemote(root),
141
+ remotesCarryingBranch: readRemotesCarryingBranch(root, remotes),
142
+ branch: branch === 'HEAD' ? null : branch,
143
+ requested,
144
+ });
145
+ }
@@ -5,6 +5,10 @@ export interface R2Bucket {
5
5
  export interface WorkerScript {
6
6
  id: string;
7
7
  }
8
+ /** One secret bound to a Worker. Cloudflare answers names and types only; a value is never readable. */
9
+ export interface WorkerSecret {
10
+ name: string;
11
+ }
8
12
  export interface WorkerRoute {
9
13
  id: string;
10
14
  pattern: string;
@@ -40,6 +44,8 @@ export interface CloudflareClient {
40
44
  deleteR2Object(bucket: string, key: string): Promise<void>;
41
45
  deleteR2Bucket(name: string): Promise<void>;
42
46
  listWorkerScripts(): Promise<WorkerScript[]>;
47
+ /** Names only; the Worker's stored values are write-only from outside. */
48
+ listWorkerSecrets(workerName: string): Promise<string[]>;
43
49
  deleteWorkerScript(name: string): Promise<void>;
44
50
  listWorkerDomains(): Promise<WorkerDomain[]>;
45
51
  createWorkerDomain(hostname: string, workerName: string, zoneId: string): Promise<void>;
@@ -75,6 +81,7 @@ export declare class CloudflareRestClient implements CloudflareClient {
75
81
  deleteR2Object(bucket: string, key: string): Promise<void>;
76
82
  deleteR2Bucket(name: string): Promise<void>;
77
83
  listWorkerScripts(): Promise<WorkerScript[]>;
84
+ listWorkerSecrets(workerName: string): Promise<string[]>;
78
85
  deleteWorkerScript(name: string): Promise<void>;
79
86
  listWorkerDomains(): Promise<WorkerDomain[]>;
80
87
  createWorkerDomain(hostname: string, workerName: string, zoneId: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/wrangler/cloudflare.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cloudflare.d.ts","sourceRoot":"","sources":["../../src/wrangler/cloudflare.ts"],"names":[],"mappings":"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"}
@@ -29,6 +29,10 @@ const isWorkerScripts = (() => {
29
29
  const _io0 = input => "string" === typeof input.id;
30
30
  return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
31
31
  })();
32
+ const isWorkerSecrets = (() => {
33
+ const _io0 = input => "string" === typeof input.name;
34
+ return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
35
+ })();
32
36
  const isWorkerDomains = (() => {
33
37
  const _io0 = input => "string" === typeof input.id && "string" === typeof input.hostname && (undefined === input.service || "string" === typeof input.service);
34
38
  return input => Array.isArray(input) && input.every(elem => "object" === typeof elem && null !== elem && _io0(elem));
@@ -158,6 +162,12 @@ export class CloudflareRestClient {
158
162
  // `workers/scripts` takes no pagination parameters: the account's scripts arrive in one answer.
159
163
  return this.listOnce(`${this.accountPath}/workers/scripts`, isWorkerScripts);
160
164
  }
165
+ async listWorkerSecrets(workerName) {
166
+ // Unpaginated, like the script listing itself. A Worker that does not exist answers 404 rather
167
+ // than an empty list, so the caller must establish that the Worker is there before asking.
168
+ const secrets = await this.listOnce(`${this.accountPath}/workers/scripts/${encodeURIComponent(workerName)}/secrets`, isWorkerSecrets);
169
+ return secrets.map((secret) => secret.name);
170
+ }
161
171
  deleteWorkerScript(name) {
162
172
  return this.mutate(`${this.accountPath}/workers/scripts/${encodeURIComponent(name)}`, { method: 'DELETE' });
163
173
  }
@@ -1 +1 @@
1
- {"version":3,"file":"deploy-stage.d.ts","sourceRoot":"","sources":["../../src/wrangler/deploy-stage.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"deploy-stage.d.ts","sourceRoot":"","sources":["../../src/wrangler/deploy-stage.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { existsSync, readFileSync } from 'node:fs';
3
2
  import { readFile, rm, writeFile } from 'node:fs/promises';
4
3
  import { dirname, join } from 'node:path';
5
4
  import { parseJsonFileText } from '../lib/json.js';
@@ -7,8 +6,8 @@ import { mergeEnv, printCommandOutput } from '../lib/run.js';
7
6
  import { CloudflareRestClient } from './cloudflare.js';
8
7
  import { parseFlatWranglerConfig, planFlatStageResources } from './flat-config.js';
9
8
  import { awaitLiveVersion, awaitVersionEndpoint, currentDeploymentVersionId, findVersionIdByTag, liveVersionCacheDirectory, writeCachedLiveVersion, } from './live-version.js';
10
- import { parseDevVarsExample } from './prepare-env.js';
11
9
  import { derivePullRequestStageConfig, derivePullRequestWranglerConfig, hasExactStageSegment, isPullRequestStage, parseDeploymentStage, planConfiguredStageResources, planPullRequestBindings, planPullRequestResources, pullRequestStage, } from './stage.js';
10
+ import { planStageSecrets, readDeclaredSecretNames, readSecretStageMap, stageSecretRefusal, } from './stage-secrets.js';
12
11
  /** The child environment `options` describe; every runner, test doubles included, spawns with exactly this. */
13
12
  export function childEnvironment(options) {
14
13
  return mergeEnv(options.env, options.unsetEnv);
@@ -40,21 +39,26 @@ export async function deployStage(cwd, options, dependencies = {}) {
40
39
  const cloudflare = dependencies.cloudflare ??
41
40
  new CloudflareRestClient(accountId, requiredEnvironmentValue(apiToken, 'CLOUDFLARE_API_TOKEN'));
42
41
  const runner = dependencies.runner ?? new BunProcessRunner();
43
- const secretNames = readSecretNames(cwd);
42
+ const secretPlan = planStageSecrets(readDeclaredSecretNames(cwd), stage, readSecretStageMap(cwd));
43
+ // Only the stage's own secrets, and only the ones a value exists for. A secret scoped to other
44
+ // stages is dropped here even when this shell exports it: that is the whole permission rule.
44
45
  const secretValues = {};
45
- for (const name of secretNames) {
46
+ for (const name of secretPlan.required) {
46
47
  const value = processEnv[name];
47
48
  if (value)
48
49
  secretValues[name] = value;
49
50
  }
50
- const missingSecrets = secretNames.filter((name) => !processEnv[name]);
51
+ const gate = stageSecretGate(secretPlan, new Set(Object.keys(secretValues)), cloudflare);
51
52
  let temporaryConfigPath;
52
53
  let temporarySecretsPath;
53
54
  try {
54
55
  const prepared = options.config
55
- ? await prepareFlatConfig(options.config, stage, accountId, missingSecrets, cloudflare)
56
- : await prepareTomlConfig(cwd, stage, accountId, missingSecrets, cloudflare);
56
+ ? await prepareFlatConfig(options.config, stage, accountId, gate, cloudflare)
57
+ : await prepareTomlConfig(cwd, stage, accountId, gate, cloudflare);
57
58
  temporaryConfigPath = prepared.temporaryConfigPath;
59
+ // The pull-request path already ran this before provisioning; the gate answers once per Worker.
60
+ // Everything below here writes to Cloudflare.
61
+ await gate(prepared.plan.workerName);
58
62
  const workerExists = await reconcileStageResources(prepared.plan, cloudflare);
59
63
  const versionTag = nxTaskVersionTag(processEnv);
60
64
  const envArgs = prepared.envFlag ? ['--env', prepared.envFlag] : [];
@@ -62,7 +66,13 @@ export async function deployStage(cwd, options, dependencies = {}) {
62
66
  // renames the worker `<name>-<CLOUDFLARE_ENV>`. The build that produced the flat config is the
63
67
  // caller that sets the variable, so the deploy would silently succeed under a name neither
64
68
  // `reconcileStageResources` nor `versions list` looks at.
65
- const run = prepared.envFlag ? { cwd } : { cwd, unsetEnv: ['CLOUDFLARE_ENV'] };
69
+ //
70
+ // A secret this stage is scoped out of goes with it. Leaving it out of the secrets payload is
71
+ // what stops it being installed; withholding it from the child as well means the deploy cannot
72
+ // read it at all, so "this stage never sees that capability" holds at the process boundary and
73
+ // not merely in the file we happen to write.
74
+ const unsetEnv = [...(prepared.envFlag ? [] : ['CLOUDFLARE_ENV']), ...secretPlan.withheld];
75
+ const run = unsetEnv.length > 0 ? { cwd, unsetEnv } : { cwd };
66
76
  const workerName = prepared.plan.workerName;
67
77
  const probe = {
68
78
  deployments: () => wranglerJson(runner, ['deployments', 'status', '--name', workerName, '--json'], run),
@@ -126,7 +136,7 @@ export async function deployStage(cwd, options, dependencies = {}) {
126
136
  }
127
137
  }
128
138
  }
129
- async function prepareTomlConfig(cwd, stage, accountId, missingSecrets, cloudflare) {
139
+ async function prepareTomlConfig(cwd, stage, accountId, gate, cloudflare) {
130
140
  const committedConfigPath = join(cwd, 'wrangler.toml');
131
141
  const committedToml = await readFile(committedConfigPath, 'utf8');
132
142
  if (!isPullRequestStage(stage)) {
@@ -139,7 +149,7 @@ async function prepareTomlConfig(cwd, stage, accountId, missingSecrets, cloudfla
139
149
  }
140
150
  const liveNamespaces = await cloudflare.listKvNamespaces();
141
151
  const plan = planPullRequestResources(committedToml, stage, liveNamespaces);
142
- const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan, missingSecrets, cloudflare, liveNamespaces);
152
+ const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces);
143
153
  const derivedToml = derivePullRequestWranglerConfig(committedToml, {
144
154
  stage,
145
155
  accountId,
@@ -156,7 +166,7 @@ async function prepareTomlConfig(cwd, stage, accountId, missingSecrets, cloudfla
156
166
  envFlag: stage,
157
167
  };
158
168
  }
159
- async function prepareFlatConfig(configPath, stage, accountId, missingSecrets, cloudflare) {
169
+ async function prepareFlatConfig(configPath, stage, accountId, gate, cloudflare) {
160
170
  const flat = parseJsonFileText(configPath, await readFile(configPath, 'utf8'), parseFlatWranglerConfig);
161
171
  if (!isPullRequestStage(stage)) {
162
172
  return {
@@ -167,7 +177,7 @@ async function prepareFlatConfig(configPath, stage, accountId, missingSecrets, c
167
177
  }
168
178
  const liveNamespaces = await cloudflare.listKvNamespaces();
169
179
  const plan = planPullRequestBindings(flat, stage, liveNamespaces);
170
- const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan, missingSecrets, cloudflare, liveNamespaces);
180
+ const { kvNamespaceIds, d1DatabaseIds } = await provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces);
171
181
  const derived = derivePullRequestStageConfig(flat, { stage, accountId, kvNamespaceIds, d1DatabaseIds });
172
182
  // Beside the original: its main/assets/migrations paths are relative to the file.
173
183
  const temporaryConfigPath = join(dirname(configPath), `.wrangler.smoo-${process.pid}-${randomUUID()}.json`);
@@ -199,15 +209,35 @@ function migrationBindings(config) {
199
209
  .filter((database) => typeof database.migrations_dir === 'string')
200
210
  .map((database) => database.binding);
201
211
  }
202
- async function refuseFirstDeploymentWithoutSecrets(cloudflare, workerName, missingSecrets) {
203
- const firstDeployment = !(await cloudflare.listWorkerScripts()).some((script) => script.id === workerName);
204
- if (firstDeployment && missingSecrets.length > 0) {
205
- throw new Error(`First deployment of ${workerName} requires process environment values for: ${missingSecrets.join(', ')}.`);
206
- }
212
+ /**
213
+ * One answer per Worker, reused by both call sites so the read costs one round trip.
214
+ *
215
+ * What the Worker holds comes from the account's script listing first. A Worker absent from it
216
+ * holds nothing — exactly the first-deployment case — and Cloudflare answers 404 rather than an
217
+ * empty list when asked for a missing Worker's secrets. Reading that failure as "then nothing is
218
+ * needed" would disarm the check at the one moment it matters most, so it is never asked.
219
+ */
220
+ function stageSecretGate(plan, exported, cloudflare) {
221
+ const answered = new Map();
222
+ return (workerName) => {
223
+ let pending = answered.get(workerName);
224
+ if (!pending) {
225
+ pending = assertStageSecrets(plan, exported, cloudflare, workerName);
226
+ answered.set(workerName, pending);
227
+ }
228
+ return pending;
229
+ };
207
230
  }
208
- /** Isolation refusals already ran in the plan; this is the first Cloudflare write. */
209
- async function provisionPullRequestResources(plan, missingSecrets, cloudflare, liveNamespaces) {
210
- await refuseFirstDeploymentWithoutSecrets(cloudflare, plan.workerName, missingSecrets);
231
+ async function assertStageSecrets(plan, exported, cloudflare, workerName) {
232
+ const live = (await cloudflare.listWorkerScripts()).some((script) => script.id === workerName);
233
+ const held = new Set(live ? await cloudflare.listWorkerSecrets(workerName) : []);
234
+ const refusal = stageSecretRefusal(plan, exported, held, workerName);
235
+ if (refusal)
236
+ throw new Error(refusal);
237
+ }
238
+ /** Isolation refusals already ran in the plan; the gate below is the last one before the first write. */
239
+ async function provisionPullRequestResources(plan, gate, cloudflare, liveNamespaces) {
240
+ await gate(plan.workerName);
211
241
  const kvNamespaceIds = await ensureKvNamespaces(plan.kvNamespaces, liveNamespaces, cloudflare);
212
242
  const d1DatabaseIds = plan.d1Databases.length === 0
213
243
  ? new Map()
@@ -495,7 +525,3 @@ function requiredEnvironmentValue(value, name) {
495
525
  throw new Error(`${name} is required.`);
496
526
  return value;
497
527
  }
498
- function readSecretNames(cwd) {
499
- const path = join(cwd, '.dev.vars.example');
500
- return existsSync(path) ? parseDevVarsExample(readFileSync(path, 'utf8')) : [];
501
- }
@@ -0,0 +1,57 @@
1
+ import { type DeploymentStage } from './stage.js';
2
+ /**
3
+ * How a declaration names a stage. The fixed stages go by their own name; every `prN` stage is
4
+ * `preview`, because a scope is written once and pull-request numbers are not knowable in advance.
5
+ */
6
+ export type SecretStageScope = 'staging' | 'production' | 'preview';
7
+ /**
8
+ * `smoo.wrangler.secretStages`: a declared secret name mapped to the stages it belongs to. The map
9
+ * answers two questions with one declaration, and both directions matter:
10
+ *
11
+ * - requirement — a stage the secret belongs to refuses to deploy without a value for it;
12
+ * - permission — a stage the secret does *not* belong to never receives it, however loudly the
13
+ * deploying shell exports it. A test-only capability exported by CI for preview stages must not
14
+ * ride along into production just because the variable happens to be set.
15
+ *
16
+ * A declared name absent from the map belongs to every stage. A name mapped to `[]` belongs to no
17
+ * stage, which is how a value that exists only for local development is declared.
18
+ */
19
+ export type SecretStageMap = Record<string, SecretStageScope[]>;
20
+ /** Secret NAMES the project declares. Values live on the Worker; the repo only ever holds the keys. */
21
+ export declare function readDeclaredSecretNames(cwd: string): string[];
22
+ /** `smoo.wrangler.secretStages` from the project's package.json; no file and no block scope nothing. */
23
+ export declare function readSecretStageMap(cwd: string): SecretStageMap;
24
+ /** Which of a project's declared secrets one stage may see, and which belong to other stages. */
25
+ export interface StageSecretPlan {
26
+ stage: DeploymentStage;
27
+ /** Declared names this stage requires — and the only ones a deploy of it may carry. */
28
+ required: string[];
29
+ /** Declared names scoped to other stages: withheld from this deploy even when a value is exported. */
30
+ withheld: string[];
31
+ /** The declaration itself, so a refusal can say why each name is where it is. */
32
+ scopes: SecretStageMap;
33
+ }
34
+ /**
35
+ * Splits the declared secrets by whether `stage` is in scope for each.
36
+ *
37
+ * A scope on an undeclared name is refused rather than ignored: it is almost always a typo of a
38
+ * real secret's name, and its effect is the dangerous direction — the misspelt entry scopes
39
+ * nothing while the real secret, still absent from the map, reaches every stage.
40
+ */
41
+ export declare function planStageSecrets(declared: string[], stage: DeploymentStage, scopes: SecretStageMap): StageSecretPlan;
42
+ /**
43
+ * Why this deploy must not proceed, or nothing when it may.
44
+ *
45
+ * Two refusals, reported together so one run names every problem:
46
+ *
47
+ * - a required secret with no value anywhere. `--secrets-file` applies additively, so a deploy
48
+ * that never mentions a secret leaves whatever the Worker already holds — silence that reads as
49
+ * success while a secret introduced after the first deploy never arrives, and the code that
50
+ * needs it fails at runtime instead of here.
51
+ * - a withheld secret the Worker already holds. Filtering it out of this deploy's payload cannot
52
+ * remove it, so the scope would be nominal rather than enforced until someone deletes it.
53
+ *
54
+ * A value is never read, never formatted, and never named beyond its key.
55
+ */
56
+ export declare function stageSecretRefusal(plan: StageSecretPlan, exported: ReadonlySet<string>, held: ReadonlySet<string>, workerName: string): string | undefined;
57
+ //# sourceMappingURL=stage-secrets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-secrets.d.ts","sourceRoot":"","sources":["../../src/wrangler/stage-secrets.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,eAAe,EAAsB,MAAM,YAAY,CAAC;AAEtE;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AAEpE;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;AAahE,uGAAuG;AACvG,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAG7D;AAED,wGAAwG;AACxG,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAQ9D;AAED,iGAAiG;AACjG,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAC;IACvB,uFAAuF;IACvF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,sGAAsG;IACtG,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iFAAiF;IACjF,MAAM,EAAE,cAAc,CAAC;CACxB;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,cAAc,GAAG,eAAe,CAsBpH;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,EAC7B,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,EACzB,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,SAAS,CAsBpB"}
@@ -0,0 +1,178 @@
1
+ import * as _accessExpressionAsString_1 from "typia/lib/internal/_accessExpressionAsString";
2
+ const __typia_transform__accessExpressionAsString = _accessExpressionAsString_1._accessExpressionAsString;
3
+ import * as _validateReport_1 from "typia/lib/internal/_validateReport";
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import typia from 'typia';
7
+ import { formatValidationErrors, parseJsonFileText } from '../lib/json.js';
8
+ import { parseDevVarsExample } from './prepare-env.js';
9
+ import { isPullRequestStage } from './stage.js';
10
+ const validateWranglerManifest = (() => {
11
+ const _io0 = input => undefined === input.smoo || "object" === typeof input.smoo && null !== input.smoo && false === Array.isArray(input.smoo) && _io1(input.smoo);
12
+ const _io1 = input => undefined === input.wrangler || "object" === typeof input.wrangler && null !== input.wrangler && false === Array.isArray(input.wrangler) && _io2(input.wrangler);
13
+ const _io2 = input => undefined === input.secretStages || "object" === typeof input.secretStages && null !== input.secretStages && false === Array.isArray(input.secretStages) && _io3(input.secretStages);
14
+ const _io3 = input => Object.keys(input).every(key => {
15
+ const value = input[key];
16
+ if (undefined === value)
17
+ return true;
18
+ return Array.isArray(value) && value.every(elem => "preview" === elem || "production" === elem || "staging" === elem);
19
+ });
20
+ const _vo0 = (input, _path, _exceptionable = true) => [undefined === input.smoo || ("object" === typeof input.smoo && null !== input.smoo && false === Array.isArray(input.smoo) || _report(_exceptionable, {
21
+ path: _path + ".smoo",
22
+ expected: "(undefined | { wrangler?: { secretStages?: SecretStageMap | undefined; } | undefined; })",
23
+ value: input.smoo
24
+ })) && _vo1(input.smoo, _path + ".smoo", true && _exceptionable) || _report(_exceptionable, {
25
+ path: _path + ".smoo",
26
+ expected: "(undefined | { wrangler?: { secretStages?: SecretStageMap | undefined; } | undefined; })",
27
+ value: input.smoo
28
+ })].every(flag => flag);
29
+ const _vo1 = (input, _path, _exceptionable = true) => [undefined === input.wrangler || ("object" === typeof input.wrangler && null !== input.wrangler && false === Array.isArray(input.wrangler) || _report(_exceptionable, {
30
+ path: _path + ".wrangler",
31
+ expected: "(undefined | { secretStages?: SecretStageMap | undefined; })",
32
+ value: input.wrangler
33
+ })) && _vo2(input.wrangler, _path + ".wrangler", true && _exceptionable) || _report(_exceptionable, {
34
+ path: _path + ".wrangler",
35
+ expected: "(undefined | { secretStages?: SecretStageMap | undefined; })",
36
+ value: input.wrangler
37
+ })].every(flag => flag);
38
+ const _vo2 = (input, _path, _exceptionable = true) => [undefined === input.secretStages || ("object" === typeof input.secretStages && null !== input.secretStages && false === Array.isArray(input.secretStages) || _report(_exceptionable, {
39
+ path: _path + ".secretStages",
40
+ expected: "(SecretStageMap | undefined)",
41
+ value: input.secretStages
42
+ })) && _vo3(input.secretStages, _path + ".secretStages", true && _exceptionable) || _report(_exceptionable, {
43
+ path: _path + ".secretStages",
44
+ expected: "(SecretStageMap | undefined)",
45
+ value: input.secretStages
46
+ })].every(flag => flag);
47
+ const _vo3 = (input, _path, _exceptionable = true) => [false === _exceptionable || Object.keys(input).map(key => {
48
+ const value = input[key];
49
+ if (undefined === value)
50
+ return true;
51
+ return (Array.isArray(value) || _report(_exceptionable, {
52
+ path: _path + __typia_transform__accessExpressionAsString(key),
53
+ expected: "Array<SecretStageScope>",
54
+ value: value
55
+ })) && value.map((elem, _index2) => "preview" === elem || "production" === elem || "staging" === elem || _report(_exceptionable, {
56
+ path: _path + __typia_transform__accessExpressionAsString(key) + "[" + _index2 + "]",
57
+ expected: "(\"preview\" | \"production\" | \"staging\")",
58
+ value: elem
59
+ })).every(flag => flag) || _report(_exceptionable, {
60
+ path: _path + __typia_transform__accessExpressionAsString(key),
61
+ expected: "Array<SecretStageScope>",
62
+ value: value
63
+ });
64
+ }).every(flag => flag)].every(flag => flag);
65
+ const __is = input => "object" === typeof input && null !== input && false === Array.isArray(input) && _io0(input);
66
+ let errors;
67
+ let _report;
68
+ const __validate = input => {
69
+ if (false === __is(input)) {
70
+ errors = [];
71
+ _report = _validateReport_1._validateReport(errors);
72
+ ((input, _path, _exceptionable = true) => ("object" === typeof input && null !== input && false === Array.isArray(input) || _report(true, {
73
+ path: _path + "",
74
+ expected: "WranglerPackageManifest",
75
+ value: input
76
+ })) && _vo0(input, _path + "", true) || _report(true, {
77
+ path: _path + "",
78
+ expected: "WranglerPackageManifest",
79
+ value: input
80
+ }))(input, "$input", true);
81
+ const success = 0 === errors.length;
82
+ return success ? {
83
+ success,
84
+ data: input
85
+ } : {
86
+ success,
87
+ errors,
88
+ data: input
89
+ };
90
+ }
91
+ return {
92
+ success: true,
93
+ data: input
94
+ };
95
+ };
96
+ return input => __validate(JSON.parse(input));
97
+ })();
98
+ /** Secret NAMES the project declares. Values live on the Worker; the repo only ever holds the keys. */
99
+ export function readDeclaredSecretNames(cwd) {
100
+ const path = join(cwd, '.dev.vars.example');
101
+ return existsSync(path) ? parseDevVarsExample(readFileSync(path, 'utf8')) : [];
102
+ }
103
+ /** `smoo.wrangler.secretStages` from the project's package.json; no file and no block scope nothing. */
104
+ export function readSecretStageMap(cwd) {
105
+ const path = join(cwd, 'package.json');
106
+ if (!existsSync(path))
107
+ return {};
108
+ const result = parseJsonFileText(path, readFileSync(path, 'utf8'), validateWranglerManifest);
109
+ if (!result.success) {
110
+ throw new Error(`${path} declares an invalid smoo.wrangler block: ${formatValidationErrors(result.errors)}`);
111
+ }
112
+ return result.data.smoo?.wrangler?.secretStages ?? {};
113
+ }
114
+ /**
115
+ * Splits the declared secrets by whether `stage` is in scope for each.
116
+ *
117
+ * A scope on an undeclared name is refused rather than ignored: it is almost always a typo of a
118
+ * real secret's name, and its effect is the dangerous direction — the misspelt entry scopes
119
+ * nothing while the real secret, still absent from the map, reaches every stage.
120
+ */
121
+ export function planStageSecrets(declared, stage, scopes) {
122
+ const declaredNames = new Set(declared);
123
+ const undeclared = Object.keys(scopes).filter((name) => !declaredNames.has(name));
124
+ if (undeclared.length > 0) {
125
+ throw new Error(`smoo.wrangler.secretStages scopes ${undeclared.join(', ')}, which .dev.vars.example does not declare. ` +
126
+ 'A scope on a name no secret has leaves the secret it was meant for unscoped, so that secret reaches every stage.');
127
+ }
128
+ // Every `prN` stage answers to one written token: a scope cannot name pull requests in advance.
129
+ const scope = isPullRequestStage(stage) ? 'preview' : stage;
130
+ const required = [];
131
+ const withheld = [];
132
+ for (const name of declared) {
133
+ const declaredScope = scopes[name];
134
+ if (declaredScope === undefined || declaredScope.includes(scope)) {
135
+ required.push(name);
136
+ }
137
+ else {
138
+ withheld.push(name);
139
+ }
140
+ }
141
+ return { stage, required, withheld, scopes };
142
+ }
143
+ /**
144
+ * Why this deploy must not proceed, or nothing when it may.
145
+ *
146
+ * Two refusals, reported together so one run names every problem:
147
+ *
148
+ * - a required secret with no value anywhere. `--secrets-file` applies additively, so a deploy
149
+ * that never mentions a secret leaves whatever the Worker already holds — silence that reads as
150
+ * success while a secret introduced after the first deploy never arrives, and the code that
151
+ * needs it fails at runtime instead of here.
152
+ * - a withheld secret the Worker already holds. Filtering it out of this deploy's payload cannot
153
+ * remove it, so the scope would be nominal rather than enforced until someone deletes it.
154
+ *
155
+ * A value is never read, never formatted, and never named beyond its key.
156
+ */
157
+ export function stageSecretRefusal(plan, exported, held, workerName) {
158
+ const unavailable = plan.required.filter((name) => !exported.has(name) && !held.has(name));
159
+ const installed = plan.withheld.filter((name) => held.has(name));
160
+ if (unavailable.length === 0 && installed.length === 0)
161
+ return undefined;
162
+ const lines = [`Refusing to deploy ${workerName} to ${plan.stage}.`];
163
+ if (unavailable.length > 0) {
164
+ lines.push(`Stage ${plan.stage} requires these secrets and no value exists for them, neither in this environment nor on the Worker:`, ...unavailable.map((name) => ` ${name} — ${scopeDescription(name, plan.scopes)}`), 'Export each one in the deploying shell before the deploy. Once the Worker exists,', `\`wrangler secret put <NAME> --name ${workerName}\` supplies it too.`);
165
+ }
166
+ if (installed.length > 0) {
167
+ lines.push(`The Worker holds these secrets, which smoo.wrangler.secretStages keeps out of ${plan.stage}:`, ...installed.map((name) => ` ${name} — ${scopeDescription(name, plan.scopes)}`), `Delete each one with \`wrangler secret delete <NAME> --name ${workerName}\`. A deploy of this stage`, 'never sends them, so leaving them installed would keep the scope nominal.');
168
+ }
169
+ return lines.join('\n');
170
+ }
171
+ function scopeDescription(name, scopes) {
172
+ const scope = scopes[name];
173
+ if (scope === undefined)
174
+ return 'unscoped, so every stage requires it';
175
+ if (scope.length === 0)
176
+ return 'scoped to no stage (local development only)';
177
+ return `scoped to ${scope.join(', ')}`;
178
+ }
@@ -354,7 +354,15 @@
354
354
  export GOFLAGS="''${GOFLAGS:--trimpath}"
355
355
  unset GOROOT
356
356
  bun "$DEVENV_ROOT/setup-environment.ts" || exit $?
357
- export NX_SOCKET_DIR="$DEVENV_RUNTIME/nx"
357
+ # One socket dir per Nx workspace. DEVENV_RUNTIME is keyed to the devenv
358
+ # ROOT, so every workspace sharing one devenv - a sibling repository, a
359
+ # copy-on-write clone, a scratch workspace created inside this shell -
360
+ # would land on one socket. The first daemon to claim it then refuses
361
+ # every message from the others ("received a message from a different
362
+ # workspace"), which reads as a hung Nx in a workspace that did nothing
363
+ # wrong. Nx's own diagnostic names this exact cause.
364
+ nx_workspace_root="$(cd "$DEVENV_ROOT/../.." >/dev/null 2>&1 && pwd || printf '%s' "$PWD")"
365
+ export NX_SOCKET_DIR="$DEVENV_RUNTIME/nx-$(printf '%s' "$nx_workspace_root" | cksum | cut -d' ' -f1)"
358
366
  mkdir -p "$NX_SOCKET_DIR"
359
367
  ${lib.optionalString pkgs.stdenv.isDarwin ''
360
368
  unset CC CXX