void 0.10.10 → 0.10.11

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 (103) hide show
  1. package/AGENT_PROMPT.md +4 -0
  2. package/README.md +1 -1
  3. package/dist/{agents-Bmr5tFFb.mjs → agents-CtgBYqld.mjs} +1 -1
  4. package/dist/{auth-cmd-Cw1edJdZ.mjs → auth-cmd-BqsdZJp5.mjs} +3 -3
  5. package/dist/{better-auth-shared-BvnM9px6.d.mts → better-auth-shared-DealXecJ.d.mts} +1 -1
  6. package/dist/{build-cmd-C6kNDJBv.mjs → build-cmd-Bujrv5q-.mjs} +3 -3
  7. package/dist/{cache-DIUSnIjJ.mjs → cache-C11V8Fxq.mjs} +3 -3
  8. package/dist/{cancel-deploy-BosnzEHC.mjs → cancel-deploy-fwFYF04b.mjs} +2 -2
  9. package/dist/cli/cli.mjs +124 -41
  10. package/dist/cli/env-schema-probe.d.mts +96 -0
  11. package/dist/cli/env-schema-probe.mjs +272 -0
  12. package/dist/{client-C9FG6Vzc.mjs → client-Gb71-XkG.mjs} +12 -2
  13. package/dist/{config-BdUctCZD.mjs → config-CutEMNGJ.mjs} +3 -3
  14. package/dist/{config-BcAeIPe3.mjs → config-E03l1C_h.mjs} +2 -2
  15. package/dist/{create-project-CjYX23M_.mjs → create-project-DsYvl3TB.mjs} +3 -3
  16. package/dist/{db-BvKV34IK.mjs → db-PZBsLSGb.mjs} +27 -27
  17. package/dist/{delete-CThKLXfi.mjs → delete-mh6p-zkQ.mjs} +3 -3
  18. package/dist/deploy-DT8wsPZd.mjs +6990 -0
  19. package/dist/{discover-BuVVSAum.mjs → discover-CJHyvYfR.mjs} +2 -2
  20. package/dist/dist-DaKKDf8D.mjs +41 -0
  21. package/dist/{domain-DMrBabBQ.mjs → domain-DiaNQbrl.mjs} +2 -2
  22. package/dist/entry-D7yy4xVH.mjs +100 -0
  23. package/dist/{env-BfG7O71F.mjs → env-AAHU02L6.mjs} +5 -5
  24. package/dist/env-mask-Dd47NbR6.mjs +90 -0
  25. package/dist/env-public-BfiLcMBk.d.mts +140 -0
  26. package/dist/{env-types-D51bnR-c.mjs → env-types-QBj-ndax.mjs} +2 -2
  27. package/dist/env-validation-BdDlGhbN.mjs +1069 -0
  28. package/dist/{gen-Bee261SV.mjs → gen-oup1xBN0.mjs} +8 -8
  29. package/dist/{github-cmd-DfeFEasr.mjs → github-cmd-BdNaOVNa.mjs} +3 -3
  30. package/dist/{handler-imD0UVDT.d.mts → handler-Cjh8uM3Y.d.mts} +1 -1
  31. package/dist/{headers-CTAjX-UO.mjs → headers-BQknpzkn.mjs} +2 -2
  32. package/dist/index.d.mts +1 -1
  33. package/dist/index.mjs +41 -33
  34. package/dist/{init-xfwW7cLp.mjs → init-Dl2PKuQn.mjs} +13 -14
  35. package/dist/{link-Co136MEs.mjs → link-CdGHSIy-.mjs} +4 -4
  36. package/dist/{list-BKDjA3M5.mjs → list-CPwFDZ_c.mjs} +3 -3
  37. package/dist/{login-CrOTnR_6.mjs → login-BT3H8PN3.mjs} +2 -2
  38. package/dist/{logs-DlAg25ww.mjs → logs-Bt313ax7.mjs} +3 -2
  39. package/dist/{mcp-BNjMGMB0.mjs → mcp-DoM3_nhd.mjs} +7 -2
  40. package/dist/{node-CnE-SZCk.mjs → node-BDx8pmhq.mjs} +5 -5
  41. package/dist/{package-json-B0NuUWGd.mjs → package-json-Cx1osYo6.mjs} +1 -1
  42. package/dist/pages/client.d.mts +1 -1
  43. package/dist/pages/index.d.mts +1 -23
  44. package/dist/pages/index.mjs +5 -5
  45. package/dist/pages/islands-plugin.mjs +2 -2
  46. package/dist/pages/protocol.d.mts +2 -2
  47. package/dist/{plugin-inference-CJxi_fWI.mjs → plugin-inference-DMeavIJ6.mjs} +4 -98
  48. package/dist/{prepare-BRyC4TCC.mjs → prepare-DOBTY0o4.mjs} +10 -10
  49. package/dist/preset-BGrvB4Bl.mjs +539 -0
  50. package/dist/{project-cmd-B2bcKefD.mjs → project-cmd-D_w-4w5B.mjs} +13 -9
  51. package/dist/{project-paths-tpdR1mJR.mjs → project-paths-BQd7OmIo.mjs} +1 -1
  52. package/dist/project-paths-GpziKeQQ.d.mts +25 -0
  53. package/dist/{project-tsconfig-D9uSVVpA.mjs → project-tsconfig-B-QtXjLQ.mjs} +2 -2
  54. package/dist/{protocol-6hTJ04T1.d.mts → protocol-Bnb0LFp3.d.mts} +1 -1
  55. package/dist/provision-BLrCEBbI.mjs +2557 -0
  56. package/dist/requests-B8sZxaFM.mjs +50 -0
  57. package/dist/{resolve-project-D2HI3TrG.mjs → resolve-project-BBMtLLV9.mjs} +1 -1
  58. package/dist/{rollback-9dfmB-YZ.mjs → rollback-CkvTFXx5.mjs} +2 -2
  59. package/dist/{route-types-CfKfhbIg.mjs → route-types-COI2DsZv.mjs} +2 -2
  60. package/dist/{runner-h272wcPj.mjs → runner-kapo9aPs.mjs} +3 -3
  61. package/dist/{runner-pg-waxJOnBb.mjs → runner-pg-CHM76xuC.mjs} +1 -1
  62. package/dist/runtime/better-auth-pg.d.mts +1 -1
  63. package/dist/runtime/better-auth.d.mts +1 -1
  64. package/dist/runtime/env-public.d.mts +1 -139
  65. package/dist/runtime/env-public.mjs +2 -90
  66. package/dist/runtime/handler.d.mts +1 -1
  67. package/dist/runtime/live.d.mts +1 -1
  68. package/dist/runtime/validator.d.mts +1 -1
  69. package/dist/runtime/ws-server.d.mts +1 -1
  70. package/dist/runtime/ws.d.mts +2 -2
  71. package/dist/{scan-Dp_Gyzs3.mjs → scan-DEwlM_Xy.mjs} +2 -2
  72. package/dist/{scan-i7Yz54fv.mjs → scan-DYXkrasO.mjs} +4 -4
  73. package/dist/{secret-DeMnMcV0.mjs → secret-Dt32J6RI.mjs} +3 -3
  74. package/dist/{skills-DsdNDtX3.mjs → skills-CLjN0uUO.mjs} +2 -2
  75. package/dist/{subcommand-prompt-DtES-oP6.mjs → subcommand-prompt-BzV8iQZo.mjs} +1 -1
  76. package/dist/sveltekit.mjs +1 -1
  77. package/dist/{validate-DT7nFMlf.mjs → validate-Cw_RLeTj.mjs} +1 -1
  78. package/dist/{yarn-pnp-CW8LB6g_.mjs → yarn-pnp-DJn3SAHF.mjs} +1 -1
  79. package/getting-started-prompt.txt +3 -1
  80. package/package.json +5 -3
  81. package/skills/void/SKILL.md +34 -33
  82. package/skills/void/docs/guide/auth.md +8 -0
  83. package/skills/void/docs/guide/deployment.md +1 -1
  84. package/skills/void/docs/guide/env-vars.md +14 -6
  85. package/skills/void/docs/guide/queues.md +4 -0
  86. package/skills/void/docs/integrations/cloudflare.md +64 -5
  87. package/skills/void/docs/node_modules/void/AGENT_PROMPT.md +4 -0
  88. package/skills/void/docs/node_modules/void/README.md +1 -1
  89. package/skills/void/docs/node_modules/void/node_modules/@types/proper-lockfile/README.md +51 -0
  90. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathslash/README.md +64 -0
  91. package/skills/void/docs/node_modules/void/node_modules/pathslash/README.md +64 -0
  92. package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  93. package/skills/void/docs/node_modules/void/node_modules/proper-lockfile/README.md +183 -0
  94. package/skills/void/docs/node_modules/void/skills/void/SKILL.md +34 -33
  95. package/skills/void/docs/node_modules/void/test/e2e/README.md +85 -0
  96. package/skills/void/docs/reference/cli.md +74 -14
  97. package/dist/deploy-B02JyKP9.mjs +0 -3748
  98. package/dist/dotenv-D_UbC_vc.mjs +0 -173
  99. package/dist/env-validation-CeC2FL66.mjs +0 -163
  100. package/dist/pathe.M-eThtNZ-CQzLbt4c.mjs +0 -150
  101. package/dist/preset-CVvwCeIy.mjs +0 -208
  102. package/skills/void/docs/node_modules/void/node_modules/@void/md/node_modules/pathe/README.md +0 -73
  103. package/skills/void/docs/node_modules/void/node_modules/pathe/README.md +0 -73
