@void/deploy-cloudflare 0.20.0 → 0.20.1

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
@@ -70,7 +70,9 @@ export declare function assertCloudflareDurableObjectLifecycleSupported(options:
70
70
  apiBaseUrl: string;
71
71
  readMigrationTag?: (options: CloudflareWorkerMigrationTagOptions) => Promise<string | null>;
72
72
  }): Promise<void>;
73
- export declare function assertCloudflareBindingsPreserved(before: ExistingCloudflareWorker, after: ExistingCloudflareWorker, approvedKv?: ReadonlyArray<ApprovedCloudflareKvBinding>): void;
73
+ export declare function assertCloudflareBindingsPreserved(before: ExistingCloudflareWorker, after: ExistingCloudflareWorker, approvedKv?: ReadonlyArray<ApprovedCloudflareKvBinding>, options?: {
74
+ staticAssetsBinding?: string;
75
+ }): void;
74
76
  /** Preserve the existing Worker, allowing only explicitly approved KV additions. */
75
77
  export declare function assertExistingCloudflareConfig(existing: ExistingCloudflareWorker, config: Record<string, unknown>, built?: boolean, approvedKv?: ReadonlyArray<ApprovedCloudflareKvBinding>): void;
76
78
  //#endregion
package/dist/index.mjs CHANGED
@@ -81,12 +81,34 @@ function resolveD1MigrateTarget(config, binding) {
81
81
  };
82
82
  }
83
83
  //#endregion
84
+ //#region src/secret-draft.ts
85
+ /**
86
+ * `wrangler secret put` creates a fetch-only draft before attaching a first secret to a Worker.
87
+ * Accept that draft as first-deployment proof only when its complete deployable shape contains no
88
+ * application code configuration or non-secret resource that an ordinary deploy could own.
89
+ */
90
+ function isWranglerSecretOnlyDraft(entry) {
91
+ if (!isRecord$1(entry.annotations)) return false;
92
+ const annotationKeys = Object.keys(entry.annotations);
93
+ 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
+ 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;
99
+ 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);
104
+ }
105
+ //#endregion
84
106
  //#region src/existing.ts
85
107
  /** Read the deployed version, not the most recently uploaded candidate. */
86
108
  async function readExistingCloudflareWorker(options) {
87
109
  const worker = await readCloudflareWorker(options, false);
88
110
  if (!worker) throw new Error(`Cloudflare Worker "${options.workerName}" does not have an active version.`);
89
- return worker;
111
+ return worker.snapshot;
90
112
  }
