@void/deploy-cloudflare 1.3.0 → 1.5.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
@@ -1,5 +1,4 @@
1
1
  import { AssetConfig, HeaderRule, RedirectRule } from "@void/deploy-core";
2
- //#region src/operations.d.ts
3
2
  /** Typed provider operations shared by app deployment and platform installation. */
4
3
  type ConfigTarget = {
5
4
  configPath?: string;
@@ -10,7 +9,7 @@ type WorkerTarget = ConfigTarget & {
10
9
  type CloudflareOperation = ({
11
10
  type: "auth.whoami" | "auth.login" | "auth.logout" | "auth.token";
12
11
  } & ConfigTarget) | ({
13
- type: "d1.list" | "kv.namespace.list" | "containers.list";
12
+ type: "d1.list" | "kv.namespace.list" | "containers.list" | "workers.subdomain.get";
14
13
  } & ConfigTarget) | ({
15
14
  type: "d1.info";
16
15
  name: string;
@@ -120,8 +119,6 @@ type CloudflareProviderError = {
120
119
  code?: number;
121
120
  message: string;
122
121
  };
123
- //#endregion
124
- //#region src/execute.d.ts
125
122
  type CloudflareCommandResult = {
126
123
  /** Structured provider response. stdout is diagnostics only. */
127
124
  data?: unknown;
@@ -153,8 +150,6 @@ export declare function executeCloudflareDeploy(options: {
153
150
  verifyUploader: () => boolean;
154
151
  configPath?: string;
155
152
  }): Promise<CloudflareDeployExecution>;
156
- //#endregion
157
- //#region src/existing.d.ts
158
153
  type ExistingCloudflareWorker = {
159
154
  versionId: string;
160
155
  bindings: Array<Record<string, unknown>>;
@@ -208,8 +203,6 @@ export declare function assertCloudflareBindingsPreserved(before: ExistingCloudf
208
203
  }): void;
209
204
  /** Preserve the existing Worker, allowing only explicitly approved KV additions. */
210
205
  export declare function assertExistingCloudflareConfig(existing: ExistingCloudflareWorker, config: Record<string, unknown>, built?: boolean, approvedKv?: ReadonlyArray<ApprovedCloudflareKvBinding>): void;
211
- //#endregion
212
- //#region src/config.d.ts
213
206
  /** Parse a root Wrangler JSON/JSONC config without accepting a partial parse. */
214
207
  export declare function parseRootWranglerConfig(configPath: string): Record<string, unknown>;
215
208
  export declare function readWranglerTopLevelString(config: Record<string, unknown>, field: string): string | undefined;
@@ -226,8 +219,6 @@ export declare function resolveD1MigrateTarget(config: Record<string, unknown>,
226
219
  ok: false;
227
220
  reason: string;
228
221
  };
229
- //#endregion
230
- //#region src/framework.d.ts
231
222
  type CloudflareFrameworkDeployConfig = Readonly<{
232
223
  configPath: string;
233
224
  deployPointerPath: string;
@@ -254,8 +245,6 @@ type CloudflareFrameworkDeployConfig = Readonly<{
254
245
  * that file, so Wrangler uploads the exact config Void verifies.
255
246
  */
256
247
  export declare function writeCloudflareFrameworkDeployConfig(options: CloudflareFrameworkDeployConfig): boolean;
257
- //#endregion
258
- //#region src/static.d.ts
259
248
  type CloudflareStaticDeployConfig = Readonly<{
260
249
  configPath: string;
261
250
  name: string;
@@ -268,6 +257,7 @@ type CloudflareStaticDeployConfig = Readonly<{
268
257
  /** Generate the deterministic thin Worker entry bundled by Wrangler. */
269
258
  export declare function renderCloudflareStaticEntry(options: {
270
259
  runtimeImport?: string;
260
+ alwaysUseHttps?: boolean;
271
261
  headerRules?: ReadonlyArray<HeaderRule>;
272
262
  redirectRules?: ReadonlyArray<RedirectRule>;
273
263
  fallbackRules?: ReadonlyArray<RedirectRule>;
@@ -289,8 +279,6 @@ export declare function stageCloudflareStaticAssets(options: {
289
279
  * Durable Object migrations, resource ids, or unrelated user configuration.
290
280
  */
291
281
  export declare function writeCloudflareStaticDeployConfig(options: CloudflareStaticDeployConfig): boolean;
292
- //#endregion
293
- //#region src/version.d.ts
294
282
  type CloudflareVersionMetadata = Readonly<{
295
283
  schema: 1;
296
284
  migrations: ReadonlyArray<string>;
@@ -311,6 +299,8 @@ type CloudflareTriggerConfig = Readonly<{
311
299
  settingsHash?: string;
312
300
  /** Explicit Worker-wide observability settings, published after version activation. */
313
301
  observability?: Readonly<Record<string, unknown>>;
302
+ /** Explicit Worker-wide Logpush setting, published after version activation. */
303
+ logpush?: boolean;
314
304
  workers_dev: boolean;
315
305
  preview_urls: boolean;
316
306
  routes: ReadonlyArray<unknown>;
@@ -318,7 +308,7 @@ type CloudflareTriggerConfig = Readonly<{
318
308
  queueConsumers: ReadonlyArray<unknown>;
319
309
  workflows: ReadonlyArray<unknown>;
320
310
  /**
321
- * Email Routing addresses (`addresses` in Wrangler config). Wrangler 4.131 applies its
311
+ * Email Routing addresses (`addresses` in Wrangler config). The Cloudflare CLI applies its
322
312
  * Email Routing plan inside `triggers deploy` — `undefined` skips the plan entirely, while
323
313
  * `[]` is a destructive plan that deletes every rule this Worker owns — so the two must stay
324
314
  * distinct through the snapshot. Absent from annotations written before this field existed.
@@ -326,6 +316,17 @@ type CloudflareTriggerConfig = Readonly<{
326
316
  addresses?: ReadonlyArray<string>;
327
317
  }>;
328
318
  type CloudflareTrafficConfig = Pick<CloudflareTriggerConfig, "workers_dev" | "preview_urls" | "routes">;
319
+ /**
320
+ * Result of an account workers.dev subdomain lookup. `uploader-changed` means the lookup refused
321
+ * to run a changed uploader with the session credentials.
322
+ */
323
+ type CloudflareWorkersDevSubdomainLookup = {
324
+ ok: true;
325
+ subdomain: string;
326
+ } | {
327
+ ok: false;
328
+ reason: "unavailable" | "uploader-changed";
329
+ };
329
330
  type CloudflareVersionActivation = {
330
331
  ok: true;
331
332
  versionId: string;
@@ -356,6 +357,11 @@ type CloudflareVersionActivation = {
356
357
  activeVersionId?: string | null;
357
358
  /** Whether Void restored the active version's immutable trigger snapshot. */
358
359
  triggerRepair?: "succeeded" | "failed" | "unavailable";
360
+ /**
361
+ * Access denied a readiness probe to this custom-route origin. The probe carried no Access
362
+ * credentials because `readinessHeadersOrigin` did not name the origin.
363
+ */
364
+ customRouteReadinessOrigin?: string;
359
365
  };
360
366
  /** Encode migration state into Cloudflare's version annotation for safe rollback diagnostics. */
361
367
  export declare function encodeCloudflareVersionMessage(migrations: ReadonlyArray<string>, triggerConfig?: CloudflareTriggerConfig, contentHash?: string): string;
@@ -430,11 +436,23 @@ type CloudflareVersionActivationOptions = {
430
436
  readinessHeaders?: Readonly<Record<string, string>>;
431
437
  /** Account workers.dev subdomain used when this Worker cannot expose version preview URLs. */
432
438
  workersDevSubdomain?: string;
439
+ /**
440
+ * Look up the account workers.dev subdomain when a first publish reports no URL and
441
+ * `workersDevSubdomain` is not set. The subdomain includes the compliance-region label, for
442
+ * example `team.fed` for FedRAMP High. A thrown error counts as `unavailable`.
443
+ */
444
+ resolveWorkersDevSubdomain?: () => Promise<CloudflareWorkersDevSubdomainLookup>;
433
445
  /**
434
446
  * Operator-provided workers.dev subdomain that may receive readiness credentials even when
435
447
  * Wrangler cannot report a preview URL. Cached/project-controlled values must not be used here.
436
448
  */
437
449
  readinessHeadersWorkersDevSubdomain?: string;
450
+ /**
451
+ * Operator-provided HTTPS origin that may receive readiness credentials when the probe uses a
452
+ * configured custom route. A route hostname is not guaranteed to reach this Worker, so only an
453
+ * exact origin match receives them. Project-controlled values must not be used here.
454
+ */
455
+ readinessHeadersOrigin?: string;
438
456
  message: string;
439
457
  /** Direct empty-domain publisher for the Wrangler reconciliation gap. */
440
458
  publishEmptyCustomDomains?: () => Promise<CloudflareCommandResult>;
@@ -474,6 +492,12 @@ type CloudflareVersionActivationOptions = {
474
492
  };
475
493
  /** Upload, probe, and atomically activate a verified Worker Version. */
476
494
  export declare function activateCloudflareVersion(options: CloudflareVersionActivationOptions): Promise<CloudflareVersionActivation>;
495
+ /**
496
+ * Normalize an operator-selected readiness origin. Returns `undefined` unless the value is one
497
+ * exact `https:` origin without a wildcard, credentials, a path, a query, or a fragment. A probe
498
+ * URL never has a wildcard host, so a wildcard value could never match.
499
+ */
500
+ export declare function normalizeReadinessAccessOrigin(value: string | undefined): string | undefined;
477
501
  /** Change version traffic without synchronizing Worker settings from local config. */
478
502
  export declare function deployCloudflareVersionTraffic(options: {
479
503
  runWrangler: CloudflareCommandRunner;
@@ -487,8 +511,6 @@ export declare function deployCloudflareVersionTraffic(options: {
487
511
  export declare function normalizeWorkersDevSubdomain(value: string | undefined): string | undefined;
488
512
  /** Extract the account subdomain from a Wrangler-reported workers.dev URL. */
489
513
  export declare function extractWorkersDevSubdomain(url: string): string | undefined;
490
- //#endregion
491
- //#region src/metadata.d.ts
492
514
  /**
493
515
  * Share one authenticated transport for the activator's read-only metadata calls.
494
516
  * Writes and unsupported commands use the pinned cf command adapter. Responses
@@ -506,12 +528,7 @@ export declare function createCloudflareMetadataRunner(options: {
506
528
  /** Environment used to decide whether the CLI's proxy-aware transport is required. */
507
529
  env?: Readonly<Record<string, string | undefined>>;
508
530
  }): CloudflareCommandRunner;
509
- //#endregion
510
- //#region src/diagnostics.d.ts
511
- /** Keep the uploader's error, excluding stdout binding tables and redacting common credential formats. */
531
+ /** Keep the uploader's error, excluding cf's log lines and redacting common credential formats. */
512
532
  export declare function cloudflareCommandDiagnostic(result: CloudflareCommandResult): string | undefined;
513
- //#endregion
514
- //#region src/secret-draft.d.ts
515
533
  export declare const VOID_SECRET_DRAFT_SOURCE = "export default { fetch() { return new Response(\"\"); } };\n";
516
- //#endregion
517
- export type { ApprovedCloudflareKvBinding, CloudflareCommandResult, CloudflareCommandRunner, CloudflareDeployExecution, CloudflareFrameworkDeployConfig, CloudflareOperation, CloudflareProviderError, CloudflareStaticDeployConfig, CloudflareTrafficConfig, CloudflareTriggerConfig, CloudflareTriggerStabilization, CloudflareUploadResult, CloudflareVersionActivation, CloudflareVersionActivationOptions, CloudflareVersionMetadata, CloudflareWorkerMigrationTagOptions, ExistingCloudflareWorker };
534
+ 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
@@ -6,7 +6,7 @@ import nodePath from "node:path";
6
6
  import { createHash } from "node:crypto";
7
7
  import { tmpdir } from "node:os";
8
8
  import { setTimeout } from "node:timers/promises";
9
- //#region src/config.ts
9
+ // src/config.ts
10
10
  /** Parse a root Wrangler JSON/JSONC config without accepting a partial parse. */
11
11
  function parseRootWranglerConfig(configPath) {
12
12
  if (!existsSync(configPath)) return {};
@@ -82,8 +82,7 @@ function resolveD1MigrateTarget(config, binding) {
82
82
  token
83
83
  };
84
84
  }
85
- //#endregion
86
- //#region src/execute.ts
85
+ // src/execute.ts
87
86
  /** Upload the already-verified Worker artifact without Wrangler autoconfiguration. */
88
87
  async function executeCloudflareDeploy(options) {
89
88
  if (!options.verifyUploader()) return {
@@ -112,8 +111,7 @@ function isCleanCommandExit(result) {
112
111
  function isWorkerNotFound(result) {
113
112
  return isCompletedCommand(result) && (result.failure === "not-found" || result.errorCode === 10007);
114
113
  }
115
- //#endregion
116
- //#region src/secret-draft.ts
114
+ // src/secret-draft.ts
117
115
  /**
118
116
  * `wrangler secret put` creates a fetch-only draft before attaching a first secret to a Worker.
119
117
  * Accept that draft as first-deployment proof only when its complete deployable shape contains no
@@ -168,8 +166,7 @@ function isVoidSecretOnlyDraft(entry) {
168
166
  }
169
167
  return true;
170
168
  }
171
- //#endregion
172
- //#region src/existing.ts
169
+ // src/existing.ts
173
170
  /** A failed remote read, distinct from an invalid or changing Worker configuration. */
174
171
  var CloudflareWorkerInspectionError = class extends Error {
175
172
  name = "CloudflareWorkerInspectionError";
@@ -531,8 +528,7 @@ function hasContent(value) {
531
528
  if (isRecord(value)) return Object.values(value).some(hasContent);
532
529
  return true;
533
530
  }
534
- //#endregion
535
- //#region ../../node_modules/.pnpm/pathslash@0.1.0/node_modules/pathslash/dist/index.mjs
531
+ // ../../node_modules/.pnpm/pathslash@0.1.0/node_modules/pathslash/dist/index.mjs
536
532
  const BACKSLASH_RE = /\\/g;
537
533
  const isWindows = process.platform === "win32";
538
534
  /**
@@ -570,8 +566,7 @@ const posix = { ...nodePath.posix };
570
566
  posix.posix = win32.posix = posix;
571
567
  posix.win32 = win32.win32 = win32;
572
568
  const { basename, delimiter, dirname, extname, format, isAbsolute, join, matchesGlob, normalize, parse: parse$1, relative, resolve, sep, toNamespacedPath } = isWindows ? win32 : posix;
573
- //#endregion
574
- //#region src/framework.ts
569
+ // src/framework.ts
575
570
  /**
576
571
  * Point a standalone framework Worker at the canonical root Cloudflare state.
577
572
  *
@@ -640,11 +635,11 @@ function writeCloudflareFrameworkDeployConfig(options) {
640
635
  }
641
636
  return changed;
642
637
  }
643
- //#endregion
644
- //#region src/static.ts
638
+ // src/static.ts
645
639
  /** Generate the deterministic thin Worker entry bundled by Wrangler. */
646
640
  function renderCloudflareStaticEntry(options) {
647
641
  const config = JSON.stringify({
642
+ ...options.alwaysUseHttps !== void 0 ? { alwaysUseHttps: options.alwaysUseHttps } : {},
648
643
  ...options.headerRules?.length ? { headerRules: options.headerRules } : {},
649
644
  ...options.redirectRules?.length ? { redirectRules: options.redirectRules } : {},
650
645
  ...options.fallbackRules?.length ? { fallbackRules: options.fallbackRules } : {},
@@ -762,11 +757,17 @@ function findDroppedComment(before, after) {
762
757
  counts.set(token, remaining - 1);
763
758
  }
764
759
  }
765
- //#endregion
766
- //#region src/diagnostics.ts
760
+ // src/diagnostics.ts
767
761
  const MAX_DIAGNOSTIC_LENGTH = 4096;
768
762
  const MAX_DIAGNOSTIC_LINES = 24;
769
- /** Keep the uploader's error, excluding stdout binding tables and redacting common credential formats. */
763
+ const ERROR_BOX = /(?:^|\n)[ \t]*┌[ \t]*(?:APIError|Error)\b[^\n]*\n(?:[ \t]*│[^\n]*\n)*[ \t]*└[^\n]*/i;
764
+ const LOG_LINE = /^[├│◆◇●▲■└|*o•!x—](?: {2}|$)/;
765
+ const UPLOAD_CONFLICT = /^[ \t]*(?:\S[ \t]+)?(Aborting the upload operation because of conflicts\..*)$/m;
766
+ const SCRIPT_API_ORIGIN = /^[ \t]*(?:\S[ \t]+)?(You are about to upload a Worker that was last updated via the script API\.)/m;
767
+ function withoutLogLines(output) {
768
+ return output.split("\n").filter((line) => !LOG_LINE.test(line)).join("\n");
769
+ }
770
+ /** Keep the uploader's error, excluding cf's log lines and redacting common credential formats. */
770
771
  function cloudflareCommandDiagnostic(result) {
771
772
  const stderr = stripVTControlCharacters(result.stderr).replace(/\r\n?/g, "\n");
772
773
  const stdout = stripVTControlCharacters(result.stdout).replace(/\r\n?/g, "\n");
@@ -779,15 +780,13 @@ function cloudflareCommandDiagnostic(result) {
779
780
  }
780
781
  }
781
782
  if (message === stderr && !/(?:^|\n)[ \t]*(?:[✘×X][ \t]+)?\[ERROR\]/i.test(stderr)) {
782
- const box = /(?:^|\n)[ \t]*┌[ \t]*(?:APIError|Error)\b[^\n]*\n(?:[ \t]*│[^\n]*\n)*[ \t]*└[^\n]*/i.exec(stdout);
783
+ const box = ERROR_BOX.exec(stdout);
784
+ const conflict = UPLOAD_CONFLICT.exec(`${stdout}\n${stderr}`);
783
785
  if (box) message = box[0].trim();
786
+ else if (conflict) message = [SCRIPT_API_ORIGIN.exec(`${stdout}\n${stderr}`)?.[1], conflict[1]].filter(Boolean).join("\n");
784
787
  else {
785
- const lines = stdout.split("\n");
786
- const abort = lines.find((line) => line.startsWith("■ Aborting the upload operation because of conflicts."));
787
- if (abort) {
788
- const reason = "You are about to upload a Worker that was last updated via the script API.";
789
- message = [lines.includes(`▲ ${reason}`) ? reason : "", abort.slice(3)].filter(Boolean).join("\n");
790
- }
788
+ const stderrBox = ERROR_BOX.exec(stderr);
789
+ message = stderrBox ? withoutLogLines(stderr.slice(0, stderrBox.index)) + stderrBox[0] + withoutLogLines(stderr.slice(stderrBox.index + stderrBox[0].length)) : withoutLogLines(stderr);
791
790
  }
792
791
  }
793
792
  message = stripVTControlCharacters(message).split(/\n\s*(?:🪵|Logs (?:were )?written|If you think this is a bug)/)[0].trim();
@@ -796,8 +795,7 @@ function cloudflareCommandDiagnostic(result) {
796
795
  const bounded = message.split("\n").slice(0, MAX_DIAGNOSTIC_LINES).join("\n").slice(0, MAX_DIAGNOSTIC_LENGTH);
797
796
  return bounded.length < message.length ? `${bounded}\n[diagnostic truncated]` : bounded;
798
797
  }
799
- //#endregion
800
- //#region src/version.ts
798
+ // src/version.ts
801
799
  const DURABLE_MIGRATION_VERSION_ERROR = 10211;
802
800
  const CLOUDFLARE_SECRET_CHANGED_ERROR = 10220;
803
801
  /** Encode migration state into Cloudflare's version annotation for safe rollback diagnostics. */
@@ -844,7 +842,8 @@ function readCloudflareTriggerConfig(configPath, sourceConfigPath) {
844
842
  const source = sourceConfigPath ? parseRootWranglerConfig(sourceConfigPath) : config;
845
843
  const result = triggerConfigFromWrangler({
846
844
  ...config,
847
- observability: source.observability
845
+ observability: source.observability,
846
+ logpush: source.logpush
848
847
  });
849
848
  if (!sourceConfigPath) return result;
850
849
  return {
@@ -1085,6 +1084,7 @@ function triggerConfigFromWrangler(config) {
1085
1084
  ...config.preview_urls === void 0 ? { preservePreviewUrls: true } : {},
1086
1085
  settingsHash: nonVersionedSettingsHash(config),
1087
1086
  ...isRecord(config.observability) ? { observability: config.observability } : {},
1087
+ ...typeof config.logpush === "boolean" ? { logpush: config.logpush } : {},
1088
1088
  workers_dev: workersDev,
1089
1089
  preview_urls: typeof config.preview_urls === "boolean" ? config.preview_urls : workersDev,
1090
1090
  routes,
@@ -1118,7 +1118,8 @@ function parseTriggerConfig(value) {
1118
1118
  "preserve",
1119
1119
  "preserveRoutes",
1120
1120
  "preserveWorkersDev",
1121
- "preservePreviewUrls"
1121
+ "preservePreviewUrls",
1122
+ "logpush"
1122
1123
  ].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;
1123
1124
  return {
1124
1125
  ...value.preserve === true ? { preserve: true } : {},
@@ -1127,6 +1128,7 @@ function parseTriggerConfig(value) {
1127
1128
  ...value.preservePreviewUrls === true ? { preservePreviewUrls: true } : {},
1128
1129
  ...typeof value.settingsHash === "string" ? { settingsHash: value.settingsHash } : {},
1129
1130
  ...isRecord(value.observability) ? { observability: value.observability } : {},
1131
+ ...typeof value.logpush === "boolean" ? { logpush: value.logpush } : {},
1130
1132
  workers_dev: value.workers_dev,
1131
1133
  preview_urls: value.preview_urls,
1132
1134
  routes: value.routes,
@@ -1144,6 +1146,7 @@ function triggerWranglerConfig(base, triggerConfig, manageRoutes = true) {
1144
1146
  const config = {
1145
1147
  ...base,
1146
1148
  ...triggerConfig.observability === void 0 ? {} : { observability: triggerConfig.observability },
1149
+ ...triggerConfig.logpush === void 0 ? {} : { logpush: triggerConfig.logpush },
1147
1150
  triggers,
1148
1151
  queues,
1149
1152
  workflows: [...triggerConfig.workflows],
@@ -1163,6 +1166,7 @@ function resolvePreservedTraffic(desired, previous, workerExists) {
1163
1166
  ...desired.preserve ? { preserve: true } : {},
1164
1167
  ...desired.settingsHash ? { settingsHash: desired.settingsHash } : {},
1165
1168
  ...desired.observability === void 0 ? {} : { observability: desired.observability },
1169
+ ...desired.logpush === void 0 ? {} : { logpush: desired.logpush },
1166
1170
  workers_dev: desired.preserveWorkersDev ? previous?.workers_dev ?? desired.workers_dev : desired.workers_dev,
1167
1171
  preview_urls: desired.preservePreviewUrls ? previous?.preview_urls ?? desired.preview_urls : desired.preview_urls,
1168
1172
  routes: desired.preserveRoutes ? previous?.routes ?? desired.routes : desired.routes,
@@ -1507,7 +1511,9 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1507
1511
  if (stagingFailure) return stagingFailure;
1508
1512
  stagedVersionOverride = true;
1509
1513
  }
1510
- 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);
1514
+ const trustedReadinessOrigin = normalizeReadinessAccessOrigin(options.readinessHeadersOrigin);
1515
+ const withholdsCustomRouteHeaders = (url) => readinessUsesCustomRoute && !isTrustedReadinessOrigin(url, trustedReadinessOrigin);
1516
+ 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);
1511
1517
  options.onProgress?.("Waiting for the Worker to become ready...");
1512
1518
  let ready = await probe(readinessUrl);
1513
1519
  if (ready === "access-denied" && !stagedVersionOverride && previous.versionId !== null && !options.requirePreviewForReadiness) {
@@ -1527,6 +1533,7 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1527
1533
  reason: ready === "access-denied" ? "readiness-access-denied" : "readiness-failed",
1528
1534
  versionId,
1529
1535
  previousVersionId: previous.versionId,
1536
+ ...ready === "access-denied" && withholdsCustomRouteHeaders(readinessUrl) ? customRouteReadinessOriginField(readinessUrl) : {},
1530
1537
  ...stagedVersionOverride ? await restorePreviousVersion(options, previous.versionId, "readiness failed", [stagedVersionPercentages(previous.versionId, versionId)]) : { rollback: "not-needed" }
1531
1538
  };
1532
1539
  if (!options.verifyUploader()) return {
@@ -1895,7 +1902,33 @@ async function bootstrapFirstDeployment(options, previousVersionId, atomic = fal
1895
1902
  const published = readUploadResult(bootstrap);
1896
1903
  const versionId = published?.versionId;
1897
1904
  const desiredTriggerConfig = desiredTriggerConfigFor(parseCloudflareVersionMessage(options.message), options.configPath);
1898
- const reportedOrConfiguredUrl = published?.previewUrl ?? published?.workersDevUrl ?? (desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes));
1905
+ let reportedOrConfiguredUrl = published?.previewUrl ?? published?.workersDevUrl ?? (desiredTriggerConfig.workers_dev ? workersDevReadinessUrl(options.workerName, options.workersDevSubdomain) : customRouteReadinessUrl(desiredTriggerConfig.routes));
1906
+ if (versionId && !reportedOrConfiguredUrl && desiredTriggerConfig.workers_dev && !desiredTriggerConfig.preserve && !desiredTriggerConfig.preserveWorkersDev && !options.preserveTriggers && options.resolveWorkersDevSubdomain) {
1907
+ if (!options.verifyUploader()) return {
1908
+ ok: false,
1909
+ reason: "uploader-changed",
1910
+ versionId,
1911
+ previousVersionId,
1912
+ rollback: "unsafe"
1913
+ };
1914
+ let lookup;
1915
+ try {
1916
+ lookup = await options.resolveWorkersDevSubdomain();
1917
+ } catch {
1918
+ lookup = {
1919
+ ok: false,
1920
+ reason: "unavailable"
1921
+ };
1922
+ }
1923
+ if (!lookup.ok && lookup.reason === "uploader-changed") return {
1924
+ ok: false,
1925
+ reason: "uploader-changed",
1926
+ versionId,
1927
+ previousVersionId,
1928
+ rollback: "unsafe"
1929
+ };
1930
+ reportedOrConfiguredUrl = lookup.ok ? workersDevReadinessUrl(options.workerName, lookup.subdomain) : void 0;
1931
+ }
1899
1932
  if (!versionId || !reportedOrConfiguredUrl) return {
1900
1933
  ok: false,
1901
1934
  reason: atomic ? "atomic-output-invalid" : "bootstrap-output-invalid",
@@ -2000,14 +2033,18 @@ async function bootstrapFirstDeployment(options, previousVersionId, atomic = fal
2000
2033
  const usesCustomDomain = !desiredTriggerConfig.workers_dev && desiredTriggerConfig.routes.some((route) => isRecord(route) && route.custom_domain === true);
2001
2034
  const customDomainBootstrap = !atomic && usesCustomDomain;
2002
2035
  const readinessTimeoutMs = customDomainBootstrap ? 3e5 : 6e4;
2036
+ const previewSubdomain = extractWorkersDevSubdomain(previewUrl);
2037
+ const trustedReadinessOrigin = previewSubdomain ? void 0 : normalizeReadinessAccessOrigin(options.readinessHeadersOrigin);
2038
+ const withholdsCustomRouteHeaders = !previewSubdomain && !isTrustedReadinessOrigin(previewUrl, trustedReadinessOrigin);
2003
2039
  options.onProgress?.("Waiting for the Worker to become ready...");
2004
- 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);
2040
+ 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);
2005
2041
  if (ready !== "ready") return {
2006
2042
  ok: false,
2007
2043
  reason: ready === "access-denied" ? atomic ? "atomic-readiness-access-denied" : "bootstrap-readiness-access-denied" : atomic ? "atomic-readiness-failed" : "bootstrap-readiness-failed",
2008
2044
  versionId,
2009
2045
  previousVersionId,
2010
2046
  rollback: "unavailable",
2047
+ ...ready === "access-denied" && withholdsCustomRouteHeaders ? customRouteReadinessOriginField(previewUrl) : {},
2011
2048
  ...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." } : {}
2012
2049
  };
2013
2050
  if (!options.verifyUploader()) return {
@@ -2183,8 +2220,8 @@ async function verifyReadinessStagingOwner(options, previousVersionId, versionId
2183
2220
  };
2184
2221
  return null;
2185
2222
  }
2186
- async function probeReadiness(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain, versionOverride, fetcher, sleep, attempts, timeoutMs = Number.POSITIVE_INFINITY, expectedVersionId) {
2187
- const headers = guardedReadinessHeaders(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain);
2223
+ async function probeReadiness(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain, trustedReadinessOrigin, versionOverride, fetcher, sleep, attempts, timeoutMs = Number.POSITIVE_INFINITY, expectedVersionId) {
2224
+ const headers = guardedReadinessHeaders(url, workerName, readinessHeaders, authenticatedWorkersDevSubdomain, trustedReadinessOrigin);
2188
2225
  if (headers === null) return "failed";
2189
2226
  const deadline = Date.now() + timeoutMs;
2190
2227
  for (let attempt = 0; attempt < Math.max(1, attempts); attempt++) {
@@ -2215,9 +2252,13 @@ async function probeReadiness(url, workerName, readinessHeaders, authenticatedWo
2215
2252
  }
2216
2253
  return "failed";
2217
2254
  }
2218
- /** Never forward Access headers to a hostname that cannot be this Worker. */
2219
- function guardedReadinessHeaders(url, workerName, headers, authenticatedWorkersDevSubdomain) {
2255
+ /**
2256
+ * Never forward Access headers to a hostname that cannot be this Worker, unless the operator
2257
+ * named that exact origin.
2258
+ */
2259
+ function guardedReadinessHeaders(url, workerName, headers, authenticatedWorkersDevSubdomain, trustedReadinessOrigin) {
2220
2260
  if (!headers || Object.keys(headers).length === 0) return {};
2261
+ if (isTrustedReadinessOrigin(url, trustedReadinessOrigin)) return headers;
2221
2262
  try {
2222
2263
  const parsed = new URL(url);
2223
2264
  const firstLabel = parsed.hostname.toLowerCase().split(".")[0] ?? "";
@@ -2229,6 +2270,36 @@ function guardedReadinessHeaders(url, workerName, headers, authenticatedWorkersD
2229
2270
  return null;
2230
2271
  }
2231
2272
  }
2273
+ /**
2274
+ * Normalize an operator-selected readiness origin. Returns `undefined` unless the value is one
2275
+ * exact `https:` origin without a wildcard, credentials, a path, a query, or a fragment. A probe
2276
+ * URL never has a wildcard host, so a wildcard value could never match.
2277
+ */
2278
+ function normalizeReadinessAccessOrigin(value) {
2279
+ if (!value) return;
2280
+ try {
2281
+ const url = new URL(value);
2282
+ return url.protocol === "https:" && !url.hostname.includes("*") && !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash ? url.origin : void 0;
2283
+ } catch {
2284
+ return;
2285
+ }
2286
+ }
2287
+ function isTrustedReadinessOrigin(url, trustedOrigin) {
2288
+ if (trustedOrigin === void 0) return false;
2289
+ try {
2290
+ const parsed = new URL(url);
2291
+ return parsed.protocol === "https:" && !parsed.username && !parsed.password && parsed.origin === trustedOrigin;
2292
+ } catch {
2293
+ return false;
2294
+ }
2295
+ }
2296
+ function customRouteReadinessOriginField(url) {
2297
+ try {
2298
+ return { customRouteReadinessOrigin: new URL(url).origin };
2299
+ } catch {
2300
+ return {};
2301
+ }
2302
+ }
2232
2303
  async function isCloudflareAccessDenial(response) {
2233
2304
  if (response.status >= 300 && response.status < 400 && response.headers.get("location")?.includes("/cdn-cgi/access/")) return true;
2234
2305
  if (response.status !== 403) return false;
@@ -2332,8 +2403,7 @@ function workersDevSubdomainResult(url) {
2332
2403
  const workersDevSubdomain = extractWorkersDevSubdomain(url);
2333
2404
  return workersDevSubdomain ? { workersDevSubdomain } : {};
2334
2405
  }
2335
- //#endregion
2336
- //#region src/metadata.ts
2406
+ // src/metadata.ts
2337
2407
  /**
2338
2408
  * Share one authenticated transport for the activator's read-only metadata calls.
2339
2409
  * Writes and unsupported commands use the pinned cf command adapter. Responses
@@ -2423,5 +2493,4 @@ function createCloudflareMetadataRunner(options) {
2423
2493
  return fallback(operation, runOptions);
2424
2494
  };
2425
2495
  }
2426
- //#endregion
2427
- 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 };
2496
+ 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.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Cloudflare deployment primitives for Void runtimes and deployment adapters.",
5
5
  "homepage": "https://void.cloud",
6
6
  "bugs": {
@@ -26,15 +26,15 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@void/deploy-core": "1.3.0",
29
+ "@void/deploy-core": "1.5.0",
30
30
  "jsonc-parser": "^3.3.1"
31
31
  },
32
32
  "devDependencies": {
33
- "@cloudflare/workers-types": "^5.20261019.1",
34
- "@types/node": "^26.6.5",
33
+ "@cloudflare/workers-types": "^5.20261102.1",
34
+ "@types/node": "^26.6.6",
35
35
  "pathslash": "^0.1.0",
36
36
  "typescript": "7.0.2",
37
- "vite-plus": "1.3.0"
37
+ "vite-plus": "1.4.0"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "vp pack",