@@ -0,0 +1,2557 @@
1
+ import { a as join, c as resolve, n as dirname, o as normalize, t as basename } from "./dist-DaKKDf8D.mjs";
2
+ import { b as readProvisionedBindingEntry, f as assertNoDuplicateMembers, w as writeProvisionedWranglerConfig } from "./deploy-DT8wsPZd.mjs";
3
+ import { i as voidWarn, r as voidLog } from "./log-ChfPKsVd.mjs";
4
+ import { o as resolveWranglerArgv, u as scanWranglerDotenvKeys } from "./preset-BGrvB4Bl.mjs";
5
+ import { createRequire } from "node:module";
6
+ import { closeSync, existsSync, fsyncSync, lstatSync, openSync, readFileSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
7
+ import { randomUUID } from "node:crypto";
8
+ import { spawn } from "node:child_process";
9
+ import { types } from "node:util";
10
+ import { parse } from "jsonc-parser";
11
+ import lockfile from "proper-lockfile";
12
+ //#region src/cli/provision.ts
13
+ /**
14
+ * Diff the DESIRED id-bearing resources against the ACTUAL bindings in the root
15
+ * wrangler config, WITHOUT any remote/wrangler call. A resource counts as
16
+ * provisioned ONLY when its binding entry exists with a REAL (non-placeholder)
17
+ * identifier — the SAME predicate the provisioner's read-first uses:
18
+ * - D1 `database_id` is a real UUID ({@link isRealD1Id}, not `'local'`);
19
+ * - KV `id` is a real 32-hex ({@link isRealKvId}, not `'local'`);
20
+ * - R2 `bucket_name` names a real bucket ({@link isRealStoredR2BucketName}, not `'default'`);
21
+ * - Hyperdrive `id` is a real 32-hex ({@link isRealHyperdriveId}).
22
+ *
23
+ * Queues are NAME-bearing (no id in the config), so they are NOT part of this
24
+ * id-drift check. A missing config file yields every desired resource as missing.
25
+ *
26
+ * Used by the deploy orchestrator (Task 6) for BOTH the no-`--provision` DRIFT
27
+ * CHECK (any `missing` → guidance error) and the CI `--provision` no-op gate
28
+ * (`missing.length === 0` ⇒ a provable no-op the committed config already covers).
29
+ * A malformed/duplicate-binding config makes {@link readProvisionedBindingEntry}
30
+ * throw — fail closed, exactly as the provisioner would.
31
+ */
32
+ function checkCloudflareProvisionDrift(configPath, desired) {
33
+ const config = readRootConfig(configPath);
34
+ const missing = [];
35
+ const isProvisioned = (field, binding, hasRealId) => {
36
+ const entry = readProvisionedBindingEntry(config, field, binding, configPath);
37
+ return entry !== void 0 && hasRealId(entry);
38
+ };
39
+ if (desired.d1) {
40
+ if (!isProvisioned("d1_databases", desired.d1.binding, (e) => isRealD1Id(e.database_id))) missing.push({
41
+ binding: desired.d1.binding,
42
+ kind: "d1",
43
+ name: desired.d1.name
44
+ });
45
+ }
46
+ for (const kv of desired.kvNamespaces) if (!isProvisioned("kv_namespaces", kv.binding, (e) => isRealKvId(e.id))) missing.push({
47
+ binding: kv.binding,
48
+ kind: "kv",
49
+ name: kv.title
50
+ });
51
+ if (desired.r2) {
52
+ if (!isProvisioned("r2_buckets", desired.r2.binding, (e) => isRealStoredR2BucketName(e.bucket_name))) missing.push({
53
+ binding: desired.r2.binding,
54
+ kind: "r2",
55
+ name: desired.r2.bucketName
56
+ });
57
+ }
58
+ if (desired.hyperdrive) {
59
+ if (!isProvisioned("hyperdrive", desired.hyperdrive.binding, (e) => isRealHyperdriveId(e.id))) missing.push({
60
+ binding: desired.hyperdrive.binding,
61
+ kind: "hyperdrive",
62
+ name: desired.hyperdrive.name
63
+ });
64
+ }
65
+ return { missing };
66
+ }
67
+ /** Where Void keeps D1 migrations — written as `migrations_dir` on the D1 entry. */
68
+ const D1_MIGRATIONS_DIR = "./db/migrations";
69
+ /** UTF-8 byte-order mark. Node's utf-8 read preserves a leading one. */
70
+ const UTF8_BOM = "";
71
+ const D1_ID_PLACEHOLDER = "local";
72
+ const KV_ID_PLACEHOLDER = "local";
73
+ const R2_BUCKET_NAME_PLACEHOLDER = "default";
74
+ const D1_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
75
+ const HEX32_ID_RE = /^[0-9a-f]{32}$/i;
76
+ /** R2 bucket names ARE the identity: wrangler 4.107's `isValidR2BucketName`. */
77
+ const R2_BUCKET_NAME_RE = /^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/;
78
+ /**
79
+ * A D1 `database_id` that is a real (non-placeholder) hyphenated UUID. Exported so
80
+ * the deploy orchestrator applies the SAME placeholder rule when deciding whether a
81
+ * stored `database_name` names a real DB (a synced `database_id: "local"` must not
82
+ * let its paired name become the production database identity).
83
+ */
84
+ function isRealD1Id(value) {
85
+ return typeof value === "string" && value !== D1_ID_PLACEHOLDER && D1_ID_RE.test(value);
86
+ }
87
+ /**
88
+ * A KV namespace `id` that is a real (non-placeholder) 32-hex string. Exported so the
89
+ * deploy orchestrator applies the SAME placeholder rule when checking whether two KV
90
+ * bindings already share one provisioned namespace.
91
+ */
92
+ function isRealKvId(value) {
93
+ return typeof value === "string" && value !== KV_ID_PLACEHOLDER && HEX32_ID_RE.test(value);
94
+ }
95
+ /** A Hyperdrive config `id` that is a real 32-hex string (no placeholder exists). */
96
+ function isRealHyperdriveId(value) {
97
+ return typeof value === "string" && HEX32_ID_RE.test(value);
98
+ }
99
+ /** An R2 `bucket_name` satisfying wrangler's bucket-name GRAMMAR (format only). */
100
+ function isValidR2BucketNameFormat(value) {
101
+ return typeof value === "string" && R2_BUCKET_NAME_RE.test(value);
102
+ }
103
+ /**
104
+ * A STORED R2 `bucket_name` that names a REAL bucket, i.e. any non-empty string
105
+ * that is not R2's own placeholder (`default`). `local` IS a real name here. Grammar is
106
+ * deliberately not required: a grammar-invalid stored name that contradicts the desired
107
+ * bucket must still fail closed as a contradiction, never be silently rewritten.
108
+ */
109
+ function isRealStoredR2BucketName(value) {
110
+ return typeof value === "string" && value.length > 0 && value !== R2_BUCKET_NAME_PLACEHOLDER;
111
+ }
112
+ const defaultLogger = {
113
+ info: (msg) => voidLog(msg),
114
+ warn: (msg) => voidWarn(msg)
115
+ };
116
+ /**
117
+ * The transport-redirecting env vars wrangler 4.107 honors. Each can point
118
+ * wrangler's authenticated API transport at a DIFFERENT endpoint (or select a
119
+ * different env-block/account) than the ONE this provision pinned:
120
+ * - `CLOUDFLARE_API_BASE_URL` / `CF_API_BASE_URL` — override the API base URL directly
121
+ * (`getCloudflareApiBaseUrlFromEnv`).
122
+ * - `WRANGLER_API_ENVIRONMENT=staging` — routes to `api.staging.cloudflare.com`.
123
+ * - `CLOUDFLARE_COMPLIANCE_REGION=fedramp_high` — routes to `api.fed.cloudflare.com`.
124
+ * - `CLOUDFLARE_ENV` — selects a named `env.*` config block (different account/bindings).
125
+ * We pin ONE resolved base explicitly (see {@link provisionEnv}) and fail closed when a
126
+ * project dotenv would reselect one (see {@link assertNoTransportRedirectInProjectDotenv}).
127
+ */
128
+ const TRANSPORT_REDIRECT_ENV_VARS = [
129
+ "CLOUDFLARE_API_BASE_URL",
130
+ "CF_API_BASE_URL",
131
+ "WRANGLER_API_ENVIRONMENT",
132
+ "CLOUDFLARE_COMPLIANCE_REGION",
133
+ "CLOUDFLARE_ENV"
134
+ ];
135
+ /**
136
+ * Wrangler 4.107's diagnostic-output sinks that would PERSIST the raw provisioning
137
+ * argv (including a Hyperdrive connection string, password and all) to disk:
138
+ * - `WRANGLER_OUTPUT_FILE_PATH` / `WRANGLER_OUTPUT_FILE_DIRECTORY` — when either is set
139
+ * (`getOutputFilePathFromEnv`/`getOutputFileDirectoryFromEnv`), wrangler's startup
140
+ * middleware appends a `wrangler-session` entry with the VERBATIM
141
+ * `command_line_args: argv` to that file (`writeOutput` in wrangler-dist/cli.js) — so
142
+ * `hyperdrive create --connection-string=postgres://user:PASSWORD@…` lands on disk.
143
+ * They are scrubbed from every provisioner child env ({@link provisionEnv}) and fenced in
144
+ * the project dotenv ({@link assertNoTransportRedirectInProjectDotenv}) — wrangler reloads
145
+ * dotenv into `process.env` BEFORE writing the session entry, so a dotenv-declared sink
146
+ * would resurrect the leak. `WRANGLER_LOG_SANITIZE` (`getSanitizeLogs`, default true) is
147
+ * PINNED to `true` instead: a truthy ambient value survives wrangler's dotenv reload
148
+ * (dotenv-expand keeps an already-set truthy `process.env` value), so the pin cannot be
149
+ * overridden into unsanitized logging of the secret-bearing API request.
150
+ */
151
+ const OUTPUT_FILE_ENV_VARS = ["WRANGLER_OUTPUT_FILE_PATH", "WRANGLER_OUTPUT_FILE_DIRECTORY"];
152
+ /**
153
+ * Resolve the ONE Cloudflare API base URL this provision is pinned to, mirroring
154
+ * wrangler 4.107's `getCloudflareApiBaseUrl` EXACTLY (wrangler-dist/cli.js) so the
155
+ * Hyperdrive REST lister and the spawned wrangler agree on the endpoint:
156
+ *
157
+ * getCloudflareApiBaseUrl = (complianceConfig) =>
158
+ * getCloudflareApiBaseUrlFromEnv() ??
159
+ * `https://api${getComplianceRegionSubdomain(complianceConfig)}${getStagingSubdomain()}.cloudflare.com/client/v4`
160
+ *
161
+ * Order + semantics mirrored exactly:
162
+ * 1. The DIRECT base override is evaluated FIRST, by PRESENCE (wrangler's env factory
163
+ * checks `variableName in process.env`), with `CLOUDFLARE_API_BASE_URL` taking
164
+ * precedence over the deprecated `CF_API_BASE_URL`. A present-but-EMPTY override is
165
+ * returned AS-IS: `"" ?? derived` keeps the empty string (NULLISH, not falsy,
166
+ * coalescing), so wrangler pins the empty base (its API calls then fail on the
167
+ * unparsable URL) — we mirror that instead of silently substituting an endpoint.
168
+ * 2. Only on the DERIVED branch are the staging/compliance selectors read — wrangler's
169
+ * `??` short-circuits past `getComplianceRegionSubdomain`/`getStagingSubdomain` when a
170
+ * direct base is present, so a direct base + conflicting compliance settings resolves
171
+ * to the direct base in wrangler (NO conflict error) and must not be rejected here.
172
+ * 3. On the derived branch both env selectors are choice-VALIDATED (wrangler's
173
+ * `getProcessEnv` → `assertOneOf` throws on any present value outside the choices,
174
+ * e.g. `WRANGLER_API_ENVIRONMENT=stagng` or an empty string), so an invalid selector
175
+ * FAILS CLOSED instead of silently pinning public production.
176
+ * 4. The config's own `compliance_region` VALUE is validated UNCONDITIONALLY — wrangler's
177
+ * config validation (`isOneOf("public", "fedramp_high")` on the `compliance_region`
178
+ * field) rejects an invalid config value for every command regardless of which branch
179
+ * resolves the base.
180
+ * Read ONCE from the REAL ambient `process.env` (which does NOT include the project `.env`
181
+ * — Node never auto-loads it), so an ambient/CI pin is honored while a project dotenv
182
+ * redirect is rejected up front by {@link assertNoTransportRedirectInProjectDotenv}.
183
+ */
184
+ function resolvePinnedApiBaseUrl(env = process.env, configComplianceRegion) {
185
+ const configRegion = configComplianceRegion !== void 0 ? assertComplianceChoice(configComplianceRegion, "the wrangler config \"compliance_region\"") : void 0;
186
+ const direct = env.CLOUDFLARE_API_BASE_URL !== void 0 ? env.CLOUDFLARE_API_BASE_URL : env.CF_API_BASE_URL;
187
+ if (direct !== void 0) return direct;
188
+ return `https://api${resolveComplianceRegion(env, configRegion) === "fedramp_high" ? ".fed" : ""}${resolveApiEnvironment(env) === "staging" ? ".staging" : ""}.cloudflare.com/client/v4`;
189
+ }
190
+ /**
191
+ * Mirror wrangler 4.107's `getCloudflareApiEnvironmentFromEnv` (env factory with
192
+ * `choices: ["production", "staging"]`, default `"production"`): an ABSENT variable
193
+ * defaults to production; a PRESENT value outside the choices — `"stagng"`, an empty
194
+ * string — makes wrangler's `assertOneOf` throw, so we fail closed identically instead of
195
+ * silently pinning public production where wrangler stops.
196
+ */
197
+ function resolveApiEnvironment(env) {
198
+ const raw = env.WRANGLER_API_ENVIRONMENT;
199
+ if (raw === void 0) return "production";
200
+ if (raw !== "production" && raw !== "staging") throw new Error(`provision: the WRANGLER_API_ENVIRONMENT environment variable must be one of "production" or "staging", got ${JSON.stringify(raw)}. wrangler rejects this value, so provisioning refuses to guess an endpoint; fix or unset it and retry.`);
201
+ return raw;
202
+ }
203
+ /** The compliance-region choices wrangler 4.107 accepts (`getCloudflareComplianceRegionFromEnv`). */
204
+ const COMPLIANCE_REGIONS = ["public", "fedramp_high"];
205
+ function assertComplianceChoice(value, source) {
206
+ if (value !== "public" && value !== "fedramp_high") throw new Error(`provision: ${source} must be one of ${COMPLIANCE_REGIONS.map((c) => JSON.stringify(c)).join(" or ")}, got ${JSON.stringify(value)}.`);
207
+ return value;
208
+ }
209
+ /**
210
+ * Reconcile the effective Cloudflare compliance region, mirroring wrangler 4.107's
211
+ * `getCloudflareComplianceRegion(complianceConfig)` EXACTLY (evaluated only on the
212
+ * DERIVED-base branch — with a direct base override wrangler never calls it):
213
+ * - reads `CLOUDFLARE_COMPLIANCE_REGION` by PRESENCE and validates it against the
214
+ * `public`/`fedramp_high` choices (wrangler's `getProcessEnv` → `assertOneOf`, which
215
+ * throws on ANY present out-of-choices value — an empty string included);
216
+ * - when BOTH the env var AND the (already-validated) config region are set to
217
+ * DIFFERENT values, THROWS (fail closed) — wrangler raises a `UserError` for exactly
218
+ * this conflict, so we must not paper over it by pinning one endpoint while the config
219
+ * declares the other region;
220
+ * - otherwise returns `env || config || "public"` (both values are validated choices by
221
+ * now, so wrangler's falsy-coalescing is equivalent to nullish here).
222
+ */
223
+ function resolveComplianceRegion(env, configRegion) {
224
+ const rawEnv = env.CLOUDFLARE_COMPLIANCE_REGION;
225
+ const envRegion = rawEnv !== void 0 ? assertComplianceChoice(rawEnv, "the CLOUDFLARE_COMPLIANCE_REGION environment variable") : void 0;
226
+ if (envRegion !== void 0 && configRegion !== void 0 && envRegion !== configRegion) throw new Error(`provision: the Cloudflare compliance region is set to conflicting values — the CLOUDFLARE_COMPLIANCE_REGION environment variable is ${JSON.stringify(envRegion)} but the wrangler config "compliance_region" is ${JSON.stringify(configRegion)}. wrangler refuses to run with a mismatched compliance region; reconcile them (remove one, or set them equal) and retry.`);
227
+ return envRegion ?? configRegion ?? "public";
228
+ }
229
+ /**
230
+ * Mirror wrangler 4.107's `getDefaultEnvFiles(env)`: base `.env`/`.env.local`, then —
231
+ * when the environment argument is DEFINED (we always pass `--env=`, so wrangler sees
232
+ * `args.env === ""`, which is `!== undefined`) — the per-env pair `.env.${env}`/`.env.${env}.local`.
233
+ * With the empty environment those are the empty-env filenames `.env.` and `.env..local`.
234
+ */
235
+ function defaultWranglerEnvFiles(env) {
236
+ return [
237
+ ".env",
238
+ ".env.local",
239
+ `.env.${env}`,
240
+ `.env.${env}.local`
241
+ ];
242
+ }
243
+ /**
244
+ * FAIL CLOSED when the project's dotenv files declare a transport-redirecting
245
+ * variable. wrangler 4.107 reloads `.env`/`.env.local` (and the dev-vars files) from the
246
+ * project root into `process.env` on EVERY command with `override: true`, so a repo `.env`
247
+ * setting `CLOUDFLARE_API_BASE_URL` / `WRANGLER_API_ENVIRONMENT` /
248
+ * `CLOUDFLARE_COMPLIANCE_REGION` / `CLOUDFLARE_ENV` would redirect wrangler's authenticated
249
+ * transport (and credentials) to a staging / FedRAMP / arbitrary endpoint AFTER we resolved
250
+ * the pinned base — while the Hyperdrive existence check runs against the pinned base.
251
+ * Single-account/single-endpoint provisioning cannot reason about a transport redirected
252
+ * out from under it, so we refuse. The fix is to export the variable in the shell/CI
253
+ * environment (which we DO honor, pinning both wrangler and the lister to it) instead of
254
+ * the project dotenv.
255
+ *
256
+ * Exported for direct unit testing of the file-set and the Windows case rule; production
257
+ * always calls it with the real `process.platform`.
258
+ */
259
+ function assertNoTransportRedirectInProjectDotenv(root, platform = process.platform) {
260
+ const dotenvCaseInsensitive = platform === "win32";
261
+ const devVarsFile = existsSync(join(root, ".dev.vars.")) ? ".dev.vars." : ".dev.vars";
262
+ const files = [...defaultWranglerEnvFiles("").map((file) => ({
263
+ file,
264
+ caseInsensitive: dotenvCaseInsensitive
265
+ })), {
266
+ file: devVarsFile,
267
+ caseInsensitive: false
268
+ }];
269
+ for (const { file, caseInsensitive } of files) {
270
+ const keys = scanWranglerDotenvKeys(join(root, file));
271
+ const lowerKeys = caseInsensitive ? new Set([...keys].map((key) => key.toLowerCase())) : void 0;
272
+ const declares = (name) => lowerKeys !== void 0 ? lowerKeys.has(name.toLowerCase()) : keys.has(name);
273
+ const offenders = TRANSPORT_REDIRECT_ENV_VARS.filter(declares);
274
+ if (offenders.length > 0) throw new Error(`provision: refusing to provision — the project file "${file}" sets ${offenders.join(", ")}, which wrangler reloads on every command and would redirect its authenticated Cloudflare API transport (and credentials) to a different endpoint/account than this provision is pinned to (the Hyperdrive existence check would then run against a DIFFERENT endpoint, risking cross-environment writes or duplicate resources). Remove ${offenders.join(", ")} from "${file}" and, if you truly need a non-default endpoint, export it in your shell/CI environment instead — that pin IS honored for both wrangler and the existence check.`);
275
+ const outputOffenders = OUTPUT_FILE_ENV_VARS.filter(declares);
276
+ if (outputOffenders.length > 0) throw new Error(`provision: refusing to provision — the project file "${file}" sets ${outputOffenders.join(", ")}, which wrangler reloads on every command and which makes wrangler persist its session diagnostics — including the verbatim command line, where a Hyperdrive connection string's password would appear — to that output file. Remove ${outputOffenders.join(", ")} from "${file}" (set it in your shell only for non-provisioning wrangler commands if you need session output).`);
277
+ }
278
+ }
279
+ /**
280
+ * Child-process env with the CF Access service-token PAIR scrubbed. Mirrors
281
+ * `buildEnv` in `deploy.ts`: a shared, long-lived Access token is needed only by
282
+ * this CLI's own API client and must never leak into the spawned wrangler
283
+ * process (which may in turn run user hooks). The cloudflared `CF_ACCESS_TOKEN`
284
+ * JWT (dev path) is left untouched.
285
+ *
286
+ * Pins ONE transport context: scrubs every {@link TRANSPORT_REDIRECT_ENV_VARS}
287
+ * from the child env, then re-sets `CLOUDFLARE_API_BASE_URL` to the SINGLE resolved
288
+ * `apiBaseUrl` so the spawned wrangler and the Hyperdrive REST lister hit the exact same
289
+ * endpoint. Scrubbing the compliance/staging/env selectors makes the explicit base
290
+ * authoritative (they can no longer re-derive a different base). Combined with
291
+ * {@link assertNoTransportRedirectInProjectDotenv} (which blocks a project dotenv from
292
+ * repopulating any of them via wrangler's per-command reload), the pin cannot be moved.
293
+ */
294
+ /**
295
+ * Delete `name` from `env` IN PLACE, honoring the platform's env-key case rule.
296
+ * Windows environment keys are CASE-INSENSITIVE: a spawned child's `process.env`
297
+ * resolves `WRANGLER_OUTPUT_FILE_PATH` from an ambient `Wrangler_Output_File_Path`
298
+ * too, so an exact-case `delete` leaves a mixed-case spelling live. On win32 every
299
+ * case-insensitive spelling is removed; elsewhere POSIX env vars are case-sensitive,
300
+ * so only the exact key is a match. Exported so the deploy orchestrator scrubs its
301
+ * OWN extra deploy-time keys (worker-name override, build-config-path) through the
302
+ * SAME rule the provisioner uses, instead of re-deriving it.
303
+ */
304
+ function deleteEnvVarCaseAware(env, name, platform = process.platform) {
305
+ if (platform !== "win32") {
306
+ delete env[name];
307
+ return;
308
+ }
309
+ const lower = name.toLowerCase();
310
+ for (const key of Object.keys(env)) if (key.toLowerCase() === lower) delete env[key];
311
+ }
312
+ function provisionEnv(ctx) {
313
+ const env = { ...process.env };
314
+ const platform = ctx?.platform ?? process.platform;
315
+ const scrub = (name) => deleteEnvVarCaseAware(env, name, platform);
316
+ scrub("CF_ACCESS_CLIENT_ID");
317
+ scrub("CF_ACCESS_CLIENT_SECRET");
318
+ for (const name of TRANSPORT_REDIRECT_ENV_VARS) scrub(name);
319
+ for (const name of OUTPUT_FILE_ENV_VARS) scrub(name);
320
+ scrub("WRANGLER_LOG_SANITIZE");
321
+ env.WRANGLER_LOG_SANITIZE = "true";
322
+ if (ctx?.apiBaseUrl !== void 0) env.CLOUDFLARE_API_BASE_URL = ctx.apiBaseUrl;
323
+ if (ctx?.accountId) {
324
+ scrub("CLOUDFLARE_ACCOUNT_ID");
325
+ env.CLOUDFLARE_ACCOUNT_ID = ctx.accountId;
326
+ }
327
+ return env;
328
+ }
329
+ /**
330
+ * Hard per-command timeout for a single wrangler invocation. A hung wrangler must
331
+ * NEVER hang the whole provision, so the default runner SIGKILLs a child that runs
332
+ * past this and reports a HARD failure (a non-null signal — see
333
+ * {@link normalizeRunWranglerResult}). With the ASYNC runner this bound is
334
+ * INDEPENDENT of {@link LOCK_STALE_MS}: awaiting the child keeps the event loop free,
335
+ * so `proper-lockfile`'s heartbeat refreshes the mutex throughout — the timeout only
336
+ * bounds a hang, it is not load-bearing for lock safety.
337
+ */
338
+ const COMMAND_TIMEOUT_MS = 12e4;
339
+ /**
340
+ * After a timeout SIGKILL, how long to keep confirming the child is ACTUALLY gone —
341
+ * BOTH its `close` event AND whole-group disappearance (`kill(-pid, 0)` → ESRCH) — before
342
+ * giving up. SIGKILL is uncatchable, so a healthy kernel reaps the group within
343
+ * milliseconds; this generous bound only guards a pathological uninterruptible-sleep child.
344
+ * The runner NEVER settles a timeout as "done" until both facts are confirmed; an expiry
345
+ * (or a failed kill) REJECTS with a {@link WranglerQuarantineError} instead, so the
346
+ * whole-provision lock — released only after a CONFIRMED settle — is intentionally
347
+ * retained while the real wrangler CLI may still be creating resources.
348
+ */
349
+ const CONFIRM_KILL_GRACE_MS = 5e3;
350
+ /** The pid-poll interval while confirming post-SIGKILL termination. */
351
+ const CONFIRM_KILL_POLL_MS = 20;
352
+ /**
353
+ * The margin the coordinator-WIDE teardown deadline adds on top of a
354
+ * runner's own confirmation grace window: a signal teardown is bounded at
355
+ * `confirmGraceMs + this` even if the runner's bounded confirmation never calls back.
356
+ */
357
+ const SIGNAL_TEARDOWN_DEADLINE_MARGIN_MS = 5e3;
358
+ /**
359
+ * Classify a process-group kill error. `ESRCH` means the group ALREADY exited (there was
360
+ * nothing left to kill) → success. ANY other code (`EPERM`, …) is a REAL kill failure: the
361
+ * child may still be running, so it must be surfaced, never swallowed. Exported for direct
362
+ * unit testing of the classification the timeout/cancel paths rely on.
363
+ */
364
+ function isAlreadyGoneKillError(error) {
365
+ return error?.code === "ESRCH";
366
+ }
367
+ /**
368
+ * A QUARANTINE failure: the runner could NOT confirm that a spawned
369
+ * wrangler child (and its whole process group) is dead — the group kill failed for a
370
+ * non-ESRCH reason (EPERM, …), or the post-SIGKILL confirmation window expired with the
371
+ * group still (possibly) alive. The child may STILL be creating resources / writing
372
+ * config, so the whole-provision mutex must be INTENTIONALLY RETAINED — never released —
373
+ * and this error surfaces with manual-recovery guidance naming the surviving pid.
374
+ * {@link provisionCloudflareResources} detects it via {@link isQuarantineError} and calls
375
+ * `lock.quarantine()` instead of `lock.release()`.
376
+ */
377
+ var WranglerQuarantineError = class extends Error {
378
+ quarantine = true;
379
+ pid;
380
+ constructor(pid, detail) {
381
+ const pidLabel = pid !== void 0 ? ` (pid ${pid})` : "";
382
+ super(`provision: the spawned wrangler could not be confirmed terminated${pidLabel} — ${detail}. It may still be running and creating resources, so the cross-process provision lock is intentionally RETAINED (not released) and its owner is recorded in a quarantine sidecar ("<wrangler config>.void-provision.lock.quarantine.json") that blocks concurrent provisions. Recovery: verify the process${pidLabel} is dead (e.g. \`kill -0 <pid>\`; terminate it if not) — the NEXT provision then confirms the recorded group is gone and clears the quarantine automatically. To clear it manually instead, remove the sidecar and the "<wrangler config>.void-provision.lock.lock" directory, then re-run.`);
383
+ this.name = "WranglerQuarantineError";
384
+ this.pid = pid;
385
+ }
386
+ };
387
+ let activeProvisionSlot;
388
+ function acquireProcessProvisionSlot() {
389
+ if (activeProvisionSlot !== void 0) throw new Error("provision: a provision is already active in this process — run provisions sequentially. provisionCloudflareResources supports at most one concurrent provision per process; concurrent provisions across PROCESSES are serialized by the on-disk provision lock instead.");
390
+ if (retentionWatchdog !== void 0) throw new Error(`provision: a previous provision in this process is still retaining its lock — an unconfirmed wrangler child${retentionWatchdog.info.pid !== void 0 ? ` (pid ${retentionWatchdog.info.pid})` : ""} could not be quarantined durably yet. No new provision may start until that retention resolves (the child is confirmed dead, or the quarantine metadata persists durably).`);
391
+ const token = Symbol("void-provision-slot");
392
+ activeProvisionSlot = token;
393
+ return () => {
394
+ if (activeProvisionSlot === token) activeProvisionSlot = void 0;
395
+ };
396
+ }
397
+ /**
398
+ * The ONE active provision's lock-quarantine hook, 1:1 with the
399
+ * provision slot above. When the parent process is about to DIE re-raising a forwarded
400
+ * SIGINT/SIGTERM while a spawned wrangler child's teardown could NOT be confirmed,
401
+ * proper-lockfile's signal-exit sweep would remove the still-TRACKED provision mutex —
402
+ * releasing the lock under a possibly-live child. {@link provisionCloudflareResources}
403
+ * sets this for the duration of the provision; a registering wrangler child CAPTURES it
404
+ * at registration (see {@link registerRunner}) so an unresolved teardown quarantines
405
+ * exactly ITS OWNING provision lock with ITS OWN pid metadata — never a broadcast
406
+ * through every lock (the previous Set this replaces stamped the FIRST unresolved
407
+ * registrant's pid into every lock's sidecar, so a foreign group's death could clear a
408
+ * quarantine whose real child was alive).
409
+ */
410
+ let activeProvisionQuarantineHook;
411
+ /** True when `error` is a quarantine failure whose provision lock must stay retained. */
412
+ function isQuarantineError(error) {
413
+ return error instanceof WranglerQuarantineError || error?.quarantine === true;
414
+ }
415
+ /** The (at most one) currently-registered {runner, owning-lock} pair. */
416
+ let activeRegistration;
417
+ let coordinatorListenersInstalled = false;
418
+ let activeTeardown;
419
+ let reRaisedOnce = false;
420
+ const onCoordinatorSigint = () => coordinatorHandleSignal("SIGINT");
421
+ const onCoordinatorSigterm = () => coordinatorHandleSignal("SIGTERM");
422
+ function installCoordinatorListeners() {
423
+ if (coordinatorListenersInstalled) return;
424
+ coordinatorListenersInstalled = true;
425
+ process.on("SIGINT", onCoordinatorSigint);
426
+ process.on("SIGTERM", onCoordinatorSigterm);
427
+ }
428
+ function removeCoordinatorListeners() {
429
+ if (!coordinatorListenersInstalled) return;
430
+ coordinatorListenersInstalled = false;
431
+ process.removeListener("SIGINT", onCoordinatorSigint);
432
+ process.removeListener("SIGTERM", onCoordinatorSigterm);
433
+ }
434
+ function registerRunner(registrant) {
435
+ if (activeRegistration !== void 0) throw new Error("provision: internal invariant violated — a second wrangler child tried to register while one is already active in this process");
436
+ activeRegistration = {
437
+ registrant,
438
+ lockQuarantine: activeProvisionQuarantineHook
439
+ };
440
+ installCoordinatorListeners();
441
+ }
442
+ function deregisterRunner(registrant) {
443
+ if (activeRegistration?.registrant === registrant) activeRegistration = void 0;
444
+ if (activeRegistration === void 0 && activeTeardown === void 0 && retentionWatchdog === void 0) removeCoordinatorListeners();
445
+ }
446
+ function coordinatorHandleSignal(signal) {
447
+ if (activeTeardown !== void 0) return;
448
+ if (retentionWatchdog !== void 0 && activeRegistration === void 0) {
449
+ process.stderr.write(`provision: ignoring ${signal} — a provision lock is retained while an unconfirmed wrangler child may live and its quarantine metadata is not yet durable. The process exits once the child is confirmed dead or the metadata persists.\n`);
450
+ return;
451
+ }
452
+ const teardown = {
453
+ signal,
454
+ pending: 0,
455
+ dispatching: true,
456
+ persistFailed: false,
457
+ finished: false,
458
+ deadlineTimer: void 0,
459
+ expirePendingPair: void 0
460
+ };
461
+ activeTeardown = teardown;
462
+ const snapshot = activeRegistration !== void 0 ? [activeRegistration] : [];
463
+ teardown.pending = snapshot.length;
464
+ if (snapshot.length > 0) {
465
+ const deadlineMs = Math.max(...snapshot.map((r) => r.registrant.teardownDeadlineMs));
466
+ teardown.deadlineTimer = setTimeout(() => {
467
+ teardown.expirePendingPair?.(`the coordinator-wide ${deadlineMs}ms teardown deadline expired before the process group's termination was confirmed`);
468
+ }, deadlineMs);
469
+ }
470
+ for (const registration of snapshot) coordinatorTearDownRegistrant(teardown, registration);
471
+ teardown.dispatching = false;
472
+ maybeFinishCoordinatorTeardown(teardown);
473
+ }
474
+ /** Finish only after the initial dispatch fully completed AND every
475
+ * snapshot registrant resolved (confirmed or quarantined). */
476
+ function maybeFinishCoordinatorTeardown(teardown) {
477
+ if (!teardown.dispatching && teardown.pending === 0) finishCoordinatorTeardown(teardown);
478
+ }
479
+ function coordinatorTearDownRegistrant(teardown, registration) {
480
+ const { registrant, lockQuarantine } = registration;
481
+ let done = false;
482
+ const complete = () => {
483
+ if (done) return;
484
+ done = true;
485
+ teardown.pending -= 1;
486
+ maybeFinishCoordinatorTeardown(teardown);
487
+ };
488
+ const quarantinePair = (detail) => {
489
+ if (done) return;
490
+ if (lockQuarantine !== void 0) try {
491
+ lockQuarantine({
492
+ pid: registrant.pid(),
493
+ reason: `the spawned wrangler could not be confirmed terminated after a forwarded ${teardown.signal} — ${detail}`
494
+ });
495
+ } catch (failure) {
496
+ teardown.persistFailed = true;
497
+ process.stderr.write(`provision: ${failure.message}\n`);
498
+ }
499
+ const pid = registrant.pid();
500
+ process.stderr.write(`provision: warning — the wrangler child process group${typeof pid === "number" ? ` (pid ${pid})` : ""} could not be confirmed terminated after ${teardown.signal} (${detail}). It may still be running; the provision lock is retained (quarantined) — verify the process is dead before re-running provision.\n`);
501
+ registrant.quarantine(detail);
502
+ complete();
503
+ };
504
+ teardown.expirePendingPair = quarantinePair;
505
+ const outcome = registrant.signalGroup(teardown.signal);
506
+ if (typeof outcome === "object") {
507
+ quarantinePair(`the process group could not be signalled: ${outcome.error.message}`);
508
+ return;
509
+ }
510
+ if (outcome === "gone") registrant.markGroupGone();
511
+ registrant.awaitConfirmedDead((result) => {
512
+ if (result.confirmed) complete();
513
+ else quarantinePair(result.detail);
514
+ });
515
+ }
516
+ function finishCoordinatorTeardown(teardown) {
517
+ if (teardown.finished) return;
518
+ teardown.finished = true;
519
+ if (teardown.deadlineTimer !== void 0) clearTimeout(teardown.deadlineTimer);
520
+ removeCoordinatorListeners();
521
+ if (teardown.persistFailed || retentionWatchdog !== void 0) {
522
+ activeTeardown = void 0;
523
+ installCoordinatorListeners();
524
+ process.stderr.write(`provision: NOT re-raising ${teardown.signal} — a provision lock's quarantine metadata could not be persisted, so the lock is intentionally still held by this process to keep it from being stolen while the unconfirmed wrangler child may live.\n`);
525
+ return;
526
+ }
527
+ if (reRaisedOnce) return;
528
+ reRaisedOnce = true;
529
+ process.kill(process.pid, teardown.signal);
530
+ setTimeout(() => {}, 2e3);
531
+ }
532
+ const RETENTION_RETRY_MIN_MS = 250;
533
+ const RETENTION_RETRY_MAX_MS = 5e3;
534
+ let retentionWatchdog;
535
+ /**
536
+ * Quarantine `lock`, and on a PERSISTENCE failure arm the retention
537
+ * watchdog BEFORE rethrowing. THE single quarantine entry point for provision-level
538
+ * code (the body-error path and the signal coordinator's hook both route through it),
539
+ * so no persistence failure can leave the process free to drain unprotected.
540
+ */
541
+ function retainQuarantinedLock(lock, info) {
542
+ try {
543
+ lock.quarantine(info);
544
+ } catch (error) {
545
+ armRetentionWatchdog(lock, info);
546
+ throw error;
547
+ }
548
+ }
549
+ function armRetentionWatchdog(lock, info) {
550
+ if (retentionWatchdog !== void 0) return;
551
+ process.stderr.write(`provision: warning — quarantine metadata for the unconfirmed wrangler child${info.pid !== void 0 ? ` (pid ${info.pid})` : ""} could not be persisted, so the provision lock "${lock.lockTargetPath}.lock" is RETAINED by this process, which stays alive until the child is confirmed dead or the metadata persists durably.\n`);
552
+ installCoordinatorListeners();
553
+ let attempt = 0;
554
+ const resolveWatchdog = () => {
555
+ if (retentionWatchdog !== void 0) clearTimeout(retentionWatchdog.timer);
556
+ retentionWatchdog = void 0;
557
+ if (activeRegistration === void 0 && activeTeardown === void 0) removeCoordinatorListeners();
558
+ };
559
+ const tick = () => {
560
+ if (info.pid !== void 0) {
561
+ let dead = false;
562
+ try {
563
+ process.kill(process.platform === "win32" ? info.pid : -info.pid, 0);
564
+ } catch (error) {
565
+ dead = error.code === "ESRCH";
566
+ }
567
+ if (dead) {
568
+ lock.release().then(() => resolveWatchdog(), (error) => {
569
+ process.stderr.write(`provision: the retained provision lock could not be released after the child's death was confirmed (${error.message}); the mutex self-heals once stale.\n`);
570
+ resolveWatchdog();
571
+ });
572
+ return;
573
+ }
574
+ }
575
+ try {
576
+ lock.quarantine(info);
577
+ resolveWatchdog();
578
+ return;
579
+ } catch {}
580
+ attempt += 1;
581
+ const delay = Math.min(RETENTION_RETRY_MIN_MS * 2 ** attempt, RETENTION_RETRY_MAX_MS);
582
+ if (retentionWatchdog !== void 0) retentionWatchdog.timer = setTimeout(tick, delay);
583
+ };
584
+ retentionWatchdog = {
585
+ lock,
586
+ info,
587
+ timer: setTimeout(tick, RETENTION_RETRY_MIN_MS)
588
+ };
589
+ }
590
+ /**
591
+ * Default `runWrangler`: resolves the project-local wrangler as an ARGV launcher
592
+ * via Task 2's {@link resolveWranglerArgv} and runs it with `spawn` and
593
+ * `shell: false`, so every argument is passed literally (no shell parsing → no
594
+ * injection). Captures stdout (for `--json` parsing) and stderr (for
595
+ * error/collision detection). Never called in unit tests — those inject a stub.
596
+ *
597
+ * ASYNC (returns a Promise). `spawn` + `await` (vs. the old blocking
598
+ * `spawnSync`) keeps the Node event loop free while wrangler runs, so
599
+ * `proper-lockfile`'s update timer keeps refreshing the mutex mtime for the WHOLE
600
+ * provision — no wrangler call, however slow, can freeze the loop long enough for
601
+ * another process to see the lock stale and steal it while we still hold it. A
602
+ * {@link COMMAND_TIMEOUT_MS} SIGKILL guards against a hung child.
603
+ */
604
+ function createDefaultRunWrangler(root, options) {
605
+ const launcher = options?.launcher ?? resolveWranglerArgv(root);
606
+ const timeoutMs = options?.timeoutMs ?? COMMAND_TIMEOUT_MS;
607
+ const confirmGraceMs = options?.confirmKillGraceMs ?? CONFIRM_KILL_GRACE_MS;
608
+ const killProcessGroup = options?.killProcessGroup ?? ((target, signal) => process.kill(target, signal));
609
+ const [command, ...prefix] = launcher;
610
+ const isWindows = process.platform === "win32";
611
+ const teardownDeadlineMs = options?.signalTeardownDeadlineMs ?? confirmGraceMs + SIGNAL_TEARDOWN_DEADLINE_MARGIN_MS;
612
+ return (args, opts) => new Promise((resolvePromise, rejectPromise) => {
613
+ if (activeTeardown !== void 0) {
614
+ rejectPromise(/* @__PURE__ */ new Error(`provision: refusing to spawn wrangler — a fatal ${activeTeardown.signal} is being handled and this process is terminating; no new wrangler work may start.`));
615
+ return;
616
+ }
617
+ if (activeRegistration !== void 0) {
618
+ rejectPromise(/* @__PURE__ */ new Error("provision: refusing to spawn wrangler — another wrangler child is already active in this process (wrangler calls must run sequentially; provisions run at most one per process)."));
619
+ return;
620
+ }
621
+ const child = spawn(command, [...prefix, ...args], {
622
+ cwd: opts.cwd ?? root,
623
+ shell: false,
624
+ env: provisionEnv({
625
+ accountId: opts.accountId,
626
+ apiBaseUrl: opts.apiBaseUrl
627
+ }),
628
+ stdio: [
629
+ "ignore",
630
+ "pipe",
631
+ "pipe"
632
+ ],
633
+ windowsHide: true,
634
+ detached: !isWindows
635
+ });
636
+ let stdout = "";
637
+ let stderr = "";
638
+ let timedOut = false;
639
+ let settled = false;
640
+ let spawned = false;
641
+ let postSpawnError;
642
+ let closed = false;
643
+ let groupGone = false;
644
+ child.once("spawn", () => {
645
+ spawned = true;
646
+ });
647
+ child.stdout?.setEncoding("utf-8");
648
+ child.stderr?.setEncoding("utf-8");
649
+ child.stdout?.on("data", (chunk) => {
650
+ stdout += chunk;
651
+ });
652
+ child.stderr?.on("data", (chunk) => {
653
+ stderr += chunk;
654
+ });
655
+ const signalGroup = (signal) => {
656
+ try {
657
+ if (!isWindows && typeof child.pid === "number") {
658
+ killProcessGroup(-child.pid, signal);
659
+ return "signaled";
660
+ }
661
+ if (isWindows && typeof child.pid === "number") {
662
+ if (child.exitCode !== null || child.signalCode !== null) return "gone";
663
+ killProcessGroup(child.pid, signal);
664
+ return "signaled";
665
+ }
666
+ if (child.kill(signal)) return "signaled";
667
+ if (child.exitCode !== null || child.signalCode !== null) return "gone";
668
+ return { error: /* @__PURE__ */ new Error(`child.kill(${signal}) returned false (signal not delivered)`) };
669
+ } catch (error) {
670
+ return isAlreadyGoneKillError(error) ? "gone" : { error };
671
+ }
672
+ };
673
+ /** True when the whole process group no longer exists (`kill(-pid, 0)` → ESRCH). */
674
+ const probeGroupGone = () => {
675
+ if (groupGone) return true;
676
+ if (typeof child.pid !== "number") {
677
+ groupGone = true;
678
+ return true;
679
+ }
680
+ if (isWindows) {
681
+ if (child.exitCode !== null || child.signalCode !== null) {
682
+ groupGone = true;
683
+ return true;
684
+ }
685
+ try {
686
+ process.kill(child.pid, 0);
687
+ } catch (error) {
688
+ if (error.code === "ESRCH") groupGone = true;
689
+ }
690
+ return groupGone;
691
+ }
692
+ try {
693
+ process.kill(-child.pid, 0);
694
+ } catch (error) {
695
+ if (error.code === "ESRCH") groupGone = true;
696
+ }
697
+ return groupGone;
698
+ };
699
+ const timeoutStderr = (extra) => [
700
+ stderr,
701
+ `provision: wrangler exceeded the ${timeoutMs}ms command timeout`,
702
+ extra
703
+ ].filter(Boolean).join("\n");
704
+ const deregisterSelf = () => deregisterRunner(registrant);
705
+ const finish = (result) => {
706
+ if (settled) return;
707
+ settled = true;
708
+ clearTimeout(commandTimer);
709
+ deregisterSelf();
710
+ resolvePromise(result);
711
+ };
712
+ const quarantine = (detail) => {
713
+ if (settled) return;
714
+ settled = true;
715
+ clearTimeout(commandTimer);
716
+ deregisterSelf();
717
+ rejectPromise(new WranglerQuarantineError(child.pid, detail));
718
+ };
719
+ const confirmPollNudges = [];
720
+ const nudgeConfirmPolls = () => {
721
+ for (const poll of confirmPollNudges) poll();
722
+ };
723
+ const awaitConfirmedDead = (handlers) => {
724
+ const deadline = Date.now() + confirmGraceMs;
725
+ let pollTimer;
726
+ const poll = () => {
727
+ clearTimeout(pollTimer);
728
+ if (handlers.stopWhenSettled && settled) return;
729
+ if (closed && probeGroupGone()) {
730
+ handlers.onConfirmed();
731
+ return;
732
+ }
733
+ if (Date.now() >= deadline) {
734
+ handlers.onExpiry();
735
+ return;
736
+ }
737
+ pollTimer = setTimeout(poll, CONFIRM_KILL_POLL_MS);
738
+ };
739
+ confirmPollNudges.push(poll);
740
+ poll();
741
+ };
742
+ const confirmThenSettle = (makeResult, what) => {
743
+ awaitConfirmedDead({
744
+ stopWhenSettled: true,
745
+ onConfirmed: () => finish(makeResult()),
746
+ onExpiry: () => quarantine(`the process group could not be confirmed terminated within ${confirmGraceMs}ms of ${what}`)
747
+ });
748
+ };
749
+ const registrant = {
750
+ pid: () => child.pid,
751
+ signalGroup,
752
+ teardownDeadlineMs,
753
+ markGroupGone: () => {
754
+ groupGone = true;
755
+ },
756
+ awaitConfirmedDead: (onDone) => {
757
+ awaitConfirmedDead({
758
+ stopWhenSettled: false,
759
+ onConfirmed: () => onDone({ confirmed: true }),
760
+ onExpiry: () => onDone({
761
+ confirmed: false,
762
+ detail: `the process group did not terminate within ${confirmGraceMs}ms of the forwarded signal`
763
+ })
764
+ });
765
+ },
766
+ quarantine
767
+ };
768
+ const commandTimer = setTimeout(() => {
769
+ timedOut = true;
770
+ const outcome = signalGroup("SIGKILL");
771
+ if (typeof outcome === "object") {
772
+ quarantine(`the process group could not be killed: ${outcome.error.message}`);
773
+ return;
774
+ }
775
+ if (outcome === "gone") groupGone = true;
776
+ confirmThenSettle(() => normalizeRunWranglerResult({
777
+ stdout,
778
+ stderr: timeoutStderr(),
779
+ status: null,
780
+ signal: "SIGKILL"
781
+ }), `the timeout SIGKILL (${timeoutStderr()})`);
782
+ }, timeoutMs);
783
+ child.on("error", (error) => {
784
+ if (!spawned && typeof child.pid !== "number") {
785
+ finish(normalizeRunWranglerResult({
786
+ stdout,
787
+ stderr,
788
+ status: null,
789
+ signal: null,
790
+ error
791
+ }));
792
+ return;
793
+ }
794
+ if (settled || timedOut || postSpawnError !== void 0) return;
795
+ postSpawnError = error;
796
+ clearTimeout(commandTimer);
797
+ const outcome = signalGroup("SIGKILL");
798
+ if (typeof outcome === "object") {
799
+ quarantine(`a post-spawn child error (${error.message}) was followed by a failed process-group kill: ${outcome.error.message}`);
800
+ return;
801
+ }
802
+ if (outcome === "gone") groupGone = true;
803
+ confirmThenSettle(() => normalizeRunWranglerResult({
804
+ stdout,
805
+ stderr: [stderr, `provision: the spawned wrangler child emitted an error after spawn: ${error.message}`].filter(Boolean).join("\n"),
806
+ status: null,
807
+ signal: "SIGKILL"
808
+ }), `a post-spawn child error (${error.message}; the group was SIGKILLed)`);
809
+ });
810
+ child.on("close", (code, signal) => {
811
+ closed = true;
812
+ if (timedOut || postSpawnError !== void 0) {
813
+ nudgeConfirmPolls();
814
+ return;
815
+ }
816
+ clearTimeout(commandTimer);
817
+ const makeResult = () => normalizeRunWranglerResult({
818
+ stdout,
819
+ stderr,
820
+ status: code,
821
+ signal
822
+ });
823
+ if (probeGroupGone()) {
824
+ finish(makeResult());
825
+ return;
826
+ }
827
+ const outcome = signalGroup("SIGKILL");
828
+ if (typeof outcome === "object") {
829
+ quarantine(`the child exited (status ${String(code)}) but its process group is still alive and could not be killed: ${outcome.error.message}`);
830
+ return;
831
+ }
832
+ if (outcome === "gone") groupGone = true;
833
+ confirmThenSettle(makeResult, `the child's exit (status ${String(code)}; surviving process-group members were SIGKILLed)`);
834
+ nudgeConfirmPolls();
835
+ });
836
+ registerRunner(registrant);
837
+ options?.onSpawnedChild?.(child);
838
+ });
839
+ }
840
+ /**
841
+ * Normalize a `spawnSync` result into a {@link RunWranglerResult}, treating a
842
+ * signal-killed child or a spawn error as a FAILURE — never success.
843
+ *
844
+ * The naive `status ?? (error ? 1 : 0)` collapses a SIGTERM'd child (which
845
+ * returns `status: null`, a non-null `signal`, and NO `error`) to `0`, so a
846
+ * killed `create` would be recorded as `created` though nothing happened. So we
847
+ * fail on ANY non-numeric `status`, a non-null `signal`, or a present `error`,
848
+ * and surface the signal/spawn-error text in `stderr` for diagnostics.
849
+ */
850
+ function normalizeRunWranglerResult(result) {
851
+ const stdout = result.stdout ?? "";
852
+ const notes = [];
853
+ if (result.signal != null) notes.push(`provision: wrangler was terminated by signal ${result.signal}`);
854
+ if (result.error != null) notes.push(`provision: wrangler could not be spawned: ${result.error.message}`);
855
+ const status = typeof result.status !== "number" || result.signal != null || result.error != null ? typeof result.status === "number" && result.status !== 0 ? result.status : 1 : result.status;
856
+ const baseStderr = result.stderr ?? "";
857
+ return {
858
+ stdout,
859
+ stderr: notes.length > 0 ? [baseStderr, ...notes].filter(Boolean).join("\n") : baseStderr,
860
+ status,
861
+ signal: result.signal ?? null,
862
+ error: result.error != null ? result.error.message : null
863
+ };
864
+ }
865
+ /**
866
+ * THE single provenance rule. A result is a HARD failure when the child was
867
+ * signal-killed or could not be spawned: it did not run to completion, so its
868
+ * buffered status/stdout/stderr is unreliable and must NEVER be read as "absent"
869
+ * or "success". Every success/absence/adopt classifier below defers to this.
870
+ */
871
+ function isHardFailure(result) {
872
+ return result.signal != null || result.error != null;
873
+ }
874
+ /** The child ran to completion and reported SUCCESS (a clean numeric exit 0). */
875
+ function isCleanSuccess(result) {
876
+ return !isHardFailure(result) && result.status === 0;
877
+ }
878
+ /**
879
+ * The child ran to completion and reported a FAILURE status (a numeric non-zero
880
+ * exit, no signal, no spawn error) — the ONLY case whose stdout/stderr may be
881
+ * classified for a resource-specific not-found signal.
882
+ */
883
+ function isCleanFailure(result) {
884
+ return !isHardFailure(result) && typeof result.status === "number" && result.status !== 0;
885
+ }
886
+ /**
887
+ * The OVERALL deadline for the Hyperdrive existence lister (across ALL its paginated
888
+ * requests). A stalled body or a never-settling injected seam must never hold the
889
+ * whole-provision lock (whose heartbeat keeps refreshing while the loop is free) forever, so
890
+ * the caller fails closed at this bound.
891
+ */
892
+ const HYPERDRIVE_LIST_TIMEOUT_MS = 6e4;
893
+ /**
894
+ * Run a lister call under an OVERALL deadline. Builds one `AbortController`, hands its
895
+ * signal to `start` (threaded into every paginated `fetch`), and races the returned promise
896
+ * against a timer that ABORTS the signal AND rejects — so even a seam that ignores the signal
897
+ * (never settling) can NOT hold the caller (and thus the provision lock) past `ms`.
898
+ */
899
+ function withListerDeadline(start, ms) {
900
+ return new Promise((resolve, reject) => {
901
+ const controller = new AbortController();
902
+ const timer = setTimeout(() => {
903
+ controller.abort();
904
+ reject(/* @__PURE__ */ new Error(`the existence check exceeded the ${ms}ms deadline`));
905
+ }, ms);
906
+ Promise.resolve().then(() => start(controller.signal)).then((value) => {
907
+ clearTimeout(timer);
908
+ resolve(value);
909
+ }, (error) => {
910
+ clearTimeout(timer);
911
+ reject(error);
912
+ });
913
+ });
914
+ }
915
+ /**
916
+ * The Cloudflare API token the default Hyperdrive existence check authenticates with:
917
+ * the caller-supplied one, else the ambient `CLOUDFLARE_API_TOKEN`. An empty value is
918
+ * treated as absent. This is the ONE resolver — the up-front Hyperdrive fence and the
919
+ * default lister below both use it, so the fence can never promise a check the lister
920
+ * then declines to make.
921
+ */
922
+ function resolveHyperdriveApiToken(apiToken) {
923
+ const token = apiToken ?? process.env.CLOUDFLARE_API_TOKEN;
924
+ return typeof token === "string" && token.length > 0 ? token : void 0;
925
+ }
926
+ /**
927
+ * Default Hyperdrive existence check: `GET /accounts/{id}/hyperdrive/configs`
928
+ * with the CF API token, matched by name in the caller. Returns `undefined` when
929
+ * it cannot be built (no account id / no token) — the caller then fails closed
930
+ * before any blind create. Never called in unit tests — those inject a stub.
931
+ */
932
+ function createDefaultListHyperdriveConfigs(apiToken) {
933
+ const token = resolveHyperdriveApiToken(apiToken);
934
+ if (!token) return;
935
+ return async ({ accountId, apiBaseUrl, signal }) => {
936
+ const perPage = 100;
937
+ const listUrl = (page) => `${apiBaseUrl}/accounts/${accountId}/hyperdrive/configs?page=${page}&per_page=${perPage}`;
938
+ const byId = /* @__PURE__ */ new Map();
939
+ let expectedTotal;
940
+ for (let page = 1;; page++) {
941
+ const res = await fetch(listUrl(page), {
942
+ headers: { Authorization: `Bearer ${token}` },
943
+ signal
944
+ });
945
+ if (!res.ok) throw new Error(`Cloudflare API returned HTTP ${res.status} listing Hyperdrive configs`);
946
+ const body = await res.json();
947
+ if (!isRecord(body) || body.success !== true || !Array.isArray(body.result)) throw new Error("Cloudflare API returned an unsuccessful or malformed Hyperdrive configs payload");
948
+ const info = body.result_info;
949
+ const totalCount = isRecord(info) && typeof info.total_count === "number" && Number.isInteger(info.total_count) ? info.total_count : void 0;
950
+ if (totalCount === void 0 || totalCount < 0) throw new Error("Cloudflare API returned Hyperdrive configs without valid pagination metadata (result_info.total_count); cannot prove the list is complete");
951
+ if (expectedTotal === void 0) expectedTotal = totalCount;
952
+ else if (totalCount !== expectedTotal) throw new Error("Cloudflare API returned inconsistent Hyperdrive pagination totals across pages; cannot prove the list is complete");
953
+ if (body.result.length === 0 && byId.size < expectedTotal) throw new Error("Cloudflare API returned an empty Hyperdrive page before all configs were listed; cannot prove the list is complete");
954
+ for (const entry of body.result) {
955
+ if (!isRecord(entry) || typeof entry.id !== "string" || entry.id.length === 0 || typeof entry.name !== "string" || entry.name.length === 0) throw new Error("Cloudflare API returned a malformed Hyperdrive config entry (missing id/name)");
956
+ if (byId.has(entry.id)) throw new Error("Cloudflare API returned a duplicate Hyperdrive config id across pages; cannot prove the list is complete");
957
+ byId.set(entry.id, {
958
+ id: entry.id,
959
+ name: entry.name
960
+ });
961
+ }
962
+ if (byId.size > expectedTotal) throw new Error("Cloudflare API returned more Hyperdrive configs than result_info.total_count; cannot prove the list is complete");
963
+ if (byId.size === expectedTotal) break;
964
+ }
965
+ return [...byId.values()];
966
+ };
967
+ }
968
+ /**
969
+ * The ONE strict Hyperdrive-list validator, applied at the CALLER after EITHER
970
+ * lister returns (the default fetch OR an injected seam), so validation can never live
971
+ * only inside the default fetch. Validates EVERY member up front — BEFORE any name
972
+ * filtering: rejects any non-object member, empty/non-string `name`, and duplicate `id`,
973
+ * AND requires every member's `id` to be a REAL 32-hex Hyperdrive id (not merely
974
+ * non-empty). A malformed id on ANY member means the list is untrustworthy, so we fail
975
+ * closed rather than skip that member and mistake the target for absent. Then requires
976
+ * EXACTLY 0 or 1 exact-name match (>1 is ambiguous — Hyperdrive names are NOT
977
+ * account-unique — and fails closed) BEFORE any match/write. Returns the single
978
+ * exact-name match, or `undefined` when none matches.
979
+ */
980
+ function selectHyperdriveMatch(configs, name, configPath) {
981
+ const seenIds = /* @__PURE__ */ new Set();
982
+ const validated = [];
983
+ for (const config of configs) {
984
+ if (!isRecord(config) || typeof config.id !== "string" || config.id.length === 0 || typeof config.name !== "string" || config.name.length === 0) throw new Error(`provision: the Hyperdrive existence check returned a malformed config entry (each must be an object with a non-empty string id and name). Cannot verify whether ${JSON.stringify(name)} already exists; refusing to create a possible duplicate.`);
985
+ if (!isRealHyperdriveId(config.id)) throw new Error(`provision: the Hyperdrive existence check returned a config ${JSON.stringify(config.name)} with a malformed id ${JSON.stringify(config.id)} (expected a 32-character hex Hyperdrive id). Cannot verify whether ${JSON.stringify(name)} already exists; refusing to create a possible duplicate.`);
986
+ if (seenIds.has(config.id)) throw new Error(`provision: the Hyperdrive existence check returned a duplicate config id ${JSON.stringify(config.id)}. Cannot verify existing configs; refusing to create a possible duplicate.`);
987
+ seenIds.add(config.id);
988
+ validated.push({
989
+ id: config.id,
990
+ name: config.name
991
+ });
992
+ }
993
+ const named = validated.filter((c) => c.name === name);
994
+ if (named.length > 1) throw new Error(`provision: cannot adopt Hyperdrive config ${JSON.stringify(name)} — ${named.length} configs in this account share that name (ids: ${named.map((c) => c.id).join(", ")}). Hyperdrive names are not account-unique; remove the duplicates or pin the id in "${configPath}", then retry.`);
995
+ return named[0];
996
+ }
997
+ /** The quarantine sidecar path for a lock target. */
998
+ function quarantineSidecarPath(lockTarget) {
999
+ return `${lockTarget}.quarantine.json`;
1000
+ }
1001
+ /**
1002
+ * Atomically persist the quarantine owner metadata: write a temp file,
1003
+ * then `rename` over the final path, so a concurrent acquisition gate can never read
1004
+ * partial JSON. On POSIX the spawned wrangler child is `detached` (its own process-group
1005
+ * leader), so the recorded `pgid` IS its pid; Windows has no process groups (the gate
1006
+ * probes the pid directly there).
1007
+ *
1008
+ * DURABLE: the temp file is fsync'd before the rename, the directory is
1009
+ * fsync'd after it (see the classification below), and the final sidecar is READ
1010
+ * BACK and verified. Any failure THROWS — callers treat persistence as a PRECONDITION
1011
+ * for detaching the lock from proper-lockfile's tracking (see
1012
+ * {@link ProvisionLock.quarantine}); a sidecar that merely "probably" landed is not a
1013
+ * durable marker the acquisition gate can rely on.
1014
+ */
1015
+ function writeQuarantineSidecar(sidecarPath, info, dirDurability) {
1016
+ const payload = JSON.stringify({
1017
+ pid: info.pid ?? null,
1018
+ pgid: process.platform === "win32" ? null : info.pid ?? null,
1019
+ reason: info.reason,
1020
+ quarantinedAt: (/* @__PURE__ */ new Date()).toISOString(),
1021
+ quarantinedBy: process.pid,
1022
+ generation: `${randomUUID()}.${process.hrtime.bigint().toString()}`
1023
+ }, null, 2);
1024
+ const tmpPath = `${sidecarPath}.tmp-${process.pid}`;
1025
+ try {
1026
+ const fd = openSync(tmpPath, "w");
1027
+ try {
1028
+ writeSync(fd, payload);
1029
+ fsyncSync(fd);
1030
+ } finally {
1031
+ closeSync(fd);
1032
+ }
1033
+ renameSync(tmpPath, sidecarPath);
1034
+ } catch (error) {
1035
+ try {
1036
+ unlinkSync(tmpPath);
1037
+ } catch {}
1038
+ throw error;
1039
+ }
1040
+ const dirOpen = dirDurability?.openSync ?? openSync;
1041
+ const dirFsync = dirDurability?.fsyncSync ?? fsyncSync;
1042
+ const dirClose = dirDurability?.closeSync ?? closeSync;
1043
+ try {
1044
+ const dirFd = dirOpen(dirname(sidecarPath), "r");
1045
+ try {
1046
+ dirFsync(dirFd);
1047
+ } finally {
1048
+ dirClose(dirFd);
1049
+ }
1050
+ } catch (error) {
1051
+ const code = error.code ?? "";
1052
+ if (![
1053
+ "EISDIR",
1054
+ "EPERM",
1055
+ "EINVAL",
1056
+ "ENOTSUP",
1057
+ "ENOSYS"
1058
+ ].includes(code)) {
1059
+ try {
1060
+ unlinkSync(sidecarPath);
1061
+ } catch {}
1062
+ throw error;
1063
+ }
1064
+ }
1065
+ const readBack = JSON.parse(readFileSync(sidecarPath, "utf-8"));
1066
+ if (!isRecord(readBack) || readBack.pid !== (info.pid ?? null) || readBack.reason !== info.reason) throw new Error(`the quarantine sidecar "${sidecarPath}" did not verify after writing`);
1067
+ }
1068
+ function readPositiveInt(value) {
1069
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
1070
+ }
1071
+ /**
1072
+ * The quarantine acquisition GATE. Every provision acquires its mutex
1073
+ * through {@link acquireProvisionLock} (the single choke point — grep-proven: no other
1074
+ * code calls `lockfile.lock`), and this gate runs BEFORE proper-lockfile is even
1075
+ * consulted, so a QUARANTINED mutex can never be stolen by the mtime-stale path while
1076
+ * the recorded wrangler process group may still be alive:
1077
+ * - no sidecar → nothing quarantined → proceed to normal acquisition;
1078
+ * - sidecar present → probe the recorded group (`kill(-pgid, 0)`; the plain pid where
1079
+ * no group exists): ESRCH (confirmed dead) → clear the sidecar and proceed; still
1080
+ * alive OR the probe fails (EPERM, no recorded pid, unreadable sidecar) → FAIL
1081
+ * CLOSED with the recorded metadata + manual-recovery guidance. PID reuse can make
1082
+ * a dead group probe "alive" — that keeps failing closed (the safe direction; the
1083
+ * guidance covers manual clearance).
1084
+ * Clearing touches ONLY the sidecar (unlink, ENOENT-tolerant — concurrent clearers are
1085
+ * fine). The retained mutex DIRECTORY is deliberately NOT removed by hand: its
1086
+ * heartbeat stopped at quarantine, so proper-lockfile's own stale lifecycle reclaims it
1087
+ * during the acquisition below — a manual rmdir here could race a concurrent acquirer's
1088
+ * fresh mutex (remove-then-acquire is not atomic across processes) and let two
1089
+ * provisions hold the lock at once.
1090
+ *
1091
+ * This gate now ALWAYS runs under the RECOVERY mutex (see
1092
+ * {@link acquireProvisionLock}), so its read IS the re-read-under-the-mutex every
1093
+ * recoverer must base its decision on, and a clear may only remove the exact sidecar
1094
+ * GENERATION it just inspected: the sidecar is re-read immediately before the unlink
1095
+ * and the decision fails closed if the generation changed (a newer quarantine was
1096
+ * persisted by the current main-mutex holder — whose quarantine writes are the one
1097
+ * interleaving the recovery mutex cannot serialize — while this decision was pending).
1098
+ * `onBeforeClear` is a test seam injecting exactly that interleaving deterministically.
1099
+ */
1100
+ function assertQuarantineClearedOrClear(lockTarget, onBeforeClear) {
1101
+ const sidecarPath = quarantineSidecarPath(lockTarget);
1102
+ if (!existsSync(sidecarPath)) return;
1103
+ let recorded;
1104
+ try {
1105
+ const parsed = JSON.parse(readFileSync(sidecarPath, "utf-8"));
1106
+ recorded = isRecord(parsed) ? parsed : void 0;
1107
+ } catch {
1108
+ recorded = void 0;
1109
+ }
1110
+ const pid = readPositiveInt(recorded?.pid);
1111
+ const pgid = readPositiveInt(recorded?.pgid);
1112
+ const reason = typeof recorded?.reason === "string" ? recorded.reason : void 0;
1113
+ const quarantinedAt = typeof recorded?.quarantinedAt === "string" ? recorded.quarantinedAt : void 0;
1114
+ const probeTarget = pgid !== void 0 && process.platform !== "win32" ? -pgid : pid ?? pgid;
1115
+ let confirmedDead = false;
1116
+ if (probeTarget !== void 0) try {
1117
+ process.kill(probeTarget, 0);
1118
+ } catch (error) {
1119
+ if (error.code === "ESRCH") confirmedDead = true;
1120
+ }
1121
+ if (!confirmedDead) {
1122
+ const who = pid !== void 0 || pgid !== void 0 ? `process group ${String(pgid ?? pid)}` : "an unrecorded process";
1123
+ throw new Error(`provision: refusing to provision — a previous provision QUARANTINED this config's lock because a spawned wrangler child could not be confirmed terminated (${who}${quarantinedAt ? `, quarantined at ${quarantinedAt}` : ""}${reason ? `; reason: ${reason}` : ""}). It may still be creating resources. Verify the process is dead (e.g. \`kill -0 ${String(pid ?? "<pid>")}\`; terminate it if not) and re-run — the quarantine then clears automatically. If you are CERTAIN the recorded process is unrelated (pid reuse) or gone, remove "${sidecarPath}" and the "${lockTarget}.lock" directory manually, then retry.`);
1124
+ }
1125
+ const inspectedGeneration = typeof recorded?.generation === "string" ? recorded.generation : void 0;
1126
+ onBeforeClear?.();
1127
+ let current;
1128
+ try {
1129
+ const reread = JSON.parse(readFileSync(sidecarPath, "utf-8"));
1130
+ current = isRecord(reread) ? reread : void 0;
1131
+ } catch (error) {
1132
+ if (error.code === "ENOENT") return;
1133
+ current = void 0;
1134
+ }
1135
+ const currentGeneration = typeof current?.generation === "string" ? current.generation : void 0;
1136
+ if (current === void 0 || currentGeneration !== inspectedGeneration) throw new Error(`provision: refusing to clear the quarantine sidecar "${sidecarPath}" — it changed while this recovery decision was being made (a newer quarantine generation was persisted). Re-run to re-evaluate the current quarantine.`);
1137
+ try {
1138
+ unlinkSync(sidecarPath);
1139
+ } catch (error) {
1140
+ if (error.code !== "ENOENT") throw error;
1141
+ }
1142
+ }
1143
+ function loadProperLockfileInternals() {
1144
+ try {
1145
+ return createRequire(import.meta.url)("proper-lockfile/lib/lockfile.js");
1146
+ } catch {
1147
+ return;
1148
+ }
1149
+ }
1150
+ /**
1151
+ * `realpathSync` is `node:fs`, so it returns a NATIVE path — backslashes on Windows —
1152
+ * while every other path in this module comes from `pathslash` and is POSIX-normalized,
1153
+ * including the not-yet-created branch below (whose `join()` already normalizes). Without
1154
+ * this, `canonicalizeConfigPath` hands back TWO different spellings of the same file
1155
+ * depending only on whether it happens to exist yet, defeating the "ONE identity" contract
1156
+ * the function exists to provide — and splitting the lock identity, the wrangler `-c` argv,
1157
+ * and the paths printed in error messages. No-op off Windows, where `\` is a legal filename
1158
+ * character that must survive byte-for-byte.
1159
+ */
1160
+ function toPosixConfigPath(nativePath) {
1161
+ return process.platform === "win32" ? normalize(nativePath) : nativePath;
1162
+ }
1163
+ /**
1164
+ * Canonicalize an already-absolute config path via `realpath` so the lock identity
1165
+ * and every read/write/`-c` key on the REAL file, not a lexical symlink alias. When the
1166
+ * config file EXISTS, its own realpath is returned (a symlink resolves to its target).
1167
+ * When it does NOT yet exist, `realpath` would throw (ENOENT), so we canonicalize the
1168
+ * PARENT directory (which normally exists — the project root) and rejoin the basename, so
1169
+ * two aliases to the same not-yet-created config still resolve to one identity. If neither
1170
+ * the file nor its parent can be canonicalized, the lexical path is returned unchanged (no
1171
+ * worse than before, and the common case — an existing project dir — is always canonical).
1172
+ */
1173
+ function canonicalizeConfigPath(absoluteConfigPath, depth = 0) {
1174
+ try {
1175
+ return toPosixConfigPath(realpathSync(absoluteConfigPath));
1176
+ } catch (error) {
1177
+ if (error.code !== "ENOENT") throw error;
1178
+ }
1179
+ let linkStat;
1180
+ try {
1181
+ linkStat = lstatSync(absoluteConfigPath);
1182
+ } catch {
1183
+ linkStat = void 0;
1184
+ }
1185
+ if (linkStat?.isSymbolicLink()) {
1186
+ if (depth >= 40) throw Object.assign(/* @__PURE__ */ new Error(`ELOOP: too many symbolic links while resolving "${absoluteConfigPath}"`), { code: "ELOOP" });
1187
+ const target = readlinkSync(absoluteConfigPath);
1188
+ return canonicalizeConfigPath(resolve(dirname(absoluteConfigPath), target), depth + 1);
1189
+ }
1190
+ const dir = dirname(absoluteConfigPath);
1191
+ try {
1192
+ return join(realpathSync(dir), basename(absoluteConfigPath));
1193
+ } catch (error) {
1194
+ if (error.code !== "ENOENT") throw error;
1195
+ return absoluteConfigPath;
1196
+ }
1197
+ }
1198
+ /**
1199
+ * A lock whose owner has not refreshed it within this window is treated as abandoned
1200
+ * (crashed) and may be compromised/stolen, so a crashed provision cannot wedge every
1201
+ * future one. A LIVE owner refreshes the mutex mtime every {@link LOCK_UPDATE_MS}
1202
+ * (strictly less than this), so it never expires by age. Generous vs. a real
1203
+ * provision's duration.
1204
+ *
1205
+ * CORRECTNESS — this heartbeat is a `setTimeout` on the Node event loop, so it only
1206
+ * fires while the loop is FREE. The default runner is ASYNC (`spawn`, awaited), so no
1207
+ * wrangler call blocks the loop; between/`during` every `await` the pending update timer
1208
+ * runs and refreshes the mtime. Fast synchronous bookkeeping (JSON parse, small config
1209
+ * reads) is « `LOCK_UPDATE_MS`, so the mtime is continuously refreshed and NO other
1210
+ * process ever sees this lock stale → it can never be stolen while we hold it. (The old
1211
+ * blocking `spawnSync` could freeze the loop past `LOCK_STALE_MS`, starving the
1212
+ * heartbeat AND `onCompromised`, which is exactly the race this closes.)
1213
+ */
1214
+ const LOCK_STALE_MS = 12e4;
1215
+ /** Refresh interval keeping a live owner's lock fresh (must be < {@link LOCK_STALE_MS}). */
1216
+ const LOCK_UPDATE_MS = 2e4;
1217
+ /**
1218
+ * Backoff budget while waiting for a held (non-stale) lock. Polls with jittered backoff
1219
+ * for roughly a minute before giving up, so a concurrent provision blocks (not spins)
1220
+ * until the holder releases, then proceeds.
1221
+ */
1222
+ const LOCK_RETRIES = {
1223
+ retries: 100,
1224
+ factor: 1.5,
1225
+ minTimeout: 200,
1226
+ maxTimeout: 2e3
1227
+ };
1228
+ /**
1229
+ * The stale window of the RECOVERY mutex (the small proper-lockfile
1230
+ * lock serializing every gate→probe→clear→acquire→revalidate decision — see
1231
+ * {@link acquireProvisionLock}). Deliberately SHORT: its critical section is a few
1232
+ * synchronous fs reads plus one mkdir, so a crashed recoverer frees it within seconds
1233
+ * (proper-lockfile's own heartbeat keeps a live recoverer's window fresh).
1234
+ */
1235
+ const RECOVERY_LOCK_STALE_MS = 6e3;
1236
+ /**
1237
+ * Normalize the `retries` knob into the ONE backoff schedule OUR
1238
+ * acquisition loop runs (proper-lockfile's internal retrying is disabled with
1239
+ * `retries: 0` so the quarantine gate runs on EVERY attempt — see
1240
+ * {@link acquireProvisionLock}). A bare number keeps {@link LOCK_RETRIES}' backoff shape.
1241
+ */
1242
+ function normalizeLockRetries(retries) {
1243
+ if (typeof retries === "number") return {
1244
+ ...LOCK_RETRIES,
1245
+ retries
1246
+ };
1247
+ return {
1248
+ retries: retries?.retries ?? LOCK_RETRIES.retries,
1249
+ factor: retries?.factor ?? LOCK_RETRIES.factor,
1250
+ minTimeout: retries?.minTimeout ?? LOCK_RETRIES.minTimeout,
1251
+ maxTimeout: retries?.maxTimeout ?? LOCK_RETRIES.maxTimeout
1252
+ };
1253
+ }
1254
+ /**
1255
+ * Acquire the cross-process provision lock over the resolved config path via
1256
+ * `proper-lockfile` (an atomic `mkdir`-based mutex at `<configPath>.void-provision.lock.lock`).
1257
+ * `proper-lockfile` requires the locked pathname to EXIST, so we lock a DEDICATED
1258
+ * adjacent marker file (`<configPath>.void-provision.lock`), creating it if absent —
1259
+ * never the real config, so the writer's missing-vs-empty config semantics stay
1260
+ * untouched. Returns a handle whose `release()` drops the mutex; ALWAYS call it in a
1261
+ * `finally` so an interruption never leaks the lock.
1262
+ *
1263
+ * `onCompromised` FAILS CLOSED (re-throws): if the lock is ever detected stolen (its
1264
+ * mtime changed, or the mutex vanished under a live owner), provisioning must NEVER
1265
+ * continue — a compromised lock means another process may be racing the same config.
1266
+ *
1267
+ * Acquisition is ONE atomic recovery protocol, applied on EVERY
1268
+ * attempt: quarantine GATE → single non-retrying proper-lockfile acquisition →
1269
+ * REVALIDATE the gate under ownership → clear-or-abort. Retrying is owned by THIS
1270
+ * function (proper-lockfile runs with `retries: 0`), so a waiter that entered while the
1271
+ * holder was healthy re-runs the gate on every backoff attempt and can never stale-take
1272
+ * a mutex whose holder QUARANTINED mid-wait; the post-acquire revalidation catches a
1273
+ * sidecar that appeared between the last gate and the acquisition itself (the fresh
1274
+ * lock is released and the acquisition fails closed).
1275
+ *
1276
+ * The ENTIRE per-attempt protocol runs under a second small
1277
+ * proper-lockfile RECOVERY mutex (`<lockTarget>.recovery`, `retries: 0`, short stale):
1278
+ * without it, two recoverers could interleave ACROSS processes — B reads a dead sidecar
1279
+ * S0, gets descheduled; A clears S0, acquires the main mutex, quarantines again writing
1280
+ * a LIVE sidecar S1; B resumes and unlinks the PATHNAME (now S1) on its stale decision,
1281
+ * then stale-reclaims A's retained mutex (the revalidation sees no sidecar). Under the
1282
+ * recovery mutex every decision re-reads the sidecar after acquiring it, and the clear
1283
+ * is additionally GENERATION-verified (see {@link assertQuarantineClearedOrClear}) for
1284
+ * the one interleaving the recovery mutex cannot serialize (the current MAIN-mutex
1285
+ * holder quarantining mid-decision). The recovery mutex deliberately does NOT take the
1286
+ * caller's `fs` seam — that seam simulates main-mutex failures.
1287
+ */
1288
+ async function acquireProvisionLock(configPath, lockOptions) {
1289
+ const lockTarget = `${configPath}.void-provision.lock`;
1290
+ const recoveryTarget = `${lockTarget}.recovery`;
1291
+ if (!existsSync(lockTarget)) writeFileSync(lockTarget, "");
1292
+ if (!existsSync(recoveryTarget)) writeFileSync(recoveryTarget, "");
1293
+ const staleMs = lockOptions?.stale ?? LOCK_STALE_MS;
1294
+ const retryCfg = normalizeLockRetries(lockOptions?.retries);
1295
+ const isElocked = (error) => error?.code === "ELOCKED";
1296
+ const backoffDelay = (attempt) => new Promise((resolveSleep) => setTimeout(resolveSleep, Math.min(retryCfg.minTimeout * retryCfg.factor ** attempt, retryCfg.maxTimeout)));
1297
+ const acquireOnce = () => lockfile.lock(lockTarget, {
1298
+ stale: staleMs,
1299
+ update: lockOptions?.update ?? LOCK_UPDATE_MS,
1300
+ retries: 0,
1301
+ ...lockOptions?.fs !== void 0 ? { fs: lockOptions.fs } : {},
1302
+ onCompromised: (error) => {
1303
+ throw error;
1304
+ }
1305
+ });
1306
+ const acquireRecoveryOnce = () => lockfile.lock(recoveryTarget, {
1307
+ stale: RECOVERY_LOCK_STALE_MS,
1308
+ retries: 0,
1309
+ onCompromised: (error) => {
1310
+ throw error;
1311
+ }
1312
+ });
1313
+ let release;
1314
+ for (let attempt = 0;; attempt++) {
1315
+ let releaseRecovery;
1316
+ try {
1317
+ releaseRecovery = await acquireRecoveryOnce();
1318
+ } catch (error) {
1319
+ if (isElocked(error) && attempt < retryCfg.retries) {
1320
+ await backoffDelay(attempt);
1321
+ continue;
1322
+ }
1323
+ throw error;
1324
+ }
1325
+ let mainHeldByOther = false;
1326
+ try {
1327
+ assertQuarantineClearedOrClear(lockTarget, lockOptions?.onBeforeQuarantineClear);
1328
+ try {
1329
+ release = await acquireOnce();
1330
+ } catch (error) {
1331
+ if (isElocked(error) && attempt < retryCfg.retries) mainHeldByOther = true;
1332
+ else throw error;
1333
+ }
1334
+ if (!mainHeldByOther) try {
1335
+ assertQuarantineClearedOrClear(lockTarget, lockOptions?.onBeforeQuarantineClear);
1336
+ } catch (gateError) {
1337
+ let revalidateReleaseError;
1338
+ try {
1339
+ await release();
1340
+ } catch (error) {
1341
+ revalidateReleaseError = error;
1342
+ }
1343
+ release = void 0;
1344
+ if (revalidateReleaseError !== void 0) throw new AggregateError([gateError, revalidateReleaseError], `provision: a quarantine was found right after acquiring the provision lock AND the fresh lock could not be released afterward. Quarantine: ${gateError.message}; release error: ${revalidateReleaseError.message}`, { cause: gateError });
1345
+ throw gateError;
1346
+ }
1347
+ } finally {
1348
+ try {
1349
+ await releaseRecovery();
1350
+ } catch {}
1351
+ }
1352
+ if (!mainHeldByOther) break;
1353
+ await backoffDelay(attempt);
1354
+ }
1355
+ const acquiredRelease = release;
1356
+ let releasePromise;
1357
+ const doRelease = async () => {
1358
+ try {
1359
+ await acquiredRelease();
1360
+ } catch (error) {
1361
+ if (error?.code === "ERELEASED") return;
1362
+ throw new Error(`provision: failed to release the cross-process provision lock "${lockTarget}.lock" (${error.message}). The mutex directory may still exist and will block future provisions until it goes stale (~${Math.round(staleMs / 1e3)}s). Remove it manually if it persists, then retry.`);
1363
+ }
1364
+ };
1365
+ return {
1366
+ lockTargetPath: lockTarget,
1367
+ release: () => releasePromise ??= doRelease(),
1368
+ quarantine: (info) => {
1369
+ if (releasePromise !== void 0) return;
1370
+ try {
1371
+ writeQuarantineSidecar(quarantineSidecarPath(lockTarget), info, lockOptions?.dirDurability);
1372
+ } catch (error) {
1373
+ throw new Error(`provision: could not persist quarantine metadata to "${quarantineSidecarPath(lockTarget)}" (${error.message}). The cross-process provision lock "${lockTarget}.lock" is intentionally STILL HELD by this process (tracked, heartbeat running) so it cannot go stale and be stolen while the unconfirmed wrangler child${info.pid !== void 0 ? ` (pid ${info.pid})` : ""} may still be running. Manual recovery: verify the process is dead${info.pid !== void 0 ? ` (e.g. \`kill -0 ${info.pid}\`)` : ""} and terminate it if not; the lock is freed when this process exits — remove the "${lockTarget}.lock" directory if it persists, then re-run.`);
1374
+ }
1375
+ releasePromise = Promise.resolve();
1376
+ const locks = loadProperLockfileInternals()?.getLocks?.();
1377
+ if (!locks) return;
1378
+ let key;
1379
+ try {
1380
+ key = realpathSync(lockTarget);
1381
+ } catch {
1382
+ key = resolve(lockTarget);
1383
+ }
1384
+ const tracked = locks[key];
1385
+ if (tracked) {
1386
+ tracked.released = true;
1387
+ if (tracked.updateTimeout !== void 0) clearTimeout(tracked.updateTimeout);
1388
+ delete locks[key];
1389
+ }
1390
+ }
1391
+ };
1392
+ }
1393
+ /**
1394
+ * Parse a wrangler `--json` stdout into an array, distinguishing a VALID JSON
1395
+ * array (returns it, possibly empty) from unparseable/non-array output (returns
1396
+ * `undefined`). Members are returned UNFILTERED — a malformed member (e.g. a
1397
+ * `null`) is preserved, NOT silently dropped, so {@link validateResourceList}
1398
+ * can fail closed on it (a dropped malformed member could BE the target and look
1399
+ * like "absent", minting a duplicate). Used everywhere an empty list must mean
1400
+ * "authoritatively absent" and garbage must NOT be coerced to "absent".
1401
+ */
1402
+ function parseJsonArrayStrict(stdout) {
1403
+ const value = parseJsonLoose(stdout);
1404
+ return Array.isArray(value) ? value : void 0;
1405
+ }
1406
+ /** Parse a wrangler `--json` stdout into an object, tolerating noise/failure. */
1407
+ function parseJsonObject(stdout) {
1408
+ const value = parseJsonLoose(stdout);
1409
+ return isRecord(value) ? value : void 0;
1410
+ }
1411
+ /**
1412
+ * wrangler may print human banners before the JSON payload; slice from the
1413
+ * first `{`/`[` so a leading banner does not break `JSON.parse`.
1414
+ */
1415
+ function parseJsonLoose(stdout) {
1416
+ const trimmed = stdout.trim();
1417
+ const start = trimmed.search(/[[{]/);
1418
+ if (start === -1) return;
1419
+ try {
1420
+ return JSON.parse(trimmed.slice(start));
1421
+ } catch {
1422
+ return;
1423
+ }
1424
+ }
1425
+ function isRecord(value) {
1426
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1427
+ }
1428
+ /**
1429
+ * Read the single provisioned binding entry for `field`/`binding` from the resolved
1430
+ * config, delegating to the STRICT canonical reader in `wrangler.ts`
1431
+ * ({@link readProvisionedBindingEntry}). Unlike the old loose first-match `.find()`, this
1432
+ * validates the target field's outer shape, fails closed on a DUPLICATE binding, and runs
1433
+ * the worker-wide CROSS-TYPE collision check (reusing the Task-3 collector) — so a
1434
+ * duplicate/collision config can never be reported `reused` or pass a create postcondition.
1435
+ */
1436
+ function readBindingEntry(configPath, field, binding) {
1437
+ return readProvisionedBindingEntry(readRootConfig(configPath), field, binding, configPath);
1438
+ }
1439
+ /**
1440
+ * Read + parse the root wrangler config (fresh each call — wrangler mutates it).
1441
+ * BOM-aware (wrangler + the Task 3 writer strip a leading BOM before parsing, so
1442
+ * we must too, else a BOM-prefixed config would look unparseable). FAILS CLOSED
1443
+ * on a genuine parse error or a non-object root: never silently treat a real
1444
+ * config as `{}`, which would miss existing ids (re-provision/orphan) and mask a
1445
+ * corrupt file. A missing file is fine (nothing to read-first → `{}`).
1446
+ */
1447
+ function readRootConfig(configPath) {
1448
+ if (!existsSync(configPath)) return {};
1449
+ const raw = readFileSync(configPath, "utf-8");
1450
+ const content = raw.startsWith(UTF8_BOM) ? raw.slice(1) : raw;
1451
+ const errors = [];
1452
+ const parsed = parse(content, errors, { allowTrailingComma: true });
1453
+ if (errors.length > 0) throw new Error(`provision: cannot read "${configPath}" — it is not valid JSON/JSONC. Fix the file and retry.`);
1454
+ if (!isRecord(parsed)) throw new Error(`provision: "${configPath}" must be a JSON object.`);
1455
+ return parsed;
1456
+ }
1457
+ /** Escape a string for literal embedding in a `RegExp`. */
1458
+ function escapeRegExp(value) {
1459
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1460
+ }
1461
+ /**
1462
+ * FAIL-CLOSED gate over a discovery/list/info result. Throws (no create) unless
1463
+ * the child exited cleanly — a NUMERIC `status === 0`. Because
1464
+ * {@link normalizeRunWranglerResult} already folds a signal-killed child or a
1465
+ * spawn error into a non-zero status, a single numeric check here rejects ALL of
1466
+ * nonzero-exit / SIGTERM / spawn-error / non-numeric status. MUST be called
1467
+ * BEFORE parsing stdout, so a failed result can never be parsed as "absent".
1468
+ */
1469
+ function assertDiscoverySucceeded(result, label) {
1470
+ if (!isCleanSuccess(result)) throw new Error(`provision: "${label}" failed (exit ${String(result.status)}): ${result.stderr || result.stdout || "no output"}. Cannot verify existing resources; refusing to create a possible duplicate.`);
1471
+ }
1472
+ /**
1473
+ * STRICT list validator shared by KV + D1. Rejects the ENTIRE list (throws) if ANY
1474
+ * member is not a well-formed object carrying a non-empty name AND a
1475
+ * FORMAT-VALID id (per {@link ResourceListSpec.idValidator}). A member is NEVER
1476
+ * silently filtered, and a member with a malformed id is NEVER accepted, because
1477
+ * either could BE the requested target (or signal a corrupt list) and treating it as
1478
+ * "absent" would mint a duplicate. Validates EVERY member up front — BEFORE any
1479
+ * name-filtering by the caller — so a nonmatching member with a bogus id can no longer
1480
+ * pass the gate as authoritative absence. Returns the validated members (every one
1481
+ * guaranteed to have a real id + name) on success.
1482
+ */
1483
+ function validateResourceList(list, spec) {
1484
+ const validated = [];
1485
+ for (const member of list) {
1486
+ if (!isRecord(member)) throw new Error(`provision: "${spec.command}" returned a malformed ${spec.resource} entry (not an object). Cannot verify existing resources; refusing to create a possible duplicate.`);
1487
+ const rawName = member[spec.nameField];
1488
+ const nameValid = typeof rawName === "string" && rawName.length > 0;
1489
+ const idValid = spec.hasValidId(member);
1490
+ if (nameValid && !idValid) throw new Error(`provision: "${spec.command}" returned a ${spec.resource} ${spec.nameLabel} ${JSON.stringify(rawName)} with no valid id (${spec.idRule}). Cannot verify existing resources; refusing to create a possible duplicate.`);
1491
+ if (!nameValid) throw new Error(`provision: "${spec.command}" returned a malformed ${spec.resource} entry with no valid ${spec.nameField}. Cannot verify existing resources; refusing to create a possible duplicate.`);
1492
+ validated.push(member);
1493
+ }
1494
+ return validated;
1495
+ }
1496
+ function reconcileD1IdAliases(entry) {
1497
+ const present = [entry.database_id, entry.uuid].filter((value) => value !== void 0 && value !== "");
1498
+ if (present.length === 0) return { kind: "absent" };
1499
+ const ids = /* @__PURE__ */ new Set();
1500
+ for (const value of present) {
1501
+ if (!isRealD1Id(value)) return { kind: "invalid" };
1502
+ ids.add(value);
1503
+ }
1504
+ if (ids.size !== 1) return { kind: "invalid" };
1505
+ return {
1506
+ kind: "id",
1507
+ id: [...ids][0]
1508
+ };
1509
+ }
1510
+ /**
1511
+ * THE single strict D1 identity normalizer, shared by `d1 list --json` and
1512
+ * `d1 info <name> --json`. It reconciles the TWO name aliases (`name`,
1513
+ * `database_name`) and TWO id aliases (`database_id`, `uuid`) — an EMPTY STRING is
1514
+ * treated as ABSENT for each — and returns a usable id ONLY when the entry
1515
+ * unambiguously IS the requested database:
1516
+ * - `{ kind: 'other' }` — no non-empty name alias equals `requestedName`, so this
1517
+ * entry is a different database (the caller skips it, never mistakes it for the
1518
+ * target, and never adopts it under a normalization of a DIFFERENT name);
1519
+ * - `{ kind: 'claimed-but-invalid' }` — a name alias DOES equal `requestedName`
1520
+ * but the entry is ambiguous: CONFLICTING non-empty name aliases (e.g.
1521
+ * `name:"wanted"` + `database_name:"other"`), OR not EXACTLY ONE distinct
1522
+ * non-empty id (0 → no id; >1 → conflicting/blank uuid vs database_id). The
1523
+ * caller fails closed (never adopt, never create a duplicate);
1524
+ * - `{ kind: 'match', id }` — exactly one requested-name alias and exactly one
1525
+ * unambiguous non-empty id.
1526
+ * The old two-helper split (`normalizeD1Identity` preferring `database_name`,
1527
+ * `d1EntryName` preferring `name`) let a `{name,database_name}` mismatch be
1528
+ * SELECTED under one alias yet KEYED under the other. This collapses both into one
1529
+ * exact-match rule so no sibling gap remains.
1530
+ */
1531
+ function normalizeD1(entry, requestedName) {
1532
+ if (!isRecord(entry)) return { kind: "other" };
1533
+ const names = [entry.name, entry.database_name].filter((value) => typeof value === "string" && value.length > 0);
1534
+ if (!names.includes(requestedName)) return { kind: "other" };
1535
+ if (new Set(names).size > 1) return { kind: "claimed-but-invalid" };
1536
+ const reconciled = reconcileD1IdAliases(entry);
1537
+ if (reconciled.kind !== "id") return { kind: "claimed-but-invalid" };
1538
+ return {
1539
+ kind: "match",
1540
+ id: reconciled.id
1541
+ };
1542
+ }
1543
+ /**
1544
+ * `wrangler kv namespace list` → the `{ id }` of every namespace whose title
1545
+ * matches. The list has ALREADY been through {@link validateResourceList} (every
1546
+ * member has a non-empty string `id` + `title`), so this only filters + collects.
1547
+ *
1548
+ * A title-matching namespace whose `id` is NOT a real KV id (32-hex) is ambiguous:
1549
+ * we can neither adopt it nor treat it as absent (which would mint a duplicate title), so
1550
+ * we FAIL CLOSED. Real wrangler always returns 32-hex ids; this guards a malformed seam.
1551
+ */
1552
+ function extractKvMatches(list, title) {
1553
+ const matches = [];
1554
+ for (const ns of list) {
1555
+ if (ns.title !== title) continue;
1556
+ if (!isRealKvId(ns.id)) throw new Error(`provision: "wrangler kv namespace list" returned a namespace titled ${JSON.stringify(title)} with a malformed id ${JSON.stringify(ns.id)} (expected a 32-character hex namespace id). Cannot verify whether it already exists; refusing to create a possible duplicate.`);
1557
+ matches.push({ id: ns.id });
1558
+ }
1559
+ return matches;
1560
+ }
1561
+ /** True when a `d1 create` failure means the name is already taken (code 7502). */
1562
+ function isAlreadyExistsError(result) {
1563
+ const text = `${result.stdout}\n${result.stderr}`;
1564
+ return text.includes("7502") || /already exists/i.test(text);
1565
+ }
1566
+ /**
1567
+ * EXACT, name-aware not-found classifier shared by R2 + queues. Given a
1568
+ * already-NON-zero `<res> info`-by-name probe, returns `true` (confirmed absent →
1569
+ * safe to create) ONLY on wrangler's exact, resource-specific not-found signal
1570
+ * for THIS name (regex-escaped). Everything else returns `false` so the caller
1571
+ * fails closed — a signal/spawn failure, an auth/network/metrics error, or a
1572
+ * GENERIC "not found" (e.g. `Account not found`) is NEVER treated as absence,
1573
+ * which would mint a duplicate resource.
1574
+ */
1575
+ function isConfirmedAbsent(result, kind, name) {
1576
+ if (!isCleanFailure(result)) return false;
1577
+ const text = `${result.stdout}\n${result.stderr}`;
1578
+ switch (kind) {
1579
+ case "r2": return /\[code:\s*10006\]/.test(text);
1580
+ case "queue": return new RegExp(`Queue\\s+"${escapeRegExp(name)}"\\s+does not exist`, "i").test(text);
1581
+ }
1582
+ }
1583
+ /**
1584
+ * A `wrangler queues info <name>` result POSITIVELY confirms THIS queue does NOT
1585
+ * exist in the account. Narrow, queue-only view of {@link isConfirmedAbsent}, shared
1586
+ * with the deploy orchestrator's pre-build queue probe (`cli/deploy.ts`) so both call
1587
+ * sites classify the SAME wrangler output the SAME way. Only the exact, name-aware
1588
+ * `Queue "<name>" does not exist` on a clean non-zero exit returns `true`; every other
1589
+ * outcome (success, signal/spawn failure, auth or network error, generic "not found")
1590
+ * returns `false` and is left for the caller to treat as inconclusive.
1591
+ */
1592
+ function isQueueConfirmedAbsent(result, name) {
1593
+ return isConfirmedAbsent(result, "queue", name);
1594
+ }
1595
+ /**
1596
+ * A `wrangler queues info <name>` result POSITIVELY confirms THIS queue
1597
+ * exists. Queues write NO config, so a create cannot be verified by re-reading the
1598
+ * config (the D1/KV/R2/Hyperdrive backstop). And wrangler's `bin.wrangler` wrapper
1599
+ * maps a signal-killed inner CLI to `process.exit(0)` (`bin/wrangler.js`:
1600
+ * `code === null → exit(0)`), so a KILLED probe/create can surface a clean exit 0
1601
+ * with empty/partial output. A bare clean 0 is therefore NOT proof the queue exists.
1602
+ * We require BOTH a clean success (numeric exit 0, no signal/spawn error — the
1603
+ * provenance rule) AND the output to name THIS exact queue: wrangler 4.107's
1604
+ * `queues info` prints `Queue Name: <queue_name>` for the queue it fetched by name
1605
+ * (account-unique), so an exact `Queue Name: <name>` line is the positive signal.
1606
+ * Used for BOTH the reuse probe and the post-create verify probe.
1607
+ */
1608
+ function queueInfoConfirmsExists(result, name) {
1609
+ if (!isCleanSuccess(result)) return false;
1610
+ const text = `${result.stdout}\n${result.stderr}`;
1611
+ return new RegExp(`^Queue Name:\\s+${escapeRegExp(name)}\\s*$`, "m").test(text);
1612
+ }
1613
+ /**
1614
+ * Throw when the writer could not attach `migrations_dir` because no matching
1615
+ * `d1_databases[]` entry exists. The provisioner only writes `migrations_dir`
1616
+ * AFTER establishing the D1 entry (create/adopt/reuse), so a skip here means the
1617
+ * D1 resource is unexpectedly absent from the config — never silently succeed
1618
+ * with the gap unfilled.
1619
+ */
1620
+ function assertD1MigrationsDirApplied(result, binding, configPath) {
1621
+ if (result.d1MigrationsDirSkipped) throw new Error(`provision: could not set migrations_dir for D1 binding ${JSON.stringify(binding)} — no matching d1_databases entry exists in "${configPath}" after provisioning it. The D1 resource was not written to the config as expected; inspect the file and retry.`);
1622
+ }
1623
+ /**
1624
+ * Backstop — verify a create's POSTCONDITION, RESOURCE-SPECIFICALLY + identity
1625
+ * checked. wrangler 4.107's `bin.wrangler` is a WRAPPER that spawns
1626
+ * `wrangler-dist/cli.js` and maps a signal-killed child (`code === null`) to
1627
+ * `process.exit(0)`, so `spawnSync` can see a `create` report "success" though the
1628
+ * real CLI was killed and `--update-config` wrote NO id. After every config-writing
1629
+ * create (d1/kv/r2/hyperdrive) reports success we RE-READ the resolved config and
1630
+ * CONFIRM the target binding now holds a REAL id AND — where the resource's name is
1631
+ * its identity (R2 `bucket_name`, D1 `database_name`) — that the recorded name EXACTLY
1632
+ * matches the requested one; otherwise we FAIL CLOSED. A bare "some non-placeholder
1633
+ * value" check is NOT enough: for R2, `bucket_name` IS the identity, so
1634
+ * `{ bucket_name: 'other' }` would otherwise pass and bind the WRONG bucket. This also
1635
+ * catches any partial/silent create failure, not only kills. (Queue creates write NO
1636
+ * config, so they cannot be verified this way — a killed queue-create self-heals on
1637
+ * the next read-first/probe run, which re-detects absence and re-creates.)
1638
+ */
1639
+ function assertCreateWroteRealId(configPath, spec, binding, name) {
1640
+ const entry = readBindingEntry(configPath, spec.field, binding);
1641
+ const idOk = !!entry && spec.idValidator(entry[spec.idField]);
1642
+ const nameOk = spec.nameField === void 0 || !!entry && entry[spec.nameField] === name;
1643
+ if (!idOk || !nameOk) throw new Error(`provision: "${spec.command} ${name}" reported success but "${configPath}" does not hold the expected ${spec.resource} for binding ${JSON.stringify(binding)} afterward (need a real ${spec.idField}${spec.nameField ? ` and ${spec.nameField} === ${JSON.stringify(name)}` : ""}). The create may have been interrupted or written a different resource — wrangler's launcher can mask a signal-killed child as exit 0. Refusing to record a wrong/unprovisioned resource; inspect the account/config and retry.`);
1644
+ }
1645
+ /** True when `value` contains an ASCII control character (0x00–0x1F or 0x7F). */
1646
+ function hasControlChar(value) {
1647
+ for (let i = 0; i < value.length; i++) {
1648
+ const code = value.charCodeAt(i);
1649
+ if (code < 32 || code === 127) return true;
1650
+ }
1651
+ return false;
1652
+ }
1653
+ /** The universal option-injection guard for ANY wrangler ARGV positional. */
1654
+ function assertNoOptionInjection(kind, name) {
1655
+ if (typeof name !== "string" || name.length === 0 || name.startsWith("-") || hasControlChar(name)) throw new Error(`provision: refusing to pass an unsafe ${kind} name ${JSON.stringify(name)} to wrangler. A name may not be empty, begin with "-" (it would be parsed as a CLI flag), or contain control characters. Rename the resource and retry.`);
1656
+ }
1657
+ /**
1658
+ * Per-resource Cloudflare naming GRAMMAR, verified 2026-07 against wrangler 4.107
1659
+ * and live account resources:
1660
+ * - `r2` — wrangler's own `isValidR2BucketName`: 3–63 chars, lowercase letters /
1661
+ * digits / `-`, begin+end alphanumeric (DNS-like). This IS the identity.
1662
+ * - `kvTitle` — the KV namespace positional is forwarded verbatim as the account TITLE;
1663
+ * Cloudflare allows a broad charset INCLUDING SPACES (≤512 chars). We only
1664
+ * forbid control chars (already covered by the injection guard) — spaces
1665
+ * and mixed case are legal, so "My Own Namespace" is accepted.
1666
+ * - `d1` / `hyperdrive` / `queue` — 1–63 chars, begin with a letter/digit, then
1667
+ * letters / digits / `-` / `_` (matches every live D1 `name`, Hyperdrive
1668
+ * config `name`, and Void's `QUEUE_<NAME>`-derived queue names).
1669
+ */
1670
+ const RESOURCE_NAME_GRAMMARS = {
1671
+ r2: {
1672
+ re: R2_BUCKET_NAME_RE,
1673
+ label: "R2 bucket",
1674
+ rule: "be 3–63 characters, contain only lowercase letters, digits and \"-\", and begin and end with a letter or digit"
1675
+ },
1676
+ kvTitle: {
1677
+ re: /^[\s\S]{1,512}$/,
1678
+ label: "KV namespace",
1679
+ rule: "be 1–512 characters and contain no control characters (spaces are allowed)"
1680
+ },
1681
+ d1: {
1682
+ re: /^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/,
1683
+ label: "D1 database",
1684
+ rule: "be 1–63 characters, start with a letter or digit, and contain only letters, digits, \"-\" or \"_\""
1685
+ },
1686
+ hyperdrive: {
1687
+ re: /^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/,
1688
+ label: "Hyperdrive config",
1689
+ rule: "be 1–63 characters, start with a letter or digit, and contain only letters, digits, \"-\" or \"_\""
1690
+ },
1691
+ queue: {
1692
+ re: /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/,
1693
+ label: "queue",
1694
+ rule: "be 1–63 characters, contain only letters, digits and \"-\", and begin and end with a letter or digit"
1695
+ }
1696
+ };
1697
+ /**
1698
+ * Validate a name destined for a wrangler ARGV positional: FIRST the universal
1699
+ * option-injection guard, THEN the resource's own Cloudflare grammar. Fail closed on any
1700
+ * violation, before the account gate or any wrangler/REST call.
1701
+ *
1702
+ * Exported — like {@link isQueueConfirmedAbsent} — so the deploy orchestrator's
1703
+ * no-`--provision` queue probe (`cli/deploy.ts`) gates names on the SAME rules this
1704
+ * provisioner enforces, instead of only the `--provision` path being protected. Queue names
1705
+ * are taken verbatim from filenames with no sanitization, so without this gate a name
1706
+ * beginning with `-` reaches wrangler as a FLAG rather than a positional.
1707
+ */
1708
+ function assertValidResourceName(kind, name) {
1709
+ assertNoOptionInjection(RESOURCE_NAME_GRAMMARS[kind].label, name);
1710
+ const grammar = RESOURCE_NAME_GRAMMARS[kind];
1711
+ if (!grammar.re.test(name)) throw new Error(`provision: refusing to pass an invalid ${grammar.label} name ${JSON.stringify(name)} to wrangler. A ${grammar.label} name must ${grammar.rule}. Rename the resource and retry.`);
1712
+ }
1713
+ /**
1714
+ * The trailing flags EVERY provisioner wrangler invocation MUST carry, built
1715
+ * ONCE so no call site can forget them:
1716
+ * - `-c <configPath>` pins the ONE resolved config (no split-brain reads/writes);
1717
+ * - `--env=` (an explicit EMPTY environment) FORCES wrangler onto the top-level
1718
+ * config environment. wrangler 4.107 selects its config env as
1719
+ * `args.env ?? getCloudflareEnv()`; passing an explicit empty `args.env`
1720
+ * short-circuits the `??` so a project `.env`/`.env.local` that sets
1721
+ * `CLOUDFLARE_ENV` (reloaded into `process.env` on every wrangler command)
1722
+ * can NEVER reselect a named `env.*` block — whose `account_id`/bindings could
1723
+ * differ from the single account this provision resolved, patched, and pinned.
1724
+ * `--env=` is wrangler's own documented sentinel for "use the top-level
1725
+ * environment" (it prints exactly this guidance when multiple env blocks exist).
1726
+ * Discovery/create AND `--update-config` therefore all operate on the SAME
1727
+ * top-level config/account.
1728
+ */
1729
+ function wranglerConfigArgs(configPath) {
1730
+ return [
1731
+ "-c",
1732
+ configPath,
1733
+ "--env="
1734
+ ];
1735
+ }
1736
+ /**
1737
+ * Build a redactor that strips connection-string secrets from every string the
1738
+ * provisioner PROPAGATES (buffered wrangler stdout/stderr, error strings, logged
1739
+ * warnings). Two layers:
1740
+ * 1. the EXACT desired connection string (when this provision carries one) is replaced
1741
+ * whole, so no derived message can echo it verbatim;
1742
+ * 2. any URL userinfo password (`scheme://user:secret@…`) is masked generically, so a
1743
+ * wrangler error that re-renders a connection string in ANY spelling still never
1744
+ * leaks the password (the scheme + user survive for diagnostics).
1745
+ * Applied at the single runWrangler choke point (every call site reads pre-redacted
1746
+ * results) and to the Hyperdrive lister's propagated failure message.
1747
+ */
1748
+ function createSecretRedactor(connectionString) {
1749
+ return (text) => {
1750
+ let out = text;
1751
+ if (typeof connectionString === "string" && connectionString.length > 0) out = out.split(connectionString).join("[REDACTED]");
1752
+ out = out.replace(/(\w[\w+.-]*:\/\/[^\s/@:]+:)[^\s@/]+(@)/g, "$1[REDACTED]$2");
1753
+ return out;
1754
+ };
1755
+ }
1756
+ /**
1757
+ * The engine's own lazy `Error#stack` accessor pair, captured ONCE at
1758
+ * module init. V8 defines `stack` as an own get/set accessor on every Error instance,
1759
+ * sharing ONE getter/setter function across all of them — so FUNCTION IDENTITY proves
1760
+ * "this is the engine's deterministic lazy-stack slot" (user code cannot forge these
1761
+ * function identities). It is the ONLY accessor the in-place sanitizer may handle;
1762
+ * every other accessor forces the inert fallback carrier.
1763
+ */
1764
+ const engineStackDescriptor = Object.getOwnPropertyDescriptor(/* @__PURE__ */ new Error(), "stack");
1765
+ const ENGINE_STACK_GET = engineStackDescriptor?.get;
1766
+ const ENGINE_STACK_SET = engineStackDescriptor?.set;
1767
+ /**
1768
+ * The ONLY prototypes a rebuilt carrier may have restored. Restoring an
1769
+ * ARBITRARY prototype would reintroduce hostile behavior on the otherwise-inert carrier
1770
+ * (a prototype getter or `toString` the carrier's readers would still hit), so an
1771
+ * unknown prototype degrades to the base Error/Object/Array prototype the fresh carrier
1772
+ * was built with. `WranglerQuarantineError` is included so a rebuilt quarantine carrier
1773
+ * keeps its `instanceof` classification.
1774
+ */
1775
+ const KNOWN_SAFE_PROTOTYPES = /* @__PURE__ */ new Set([
1776
+ Error.prototype,
1777
+ TypeError.prototype,
1778
+ RangeError.prototype,
1779
+ ReferenceError.prototype,
1780
+ SyntaxError.prototype,
1781
+ EvalError.prototype,
1782
+ URIError.prototype,
1783
+ AggregateError.prototype,
1784
+ WranglerQuarantineError.prototype,
1785
+ Object.prototype,
1786
+ Array.prototype
1787
+ ]);
1788
+ function tryRedactValue(value, redactText) {
1789
+ try {
1790
+ if (typeof value === "string") return {
1791
+ ok: true,
1792
+ value: redactText(value)
1793
+ };
1794
+ if (typeof value !== "object" || value === null) return {
1795
+ ok: true,
1796
+ value
1797
+ };
1798
+ if (!types.isProxy(value) && sanitizeInPlace(value, redactText)) return {
1799
+ ok: true,
1800
+ value
1801
+ };
1802
+ return {
1803
+ ok: true,
1804
+ value: deepSanitizedCopy(value, redactText)
1805
+ };
1806
+ } catch {
1807
+ return { ok: false };
1808
+ }
1809
+ }
1810
+ /**
1811
+ * Attempt to redact `root` fully IN PLACE. Returns `true` only when EVERY string
1812
+ * that needed redaction was verifiably rewritten (assignment read back); any immutable
1813
+ * or unverifiable slot holding a secret returns `false` so the caller switches to the
1814
+ * fallback-carrier copy. Partial rewrites applied before a `false` are harmless — they
1815
+ * only ever REMOVE secret text, and the caller then discards the original anyway.
1816
+ */
1817
+ function sanitizeInPlace(root, redactText) {
1818
+ const seen = /* @__PURE__ */ new WeakSet();
1819
+ let ok = true;
1820
+ const visitChild = (child) => {
1821
+ if (typeof child === "object" && child !== null) visit(child);
1822
+ };
1823
+ const visit = (v) => {
1824
+ if (!ok || seen.has(v)) return;
1825
+ seen.add(v);
1826
+ if (types.isProxy(v)) {
1827
+ ok = false;
1828
+ return;
1829
+ }
1830
+ if (v instanceof Map) {
1831
+ for (const [key, val] of Array.from(v.entries())) {
1832
+ const newKey = typeof key === "string" ? redactText(key) : key;
1833
+ const newVal = typeof val === "string" ? redactText(val) : val;
1834
+ visitChild(key);
1835
+ visitChild(val);
1836
+ if (newKey !== key) v.delete(key);
1837
+ if (newKey !== key || newVal !== val) v.set(newKey, newVal);
1838
+ if (!ok) return;
1839
+ }
1840
+ return;
1841
+ }
1842
+ if (v instanceof Set) {
1843
+ for (const member of Array.from(v.values())) {
1844
+ if (typeof member === "string") {
1845
+ const red = redactText(member);
1846
+ if (red !== member) {
1847
+ v.delete(member);
1848
+ v.add(red);
1849
+ }
1850
+ } else visitChild(member);
1851
+ if (!ok) return;
1852
+ }
1853
+ return;
1854
+ }
1855
+ for (const key of Reflect.ownKeys(v)) {
1856
+ const desc = Object.getOwnPropertyDescriptor(v, key);
1857
+ if (!desc) continue;
1858
+ if (desc.get !== void 0 || desc.set !== void 0) {
1859
+ if (!(key === "stack" && ENGINE_STACK_GET !== void 0 && desc.get === ENGINE_STACK_GET && desc.set === ENGINE_STACK_SET)) {
1860
+ ok = false;
1861
+ return;
1862
+ }
1863
+ let current;
1864
+ try {
1865
+ current = desc.get !== void 0 ? desc.get.call(v) : void 0;
1866
+ } catch {
1867
+ ok = false;
1868
+ return;
1869
+ }
1870
+ if (typeof current === "string") {
1871
+ const red = redactText(current);
1872
+ if (red !== current) {
1873
+ if (desc.set === void 0) {
1874
+ ok = false;
1875
+ return;
1876
+ }
1877
+ try {
1878
+ desc.set.call(v, red);
1879
+ } catch {
1880
+ ok = false;
1881
+ return;
1882
+ }
1883
+ let after;
1884
+ try {
1885
+ const afterDesc = Object.getOwnPropertyDescriptor(v, key);
1886
+ after = afterDesc?.get !== void 0 ? afterDesc.get.call(v) : afterDesc?.value;
1887
+ } catch {
1888
+ ok = false;
1889
+ return;
1890
+ }
1891
+ if (after !== red) {
1892
+ ok = false;
1893
+ return;
1894
+ }
1895
+ }
1896
+ } else visitChild(current);
1897
+ if (!ok) return;
1898
+ continue;
1899
+ }
1900
+ const val = desc.value;
1901
+ if (typeof val === "string") {
1902
+ const red = redactText(val);
1903
+ if (red === val) continue;
1904
+ if (desc.writable !== true) {
1905
+ ok = false;
1906
+ return;
1907
+ }
1908
+ try {
1909
+ v[key] = red;
1910
+ } catch {
1911
+ ok = false;
1912
+ return;
1913
+ }
1914
+ if (Object.getOwnPropertyDescriptor(v, key)?.value !== red) {
1915
+ ok = false;
1916
+ return;
1917
+ }
1918
+ } else {
1919
+ visitChild(val);
1920
+ if (!ok) return;
1921
+ }
1922
+ }
1923
+ };
1924
+ visit(root);
1925
+ return ok;
1926
+ }
1927
+ /**
1928
+ * The fallback CARRIER: a fully sanitized deep copy of `root`, built when in-place
1929
+ * redaction is impossible. Fresh objects/arrays/Errors/Maps/Sets (all mutable), every
1930
+ * string redacted, every own property — enumerable or not, string or symbol key —
1931
+ * carried over as a plain data property, accessors snapshotted (an unreadable getter
1932
+ * becomes `'[unredactable]'`), the original PROTOTYPE restored so `instanceof`
1933
+ * classification (e.g. {@link WranglerQuarantineError}) survives the rebuild. Cycle-safe
1934
+ * (a cycle in the original maps to the same cycle among the copies).
1935
+ */
1936
+ function deepSanitizedCopy(root, redactText) {
1937
+ const copies = /* @__PURE__ */ new WeakMap();
1938
+ const copy = (v) => {
1939
+ if (typeof v === "string") return redactText(v);
1940
+ if (typeof v !== "object" || v === null) return v;
1941
+ if (copies.has(v)) return copies.get(v);
1942
+ if (v instanceof Map) {
1943
+ const out = /* @__PURE__ */ new Map();
1944
+ copies.set(v, out);
1945
+ for (const [key, val] of v.entries()) out.set(copy(key), copy(val));
1946
+ return out;
1947
+ }
1948
+ if (v instanceof Set) {
1949
+ const out = /* @__PURE__ */ new Set();
1950
+ copies.set(v, out);
1951
+ for (const member of v.values()) out.add(copy(member));
1952
+ return out;
1953
+ }
1954
+ const out = Array.isArray(v) ? [] : v instanceof Error ? /* @__PURE__ */ new Error() : {};
1955
+ const proto = Object.getPrototypeOf(v);
1956
+ if (typeof proto === "object" && proto !== null && KNOWN_SAFE_PROTOTYPES.has(proto)) try {
1957
+ Object.setPrototypeOf(out, proto);
1958
+ } catch {}
1959
+ copies.set(v, out);
1960
+ for (const key of Reflect.ownKeys(v)) {
1961
+ const desc = Object.getOwnPropertyDescriptor(v, key);
1962
+ if (!desc) continue;
1963
+ let val;
1964
+ if (desc.get !== void 0 || desc.set !== void 0) try {
1965
+ val = desc.get !== void 0 ? desc.get.call(v) : void 0;
1966
+ } catch {
1967
+ val = "[unredactable]";
1968
+ }
1969
+ else val = desc.value;
1970
+ try {
1971
+ Object.defineProperty(out, key, {
1972
+ value: copy(val),
1973
+ writable: true,
1974
+ enumerable: desc.enumerable === true,
1975
+ configurable: true
1976
+ });
1977
+ } catch {}
1978
+ }
1979
+ return out;
1980
+ };
1981
+ return copy(root);
1982
+ }
1983
+ /**
1984
+ * Extract a SAFELY-READ, redacted one-line message from an unknown
1985
+ * value, for logging. Never coerces an object (`String()`/template literals invoke an
1986
+ * arbitrary — possibly hostile — `toString`): only primitives are stringified, and for
1987
+ * objects only a plain string `message` field is read (inside the no-throw boundary; a
1988
+ * throwing trap/getter degrades to the generic placeholder). THE one helper every
1989
+ * logging path uses for unknown rejection values (grep-proven: the Hyperdrive lister
1990
+ * warning and the rebuilt-carrier message below).
1991
+ */
1992
+ function safeRedactedErrorMessage(value, redactText) {
1993
+ try {
1994
+ if (typeof value === "string") return redactText(value);
1995
+ if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
1996
+ const message = value.message;
1997
+ if (typeof message === "string") return redactText(message);
1998
+ } catch {}
1999
+ return "the rejection could not be rendered safely; its content was withheld";
2000
+ }
2001
+ /** The conservative carrier for a rejection that resists inspection. */
2002
+ function unsafeInspectionQuarantineCarrier() {
2003
+ const carrier = /* @__PURE__ */ new Error("provision: a propagated rejection could not be safely inspected (a property or prototype trap threw during redaction); its content was withheld. Treating it conservatively as an unconfirmed-wrangler-child (quarantine) failure: the whole-provision lock is retained, not released.");
2004
+ Object.assign(carrier, {
2005
+ quarantine: true,
2006
+ pid: void 0
2007
+ });
2008
+ return carrier;
2009
+ }
2010
+ /**
2011
+ * Redact a seam REJECTION with EVERYTHING — the quarantine
2012
+ * classification included — inside the ONE no-throw boundary, GUARANTEEING:
2013
+ * 1. this function NEVER throws (pre-fix, `isQuarantineError`'s `instanceof` ran on
2014
+ * the ORIGINAL value before any boundary — a Proxy with throwing
2015
+ * `getPrototypeOf`/property traps made the redactor itself throw a
2016
+ * secret-bearing, classification-free replacement error, and the outer path then
2017
+ * RELEASED the mutex under a possibly-live child);
2018
+ * 2. if inspection is unsafe ANYWHERE (classification, redaction, message reads), it
2019
+ * FAILS CLOSED AS QUARANTINE: a fresh INERT sanitized carrier marked
2020
+ * `{ quarantine: true }` propagates, so the mutex is retained (conservative — the
2021
+ * hostile value could have been hiding a real quarantine) and no trap error or
2022
+ * secret ever escapes;
2023
+ * 3. a quarantined rejection can never degrade into an ordinary failure: the
2024
+ * classification + pid are captured inside the boundary and restored structurally
2025
+ * on the carrier when a rebuild lost them.
2026
+ */
2027
+ function redactRejection(error, redactText) {
2028
+ try {
2029
+ const wasQuarantine = isQuarantineError(error);
2030
+ const quarantinePid = wasQuarantine && typeof error.pid === "number" ? error.pid : void 0;
2031
+ const attempt = tryRedactValue(error, redactText);
2032
+ if (!attempt.ok) return unsafeInspectionQuarantineCarrier();
2033
+ const redacted = attempt.value;
2034
+ if (!wasQuarantine || isQuarantineError(redacted)) return redacted;
2035
+ const carrier = redacted instanceof Error ? redacted : new Error(safeRedactedErrorMessage(redacted, redactText));
2036
+ Object.assign(carrier, {
2037
+ quarantine: true,
2038
+ pid: quarantinePid
2039
+ });
2040
+ return carrier;
2041
+ } catch {
2042
+ return unsafeInspectionQuarantineCarrier();
2043
+ }
2044
+ }
2045
+ /**
2046
+ * Ensure every resource in `desired` exists in the user's own Cloudflare account,
2047
+ * creating the missing ones by shelling out to `wrangler` and letting
2048
+ * `wrangler <res> create --update-config` write real ids into the root config.
2049
+ * For resources that already exist (adopt path) — where `create` cannot run —
2050
+ * the real id is captured from `wrangler <res> list/info` (or, for Hyperdrive,
2051
+ * the account-list existence seam) and written through the Task 3 writer's adopt
2052
+ * fields. Pure provisioning: NO build, deploy, or drift-check. Idempotent +
2053
+ * read-first: a binding already pointing at a real id is left untouched.
2054
+ */
2055
+ async function provisionCloudflareResources(root, rootWranglerPath, desired, opts) {
2056
+ const releaseProvisionSlot = acquireProcessProvisionSlot();
2057
+ try {
2058
+ return await provisionWithProcessSlot(root, rootWranglerPath, desired, opts);
2059
+ } finally {
2060
+ releaseProvisionSlot();
2061
+ }
2062
+ }
2063
+ /** The provision body, run while holding this process's single provision slot. */
2064
+ async function provisionWithProcessSlot(root, rootWranglerPath, desired, opts) {
2065
+ await Promise.resolve();
2066
+ const configPath = canonicalizeConfigPath(resolve(root, rootWranglerPath));
2067
+ const lock = await acquireProvisionLock(configPath, opts?.lockOptions);
2068
+ const quarantineHook = (info) => retainQuarantinedLock(lock, info);
2069
+ activeProvisionQuarantineHook = quarantineHook;
2070
+ let result;
2071
+ let threw = false;
2072
+ let bodyError;
2073
+ try {
2074
+ result = await provisionCloudflareResourcesLocked(root, configPath, desired, opts);
2075
+ } catch (error) {
2076
+ threw = true;
2077
+ bodyError = error;
2078
+ }
2079
+ if (activeProvisionQuarantineHook === quarantineHook) activeProvisionQuarantineHook = void 0;
2080
+ if (threw) {
2081
+ if (isQuarantineError(bodyError)) {
2082
+ try {
2083
+ retainQuarantinedLock(lock, {
2084
+ pid: typeof bodyError.pid === "number" ? bodyError.pid : void 0,
2085
+ reason: typeof bodyError.message === "string" ? bodyError.message : String(bodyError)
2086
+ });
2087
+ } catch (quarantineError) {
2088
+ throw new AggregateError([bodyError, quarantineError], `provision failed with an unconfirmed wrangler child AND its quarantine metadata could not be persisted — the cross-process provision lock is intentionally still held by this process (not released, not stealable) while the child may live. Primary error: ${bodyError instanceof Error ? bodyError.message : String(bodyError)}; quarantine error: ${quarantineError instanceof Error ? quarantineError.message : String(quarantineError)}.`, { cause: bodyError });
2089
+ }
2090
+ throw bodyError;
2091
+ }
2092
+ let releaseError;
2093
+ try {
2094
+ await lock.release();
2095
+ } catch (error) {
2096
+ releaseError = error;
2097
+ }
2098
+ if (releaseError !== void 0) throw new AggregateError([bodyError, releaseError], `provision failed AND the cross-process provision lock could not be released afterward — a leaked mutex may block future provisions. Primary error: ${bodyError instanceof Error ? bodyError.message : String(bodyError)}; release error: ${releaseError instanceof Error ? releaseError.message : String(releaseError)}.`, { cause: bodyError });
2099
+ throw bodyError;
2100
+ }
2101
+ await lock.release();
2102
+ return result;
2103
+ }
2104
+ /**
2105
+ * The provision body, run entirely UNDER the whole-provision lock (see
2106
+ * {@link provisionCloudflareResources}). `configPath` is the ONE resolved absolute
2107
+ * config path the lock is keyed on — threaded through read-first, wrangler `-c`, and
2108
+ * the writer so reads/writes can never hit different files.
2109
+ */
2110
+ async function provisionCloudflareResourcesLocked(root, configPath, desired, opts) {
2111
+ if (existsSync(configPath)) {
2112
+ const raw = readFileSync(configPath, "utf-8");
2113
+ assertNoDuplicateMembers(raw.startsWith(UTF8_BOM) ? raw.slice(1) : raw, configPath);
2114
+ }
2115
+ if (desired.d1) assertValidResourceName("d1", desired.d1.name);
2116
+ for (const { title } of desired.kvNamespaces) assertValidResourceName("kvTitle", title);
2117
+ if (desired.r2) assertValidResourceName("r2", desired.r2.bucketName);
2118
+ if (desired.hyperdrive) assertValidResourceName("hyperdrive", desired.hyperdrive.name);
2119
+ for (const queueName of desired.queues) assertValidResourceName("queue", queueName);
2120
+ const rootConfig = readRootConfig(configPath);
2121
+ const rawConfigAccountId = rootConfig.account_id;
2122
+ const configAccountId = typeof rawConfigAccountId === "string" && rawConfigAccountId.length > 0 ? rawConfigAccountId : void 0;
2123
+ const rawComplianceRegion = rootConfig.compliance_region;
2124
+ if (rawComplianceRegion !== void 0 && typeof rawComplianceRegion !== "string") throw new Error(`provision: "${configPath}" has a non-string "compliance_region"; it must be "public" or "fedramp_high". Fix the config and retry.`);
2125
+ const configComplianceRegion = typeof rawComplianceRegion === "string" ? rawComplianceRegion : void 0;
2126
+ if (configAccountId && opts?.accountId && configAccountId !== opts.accountId) throw new Error(`provision: Cloudflare account mismatch — "${configPath}" pins account_id ${JSON.stringify(configAccountId)} but deploy requested ${JSON.stringify(opts.accountId)}. Reconcile them (correct or remove the config's account_id, or deploy to the pinned account) and retry.`);
2127
+ const ambientAccountId = typeof process.env.CLOUDFLARE_ACCOUNT_ID === "string" && process.env.CLOUDFLARE_ACCOUNT_ID.length > 0 ? process.env.CLOUDFLARE_ACCOUNT_ID : void 0;
2128
+ const effectiveAccountId = configAccountId ?? opts?.accountId ?? ambientAccountId;
2129
+ const log = opts?.log ?? defaultLogger;
2130
+ const resources = [];
2131
+ const write = (patch) => writeProvisionedWranglerConfig(root, patch, { configPath });
2132
+ const hasRemoteWork = !!desired.d1 || desired.kvNamespaces.length > 0 || !!desired.r2 || !!desired.hyperdrive || desired.queues.length > 0;
2133
+ if (hasRemoteWork && !effectiveAccountId) throw new Error("provision: no Cloudflare account is resolvable — set \"account_id\" in your root wrangler config or export CLOUDFLARE_ACCOUNT_ID before deploying. Refusing to provision resources without a pinned account (wrangler would otherwise fall back to an ambient/interactive account that is never persisted, so a later retry could rebind them elsewhere).");
2134
+ if (hasRemoteWork) assertNoTransportRedirectInProjectDotenv(root);
2135
+ if (desired.hyperdrive && !opts?.listHyperdriveConfigs) {
2136
+ const { binding, name, connectionString } = desired.hyperdrive;
2137
+ const entry = readBindingEntry(configPath, "hyperdrive", binding);
2138
+ if (!(entry !== void 0 && isRealHyperdriveId(entry.id)) && !resolveHyperdriveApiToken(opts?.apiToken)) {
2139
+ if (connectionString) throw new Error(`provision: Hyperdrive config ${JSON.stringify(name)} is not in "${configPath}" yet, and creating it needs a Cloudflare API token. wrangler exposes no machine-readable Hyperdrive list, so Void checks the account over the Cloudflare REST API, which a "wrangler login" (OAuth) session cannot authenticate. Without that check a create would silently duplicate an existing same-named config, so nothing was provisioned. Either (a) export CLOUDFLARE_API_TOKEN with Hyperdrive edit permission and rerun, or (b) create the Hyperdrive config yourself (dashboard / "wrangler hyperdrive create") and add its id to the ${JSON.stringify(binding)} hyperdrive binding in "${configPath}" — deploying an already-provisioned Hyperdrive needs no token.`);
2140
+ throw new Error(`provision: Hyperdrive config ${JSON.stringify(name)} is not in "${configPath}" yet, and there is no DATABASE_URL to create it from. Nothing was provisioned. Either (a) export DATABASE_URL with your PostgreSQL connection string and rerun (Void reads it from the shell — a value in .env/.env.local is NOT used here, so it never ships as a plaintext worker var), or (b) create the Hyperdrive config yourself (dashboard / "wrangler hyperdrive create") and add its id to the ${JSON.stringify(binding)} hyperdrive binding in "${configPath}" — deploying an already-provisioned Hyperdrive needs neither DATABASE_URL nor an API token.`);
2141
+ }
2142
+ }
2143
+ let accountIdWritten = false;
2144
+ if (effectiveAccountId && configAccountId !== effectiveAccountId) {
2145
+ write({ accountId: effectiveAccountId });
2146
+ accountIdWritten = true;
2147
+ }
2148
+ const transportAccountId = effectiveAccountId;
2149
+ const transportApiBaseUrl = resolvePinnedApiBaseUrl(process.env, configComplianceRegion);
2150
+ const selectedRunWrangler = opts?.runWrangler ?? createDefaultRunWrangler(root);
2151
+ const redactSecrets = createSecretRedactor(desired.hyperdrive?.connectionString);
2152
+ const redactResult = (result) => ({
2153
+ ...result,
2154
+ stdout: redactSecrets(result.stdout),
2155
+ stderr: redactSecrets(result.stderr),
2156
+ error: result.error != null ? redactSecrets(result.error) : result.error
2157
+ });
2158
+ const runWrangler = (args, callOpts) => Promise.resolve(selectedRunWrangler(args, {
2159
+ ...callOpts,
2160
+ accountId: transportAccountId,
2161
+ apiBaseUrl: transportApiBaseUrl
2162
+ })).then(redactResult, (error) => {
2163
+ throw redactRejection(error, redactSecrets);
2164
+ });
2165
+ if (desired.d1) {
2166
+ const { binding, name } = desired.d1;
2167
+ const migrationsDir = desired.d1.migrationsDir ?? D1_MIGRATIONS_DIR;
2168
+ const entry = readBindingEntry(configPath, "d1_databases", binding);
2169
+ if (entry && isRealD1Id(entry.database_id)) {
2170
+ const storedName = entry.database_name;
2171
+ if (storedName !== void 0 && storedName !== name) throw new Error(`provision: D1 binding ${JSON.stringify(binding)} in "${configPath}" stores database_name ${JSON.stringify(storedName)} (with a real database_id), but this deploy wants database ${JSON.stringify(name)}. Refusing to reuse a database that contradicts the desired name. Fix "${configPath}" — correct the entry's database_name/database_id (or remove the entry to re-provision) — or rename the desired database, then retry.`);
2172
+ assertD1MigrationsDirApplied(write({ d1MigrationsDir: {
2173
+ binding,
2174
+ dir: migrationsDir
2175
+ } }), binding, configPath);
2176
+ resources.push({
2177
+ kind: "d1",
2178
+ binding,
2179
+ name,
2180
+ action: "reused",
2181
+ id: entry.database_id
2182
+ });
2183
+ } else {
2184
+ const listResult = await runWrangler([
2185
+ "d1",
2186
+ "list",
2187
+ "--json",
2188
+ ...wranglerConfigArgs(configPath)
2189
+ ], { captureJson: true });
2190
+ assertDiscoverySucceeded(listResult, "wrangler d1 list");
2191
+ const rawList = parseJsonArrayStrict(listResult.stdout);
2192
+ if (!rawList) throw new Error(`provision: "wrangler d1 list --json" did not return a JSON array; cannot verify whether D1 database ${JSON.stringify(name)} exists. Refusing to create a possible duplicate.`);
2193
+ const claimants = validateResourceList(rawList, {
2194
+ resource: "D1 database",
2195
+ command: "wrangler d1 list",
2196
+ hasValidId: (member) => reconcileD1IdAliases(member).kind === "id",
2197
+ idRule: "its uuid/database_id aliases must be present, well-formed D1 UUIDs agreeing on one id",
2198
+ nameField: "name",
2199
+ nameLabel: "named"
2200
+ }).map((db) => normalizeD1(db, name)).filter((m) => m.kind !== "other");
2201
+ if (claimants.length > 1) throw new Error(`provision: "wrangler d1 list" returned ${claimants.length} D1 databases named ${JSON.stringify(name)}. D1 database names are account-unique, so this is ambiguous; cannot pick one to adopt and refusing to create a possible duplicate. Inspect the account and retry.`);
2202
+ if (claimants.length === 1 && claimants[0].kind === "claimed-but-invalid") throw new Error(`provision: "wrangler d1 list" returned a D1 database named ${JSON.stringify(name)} with an ambiguous or empty id (conflicting/blank uuid/database_id, or conflicting name aliases). Cannot adopt it, and refusing to create a possible duplicate; inspect the account and retry.`);
2203
+ const existing = claimants.length === 1 && claimants[0].kind === "match" ? claimants[0] : void 0;
2204
+ if (existing) {
2205
+ log.info(`adopted existing D1 database ${name} (${existing.id})`);
2206
+ assertD1MigrationsDirApplied(write({
2207
+ d1: {
2208
+ binding,
2209
+ databaseName: name,
2210
+ databaseId: existing.id
2211
+ },
2212
+ d1MigrationsDir: {
2213
+ binding,
2214
+ dir: migrationsDir
2215
+ }
2216
+ }), binding, configPath);
2217
+ resources.push({
2218
+ kind: "d1",
2219
+ binding,
2220
+ name,
2221
+ action: "adopted",
2222
+ id: existing.id
2223
+ });
2224
+ } else {
2225
+ const created = await runWrangler([
2226
+ "d1",
2227
+ "create",
2228
+ name,
2229
+ "--binding",
2230
+ binding,
2231
+ "--update-config",
2232
+ ...wranglerConfigArgs(configPath)
2233
+ ]);
2234
+ if (isCleanSuccess(created)) {
2235
+ assertCreateWroteRealId(configPath, {
2236
+ field: "d1_databases",
2237
+ idField: "database_id",
2238
+ idValidator: isRealD1Id,
2239
+ nameField: "database_name",
2240
+ resource: "D1 database",
2241
+ command: "wrangler d1 create"
2242
+ }, binding, name);
2243
+ assertD1MigrationsDirApplied(write({ d1MigrationsDir: {
2244
+ binding,
2245
+ dir: migrationsDir
2246
+ } }), binding, configPath);
2247
+ resources.push({
2248
+ kind: "d1",
2249
+ binding,
2250
+ name,
2251
+ action: "created"
2252
+ });
2253
+ } else if (isCleanFailure(created) && isAlreadyExistsError(created)) {
2254
+ const infoResult = await runWrangler([
2255
+ "d1",
2256
+ "info",
2257
+ name,
2258
+ "--json",
2259
+ ...wranglerConfigArgs(configPath)
2260
+ ], { captureJson: true });
2261
+ assertDiscoverySucceeded(infoResult, `wrangler d1 info ${name}`);
2262
+ const info = normalizeD1(parseJsonObject(infoResult.stdout), name);
2263
+ if (info.kind !== "match") throw new Error(`provision: "wrangler d1 create ${name}" reported the database already exists, but "wrangler d1 info ${name} --json" did not return a database with a matching name and an unambiguous id. Refusing to adopt an unverified database.`);
2264
+ log.info(`adopted existing D1 database ${name} (${info.id})`);
2265
+ assertD1MigrationsDirApplied(write({
2266
+ d1: {
2267
+ binding,
2268
+ databaseName: name,
2269
+ databaseId: info.id
2270
+ },
2271
+ d1MigrationsDir: {
2272
+ binding,
2273
+ dir: migrationsDir
2274
+ }
2275
+ }), binding, configPath);
2276
+ resources.push({
2277
+ kind: "d1",
2278
+ binding,
2279
+ name,
2280
+ action: "adopted",
2281
+ id: info.id
2282
+ });
2283
+ } else throw new Error(`provision: "wrangler d1 create ${name}" failed: ${created.stderr || created.stdout}`);
2284
+ }
2285
+ }
2286
+ }
2287
+ for (const { binding, title } of desired.kvNamespaces) {
2288
+ const entry = readBindingEntry(configPath, "kv_namespaces", binding);
2289
+ if (entry && isRealKvId(entry.id)) {
2290
+ resources.push({
2291
+ kind: "kv",
2292
+ binding,
2293
+ name: title,
2294
+ action: "reused",
2295
+ id: entry.id
2296
+ });
2297
+ continue;
2298
+ }
2299
+ const listResult = await runWrangler([
2300
+ "kv",
2301
+ "namespace",
2302
+ "list",
2303
+ ...wranglerConfigArgs(configPath)
2304
+ ], { captureJson: true });
2305
+ assertDiscoverySucceeded(listResult, "wrangler kv namespace list");
2306
+ const rawList = parseJsonArrayStrict(listResult.stdout);
2307
+ if (!rawList) throw new Error(`provision: "wrangler kv namespace list" did not return a JSON array; cannot verify whether KV namespace ${JSON.stringify(title)} exists. Refusing to create a possible duplicate.`);
2308
+ const matches = extractKvMatches(validateResourceList(rawList, {
2309
+ resource: "KV namespace",
2310
+ command: "wrangler kv namespace list",
2311
+ hasValidId: (member) => isRealKvId(member.id),
2312
+ idRule: "its id is missing or not a well-formed 32-hex namespace id",
2313
+ nameField: "title",
2314
+ nameLabel: "titled"
2315
+ }), title);
2316
+ if (matches.length > 0) {
2317
+ const chosen = matches.reduce((min, ns) => ns.id < min.id ? ns : min);
2318
+ if (matches.length > 1) log.warn(`multiple KV namespaces titled "${title}" (${matches.map((m) => m.id).join(", ")}); adopting the lexicographically-smallest id ${chosen.id}`);
2319
+ log.info(`adopted existing KV namespace ${title} (${chosen.id})`);
2320
+ write({ kv: {
2321
+ binding,
2322
+ id: chosen.id
2323
+ } });
2324
+ resources.push({
2325
+ kind: "kv",
2326
+ binding,
2327
+ name: title,
2328
+ action: "adopted",
2329
+ id: chosen.id
2330
+ });
2331
+ } else {
2332
+ const created = await runWrangler([
2333
+ "kv",
2334
+ "namespace",
2335
+ "create",
2336
+ title,
2337
+ "--binding",
2338
+ binding,
2339
+ "--update-config",
2340
+ ...wranglerConfigArgs(configPath)
2341
+ ]);
2342
+ if (!isCleanSuccess(created)) throw new Error(`provision: "wrangler kv namespace create ${title}" failed: ${created.stderr || created.stdout}`);
2343
+ assertCreateWroteRealId(configPath, {
2344
+ field: "kv_namespaces",
2345
+ idField: "id",
2346
+ idValidator: isRealKvId,
2347
+ resource: "KV namespace",
2348
+ command: "wrangler kv namespace create"
2349
+ }, binding, title);
2350
+ resources.push({
2351
+ kind: "kv",
2352
+ binding,
2353
+ name: title,
2354
+ action: "created"
2355
+ });
2356
+ }
2357
+ }
2358
+ if (desired.r2) {
2359
+ const { binding, bucketName } = desired.r2;
2360
+ const entry = readBindingEntry(configPath, "r2_buckets", binding);
2361
+ const storedBucketName = entry?.bucket_name;
2362
+ if (entry && isRealStoredR2BucketName(storedBucketName)) {
2363
+ if (storedBucketName !== bucketName) throw new Error(`provision: R2 binding ${JSON.stringify(binding)} in "${configPath}" stores bucket_name ${JSON.stringify(storedBucketName)}, but this deploy wants bucket ${JSON.stringify(bucketName)}. bucket_name IS the R2 identity, so this entry contradicts the desired bucket. Fix "${configPath}" — correct the entry's bucket_name (or remove the entry to re-provision) — or rename the desired bucket, then retry.`);
2364
+ resources.push({
2365
+ kind: "r2",
2366
+ binding,
2367
+ name: bucketName,
2368
+ action: "reused"
2369
+ });
2370
+ } else {
2371
+ const info = await runWrangler([
2372
+ "r2",
2373
+ "bucket",
2374
+ "info",
2375
+ bucketName,
2376
+ "--json",
2377
+ ...wranglerConfigArgs(configPath)
2378
+ ], { captureJson: true });
2379
+ if (isCleanSuccess(info)) {
2380
+ const payload = parseJsonObject(info.stdout);
2381
+ if (!payload || payload.name !== bucketName) throw new Error(`provision: "wrangler r2 bucket info ${bucketName} --json" exited 0 but did not identify the requested bucket (expected name ${JSON.stringify(bucketName)}). Refusing to adopt an unverified bucket. Output: ${info.stdout || info.stderr}`);
2382
+ log.info(`adopted existing R2 bucket ${bucketName}`);
2383
+ write({ r2: {
2384
+ binding,
2385
+ bucketName
2386
+ } });
2387
+ resources.push({
2388
+ kind: "r2",
2389
+ binding,
2390
+ name: bucketName,
2391
+ action: "adopted"
2392
+ });
2393
+ } else if (isConfirmedAbsent(info, "r2", bucketName)) {
2394
+ const created = await runWrangler([
2395
+ "r2",
2396
+ "bucket",
2397
+ "create",
2398
+ bucketName,
2399
+ "--binding",
2400
+ binding,
2401
+ "--update-config",
2402
+ ...wranglerConfigArgs(configPath)
2403
+ ]);
2404
+ if (!isCleanSuccess(created)) throw new Error(`provision: "wrangler r2 bucket create ${bucketName}" failed: ${created.stderr || created.stdout}`);
2405
+ assertCreateWroteRealId(configPath, {
2406
+ field: "r2_buckets",
2407
+ idField: "bucket_name",
2408
+ idValidator: isValidR2BucketNameFormat,
2409
+ nameField: "bucket_name",
2410
+ resource: "R2 bucket",
2411
+ command: "wrangler r2 bucket create"
2412
+ }, binding, bucketName);
2413
+ if (bucketName === R2_BUCKET_NAME_PLACEHOLDER) {
2414
+ const verify = await runWrangler([
2415
+ "r2",
2416
+ "bucket",
2417
+ "info",
2418
+ bucketName,
2419
+ "--json",
2420
+ ...wranglerConfigArgs(configPath)
2421
+ ], { captureJson: true });
2422
+ const payload = isCleanSuccess(verify) ? parseJsonObject(verify.stdout) : void 0;
2423
+ if (!payload || payload.name !== bucketName) throw new Error(`provision: "wrangler r2 bucket create ${bucketName}" reported success but a follow-up "wrangler r2 bucket info ${bucketName}" did not confirm the bucket exists. The create may have been interrupted — wrangler's launcher can mask a signal-killed child as exit 0, and the config entry is indistinguishable from the local-dev placeholder for this name. Refusing to record an unprovisioned bucket; inspect the account and retry.`);
2424
+ }
2425
+ resources.push({
2426
+ kind: "r2",
2427
+ binding,
2428
+ name: bucketName,
2429
+ action: "created"
2430
+ });
2431
+ } else throw new Error(`provision: "wrangler r2 bucket info ${bucketName}" failed: ${info.stderr || info.stdout}. Cannot determine whether the bucket exists; refusing to create a possible duplicate.`);
2432
+ }
2433
+ }
2434
+ if (desired.hyperdrive) {
2435
+ const { binding, name, connectionString } = desired.hyperdrive;
2436
+ const entry = readBindingEntry(configPath, "hyperdrive", binding);
2437
+ if (entry && isRealHyperdriveId(entry.id)) resources.push({
2438
+ kind: "hyperdrive",
2439
+ binding,
2440
+ name,
2441
+ action: "reused",
2442
+ id: entry.id
2443
+ });
2444
+ else {
2445
+ const lister = opts?.listHyperdriveConfigs ?? createDefaultListHyperdriveConfigs(opts?.apiToken);
2446
+ let validatedConfigs;
2447
+ let existenceUnknown = true;
2448
+ if (lister) try {
2449
+ const result = await withListerDeadline((signal) => lister({
2450
+ accountId: transportAccountId,
2451
+ apiBaseUrl: transportApiBaseUrl,
2452
+ signal
2453
+ }), opts?.listTimeoutMs ?? HYPERDRIVE_LIST_TIMEOUT_MS);
2454
+ if (!Array.isArray(result)) throw new Error(`the existence check returned a ${result === null ? "null" : typeof result} result instead of an array`);
2455
+ validatedConfigs = result;
2456
+ existenceUnknown = false;
2457
+ } catch (error) {
2458
+ log.warn(`Hyperdrive existence check for ${name} failed: ` + safeRedactedErrorMessage(error, redactSecrets));
2459
+ existenceUnknown = true;
2460
+ validatedConfigs = void 0;
2461
+ }
2462
+ const existing = validatedConfigs !== void 0 ? selectHyperdriveMatch(validatedConfigs, name, configPath) : void 0;
2463
+ if (existing) {
2464
+ log.info(`adopted existing Hyperdrive config ${name} (${existing.id})`);
2465
+ write({ hyperdrive: {
2466
+ binding,
2467
+ id: existing.id
2468
+ } });
2469
+ resources.push({
2470
+ kind: "hyperdrive",
2471
+ binding,
2472
+ name,
2473
+ action: "adopted",
2474
+ id: existing.id
2475
+ });
2476
+ } else if (!connectionString) {
2477
+ const reason = "no connection string provided";
2478
+ log.warn(`skipping Hyperdrive config ${name}: ${reason}`);
2479
+ resources.push({
2480
+ kind: "hyperdrive",
2481
+ binding,
2482
+ name,
2483
+ action: "skipped",
2484
+ reason
2485
+ });
2486
+ } else if (existenceUnknown) throw new Error(`provision: cannot verify whether Hyperdrive config ${JSON.stringify(name)} already exists (no existence check available — provide an accountId + API token, or inject a listHyperdriveConfigs seam). "wrangler hyperdrive create" silently duplicates a same-named config, so provisioning refuses to create it blindly.`);
2487
+ else {
2488
+ const created = await runWrangler([
2489
+ "hyperdrive",
2490
+ "create",
2491
+ name,
2492
+ `--connection-string=${connectionString}`,
2493
+ "--binding",
2494
+ binding,
2495
+ "--update-config",
2496
+ ...wranglerConfigArgs(configPath)
2497
+ ]);
2498
+ if (!isCleanSuccess(created)) throw new Error(`provision: "wrangler hyperdrive create ${name}" failed: ${created.stderr || created.stdout}`);
2499
+ assertCreateWroteRealId(configPath, {
2500
+ field: "hyperdrive",
2501
+ idField: "id",
2502
+ idValidator: isRealHyperdriveId,
2503
+ resource: "Hyperdrive config",
2504
+ command: "wrangler hyperdrive create"
2505
+ }, binding, name);
2506
+ resources.push({
2507
+ kind: "hyperdrive",
2508
+ binding,
2509
+ name,
2510
+ action: "created"
2511
+ });
2512
+ }
2513
+ }
2514
+ }
2515
+ for (const name of desired.queues) {
2516
+ const info = await runWrangler([
2517
+ "queues",
2518
+ "info",
2519
+ name,
2520
+ ...wranglerConfigArgs(configPath)
2521
+ ]);
2522
+ if (queueInfoConfirmsExists(info, name)) {
2523
+ resources.push({
2524
+ kind: "queue",
2525
+ name,
2526
+ action: "reused"
2527
+ });
2528
+ continue;
2529
+ }
2530
+ if (!isConfirmedAbsent(info, "queue", name)) throw new Error(`provision: "wrangler queues info ${name}" failed or did not confirm the queue exists: ${info.stderr || info.stdout || "no output"}`);
2531
+ const created = await runWrangler([
2532
+ "queues",
2533
+ "create",
2534
+ name,
2535
+ ...wranglerConfigArgs(configPath)
2536
+ ]);
2537
+ if (!isCleanSuccess(created)) throw new Error(`provision: "wrangler queues create ${name}" failed: ${created.stderr || created.stdout}`);
2538
+ if (!queueInfoConfirmsExists(await runWrangler([
2539
+ "queues",
2540
+ "info",
2541
+ name,
2542
+ ...wranglerConfigArgs(configPath)
2543
+ ]), name)) throw new Error(`provision: "wrangler queues create ${name}" reported success but "wrangler queues info ${name}" did not confirm the queue exists afterward. The create may have been interrupted — wrangler's launcher can mask a signal-killed child as exit 0. Refusing to record an unprovisioned queue; inspect the account and retry.`);
2544
+ resources.push({
2545
+ kind: "queue",
2546
+ name,
2547
+ action: "created"
2548
+ });
2549
+ }
2550
+ return {
2551
+ resources,
2552
+ configPath,
2553
+ accountIdWritten
2554
+ };
2555
+ }
2556
+ //#endregion
2557
+ export { WranglerQuarantineError, acquireProvisionLock, assertNoTransportRedirectInProjectDotenv, assertValidResourceName, checkCloudflareProvisionDrift, createDefaultRunWrangler, deleteEnvVarCaseAware, isAlreadyGoneKillError, isQuarantineError, isQueueConfirmedAbsent, isRealD1Id, isRealKvId, normalizeRunWranglerResult, provisionCloudflareResources, provisionEnv, resolvePinnedApiBaseUrl, scanWranglerDotenvKeys };