@void/deploy-cloudflare 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,28 +1,5 @@
1
1
  # @void/deploy-cloudflare
2
2
 
3
- Public Cloudflare target adapter primitives for Void deployments.
3
+ Cloudflare deployment primitives used by the Void CLI. Provides Worker configuration reads, uploads, activation, and deployment verification.
4
4
 
5
- This package owns Wrangler JSON/JSONC config reads and the final verified
6
- Cloudflare upload boundary. Source inference and app builds remain in `void`;
7
- provider-neutral contracts and plans remain in `@void/deploy-core`.
8
-
9
- Existing Worker handoffs read the active version, validate resource identities,
10
- and check the uploaded bindings before activation. Traffic-only activation uses
11
- an isolated config to retain Worker settings and triggers. Trigger snapshots
12
- record when configuration was preserved, so rollback can keep those settings
13
- for versions originally deployed outside Void.
14
-
15
- Handoff verifiers can accept an explicit list of new KV binding identities.
16
- Only those exact namespaces may be added; existing bindings, variables, secrets,
17
- and handlers must remain unchanged. New bindings cannot alias an existing KV
18
- namespace. The Void CLI uses this for the migration's explicit ISR cache choice.
19
-
20
- First-deployment readiness allows up to one minute for the new Worker's public
21
- route to propagate, with bounded requests and backoff. A successful check still
22
- requires the ready response, and Cloudflare Access denials fail immediately.
23
- Subsequent version previews retain the shorter retry window.
24
-
25
- Failed version uploads and first-deployment bootstraps return a bounded diagnostic
26
- alongside the exit status and Cloudflare error code. It retains validation messages
27
- and stack locations, strips terminal formatting, excludes stdout binding tables,
28
- and redacts common credential formats before callers display or log it.
5
+ Applications normally deploy with `void deploy --platform cloudflare`. See the [Cloudflare guide](https://void.cloud/integrations/cloudflare) for configuration and credentials, or [Platform Development](https://void.cloud/guide/platform-development) for runtime and adapter work.
package/dist/index.d.mts CHANGED
@@ -66,6 +66,7 @@ type CloudflareOperation = ({
66
66
  } & WorkerTarget) | ({
67
67
  type: "versions.view";
68
68
  versionId: string;
69
+ includeSource?: boolean;
69
70
  } & WorkerTarget) | ({
70
71
  type: "versions.deploy";
71
72
  versions: Array<{
@@ -429,7 +430,7 @@ type CloudflareVersionActivationOptions = {
429
430
  requirePreviewForReadiness?: boolean;
430
431
  /** Verify that omitted remote secrets come from the active Worker Version before uploading. */
431
432
  verifyInheritedSecrets?: boolean;
432
- /** Pin opaque secret bindings to the live version when an unpublished upload is newer. */
433
+ /** Pin inherited opaque secret bindings to the inspected live version before uploading. */
433
434
  pinActiveSecrets?: (activeVersionId: string, bindings: ReadonlyArray<{
434
435
  name: string;
435
436
  type: "secret_text" | "secret_key";
@@ -477,4 +478,7 @@ export declare function createCloudflareMetadataRunner(options: {
477
478
  env?: Readonly<Record<string, string | undefined>>;
478
479
  }): CloudflareCommandRunner;
479
480
  //#endregion
481
+ //#region src/secret-draft.d.ts
482
+ export declare const VOID_SECRET_DRAFT_SOURCE = "export default { fetch() { return new Response(\"\"); } };\n";
483
+ //#endregion
480
484
  export type { ApprovedCloudflareKvBinding, CloudflareCommandResult, CloudflareCommandRunner, CloudflareDeployExecution, CloudflareFrameworkDeployConfig, CloudflareOperation, CloudflareProviderError, CloudflareStaticDeployConfig, CloudflareTrafficConfig, CloudflareTriggerConfig, CloudflareTriggerStabilization, CloudflareUploadResult, CloudflareVersionActivation, CloudflareVersionActivationOptions, CloudflareVersionMetadata, CloudflareWorkerMigrationTagOptions, ExistingCloudflareWorker };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { isDeepStrictEqual, stripVTControlCharacters } from "node:util";
2
2
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { SyntaxKind, applyEdits, createScanner, modify, parse, parseTree } from "jsonc-parser";
4
+ import { isRecord } from "@void/deploy-core";
4
5
  import nodePath from "node:path";
5
6
  import { createHash } from "node:crypto";
6
7
  import { tmpdir } from "node:os";
@@ -88,19 +89,53 @@ function resolveD1MigrateTarget(config, binding) {
88
89
  * application code configuration or non-secret resource that an ordinary deploy could own.
89
90
  */
90
91
  function isWranglerSecretOnlyDraft(entry) {
91
- if (!isRecord$1(entry.annotations)) return false;
92
+ if (!isRecord(entry.annotations)) return false;
92
93
  const annotationKeys = Object.keys(entry.annotations);
93
94
  if (entry.annotations["workers/triggered_by"] !== "secret" || annotationKeys.some((key) => key !== "workers/triggered_by")) return false;
94
- if (!isRecord$1(entry.resources)) return false;
95
+ if (!isRecord(entry.resources)) return false;
95
96
  const { bindings, script, script_runtime: scriptRuntime } = entry.resources;
96
- if (!Array.isArray(bindings) || bindings.length === 0 || !bindings.every((binding) => isRecord$1(binding) && typeof binding.name === "string" && binding.name.length > 0 && binding.type === "secret_text" && Object.keys(binding).every((key) => key === "name" || key === "type"))) return false;
97
- if (!isRecord$1(script) || !Array.isArray(script.handlers) || script.handlers.length !== 1 || script.handlers[0] !== "fetch" || script.placement_mode !== void 0) return false;
98
- if (!isRecord$1(scriptRuntime)) return false;
97
+ if (!Array.isArray(bindings) || bindings.length === 0 || !bindings.every((binding) => isRecord(binding) && typeof binding.name === "string" && binding.name.length > 0 && binding.type === "secret_text" && Object.keys(binding).every((key) => key === "name" || key === "type"))) return false;
98
+ if (!isRecord(script) || !Array.isArray(script.handlers) || script.handlers.length !== 1 || script.handlers[0] !== "fetch" || script.placement_mode !== void 0) return false;
99
+ if (!isRecord(scriptRuntime)) return false;
99
100
  const runtimeKeys = Object.keys(scriptRuntime);
100
- return scriptRuntime.usage_model === "standard" && (scriptRuntime.limits === void 0 || isRecord$1(scriptRuntime.limits) && Object.keys(scriptRuntime.limits).length === 0) && runtimeKeys.every((key) => key === "usage_model" || key === "limits");
101
- }
102
- function isRecord$1(value) {
103
- return typeof value === "object" && value !== null && !Array.isArray(value);
101
+ return scriptRuntime.usage_model === "standard" && (scriptRuntime.limits === void 0 || isRecord(scriptRuntime.limits) && Object.keys(scriptRuntime.limits).length === 0) && runtimeKeys.every((key) => key === "usage_model" || key === "limits");
102
+ }
103
+ const VOID_SECRET_DRAFT_SOURCE = "export default { fetch() { return new Response(\"\"); } };\n";
104
+ /** Prove a cf-created placeholder from the exact immutable version's source. */
105
+ function isVoidSecretOnlyDraft(entry) {
106
+ const metadataFields = /* @__PURE__ */ new Set([
107
+ "id",
108
+ "number",
109
+ "startup_time_ms",
110
+ "created_on",
111
+ "source",
112
+ "author_id",
113
+ "author_email",
114
+ "urls",
115
+ "annotations",
116
+ "compatibility_date",
117
+ "usage_model",
118
+ "main_module",
119
+ "modules",
120
+ "bindings",
121
+ "env",
122
+ "resources"
123
+ ]);
124
+ if (Object.keys(entry).some((key) => !metadataFields.has(key)) || entry.source !== "cf_cli" || !isRecord(entry.annotations) || Object.keys(entry.annotations).length !== 1 || entry.annotations["workers/triggered_by"] !== "secret" || entry.main_module !== "index.js" || !Array.isArray(entry.urls) || entry.urls.length !== 0 || !Array.isArray(entry.modules) || entry.modules.length !== 1 || !Array.isArray(entry.bindings) || entry.bindings.length === 0) return false;
125
+ const module = entry.modules[0];
126
+ if (!isRecord(module) || Object.keys(module).length !== 3 || module.name !== "index.js" || module.content_type !== "application/javascript+module" || module.content_base64 !== Buffer.from("export default { fetch() { return new Response(\"\"); } };\n").toString("base64")) return false;
127
+ const names = /* @__PURE__ */ new Set();
128
+ for (const binding of entry.bindings) {
129
+ if (!isRecord(binding) || Object.keys(binding).length !== 2 || typeof binding.name !== "string" || binding.name.length === 0 || names.has(binding.name) || binding.type !== "secret_text") return false;
130
+ names.add(binding.name);
131
+ }
132
+ if (entry.env !== void 0 && (!isRecord(entry.env) || Object.keys(entry.env).length !== names.size || Object.entries(entry.env).some(([name, binding]) => !names.has(name) || !isRecord(binding) || Object.keys(binding).length !== 1 || binding.type !== "secret_text"))) return false;
133
+ if (entry.resources !== void 0) {
134
+ if (!isRecord(entry.resources) || Object.keys(entry.resources).length !== 1) return false;
135
+ const bindings = entry.resources.bindings;
136
+ if (!Array.isArray(bindings) || !isDeepStrictEqual(bindings, entry.bindings)) return false;
137
+ }
138
+ return true;
104
139
  }
105
140
  //#endregion
106
141
  //#region src/existing.ts
@@ -114,7 +149,7 @@ async function readExistingCloudflareWorker(options) {
114
149
  if (!worker) throw new Error(`Cloudflare Worker "${options.workerName}" does not have an active version.`);
115
150
  return worker.snapshot;
116
151
  }
117
- async function readCloudflareWorker(options, allowMissing) {
152
+ async function readCloudflareWorker(options, allowMissing, proveSecretDraft = false) {
118
153
  const target = {
119
154
  configPath: options.configPath,
120
155
  workerName: options.workerName
@@ -151,6 +186,16 @@ async function readCloudflareWorker(options, allowMissing) {
151
186
  if (version === null) throw new Error("Cloudflare did not return the existing Worker configuration.");
152
187
  const resources = version.resources;
153
188
  if (version.id !== versionId || !record$1(resources) || !Array.isArray(resources.bindings) || !resources.bindings.every(record$1) || !record$1(resources.script_runtime) || !record$1(resources.script)) throw new Error("Cloudflare did not return the existing Worker configuration.");
189
+ let isSecretOnlyDraft = isWranglerSecretOnlyDraft(version);
190
+ if (proveSecretDraft && !isSecretOnlyDraft && record$1(version.annotations) && Object.keys(version.annotations).length === 1 && version.annotations["workers/triggered_by"] === "secret") {
191
+ const source = await read({
192
+ type: "versions.view",
193
+ versionId,
194
+ includeSource: true,
195
+ ...target
196
+ });
197
+ if (source?.id === versionId) isSecretOnlyDraft = isVoidSecretOnlyDraft(source);
198
+ }
154
199
  const after = options.versionId ? deployment : await read({
155
200
  type: "deployments.status",
156
201
  ...target
@@ -163,7 +208,7 @@ async function readCloudflareWorker(options, allowMissing) {
163
208
  runtime: resources.script_runtime,
164
209
  script: resources.script
165
210
  },
166
- isSecretOnlyDraft: isWranglerSecretOnlyDraft(version)
211
+ isSecretOnlyDraft
167
212
  };
168
213
  }
169
214
  /** Parse legacy version-detail shapes; this is not authoritative Worker migration state. */
@@ -231,7 +276,7 @@ async function readCloudflareApiAuthentication(runWrangler) {
231
276
  async function assertCloudflareDurableObjectLifecycleSupported(options) {
232
277
  const migrations = parseDurableObjectMigrations(parseRootWranglerConfig(options.configPath).migrations);
233
278
  if (!migrations.some((migration) => migration.changesClasses)) return;
234
- const existing = await readCloudflareWorker(options, true);
279
+ const existing = await readCloudflareWorker(options, true, true);
235
280
  if (!existing) return;
236
281
  const readMigrationTag = options.readMigrationTag ?? readCloudflareWorkerMigrationTag;
237
282
  const migrationOptions = {
@@ -241,7 +286,7 @@ async function assertCloudflareDurableObjectLifecycleSupported(options) {
241
286
  workerName: options.workerName
242
287
  };
243
288
  const activeTag = await readMigrationTag(migrationOptions);
244
- const after = await readCloudflareWorker(options, false);
289
+ const after = await readCloudflareWorker(options, false, true);
245
290
  const afterTag = await readMigrationTag(migrationOptions);
246
291
  if (!after || after.snapshot.versionId !== existing.snapshot.versionId || afterTag !== activeTag) throw new Error("Cloudflare production or Durable Object migration state changed during verification. Retry from the current deployment.");
247
292
  if (activeTag === null && existing.isSecretOnlyDraft && after.isSecretOnlyDraft) return;
@@ -1011,9 +1056,10 @@ function hasCustomDomains(routes) {
1011
1056
  /** Select a configured HTTPS route that is guaranteed to cover /__void/ready. */
1012
1057
  function customRouteReadinessUrl(routes) {
1013
1058
  for (const route of routes) {
1014
- const pattern = typeof route === "string" ? route : isRecord(route) && typeof route.pattern === "string" ? route.pattern : void 0;
1059
+ const record = isRecord(route) ? route : void 0;
1060
+ const pattern = typeof route === "string" ? route : typeof record?.pattern === "string" ? record.pattern : void 0;
1015
1061
  if (!pattern) continue;
1016
- const customDomain = isRecord(route) && route.custom_domain === true;
1062
+ const customDomain = record?.custom_domain === true;
1017
1063
  const withoutScheme = pattern.replace(/^https?:\/\//i, "");
1018
1064
  const slash = withoutScheme.indexOf("/");
1019
1065
  const hostname = (slash < 0 ? withoutScheme : withoutScheme.slice(0, slash)).trim();
@@ -1061,7 +1107,6 @@ function stableJson(value) {
1061
1107
  return JSON.stringify(value);
1062
1108
  }
1063
1109
  function parseTriggerConfig(value) {
1064
- if (value === void 0) return null;
1065
1110
  if (!isRecord(value)) return null;
1066
1111
  if ([
1067
1112
  "preserve",
@@ -1118,9 +1163,6 @@ function resolvePreservedTraffic(desired, previous, workerExists) {
1118
1163
  ...desired.addresses === void 0 ? {} : { addresses: desired.addresses }
1119
1164
  };
1120
1165
  }
1121
- function isRecord(value) {
1122
- return typeof value === "object" && value !== null && !Array.isArray(value);
1123
- }
1124
1166
  function isCandidatePreviewUrl(value, workerName, versionId, alias) {
1125
1167
  try {
1126
1168
  const url = new URL(value);
@@ -1188,6 +1230,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1188
1230
  let inheritanceBaseline;
1189
1231
  let trustedCandidateChain = false;
1190
1232
  let activeSecretsPinned = false;
1233
+ let pinnedActiveSecrets;
1191
1234
  if (options.expectedPreviousVersionId !== void 0 || options.verifyInheritedSecrets === true) {
1192
1235
  const recent = await readRecentCloudflareVersionIds(options, configPath);
1193
1236
  if (recent === null) return {
@@ -1222,6 +1265,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1222
1265
  rollback: "not-needed"
1223
1266
  };
1224
1267
  activeSecretsPinned = true;
1268
+ pinnedActiveSecrets = activeSecrets;
1225
1269
  }
1226
1270
  }
1227
1271
  inheritanceBaseline = recent;
@@ -1263,15 +1307,106 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1263
1307
  } : versionedPreviousTriggerConfig;
1264
1308
  const previousTriggerConfig = triggerConfigWithQueueConsumerCleanup(versionedPreviousTriggerConfigWithTraffic, options.queueConsumerCleanup);
1265
1309
  const alias = `void-ready-${Date.now().toString(36)}`;
1266
- if (options.atomic && previous.versionId !== null) {
1267
- if (!activeSecretsPinned && options.pinActiveSecrets) {
1268
- const activeSecrets = await readCloudflareVersionSecretBindings(options, configPath, previous.versionId);
1269
- if (activeSecrets?.length) await options.pinActiveSecrets(previous.versionId, activeSecrets);
1310
+ const bootstrapVoidDraft = !options.preserveTriggers && !options.requirePreviewForReadiness && previousVersion?.isVoidSecretOnlyDraft === true;
1311
+ if (previous.versionId !== null && (options.atomic || bootstrapVoidDraft)) {
1312
+ if (bootstrapVoidDraft && !activeSecretsPinned && !options.pinActiveSecrets) return {
1313
+ ok: false,
1314
+ reason: "inheritance-source-unavailable"
1315
+ };
1316
+ if (bootstrapVoidDraft || !activeSecretsPinned && options.pinActiveSecrets) {
1317
+ let activeSecrets;
1318
+ try {
1319
+ activeSecrets = await readCloudflareVersionSecretBindings(options, configPath, previous.versionId);
1320
+ } catch {
1321
+ return {
1322
+ ok: false,
1323
+ reason: "inheritance-source-unavailable"
1324
+ };
1325
+ }
1326
+ if (activeSecrets === null) return {
1327
+ ok: false,
1328
+ reason: "inheritance-source-unavailable"
1329
+ };
1330
+ if (bootstrapVoidDraft && (activeSecrets.length === 0 || activeSecrets.some((secret) => secret.type !== "secret_text") || !arraysEqual(activeSecrets.map((secret) => secret.name).sort(), [...previousVersion.voidDraftSecretNames].sort()) || activeSecretsPinned && !isDeepStrictEqual([...activeSecrets].sort((a, b) => a.name.localeCompare(b.name)), [...pinnedActiveSecrets].sort((a, b) => a.name.localeCompare(b.name))))) return {
1331
+ ok: false,
1332
+ reason: "inheritance-source-changed"
1333
+ };
1334
+ if (!activeSecretsPinned && activeSecrets.length) {
1335
+ let pinned;
1336
+ try {
1337
+ pinned = await options.pinActiveSecrets(previous.versionId, activeSecrets);
1338
+ } catch {
1339
+ return {
1340
+ ok: false,
1341
+ reason: "inheritance-source-unavailable"
1342
+ };
1343
+ }
1344
+ if (!pinned) return {
1345
+ ok: false,
1346
+ reason: "inheritance-source-changed"
1347
+ };
1348
+ }
1270
1349
  }
1271
1350
  return bootstrapFirstDeployment({
1272
1351
  ...options,
1273
1352
  message
1274
- }, configPath, previous.versionId, true, previousTriggerConfig);
1353
+ }, configPath, previous.versionId, options.atomic === true, previousTriggerConfig);
1354
+ }
1355
+ if (previous.versionId !== null && !activeSecretsPinned && options.pinActiveSecrets && options.candidateSecretsFilePath === void 0) {
1356
+ let activeSecrets;
1357
+ try {
1358
+ activeSecrets = await readCloudflareVersionSecretBindings(options, configPath, previous.versionId);
1359
+ } catch {
1360
+ return {
1361
+ ok: false,
1362
+ reason: "inheritance-source-unavailable"
1363
+ };
1364
+ }
1365
+ if (activeSecrets === null) return {
1366
+ ok: false,
1367
+ reason: "inheritance-source-unavailable"
1368
+ };
1369
+ if (activeSecrets.some((secret) => !secret.name.trim() || secret.name.trim() !== secret.name) || new Set(activeSecrets.map((secret) => secret.name)).size !== activeSecrets.length) return {
1370
+ ok: false,
1371
+ reason: "inheritance-source-changed"
1372
+ };
1373
+ if (activeSecrets.length) {
1374
+ let pinned;
1375
+ try {
1376
+ pinned = await options.pinActiveSecrets(previous.versionId, activeSecrets);
1377
+ } catch {
1378
+ return {
1379
+ ok: false,
1380
+ reason: "inheritance-source-unavailable"
1381
+ };
1382
+ }
1383
+ if (!pinned) return {
1384
+ ok: false,
1385
+ reason: "inheritance-source-changed"
1386
+ };
1387
+ activeSecretsPinned = true;
1388
+ }
1389
+ }
1390
+ if (activeSecretsPinned) {
1391
+ if (!options.verifyUploader()) return {
1392
+ ok: false,
1393
+ reason: "uploader-changed"
1394
+ };
1395
+ const owner = await readCurrentProductionVersion(options, configPath);
1396
+ if (owner.kind === "failed") return {
1397
+ ok: false,
1398
+ reason: "status-failed",
1399
+ status: owner.status
1400
+ };
1401
+ if (owner.kind !== "ok" || owner.versionId !== previous.versionId || !productionVersionPercentagesEqual(owner.versions, [{
1402
+ versionId: previous.versionId,
1403
+ percentage: 100
1404
+ }])) return {
1405
+ ok: false,
1406
+ reason: "deployment-changed",
1407
+ activeVersionId: owner.kind === "ok" ? owner.versionId : null,
1408
+ rollback: "not-needed"
1409
+ };
1275
1410
  }
1276
1411
  options.onProgress?.("Uploading the Worker and assets...");
1277
1412
  const upload = await Promise.resolve(options.runWrangler({
@@ -1647,21 +1782,25 @@ async function readCloudflareVersionInspection(options, configPath, versionId, k
1647
1782
  const annotations = isRecord(entry.annotations) ? entry.annotations : void 0;
1648
1783
  const message = annotations?.["workers/message"];
1649
1784
  let draft = entry;
1650
- if (annotations?.["workers/triggered_by"] === "secret" && Object.keys(annotations).length === 1 && !isRecord(entry.resources)) {
1785
+ if (annotations?.["workers/triggered_by"] === "secret" && Object.keys(annotations).length === 1 && !isWranglerSecretOnlyDraft(entry)) {
1651
1786
  const inspection = await Promise.resolve(options.runWrangler({
1652
1787
  type: "versions.view",
1653
1788
  versionId,
1654
1789
  configPath,
1655
- workerName: options.workerName
1790
+ workerName: options.workerName,
1791
+ includeSource: true
1656
1792
  }, { capture: true }));
1657
1793
  if (isCleanCommandExit(inspection)) {
1658
1794
  const inspected = inspection.data;
1659
1795
  if (isRecord(inspected) && inspected.id === versionId) draft = inspected;
1660
1796
  }
1661
1797
  }
1798
+ const isVoidDraft = isVoidSecretOnlyDraft(draft);
1662
1799
  return {
1663
1800
  metadata: typeof message === "string" ? parseCloudflareVersionMessage(message) : null,
1664
- isSecretOnlyDraft: isWranglerSecretOnlyDraft(draft)
1801
+ isSecretOnlyDraft: isWranglerSecretOnlyDraft(draft) || isVoidDraft,
1802
+ isVoidSecretOnlyDraft: isVoidDraft,
1803
+ voidDraftSecretNames: isVoidDraft ? draft.bindings.map((binding) => binding.name) : null
1665
1804
  };
1666
1805
  }
1667
1806
  } catch {
@@ -1742,14 +1881,17 @@ async function bootstrapFirstDeployment(options, configPath, previousVersionId,
1742
1881
  ok: false,
1743
1882
  reason: "uploader-changed"
1744
1883
  };
1745
- if (atomic) {
1884
+ {
1746
1885
  const current = await readCurrentProductionVersion(options, configPath);
1747
1886
  if (current.kind === "failed") return {
1748
1887
  ok: false,
1749
1888
  reason: "status-failed",
1750
1889
  status: current.status
1751
1890
  };
1752
- if (current.kind !== "ok" || current.versionId !== previousVersionId) return {
1891
+ if (current.kind !== "ok" || current.versionId !== previousVersionId || !productionVersionPercentagesEqual(current.versions, previousVersionId ? [{
1892
+ versionId: previousVersionId,
1893
+ percentage: 100
1894
+ }] : [])) return {
1753
1895
  ok: false,
1754
1896
  reason: "deployment-changed",
1755
1897
  activeVersionId: current.kind === "ok" ? current.versionId : null,
@@ -1794,7 +1936,6 @@ async function bootstrapFirstDeployment(options, configPath, previousVersionId,
1794
1936
  previousVersionId,
1795
1937
  rollback: "unsafe"
1796
1938
  };
1797
- options.onProgress?.("Updating routes and triggers...");
1798
1939
  const triggerOptions = {
1799
1940
  runWrangler: options.runWrangler,
1800
1941
  workerName: options.workerName,
@@ -1809,6 +1950,65 @@ async function bootstrapFirstDeployment(options, configPath, previousVersionId,
1809
1950
  publishEmptyCustomDomains: options.publishEmptyCustomDomains,
1810
1951
  publishEmptyCrons: options.publishEmptyCrons
1811
1952
  };
1953
+ const verifyPublishedOwner = async () => {
1954
+ const owner = await readCurrentProductionVersion(options, configPath);
1955
+ if (owner.kind === "failed") return {
1956
+ ok: false,
1957
+ reason: "status-failed",
1958
+ status: owner.status,
1959
+ versionId,
1960
+ previousVersionId,
1961
+ rollback: "unsafe"
1962
+ };
1963
+ if (owner.kind !== "ok" || owner.versionId !== versionId || !productionVersionPercentagesEqual(owner.versions, [{
1964
+ versionId,
1965
+ percentage: 100
1966
+ }])) {
1967
+ const repair = owner.kind === "ok" && owner.versionId && owner.versionId !== versionId && options.verifyUploader() ? await stabilizeCloudflareVersionTriggers({
1968
+ ...triggerOptions,
1969
+ targetTriggerConfig: {
1970
+ ...desiredTriggerConfig,
1971
+ preserve: true
1972
+ }
1973
+ }) : null;
1974
+ return {
1975
+ ok: false,
1976
+ reason: "deployment-changed",
1977
+ versionId,
1978
+ previousVersionId,
1979
+ activeVersionId: repair?.activeVersionId ?? (owner.kind === "ok" ? owner.versionId : null),
1980
+ rollback: "not-needed",
1981
+ ...repair ? { triggerRepair: repair.ok && !repair.targetOwnsTraffic ? "succeeded" : !repair.ok && repair.reason !== "metadata-unavailable" ? "failed" : "unavailable" } : {}
1982
+ };
1983
+ }
1984
+ return null;
1985
+ };
1986
+ const publishedOwnerFailure = await verifyPublishedOwner();
1987
+ if (publishedOwnerFailure) return publishedOwnerFailure;
1988
+ if (options.validateUploadedVersion || options.recordVerifiedCandidate) {
1989
+ options.onProgress?.("Verifying the uploaded Worker...");
1990
+ try {
1991
+ await options.validateUploadedVersion?.(versionId);
1992
+ const validatedOwnerFailure = await verifyPublishedOwner();
1993
+ if (validatedOwnerFailure) return validatedOwnerFailure;
1994
+ if (options.recordVerifiedCandidate) {
1995
+ if (!options.validateUploadedVersion && !options.verifyInheritedSecrets && options.expectedPreviousVersionId === void 0) throw new Error("The bootstrap upload has no verified candidate checkpoint.");
1996
+ await options.recordVerifiedCandidate(versionId, previousVersionId);
1997
+ }
1998
+ } catch {
1999
+ return {
2000
+ ok: false,
2001
+ reason: atomic ? "atomic-failed" : "bootstrap-failed",
2002
+ versionId,
2003
+ previousVersionId,
2004
+ rollback: "unavailable",
2005
+ diagnostic: `Worker version ${versionId} was published, but its upload validation or candidate checkpoint failed. Inspect the current Worker deployment before retrying.`
2006
+ };
2007
+ }
2008
+ const checkpointOwnerFailure = await verifyPublishedOwner();
2009
+ if (checkpointOwnerFailure) return checkpointOwnerFailure;
2010
+ }
2011
+ options.onProgress?.("Updating routes and triggers...");
1812
2012
  const triggers = await stabilizeCloudflareVersionTriggers(triggerOptions);
1813
2013
  if (!triggers.ok || !triggers.targetOwnsTraffic) {
1814
2014
  const superseded = triggers.activeVersionId !== void 0 && triggers.activeVersionId !== null && triggers.activeVersionId !== versionId;
@@ -2153,7 +2353,7 @@ function createCloudflareMetadataRunner(options) {
2153
2353
  } else if (operation.type === "versions.list") {
2154
2354
  kind = "versions";
2155
2355
  resource = "/versions?deployable=true";
2156
- } else if (operation.type === "versions.view" && /^[0-9a-f-]{36}$/i.test(operation.versionId)) {
2356
+ } else if (operation.type === "versions.view" && !operation.includeSource && /^[0-9a-f-]{36}$/i.test(operation.versionId)) {
2157
2357
  kind = "version";
2158
2358
  resource = "/versions/" + encodeURIComponent(operation.versionId);
2159
2359
  }
@@ -2219,4 +2419,4 @@ function record(value) {
2219
2419
  return typeof value === "object" && value !== null && !Array.isArray(value);
2220
2420
  }
2221
2421
  //#endregion
2222
- export { CloudflareWorkerInspectionError, activateCloudflareVersion, assertCloudflareBindingsPreserved, assertCloudflareDurableObjectLifecycleSupported, assertExistingCloudflareConfig, createCloudflareMetadataRunner, deployCloudflareVersionTraffic, deployCloudflareVersionTriggers, encodeCloudflareVersionMessage, executeCloudflareDeploy, extractWorkersDevSubdomain, normalizeWorkersDevSubdomain, parseCloudflareVersionMessage, parseRootWranglerConfig, readBindingFieldPresence, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, readCloudflareWorkerMigrationTag, readConfigEntryString, readExistingCloudflareWorker, readStoredResourceName, readWranglerTopLevelString, renderCloudflareStaticEntry, resolveD1MigrateTarget, stabilizeCloudflareVersionTriggers, stageCloudflareStaticAssets, writeCloudflareFrameworkDeployConfig, writeCloudflareStaticDeployConfig };
2422
+ export { CloudflareWorkerInspectionError, VOID_SECRET_DRAFT_SOURCE, activateCloudflareVersion, assertCloudflareBindingsPreserved, assertCloudflareDurableObjectLifecycleSupported, assertExistingCloudflareConfig, createCloudflareMetadataRunner, deployCloudflareVersionTraffic, deployCloudflareVersionTriggers, encodeCloudflareVersionMessage, executeCloudflareDeploy, extractWorkersDevSubdomain, normalizeWorkersDevSubdomain, parseCloudflareVersionMessage, parseRootWranglerConfig, readBindingFieldPresence, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, readCloudflareWorkerMigrationTag, readConfigEntryString, readExistingCloudflareWorker, readStoredResourceName, readWranglerTopLevelString, renderCloudflareStaticEntry, resolveD1MigrateTarget, stabilizeCloudflareVersionTriggers, stageCloudflareStaticAssets, writeCloudflareFrameworkDeployConfig, writeCloudflareStaticDeployConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/deploy-cloudflare",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -20,12 +20,12 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@void/deploy-core": "0.23.0",
23
+ "@void/deploy-core": "0.25.0",
24
24
  "jsonc-parser": "3.3.1"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@cloudflare/workers-types": "^4.20260702.1",
28
- "@types/node": "^26.6.3",
28
+ "@types/node": "^26.6.4",
29
29
  "pathslash": "^0.1.0",
30
30
  "typescript": "7.0.2",
31
31
  "vite-plus": "1.0.0"