@void/deploy-cloudflare 1.2.1 → 1.4.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/dist/index.d.mts CHANGED
@@ -10,7 +10,7 @@ type WorkerTarget = ConfigTarget & {
10
10
  type CloudflareOperation = ({
11
11
  type: "auth.whoami" | "auth.login" | "auth.logout" | "auth.token";
12
12
  } & ConfigTarget) | ({
13
- type: "d1.list" | "kv.namespace.list" | "containers.list";
13
+ type: "d1.list" | "kv.namespace.list" | "containers.list" | "workers.subdomain.get";
14
14
  } & ConfigTarget) | ({
15
15
  type: "d1.info";
16
16
  name: string;
@@ -87,6 +87,7 @@ type CloudflareOperation = ({
87
87
  percentage: number;
88
88
  }>;
89
89
  message?: string;
90
+ bypassDeploymentChecks?: boolean;
90
91
  } & WorkerTarget) | ({
91
92
  type: "versions.upload" | "worker.deploy";
92
93
  message?: string;
@@ -267,6 +268,7 @@ type CloudflareStaticDeployConfig = Readonly<{
267
268
  /** Generate the deterministic thin Worker entry bundled by Wrangler. */
268
269
  export declare function renderCloudflareStaticEntry(options: {
269
270
  runtimeImport?: string;
271
+ alwaysUseHttps?: boolean;
270
272
  headerRules?: ReadonlyArray<HeaderRule>;
271
273
  redirectRules?: ReadonlyArray<RedirectRule>;
272
274
  fallbackRules?: ReadonlyArray<RedirectRule>;
@@ -310,6 +312,8 @@ type CloudflareTriggerConfig = Readonly<{
310
312
  settingsHash?: string;
311
313
  /** Explicit Worker-wide observability settings, published after version activation. */
312
314
  observability?: Readonly<Record<string, unknown>>;
315
+ /** Explicit Worker-wide Logpush setting, published after version activation. */
316
+ logpush?: boolean;
313
317
  workers_dev: boolean;
314
318
  preview_urls: boolean;
315
319
  routes: ReadonlyArray<unknown>;
@@ -317,7 +321,7 @@ type CloudflareTriggerConfig = Readonly<{
317
321
  queueConsumers: ReadonlyArray<unknown>;
318
322
  workflows: ReadonlyArray<unknown>;
319
323
  /**
320
- * Email Routing addresses (`addresses` in Wrangler config). Wrangler 4.131 applies its
324
+ * Email Routing addresses (`addresses` in Wrangler config). The Cloudflare CLI applies its
321
325
  * Email Routing plan inside `triggers deploy` — `undefined` skips the plan entirely, while
322
326
  * `[]` is a destructive plan that deletes every rule this Worker owns — so the two must stay
323
327
  * distinct through the snapshot. Absent from annotations written before this field existed.
@@ -325,6 +329,17 @@ type CloudflareTriggerConfig = Readonly<{
325
329
  addresses?: ReadonlyArray<string>;
326
330
  }>;
327
331
  type CloudflareTrafficConfig = Pick<CloudflareTriggerConfig, "workers_dev" | "preview_urls" | "routes">;
332
+ /**
333
+ * Result of an account workers.dev subdomain lookup. `uploader-changed` means the lookup refused
334
+ * to run a changed uploader with the session credentials.
335
+ */
336
+ type CloudflareWorkersDevSubdomainLookup = {
337
+ ok: true;
338
+ subdomain: string;
339
+ } | {
340
+ ok: false;
341
+ reason: "unavailable" | "uploader-changed";
342
+ };
328
343
  type CloudflareVersionActivation = {
329
344
  ok: true;
330
345
  versionId: string;
@@ -345,10 +360,21 @@ type CloudflareVersionActivation = {
345
360
  versionId?: string;
346
361
  previousVersionId?: string | null;
347
362
  rollback?: "not-needed" | "succeeded" | "failed" | "unavailable" | "unsafe";
363
+ /**
364
+ * Cloudflare refused to restore the previous version because a secret
365
+ * changed with this deploy, so Void forced it. The previous version runs
366
+ * with its earlier secret values.
367
+ */
368
+ restoredOlderSecrets?: true;
348
369
  /** Version that owned production after a concurrent operation superseded this command. */
349
370
  activeVersionId?: string | null;
350
371
  /** Whether Void restored the active version's immutable trigger snapshot. */
351
372
  triggerRepair?: "succeeded" | "failed" | "unavailable";
373
+ /**
374
+ * Access denied a readiness probe to this custom-route origin. The probe carried no Access
375
+ * credentials because `readinessHeadersOrigin` did not name the origin.
376
+ */
377
+ customRouteReadinessOrigin?: string;
352
378
  };
353
379
  /** Encode migration state into Cloudflare's version annotation for safe rollback diagnostics. */
354
380
  export declare function encodeCloudflareVersionMessage(migrations: ReadonlyArray<string>, triggerConfig?: CloudflareTriggerConfig, contentHash?: string): string;
@@ -423,11 +449,23 @@ type CloudflareVersionActivationOptions = {
423
449
  readinessHeaders?: Readonly<Record<string, string>>;
424
450
  /** Account workers.dev subdomain used when this Worker cannot expose version preview URLs. */
425
451
  workersDevSubdomain?: string;
452
+ /**
453
+ * Look up the account workers.dev subdomain when a first publish reports no URL and
454
+ * `workersDevSubdomain` is not set. The subdomain includes the compliance-region label, for
455
+ * example `team.fed` for FedRAMP High. A thrown error counts as `unavailable`.
456
+ */
457
+ resolveWorkersDevSubdomain?: () => Promise<CloudflareWorkersDevSubdomainLookup>;
426
458
  /**
427
459
  * Operator-provided workers.dev subdomain that may receive readiness credentials even when
428
460
  * Wrangler cannot report a preview URL. Cached/project-controlled values must not be used here.
429
461
  */
430
462
  readinessHeadersWorkersDevSubdomain?: string;
463
+ /**
464
+ * Operator-provided HTTPS origin that may receive readiness credentials when the probe uses a
465
+ * configured custom route. A route hostname is not guaranteed to reach this Worker, so only an
466
+ * exact origin match receives them. Project-controlled values must not be used here.
467
+ */
468
+ readinessHeadersOrigin?: string;
431
469
  message: string;
432
470
  /** Direct empty-domain publisher for the Wrangler reconciliation gap. */
433
471
  publishEmptyCustomDomains?: () => Promise<CloudflareCommandResult>;
@@ -467,6 +505,12 @@ type CloudflareVersionActivationOptions = {
467
505
  };
468
506
  /** Upload, probe, and atomically activate a verified Worker Version. */
469
507
  export declare function activateCloudflareVersion(options: CloudflareVersionActivationOptions): Promise<CloudflareVersionActivation>;
508
+ /**
509
+ * Normalize an operator-selected readiness origin. Returns `undefined` unless the value is one
510
+ * exact `https:` origin without a wildcard, credentials, a path, a query, or a fragment. A probe
511
+ * URL never has a wildcard host, so a wildcard value could never match.
512
+ */
513
+ export declare function normalizeReadinessAccessOrigin(value: string | undefined): string | undefined;
470
514
  /** Change version traffic without synchronizing Worker settings from local config. */
471
515
  export declare function deployCloudflareVersionTraffic(options: {
472
516
  runWrangler: CloudflareCommandRunner;
@@ -474,6 +518,7 @@ export declare function deployCloudflareVersionTraffic(options: {
474
518
  workerName: string;
475
519
  percentages: Array<string>;
476
520
  message: string;
521
+ bypassDeploymentChecks?: boolean;
477
522
  }): Promise<CloudflareCommandResult>;
478
523
  /** Normalize an account workers.dev subdomain supplied by config or the environment. */
479
524
  export declare function normalizeWorkersDevSubdomain(value: string | undefined): string | undefined;
@@ -506,4 +551,4 @@ export declare function cloudflareCommandDiagnostic(result: CloudflareCommandRes
506
551
  //#region src/secret-draft.d.ts
507
552
  export declare const VOID_SECRET_DRAFT_SOURCE = "export default { fetch() { return new Response(\"\"); } };\n";
508
553
  //#endregion
509
- export type { ApprovedCloudflareKvBinding, CloudflareCommandResult, CloudflareCommandRunner, CloudflareDeployExecution, CloudflareFrameworkDeployConfig, CloudflareOperation, CloudflareProviderError, CloudflareStaticDeployConfig, CloudflareTrafficConfig, CloudflareTriggerConfig, CloudflareTriggerStabilization, CloudflareUploadResult, CloudflareVersionActivation, CloudflareVersionActivationOptions, CloudflareVersionMetadata, CloudflareWorkerMigrationTagOptions, ExistingCloudflareWorker };
554
+ export type { ApprovedCloudflareKvBinding, CloudflareCommandResult, CloudflareCommandRunner, CloudflareDeployExecution, CloudflareFrameworkDeployConfig, CloudflareOperation, CloudflareProviderError, CloudflareStaticDeployConfig, CloudflareTrafficConfig, CloudflareTriggerConfig, CloudflareTriggerStabilization, CloudflareUploadResult, CloudflareVersionActivation, CloudflareVersionActivationOptions, CloudflareVersionMetadata, CloudflareWorkerMigrationTagOptions, CloudflareWorkersDevSubdomainLookup, ExistingCloudflareWorker };
package/dist/index.mjs CHANGED
@@ -645,6 +645,7 @@ function writeCloudflareFrameworkDeployConfig(options) {
645
645
  /** Generate the deterministic thin Worker entry bundled by Wrangler. */
646
646
  function renderCloudflareStaticEntry(options) {
647
647
  const config = JSON.stringify({
648
+ ...options.alwaysUseHttps !== void 0 ? { alwaysUseHttps: options.alwaysUseHttps } : {},
648
649
  ...options.headerRules?.length ? { headerRules: options.headerRules } : {},
649
650
  ...options.redirectRules?.length ? { redirectRules: options.redirectRules } : {},
650
651
  ...options.fallbackRules?.length ? { fallbackRules: options.fallbackRules } : {},
@@ -799,6 +800,7 @@ function cloudflareCommandDiagnostic(result) {
799
800
  //#endregion
800
801
  //#region src/version.ts
801
802
  const DURABLE_MIGRATION_VERSION_ERROR = 10211;
803
+ const CLOUDFLARE_SECRET_CHANGED_ERROR = 10220;
802
804
  /** Encode migration state into Cloudflare's version annotation for safe rollback diagnostics. */
803
805
  function encodeCloudflareVersionMessage(migrations, triggerConfig, contentHash) {
804
806
  const metadata = triggerConfig ? {
@@ -843,7 +845,8 @@ function readCloudflareTriggerConfig(configPath, sourceConfigPath) {
843
845
  const source = sourceConfigPath ? parseRootWranglerConfig(sourceConfigPath) : config;
844
846
  const result = triggerConfigFromWrangler({
845
847
  ...config,
846
- observability: source.observability
848
+ observability: source.observability,
849
+ logpush: source.logpush
847
850
  });
848
851
  if (!sourceConfigPath) return result;
849
852
  return {
@@ -1084,6 +1087,7 @@ function triggerConfigFromWrangler(config) {
1084
1087
  ...config.preview_urls === void 0 ? { preservePreviewUrls: true } : {},
1085
1088
  settingsHash: nonVersionedSettingsHash(config),
1086
1089
  ...isRecord(config.observability) ? { observability: config.observability } : {},
1090
+ ...typeof config.logpush === "boolean" ? { logpush: config.logpush } : {},
1087
1091
  workers_dev: workersDev,
1088
1092
  preview_urls: typeof config.preview_urls === "boolean" ? config.preview_urls : workersDev,
1089
1093
  routes,
@@ -1117,7 +1121,8 @@ function parseTriggerConfig(value) {
1117
1121
  "preserve",
1118
1122
  "preserveRoutes",
1119
1123
  "preserveWorkersDev",
1120
- "preservePreviewUrls"
1124
+ "preservePreviewUrls",
1125
+ "logpush"
1121
1126
  ].some((key) => value[key] !== void 0 && typeof value[key] !== "boolean") || value.settingsHash !== void 0 && typeof value.settingsHash !== "string" || value.observability !== void 0 && !isRecord(value.observability) || typeof value.workers_dev !== "boolean" || typeof value.preview_urls !== "boolean" || !Array.isArray(value.routes) || !Array.isArray(value.crons) || !Array.isArray(value.queueConsumers) || !Array.isArray(value.workflows) || value.addresses !== void 0 && readEmailAddresses(value.addresses) === void 0) return null;
1122
1127
  return {
1123
1128
  ...value.preserve === true ? { preserve: true } : {},
@@ -1126,6 +1131,7 @@ function parseTriggerConfig(value) {
1126
1131
  ...value.preservePreviewUrls === true ? { preservePreviewUrls: true } : {},
1127
1132
  ...typeof value.settingsHash === "string" ? { settingsHash: value.settingsHash } : {},
1128
1133
  ...isRecord(value.observability) ? { observability: value.observability } : {},
1134
+ ...typeof value.logpush === "boolean" ? { logpush: value.logpush } : {},
1129
1135
  workers_dev: value.workers_dev,
1130
1136
  preview_urls: value.preview_urls,
1131
1137
  routes: value.routes,
@@ -1143,6 +1149,7 @@ function triggerWranglerConfig(base, triggerConfig, manageRoutes = true) {
1143
1149
  const config = {
1144
1150
  ...base,
1145
1151
  ...triggerConfig.observability === void 0 ? {} : { observability: triggerConfig.observability },
1152
+ ...triggerConfig.logpush === void 0 ? {} : { logpush: triggerConfig.logpush },
1146
1153
  triggers,
1147
1154
  queues,
1148
1155
  workflows: [...triggerConfig.workflows],
@@ -1162,6 +1169,7 @@ function resolvePreservedTraffic(desired, previous, workerExists) {
1162
1169
  ...desired.preserve ? { preserve: true } : {},
1163
1170
  ...desired.settingsHash ? { settingsHash: desired.settingsHash } : {},
1164
1171
  ...desired.observability === void 0 ? {} : { observability: desired.observability },
1172
+ ...desired.logpush === void 0 ? {} : { logpush: desired.logpush },
1165
1173
  workers_dev: desired.preserveWorkersDev ? previous?.workers_dev ?? desired.workers_dev : desired.workers_dev,
1166
1174
  preview_urls: desired.preservePreviewUrls ? previous?.preview_urls ?? desired.preview_urls : desired.preview_urls,
1167
1175
  routes: desired.preserveRoutes ? previous?.routes ?? desired.routes : desired.routes,
@@ -1506,7 +1514,9 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1506
1514
  if (stagingFailure) return stagingFailure;
1507
1515
  stagedVersionOverride = true;
1508
1516
  }
1509
- const probe = (url) => probeReadiness(`${url.replace(/\/$/, "")}/__void/ready`, options.workerName, readinessUsesCustomRoute ? void 0 : options.readinessHeaders, authenticatedWorkersDevSubdomain, stagedVersionOverride ? versionId : void 0, options.fetch ?? globalThis.fetch, options.sleep ?? setTimeout, options.readinessAttempts ?? 6, Number.POSITIVE_INFINITY, options.requireVersionMetadata ? versionId : void 0);
1517
+ const trustedReadinessOrigin = normalizeReadinessAccessOrigin(options.readinessHeadersOrigin);
1518
+ const withholdsCustomRouteHeaders = (url) => readinessUsesCustomRoute && !isTrustedReadinessOrigin(url, trustedReadinessOrigin);
1519
+ const probe = (url) => probeReadiness(`${url.replace(/\/$/, "")}/__void/ready`, options.workerName, withholdsCustomRouteHeaders(url) ? void 0 : options.readinessHeaders, authenticatedWorkersDevSubdomain, readinessUsesCustomRoute ? trustedReadinessOrigin : void 0, stagedVersionOverride ? versionId : void 0, options.fetch ?? globalThis.fetch, options.sleep ?? setTimeout, options.readinessAttempts ?? 6, Number.POSITIVE_INFINITY, options.requireVersionMetadata ? versionId : void 0);
1510
1520
  options.onProgress?.("Waiting for the Worker to become ready...");
1511
1521
  let ready = await probe(readinessUrl);
1512
1522
  if (ready === "access-denied" && !stagedVersionOverride && previous.versionId !== null && !options.requirePreviewForReadiness) {
@@ -1526,14 +1536,15 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1526
1536
  reason: ready === "access-denied" ? "readiness-access-denied" : "readiness-failed",
1527
1537
  versionId,
1528
1538
  previousVersionId: previous.versionId,
1529
- rollback: stagedVersionOverride ? await restorePreviousVersion(options, previous.versionId, "readiness failed", [stagedVersionPercentages(previous.versionId, versionId)]) : "not-needed"
1539
+ ...ready === "access-denied" && withholdsCustomRouteHeaders(readinessUrl) ? customRouteReadinessOriginField(readinessUrl) : {},
1540
+ ...stagedVersionOverride ? await restorePreviousVersion(options, previous.versionId, "readiness failed", [stagedVersionPercentages(previous.versionId, versionId)]) : { rollback: "not-needed" }
1530
1541
  };
1531
1542
  if (!options.verifyUploader()) return {
1532
1543
  ok: false,
1533
1544
  reason: "uploader-changed",
1534
1545
  versionId,
1535
1546
  previousVersionId: previous.versionId,
1536
- rollback: stagedVersionOverride ? await restorePreviousVersion(options, previous.versionId, "uploader verification failed", [stagedVersionPercentages(previous.versionId, versionId)]) : "not-needed"
1547
+ ...stagedVersionOverride ? await restorePreviousVersion(options, previous.versionId, "uploader verification failed", [stagedVersionPercentages(previous.versionId, versionId)]) : { rollback: "not-needed" }
1537
1548
  };
1538
1549
  const ownerBeforeActivation = await readCurrentProductionVersion(options);
1539
1550
  if (ownerBeforeActivation.kind === "failed") return {
@@ -1570,10 +1581,10 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1570
1581
  status: activate.status,
1571
1582
  versionId,
1572
1583
  previousVersionId: previous.versionId,
1573
- rollback: stagedVersionOverride ? await restorePreviousVersion(options, previous.versionId, "version activation failed", [stagedVersionPercentages(previous.versionId, versionId), [{
1584
+ ...stagedVersionOverride ? await restorePreviousVersion(options, previous.versionId, "version activation failed", [stagedVersionPercentages(previous.versionId, versionId), [{
1574
1585
  versionId,
1575
1586
  percentage: 100
1576
- }]]) : "not-needed"
1587
+ }]]) : { rollback: "not-needed" }
1577
1588
  };
1578
1589
  if (!options.verifyUploader()) return {
1579
1590
  ok: false,
@@ -1666,14 +1677,14 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1666
1677
  triggerRepair: triggerRepairOutcome(repair)
1667
1678
  };
1668
1679
  }
1669
- const rollback = await restorePreviousVersionAndTriggers(options, previous.versionId, "trigger activation failed", desiredTriggerConfig, versionId, versionedPreviousTriggerConfigWithTraffic);
1680
+ const restore = await restorePreviousVersionAndTriggers(options, previous.versionId, "trigger activation failed", desiredTriggerConfig, versionId, versionedPreviousTriggerConfigWithTraffic);
1670
1681
  return {
1671
1682
  ok: false,
1672
1683
  reason: "triggers-failed",
1673
1684
  status: triggers.status,
1674
1685
  versionId,
1675
1686
  previousVersionId: previous.versionId,
1676
- rollback
1687
+ ...restore
1677
1688
  };
1678
1689
  }
1679
1690
  if (!triggers.targetOwnsTraffic) return {
@@ -1704,16 +1715,28 @@ async function restorePreviousVersionAndTriggers(options, previousVersionId, cau
1704
1715
  versionId: failedVersionId,
1705
1716
  percentage: 100
1706
1717
  }]]);
1707
- if (traffic !== "succeeded" || !previousVersionId) return traffic;
1708
- if (!options.verifyUploader()) return "unsafe";
1718
+ if (traffic.rollback !== "succeeded" || !previousVersionId) return traffic;
1719
+ if (!options.verifyUploader()) return {
1720
+ ...traffic,
1721
+ rollback: "unsafe"
1722
+ };
1709
1723
  let targetTriggerConfig = capturedTriggerConfig;
1710
1724
  if (!targetTriggerConfig) {
1711
1725
  const metadata = await readCloudflareVersionMetadata(options, previousVersionId);
1712
- if (metadata?.schema !== 2) return "unavailable";
1726
+ if (metadata?.schema !== 2) return {
1727
+ ...traffic,
1728
+ rollback: "unavailable"
1729
+ };
1713
1730
  targetTriggerConfig = metadata.triggerConfig;
1714
1731
  }
1715
- if (targetTriggerConfig.preserve) return "unavailable";
1716
- if (!options.verifyUploader()) return "unsafe";
1732
+ if (targetTriggerConfig.preserve) return {
1733
+ ...traffic,
1734
+ rollback: "unavailable"
1735
+ };
1736
+ if (!options.verifyUploader()) return {
1737
+ ...traffic,
1738
+ rollback: "unsafe"
1739
+ };
1717
1740
  const triggers = await stabilizeCloudflareVersionTriggers({
1718
1741
  runWrangler: options.runWrangler,
1719
1742
  workerName: options.workerName,
@@ -1725,7 +1748,10 @@ async function restorePreviousVersionAndTriggers(options, previousVersionId, cau
1725
1748
  publishEmptyCustomDomains: options.publishEmptyCustomDomains,
1726
1749
  publishEmptyCrons: options.publishEmptyCrons
1727
1750
  });
1728
- return triggers.ok && triggers.targetOwnsTraffic ? "succeeded" : "failed";
1751
+ return {
1752
+ ...traffic,
1753
+ rollback: triggers.ok && triggers.targetOwnsTraffic ? "succeeded" : "failed"
1754
+ };
1729
1755
  }
1730
1756
  async function readCloudflareVersionMetadata(options, versionId) {
1731
1757
  return (await readCloudflareVersionInspection(options, versionId))?.metadata ?? null;
@@ -1879,7 +1905,33 @@ async function bootstrapFirstDeployment(options, previousVersionId, atomic = fal
1879
1905
  const published = readUploadResult(bootstrap);
1880
1906
  const versionId = published?.versionId;
1881
1907
  const desiredTriggerConfig = desiredTriggerConfigFor(parseCloudflareVersionMessage(options.message), options.configPath);
1882
- const reportedOrConfiguredUrl = published?.previewUrl ?? published?.workersDevUrl ?? (desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes));
1908
+ let reportedOrConfiguredUrl = published?.previewUrl ?? published?.workersDevUrl ?? (desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes));
1909
+ if (versionId && !reportedOrConfiguredUrl && desiredTriggerConfig.workers_dev && !desiredTriggerConfig.preserve && !desiredTriggerConfig.preserveWorkersDev && !options.preserveTriggers && options.resolveWorkersDevSubdomain) {
1910
+ if (!options.verifyUploader()) return {
1911
+ ok: false,
1912
+ reason: "uploader-changed",
1913
+ versionId,
1914
+ previousVersionId,
1915
+ rollback: "unsafe"
1916
+ };
1917
+ let lookup;
1918
+ try {
1919
+ lookup = await options.resolveWorkersDevSubdomain();
1920
+ } catch {
1921
+ lookup = {
1922
+ ok: false,
1923
+ reason: "unavailable"
1924
+ };
1925
+ }
1926
+ if (!lookup.ok && lookup.reason === "uploader-changed") return {
1927
+ ok: false,
1928
+ reason: "uploader-changed",
1929
+ versionId,
1930
+ previousVersionId,
1931
+ rollback: "unsafe"
1932
+ };
1933
+ reportedOrConfiguredUrl = lookup.ok ? workersDevReadinessUrl(options.workerName, lookup.subdomain) : void 0;
1934
+ }
1883
1935
  if (!versionId || !reportedOrConfiguredUrl) return {
1884
1936
  ok: false,
1885
1937
  reason: atomic ? "atomic-output-invalid" : "bootstrap-output-invalid",
@@ -1984,14 +2036,18 @@ async function bootstrapFirstDeployment(options, previousVersionId, atomic = fal
1984
2036
  const usesCustomDomain = !desiredTriggerConfig.workers_dev && desiredTriggerConfig.routes.some((route) => isRecord(route) && route.custom_domain === true);
1985
2037
  const customDomainBootstrap = !atomic && usesCustomDomain;
1986
2038
  const readinessTimeoutMs = customDomainBootstrap ? 3e5 : 6e4;
2039
+ const previewSubdomain = extractWorkersDevSubdomain(previewUrl);
2040
+ const trustedReadinessOrigin = previewSubdomain ? void 0 : normalizeReadinessAccessOrigin(options.readinessHeadersOrigin);
2041
+ const withholdsCustomRouteHeaders = !previewSubdomain && !isTrustedReadinessOrigin(previewUrl, trustedReadinessOrigin);
1987
2042
  options.onProgress?.("Waiting for the Worker to become ready...");
1988
- const ready = await probeReadiness(`${previewUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, extractWorkersDevSubdomain(previewUrl) ? options.readinessHeaders : void 0, extractWorkersDevSubdomain(previewUrl), void 0, options.fetch ?? globalThis.fetch, options.sleep ?? setTimeout, options.readinessAttempts ?? (customDomainBootstrap ? 150 : 30), readinessTimeoutMs, options.requireVersionMetadata ? versionId : void 0);
2043
+ const ready = await probeReadiness(`${previewUrl.replace(/\/$/, "")}/__void/ready`, options.workerName, withholdsCustomRouteHeaders ? void 0 : options.readinessHeaders, previewSubdomain, trustedReadinessOrigin, void 0, options.fetch ?? globalThis.fetch, options.sleep ?? setTimeout, options.readinessAttempts ?? (customDomainBootstrap ? 150 : 30), readinessTimeoutMs, options.requireVersionMetadata ? versionId : void 0);
1989
2044
  if (ready !== "ready") return {
1990
2045
  ok: false,
1991
2046
  reason: ready === "access-denied" ? atomic ? "atomic-readiness-access-denied" : "bootstrap-readiness-access-denied" : atomic ? "atomic-readiness-failed" : "bootstrap-readiness-failed",
1992
2047
  versionId,
1993
2048
  previousVersionId,
1994
2049
  rollback: "unavailable",
2050
+ ...ready === "access-denied" && withholdsCustomRouteHeaders ? customRouteReadinessOriginField(previewUrl) : {},
1995
2051
  ...ready === "failed" && usesCustomDomain ? { diagnostic: "The Custom Domain may still be propagating DNS or its TLS certificate. Check the configured domain in Cloudflare, then retry `void deploy --platform cloudflare` once its /__void/ready endpoint is reachable." } : {}
1996
2052
  };
1997
2053
  if (!options.verifyUploader()) return {
@@ -2167,8 +2223,8 @@ async function verifyReadinessStagingOwner(options, previousVersionId, versionId
2167
2223
  };
2168
2224
  return null;
2169
2225
  }
2170
- async function probeReadiness(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain, versionOverride, fetcher, sleep, attempts, timeoutMs = Number.POSITIVE_INFINITY, expectedVersionId) {
2171
- const headers = guardedReadinessHeaders(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain);
2226
+ async function probeReadiness(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain, trustedReadinessOrigin, versionOverride, fetcher, sleep, attempts, timeoutMs = Number.POSITIVE_INFINITY, expectedVersionId) {
2227
+ const headers = guardedReadinessHeaders(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain, trustedReadinessOrigin);
2172
2228
  if (headers === null) return "failed";
2173
2229
  const deadline = Date.now() + timeoutMs;
2174
2230
  for (let attempt = 0; attempt < Math.max(1, attempts); attempt++) {
@@ -2199,9 +2255,13 @@ async function probeReadiness(url, workerName, readinessHeaders, authenticatedWo
2199
2255
  }
2200
2256
  return "failed";
2201
2257
  }
2202
- /** Never forward Access headers to a hostname that cannot be this Worker. */
2203
- function guardedReadinessHeaders(url, workerName, headers, authenticatedWorkersDevSubdomain) {
2258
+ /**
2259
+ * Never forward Access headers to a hostname that cannot be this Worker, unless the operator
2260
+ * named that exact origin.
2261
+ */
2262
+ function guardedReadinessHeaders(url, workerName, headers, authenticatedWorkersDevSubdomain, trustedReadinessOrigin) {
2204
2263
  if (!headers || Object.keys(headers).length === 0) return {};
2264
+ if (isTrustedReadinessOrigin(url, trustedReadinessOrigin)) return headers;
2205
2265
  try {
2206
2266
  const parsed = new URL(url);
2207
2267
  const firstLabel = parsed.hostname.toLowerCase().split(".")[0] ?? "";
@@ -2213,6 +2273,36 @@ function guardedReadinessHeaders(url, workerName, headers, authenticatedWorkersD
2213
2273
  return null;
2214
2274
  }
2215
2275
  }
2276
+ /**
2277
+ * Normalize an operator-selected readiness origin. Returns `undefined` unless the value is one
2278
+ * exact `https:` origin without a wildcard, credentials, a path, a query, or a fragment. A probe
2279
+ * URL never has a wildcard host, so a wildcard value could never match.
2280
+ */
2281
+ function normalizeReadinessAccessOrigin(value) {
2282
+ if (!value) return;
2283
+ try {
2284
+ const url = new URL(value);
2285
+ return url.protocol === "https:" && !url.hostname.includes("*") && !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash ? url.origin : void 0;
2286
+ } catch {
2287
+ return;
2288
+ }
2289
+ }
2290
+ function isTrustedReadinessOrigin(url, trustedOrigin) {
2291
+ if (trustedOrigin === void 0) return false;
2292
+ try {
2293
+ const parsed = new URL(url);
2294
+ return parsed.protocol === "https:" && !parsed.username && !parsed.password && parsed.origin === trustedOrigin;
2295
+ } catch {
2296
+ return false;
2297
+ }
2298
+ }
2299
+ function customRouteReadinessOriginField(url) {
2300
+ try {
2301
+ return { customRouteReadinessOrigin: new URL(url).origin };
2302
+ } catch {
2303
+ return {};
2304
+ }
2305
+ }
2216
2306
  async function isCloudflareAccessDenial(response) {
2217
2307
  if (response.status >= 300 && response.status < 400 && response.headers.get("location")?.includes("/cdn-cgi/access/")) return true;
2218
2308
  if (response.status !== 403) return false;
@@ -2236,7 +2326,8 @@ async function deployVersionPercentages(options, percentages, message) {
2236
2326
  versions: versionPercentages(percentages),
2237
2327
  configPath: options.configPath,
2238
2328
  workerName: options.workerName,
2239
- message
2329
+ message,
2330
+ ...options.bypassDeploymentChecks ? { bypassDeploymentChecks: true } : {}
2240
2331
  }, { capture: false });
2241
2332
  }
2242
2333
  /** Change version traffic without synchronizing Worker settings from local config. */
@@ -2251,7 +2342,8 @@ async function deployCloudflareVersionTraffic(options) {
2251
2342
  versions: versionPercentages(options.percentages),
2252
2343
  configPath: path,
2253
2344
  workerName: options.workerName,
2254
- message: options.message
2345
+ message: options.message,
2346
+ ...options.bypassDeploymentChecks ? { bypassDeploymentChecks: true } : {}
2255
2347
  }, { capture: false });
2256
2348
  } finally {
2257
2349
  rmSync(directory, {
@@ -2261,11 +2353,26 @@ async function deployCloudflareVersionTraffic(options) {
2261
2353
  }
2262
2354
  }
2263
2355
  async function restorePreviousVersion(options, previousVersionId, cause, expectedOwnedTraffic) {
2264
- if (!previousVersionId) return "unavailable";
2265
- if (!options.verifyUploader()) return "unsafe";
2266
- const owner = await readCurrentProductionVersion(options);
2267
- if (owner.kind !== "ok" || !expectedOwnedTraffic.some((expected) => productionVersionPercentagesEqual(owner.versions, expected))) return "unsafe";
2268
- return isCleanCommandExit(await deployVersionPercentages(options, [`${previousVersionId}@100`], `Void automatic rollback after ${cause}.`)) ? "succeeded" : "failed";
2356
+ if (!previousVersionId) return { rollback: "unavailable" };
2357
+ if (!options.verifyUploader()) return { rollback: "unsafe" };
2358
+ const ownsExpectedTraffic = async () => {
2359
+ const owner = await readCurrentProductionVersion(options);
2360
+ return owner.kind === "ok" && expectedOwnedTraffic.some((expected) => productionVersionPercentagesEqual(owner.versions, expected));
2361
+ };
2362
+ if (!await ownsExpectedTraffic()) return { rollback: "unsafe" };
2363
+ const restore = (bypassDeploymentChecks) => deployVersionPercentages({
2364
+ ...options,
2365
+ bypassDeploymentChecks
2366
+ }, [`${previousVersionId}@100`], `Void automatic rollback after ${cause}.`);
2367
+ const result = await restore(false);
2368
+ if (result.errorCode === CLOUDFLARE_SECRET_CHANGED_ERROR) {
2369
+ if (!await ownsExpectedTraffic()) return { rollback: "unsafe" };
2370
+ return isCleanCommandExit(await restore(true)) ? {
2371
+ rollback: "succeeded",
2372
+ restoredOlderSecrets: true
2373
+ } : { rollback: "failed" };
2374
+ }
2375
+ return { rollback: isCleanCommandExit(result) ? "succeeded" : "failed" };
2269
2376
  }
2270
2377
  function workersDevReadinessUrl(workerName, value) {
2271
2378
  const subdomain = normalizeWorkersDevSubdomain(value);
@@ -2391,4 +2498,4 @@ function createCloudflareMetadataRunner(options) {
2391
2498
  };
2392
2499
  }
2393
2500
  //#endregion
2394
- export { CloudflareWorkerInspectionError, VOID_SECRET_DRAFT_SOURCE, activateCloudflareVersion, assertCloudflareBindingsPreserved, assertCloudflareDurableObjectLifecycleSupported, assertExistingCloudflareConfig, cloudflareCommandDiagnostic, createCloudflareMetadataRunner, deployCloudflareVersionTraffic, deployCloudflareVersionTriggers, encodeCloudflareVersionMessage, executeCloudflareDeploy, extractWorkersDevSubdomain, normalizeWorkersDevSubdomain, parseCloudflareVersionMessage, parseRootWranglerConfig, readBindingFieldPresence, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, readCloudflareWorkerMigrationTag, readConfigEntryString, readExistingCloudflareWorker, readStoredResourceName, readWranglerTopLevelString, renderCloudflareStaticEntry, resolveD1MigrateTarget, stabilizeCloudflareVersionTriggers, stageCloudflareStaticAssets, writeCloudflareFrameworkDeployConfig, writeCloudflareStaticDeployConfig };
2501
+ export { CloudflareWorkerInspectionError, VOID_SECRET_DRAFT_SOURCE, activateCloudflareVersion, assertCloudflareBindingsPreserved, assertCloudflareDurableObjectLifecycleSupported, assertExistingCloudflareConfig, cloudflareCommandDiagnostic, createCloudflareMetadataRunner, deployCloudflareVersionTraffic, deployCloudflareVersionTriggers, encodeCloudflareVersionMessage, executeCloudflareDeploy, extractWorkersDevSubdomain, normalizeReadinessAccessOrigin, 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": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "Cloudflare deployment primitives for Void runtimes and deployment adapters.",
5
5
  "homepage": "https://void.cloud",
6
6
  "bugs": {
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@void/deploy-core": "1.2.1",
29
+ "@void/deploy-core": "1.4.0",
30
30
  "jsonc-parser": "^3.3.1"
31
31
  },
32
32
  "devDependencies": {