91
113
  async function readCloudflareWorker(options, allowMissing) {
92
114
  const target = [
@@ -132,10 +154,13 @@ async function readCloudflareWorker(options, allowMissing) {
132
154
  const after = options.versionId ? deployment : await read(["deployments", "status"]);
133
155
  if (after === null || !isDeepStrictEqual(deployment.versions, after.versions)) throw new Error("Cloudflare production changed while linking. Retry from the current deployment.");
134
156
  return {
135
- versionId,
136
- bindings: resources.bindings,
137
- runtime: resources.script_runtime,
138
- script: resources.script
157
+ snapshot: {
158
+ versionId,
159
+ bindings: resources.bindings,
160
+ runtime: resources.script_runtime,
161
+ script: resources.script
162
+ },
163
+ isSecretOnlyDraft: isWranglerSecretOnlyDraft(version)
139
164
  };
140
165
  }
141
166
  /** Parse legacy version-detail shapes; this is not authoritative Worker migration state. */
@@ -219,7 +244,8 @@ async function assertCloudflareDurableObjectLifecycleSupported(options) {
219
244
  const activeTag = await readMigrationTag(migrationOptions);
220
245
  const after = await readCloudflareWorker(options, false);
221
246
  const afterTag = await readMigrationTag(migrationOptions);
222
- if (!after || after.versionId !== existing.versionId || afterTag !== activeTag) throw new Error("Cloudflare production or Durable Object migration state changed during verification. Retry from the current deployment.");
247
+ 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.");
248
+ if (activeTag === null && existing.isSecretOnlyDraft && after.isSecretOnlyDraft) return;
223
249
  const activeIndex = activeTag === null ? -1 : migrations.findIndex((migration) => migration.tag === activeTag);
224
250
  const pending = migrations.slice(activeIndex + 1).filter((migration) => migration.changesClasses);
225
251
  if (pending.length === 0) return;
@@ -251,7 +277,9 @@ function parseDurableObjectMigrations(value) {
251
277
  function isMissingWorkerOutput$1(output) {
252
278
  return /\[code:\s*10007\]/i.test(output) || /no deployments|\bworker\b.*\b(?:does not exist|not found)\b/i.test(output);
253
279
  }
254
- function assertCloudflareBindingsPreserved(before, after, approvedKv = []) {
280
+ function assertCloudflareBindingsPreserved(before, after, approvedKv = [], options = {}) {
281
+ const staticAssetsBinding = isAssetsOnlyWorker(before) ? options.staticAssetsBinding : void 0;
282
+ const addsAssetsBinding = staticAssetsBinding !== void 0 && !before.bindings.some((binding) => binding.name === staticAssetsBinding);
255
283
  const additions = newApprovedKvBindings(before, approvedKv);
256
284
  for (const addition of additions) {
257
285
  const matches = after.bindings.filter((binding) => binding.name === addition.binding);
@@ -259,12 +287,24 @@ function assertCloudflareBindingsPreserved(before, after, approvedKv = []) {
259
287
  }
260
288
  const preservedBindings = after.bindings.filter((binding) => !additions.some((addition) => addition.binding === binding.name));
261
289
  const sorted = (bindings) => [...bindings].sort((a, b) => String(a.name).localeCompare(String(b.name)));
262
- if (!isDeepStrictEqual(sorted(before.bindings), sorted(preservedBindings))) throw new Error("The uploaded version did not preserve the existing Worker bindings and variables. It has not been activated.");
290
+ const expectedBindings = addsAssetsBinding ? [...before.bindings, {
291
+ type: "assets",
292
+ name: staticAssetsBinding
293
+ }] : before.bindings;
294
+ if (staticAssetsBinding !== void 0 && !after.bindings.some((binding) => binding.type === "assets" && binding.name === staticAssetsBinding) || !isDeepStrictEqual(sorted(expectedBindings), sorted(preservedBindings))) throw new Error("The uploaded version did not preserve the existing Worker bindings and variables. It has not been activated.");
263
295
  const handlers = (worker) => {
264
- const value = worker.script.handlers;
296
+ const value = existingWorkerHandlers(worker);
265
297
  return Array.isArray(value) && value.every((handler) => typeof handler === "string") ? [...new Set(value)].sort() : null;
266
298
  };
267
- if (!isDeepStrictEqual(handlers(before), handlers(after))) throw new Error("The uploaded version did not preserve the existing Worker event handlers. It has not been activated.");
299
+ const expectedHandlers = staticAssetsBinding !== void 0 ? ["fetch"] : handlers(before);
300
+ if (!isDeepStrictEqual(expectedHandlers, handlers(after))) throw new Error("The uploaded version did not preserve the existing Worker event handlers. It has not been activated.");
301
+ }
302
+ function isAssetsOnlyWorker(worker) {
303
+ const handlers = worker.script.handlers;
304
+ return record(worker.runtime.assets) && (handlers === null || Array.isArray(handlers) && handlers.length === 0);
305
+ }
306
+ function existingWorkerHandlers(worker) {
307
+ return isAssetsOnlyWorker(worker) ? [] : worker.script.handlers;
268
308
  }
269
309
  /** Preserve the existing Worker, allowing only explicitly approved KV additions. */
270
310
  function assertExistingCloudflareConfig(existing, config, built = false, approvedKv = []) {
@@ -280,7 +320,7 @@ function assertExistingCloudflareConfig(existing, config, built = false, approve
280
320
  kv_namespaces: entries.filter((entry) => !record(entry) || !additions.some((addition) => addition.binding === entry.binding))
281
321
  };
282
322
  }
283
- const handlers = existing.script.handlers;
323
+ const handlers = existingWorkerHandlers(existing);
284
324
  if (!Array.isArray(handlers) || handlers.some((handler) => handler !== "fetch" && handler !== "scheduled")) throw new Error("Linking an existing Worker currently supports fetch and scheduled handlers only. Queue and Durable Object handlers need a separate migration.");
285
325
  for (const field of [
286
326
  "durable_objects",
@@ -1178,9 +1218,9 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1178
1218
  "--experimental-auto-create=false"
1179
1219
  ], { capture: true }));
1180
1220
  if (!isCleanCommandExit(upload)) {
1181
- const output = `${upload.stdout}\n${upload.stderr}`;
1221
+ const output = stripVTControlCharacters(`${upload.stdout}\n${upload.stderr}`);
1182
1222
  const cloudflareErrorCode = parseCloudflareErrorCode(output);
1183
- const isBootstrapError = cloudflareErrorCode === DURABLE_MIGRATION_VERSION_ERROR || /cannot upload a new version of a worker that does not yet exist/i.test(output);
1223
+ const isBootstrapError = cloudflareErrorCode === DURABLE_MIGRATION_VERSION_ERROR || /This\s+Worker\s+has\s+a\s+pending\s+Durable\s+Object\s+migration,\s+which\s+cannot\s+be\s+applied\s+by\s+`wrangler\s+versions\s+upload`/i.test(output) || /cannot upload a new version of a worker that does not yet exist/i.test(output);
1184
1224
  if (!options.preserveTriggers && isBootstrapError && (previous.versionId === null || options.allowFirstDeploymentBootstrap === true || await previousIsSecretOnlyDraft())) return bootstrapFirstDeployment({
1185
1225
  ...options,
1186
1226
  message
@@ -1514,10 +1554,27 @@ async function readCloudflareVersionInspection(options, commonArgs, versionId) {
1514
1554
  if (!Array.isArray(versions)) return null;
1515
1555
  for (const entry of versions) {
1516
1556
  if (!isRecord(entry) || entry.id !== versionId) continue;
1517
- const message = (isRecord(entry.annotations) ? entry.annotations : void 0)?.["workers/message"];
1557
+ const annotations = isRecord(entry.annotations) ? entry.annotations : void 0;
1558
+ const message = annotations?.["workers/message"];
1559
+ let draft = entry;
1560
+ if (annotations?.["workers/triggered_by"] === "secret" && Object.keys(annotations).length === 1 && !isRecord(entry.resources)) {
1561
+ const inspection = await Promise.resolve(options.runWrangler([
1562
+ "versions",
1563
+ "view",
1564
+ versionId,
1565
+ ...commonArgs,
1566
+ "--name",
1567
+ options.workerName,
1568
+ "--json"
1569
+ ], { capture: true }));
1570
+ if (isCleanCommandExit(inspection)) {
1571
+ const inspected = JSON.parse(inspection.stdout);
1572
+ if (isRecord(inspected) && inspected.id === versionId) draft = inspected;
1573
+ }
1574
+ }
1518
1575
  return {
1519
1576
  metadata: typeof message === "string" ? parseCloudflareVersionMessage(message) : null,
1520
- isSecretOnlyDraft: isWranglerSecretOnlyDraft(entry)
1577
+ isSecretOnlyDraft: isWranglerSecretOnlyDraft(draft)
1521
1578
  };
1522
1579
  }
1523
1580
  } catch {
@@ -1593,23 +1650,6 @@ async function readCloudflareVersionSecretBindings(options, commonArgs, versionI
1593
1650
  }
1594
1651
  }
1595
1652
  /**
1596
- * `wrangler secret put` creates a fetch-only draft before attaching a first secret to a Worker.
1597
- * Accept that draft as first-deployment proof only when its complete deployable shape contains no
1598
- * application code configuration or non-secret resource that an ordinary deploy could own.
1599
- */
1600
- function isWranglerSecretOnlyDraft(entry) {
1601
- if (!isRecord(entry.annotations)) return false;
1602
- const annotationKeys = Object.keys(entry.annotations);
1603
- if (entry.annotations["workers/triggered_by"] !== "secret" || annotationKeys.some((key) => key !== "workers/triggered_by")) return false;
1604
- if (!isRecord(entry.resources)) return false;
1605
- const { bindings, script, script_runtime: scriptRuntime } = entry.resources;
1606
- 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;
1607
- if (!isRecord(script) || !Array.isArray(script.handlers) || script.handlers.length !== 1 || script.handlers[0] !== "fetch" || script.placement_mode !== void 0) return false;
1608
- if (!isRecord(scriptRuntime)) return false;
1609
- const runtimeKeys = Object.keys(scriptRuntime);
1610
- return scriptRuntime.usage_model === "standard" && isRecord(scriptRuntime.limits) && Object.keys(scriptRuntime.limits).length === 0 && runtimeKeys.every((key) => key === "usage_model" || key === "limits");
1611
- }
1612
- /**
1613
1653
  * The Versions API cannot create a Worker or its initial Durable Object migration history, and
1614
1654
  * some accounts do not expose a version preview until their first ordinary deployment. A
1615
1655
  * brand-new Worker can therefore need one ordinary deploy before later releases use preview-first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@void/deploy-cloudflare",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/voidzero-dev/void.git",
@@ -20,7 +20,7 @@
20
20
  "access": "public"
21
21
  },
22
22
  "dependencies": {
23
- "@void/deploy-core": "0.20.0",
23
+ "@void/deploy-core": "0.20.1",
24
24
  "jsonc-parser": "3.3.1"
25
25
  },
26
26
  "devDependencies": {