@void/deploy-cloudflare 0.20.0 → 0.20.2

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
@@ -319,4 +321,23 @@ export declare function normalizeWorkersDevSubdomain(value: string | undefined):
319
321
  /** Extract the account subdomain from a Wrangler-reported workers.dev URL. */
320
322
  export declare function extractWorkersDevSubdomain(url: string): string | undefined;
321
323
  //#endregion
324
+ //#region src/metadata.d.ts
325
+ /**
326
+ * Share one authenticated transport for the activator's read-only metadata calls.
327
+ * Writes and unsupported commands retain the pinned Wrangler runner. Responses
328
+ * keep its JSON contract so activation and concurrency checks remain unchanged.
329
+ */
330
+ export declare function createCloudflareMetadataRunner(options: {
331
+ runWrangler: CloudflareCommandRunner;
332
+ accountId: string;
333
+ apiBaseUrl: string;
334
+ workerName: string;
335
+ configPath?: string;
336
+ getToken: () => Promise<string | null>;
337
+ verifyUploader: () => boolean;
338
+ fetch?: typeof globalThis.fetch;
339
+ /** Environment used to decide whether Wrangler's proxy-aware transport is required. */
340
+ env?: Readonly<Record<string, string | undefined>>;
341
+ }): CloudflareCommandRunner;
342
+ //#endregion
322
343
  export type { ApprovedCloudflareKvBinding, CloudflareCommandResult, CloudflareCommandRunner, CloudflareDeployExecution, CloudflareFrameworkDeployConfig, CloudflareStaticDeployConfig, CloudflareTrafficConfig, CloudflareTriggerConfig, CloudflareTriggerStabilization, CloudflareVersionActivation, CloudflareVersionActivationOptions, CloudflareVersionMetadata, CloudflareWorkerMigrationTagOptions, ExistingCloudflareWorker };
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 = [
@@ -108,7 +130,7 @@ async function readCloudflareWorker(options, allowMissing) {
108
130
  }
109
131
  try {
110
132
  const value = JSON.parse(result.stdout);
111
- if (record(value)) return value;
133
+ if (record$1(value)) return value;
112
134
  } catch {}
113
135
  throw new Error("Cloudflare returned an invalid existing Worker response.");
114
136
  };
@@ -119,7 +141,7 @@ async function readCloudflareWorker(options, allowMissing) {
119
141
  if (deployment === null) return null;
120
142
  const versions = deployment.versions;
121
143
  if (allowMissing && Array.isArray(versions) && versions.length === 0) return null;
122
- if (!Array.isArray(versions) || versions.length !== 1 || !record(versions[0]) || versions[0].percentage !== 100 || typeof versions[0].version_id !== "string") throw new Error("The existing Worker must have one deployed version receiving all traffic. Finish any gradual rollout before linking.");
144
+ if (!Array.isArray(versions) || versions.length !== 1 || !record$1(versions[0]) || versions[0].percentage !== 100 || typeof versions[0].version_id !== "string") throw new Error("The existing Worker must have one deployed version receiving all traffic. Finish any gradual rollout before linking.");
123
145
  const versionId = versions[0].version_id;
124
146
  const version = await read([
125
147
  "versions",
@@ -128,24 +150,27 @@ async function readCloudflareWorker(options, allowMissing) {
128
150
  ]);
129
151
  if (version === null) throw new Error("Cloudflare did not return the existing Worker configuration.");
130
152
  const resources = version.resources;
131
- if (version.id !== versionId || !record(resources) || !Array.isArray(resources.bindings) || !resources.bindings.every(record) || !record(resources.script_runtime) || !record(resources.script)) throw new Error("Cloudflare did not return the existing Worker configuration.");
153
+ 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.");
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. */
142
167
  function readCloudflareVersionMigrationTag(version) {
143
- if (!record(version)) return null;
144
- const resources = record(version.resources) ? version.resources : void 0;
168
+ if (!record$1(version)) return null;
169
+ const resources = record$1(version.resources) ? version.resources : void 0;
145
170
  const tag = [
146
- resources && record(resources.script_runtime) ? resources.script_runtime.migration_tag : null,
147
- resources && record(resources.script) ? resources.script.migration_tag : null,
148
- record(version.script) ? version.script.migration_tag : null,
171
+ resources && record$1(resources.script_runtime) ? resources.script_runtime.migration_tag : null,
172
+ resources && record$1(resources.script) ? resources.script.migration_tag : null,
173
+ record$1(version.script) ? version.script.migration_tag : null,
149
174
  version.migration_tag
150
175
  ].find((candidate) => typeof candidate === "string");
151
176
  return typeof tag === "string" && tag.length > 0 ? tag : null;
@@ -170,9 +195,9 @@ async function readCloudflareWorkerMigrationTag(options) {
170
195
  } catch {
171
196
  throw new Error(`Cloudflare returned an invalid Durable Object migration response for Worker "${options.workerName}".`);
172
197
  }
173
- if (!response.ok || !record(body) || body.success !== true || !Array.isArray(body.result)) throw new Error(`Could not verify Durable Object migration state for Cloudflare Worker "${options.workerName}".`);
174
- const worker = body.result.find((candidate) => record(candidate) && candidate.id === options.workerName);
175
- if (!record(worker)) throw new Error(`Cloudflare did not return migration state for Worker "${options.workerName}".`);
198
+ if (!response.ok || !record$1(body) || body.success !== true || !Array.isArray(body.result)) throw new Error(`Could not verify Durable Object migration state for Cloudflare Worker "${options.workerName}".`);
199
+ const worker = body.result.find((candidate) => record$1(candidate) && candidate.id === options.workerName);
200
+ if (!record$1(worker)) throw new Error(`Cloudflare did not return migration state for Worker "${options.workerName}".`);
176
201
  if (worker.migration_tag === void 0 || worker.migration_tag === null) return null;
177
202
  if (typeof worker.migration_tag !== "string" || worker.migration_tag.length === 0) throw new Error(`Cloudflare returned an invalid Durable Object migration response for Worker "${options.workerName}".`);
178
203
  return worker.migration_tag;
@@ -190,7 +215,7 @@ async function readCloudflareApiAuthentication(runWrangler) {
190
215
  } catch {
191
216
  throw new Error("Cloudflare returned an invalid authentication response.");
192
217
  }
193
- if (!record(value)) throw new Error("Cloudflare returned an invalid authentication response.");
218
+ if (!record$1(value)) throw new Error("Cloudflare returned an invalid authentication response.");
194
219
  if ((value.type === "oauth" || value.type === "api_token") && typeof value.token === "string" && value.token.length > 0) return { Authorization: `Bearer ${value.token}` };
195
220
  if (value.type === "api_key" && typeof value.key === "string" && value.key.length > 0 && typeof value.email === "string" && value.email.length > 0) return {
196
221
  "X-Auth-Key": value.key,
@@ -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;
@@ -234,7 +260,7 @@ function parseDurableObjectMigrations(value) {
234
260
  if (value === void 0) return [];
235
261
  if (!Array.isArray(value)) throw new Error("deploy: generated Cloudflare config has a malformed migrations field.");
236
262
  return value.map((migration) => {
237
- if (!record(migration) || typeof migration.tag !== "string") throw new Error("deploy: generated Cloudflare config has a malformed class migration.");
263
+ if (!record$1(migration) || typeof migration.tag !== "string") throw new Error("deploy: generated Cloudflare config has a malformed class migration.");
238
264
  const fields = [
239
265
  migration.new_classes,
240
266
  migration.new_sqlite_classes,
@@ -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$1(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 = []) {
@@ -272,15 +312,15 @@ function assertExistingCloudflareConfig(existing, config, built = false, approve
272
312
  if (additions.length > 0) {
273
313
  const entries = config.kv_namespaces;
274
314
  for (const addition of additions) {
275
- const matches = Array.isArray(entries) ? entries.filter((entry) => record(entry) && entry.binding === addition.binding) : [];
315
+ const matches = Array.isArray(entries) ? entries.filter((entry) => record$1(entry) && entry.binding === addition.binding) : [];
276
316
  if ((built || matches.length > 0) && (matches.length !== 1 || matches[0].id !== addition.id)) throw new Error(`Binding ${addition.binding} must reference its approved KV namespace when linking.`);
277
317
  }
278
318
  if (Array.isArray(entries)) config = {
279
319
  ...config,
280
- kv_namespaces: entries.filter((entry) => !record(entry) || !additions.some((addition) => addition.binding === entry.binding))
320
+ kv_namespaces: entries.filter((entry) => !record$1(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",
@@ -294,7 +334,7 @@ function assertExistingCloudflareConfig(existing, config, built = false, approve
294
334
  const value = config[field];
295
335
  if (hasContent(value)) throw new Error(`The existing Worker handoff cannot change ${field}. Keep this deployment limited to the existing site.`);
296
336
  }
297
- if (record(existing.runtime.exports) && Object.values(existing.runtime.exports).some((value) => record(value) && value.type !== "worker")) throw new Error("The existing Worker exports stateful classes. Migrate those classes separately before linking.");
337
+ if (record$1(existing.runtime.exports) && Object.values(existing.runtime.exports).some((value) => record$1(value) && value.type !== "worker")) throw new Error("The existing Worker exports stateful classes. Migrate those classes separately before linking.");
298
338
  for (const field of [
299
339
  "compatibility_date",
300
340
  "compatibility_flags",
@@ -305,7 +345,7 @@ function assertExistingCloudflareConfig(existing, config, built = false, approve
305
345
  const equal = field === "compatibility_flags" ? stringSetEqual(actual, expected) : isDeepStrictEqual(actual, expected);
306
346
  if ((built || config[field] !== void 0) && !equal) throw new Error(`Keep the existing Worker's ${field} when linking. Update it in a later deployment.`);
307
347
  }
308
- const vars = record(config.vars) ? config.vars : {};
348
+ const vars = record$1(config.vars) ? config.vars : {};
309
349
  const remote = new Map(existing.bindings.map((binding) => [binding.name, binding]));
310
350
  for (const [name, value] of Object.entries(vars)) {
311
351
  const binding = remote.get(name);
@@ -349,21 +389,21 @@ function assertExistingCloudflareConfig(existing, config, built = false, approve
349
389
  "secret_key"
350
390
  ].includes(String(binding.type))) continue;
351
391
  if (binding.type === "assets") {
352
- if (built && (!record(config.assets) || config.assets.binding !== binding.name)) throw new Error(`Keep the existing assets binding ${binding.name} when linking.`);
392
+ if (built && (!record$1(config.assets) || config.assets.binding !== binding.name)) throw new Error(`Keep the existing assets binding ${binding.name} when linking.`);
353
393
  continue;
354
394
  }
355
395
  const mapping = supported[String(binding.type)];
356
396
  if (!mapping) throw new Error(`The existing binding ${binding.name} (${String(binding.type)}) is not yet supported when linking.`);
357
397
  const [field, localId, remoteId] = mapping;
358
398
  const entries = config[field];
359
- const matches = Array.isArray(entries) ? entries.filter((entry) => record(entry) && entry.binding === binding.name) : [];
399
+ const matches = Array.isArray(entries) ? entries.filter((entry) => record$1(entry) && entry.binding === binding.name) : [];
360
400
  const entry = matches[0];
361
- if (matches.length !== 1 || !record(entry) || typeof binding[remoteId] !== "string" || entry[localId] !== binding[remoteId] || binding.type === "service" && ((entry.environment ?? "production") !== (binding.environment ?? "production") || entry.entrypoint !== binding.entrypoint) || binding.type === "r2_bucket" && (entry.jurisdiction ?? "default") !== (binding.jurisdiction ?? "default")) throw new Error(`Binding ${binding.name} must reference the existing Cloudflare resource when linking.`);
401
+ if (matches.length !== 1 || !record$1(entry) || typeof binding[remoteId] !== "string" || entry[localId] !== binding[remoteId] || binding.type === "service" && ((entry.environment ?? "production") !== (binding.environment ?? "production") || entry.entrypoint !== binding.entrypoint) || binding.type === "r2_bucket" && (entry.jurisdiction ?? "default") !== (binding.jurisdiction ?? "default")) throw new Error(`Binding ${binding.name} must reference the existing Cloudflare resource when linking.`);
362
402
  found.add(binding.name);
363
403
  }
364
404
  for (const [field] of Object.values(supported)) {
365
405
  const entries = config[field];
366
- if (Array.isArray(entries) && entries.some((entry) => !record(entry) || !found.has(String(entry.binding)))) throw new Error(`Linking cannot add or replace ${field}. Use the existing resource bindings.`);
406
+ if (Array.isArray(entries) && entries.some((entry) => !record$1(entry) || !found.has(String(entry.binding)))) throw new Error(`Linking cannot add or replace ${field}. Use the existing resource bindings.`);
367
407
  }
368
408
  }
369
409
  function newApprovedKvBindings(existing, approved) {
@@ -386,13 +426,13 @@ function stringSetEqual(left, right) {
386
426
  if (!Array.isArray(left) || !left.every((value) => typeof value === "string") || !Array.isArray(right) || !right.every((value) => typeof value === "string")) return false;
387
427
  return isDeepStrictEqual([...new Set(left)].sort(), [...new Set(right)].sort());
388
428
  }
389
- function record(value) {
429
+ function record$1(value) {
390
430
  return typeof value === "object" && value !== null && !Array.isArray(value);
391
431
  }
392
432
  function hasContent(value) {
393
433
  if (value === void 0 || value === null) return false;
394
434
  if (Array.isArray(value)) return value.length > 0;
395
- if (record(value)) return Object.values(value).some(hasContent);
435
+ if (record$1(value)) return Object.values(value).some(hasContent);
396
436
  return true;
397
437
  }
398
438
  //#endregion
@@ -1126,10 +1166,10 @@ async function activateCloudflareVersionWithSecretsFile(options) {
1126
1166
  }
1127
1167
  const desiredMetadata = parseCloudflareVersionMessage(options.message);
1128
1168
  let desiredTriggerConfig = desiredMetadata?.schema === 2 ? desiredMetadata.triggerConfig : options.configPath ? readCloudflareTriggerConfig(options.configPath) : triggerConfigFromWrangler({});
1129
- let previousVersion = previousVersionId && desiredMetadata?.schema === 2 ? await readCloudflareVersionInspection(options, commonArgs, previousVersionId) : null;
1169
+ let previousVersion = previousVersionId && desiredMetadata?.schema === 2 ? await readCloudflareVersionInspection(options, commonArgs, previousVersionId, inheritanceBaseline?.map((version) => version.inspection)) : null;
1130
1170
  async function previousIsSecretOnlyDraft() {
1131
1171
  if (!previousVersionId) return false;
1132
- previousVersion ??= await readCloudflareVersionInspection(options, commonArgs, previousVersionId);
1172
+ previousVersion ??= await readCloudflareVersionInspection(options, commonArgs, previousVersionId, inheritanceBaseline?.map((version) => version.inspection));
1133
1173
  return previousVersion?.isSecretOnlyDraft === true;
1134
1174
  }
1135
1175
  const previousMetadata = desiredMetadata?.schema === 2 ? previousVersion?.metadata : null;
@@ -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
@@ -1499,25 +1539,45 @@ async function restorePreviousVersionAndTriggers(options, commonArgs, previousVe
1499
1539
  async function readCloudflareVersionMetadata(options, commonArgs, versionId) {
1500
1540
  return (await readCloudflareVersionInspection(options, commonArgs, versionId))?.metadata ?? null;
1501
1541
  }
1502
- async function readCloudflareVersionInspection(options, commonArgs, versionId) {
1503
- const result = await Promise.resolve(options.runWrangler([
1504
- "versions",
1505
- "list",
1506
- ...commonArgs,
1507
- "--name",
1508
- options.workerName,
1509
- "--json"
1510
- ], { capture: true }));
1511
- if (!isCleanCommandExit(result)) return null;
1542
+ async function readCloudflareVersionInspection(options, commonArgs, versionId, knownVersions) {
1512
1543
  try {
1513
- const versions = JSON.parse(result.stdout);
1544
+ let versions = knownVersions;
1545
+ if (versions === void 0) {
1546
+ const result = await options.runWrangler([
1547
+ "versions",
1548
+ "list",
1549
+ ...commonArgs,
1550
+ "--name",
1551
+ options.workerName,
1552
+ "--json"
1553
+ ], { capture: true });
1554
+ if (!isCleanCommandExit(result)) return null;
1555
+ versions = JSON.parse(result.stdout);
1556
+ }
1514
1557
  if (!Array.isArray(versions)) return null;
1515
1558
  for (const entry of versions) {
1516
1559
  if (!isRecord(entry) || entry.id !== versionId) continue;
1517
- const message = (isRecord(entry.annotations) ? entry.annotations : void 0)?.["workers/message"];
1560
+ const annotations = isRecord(entry.annotations) ? entry.annotations : void 0;
1561
+ const message = annotations?.["workers/message"];
1562
+ let draft = entry;
1563
+ if (annotations?.["workers/triggered_by"] === "secret" && Object.keys(annotations).length === 1 && !isRecord(entry.resources)) {
1564
+ const inspection = await Promise.resolve(options.runWrangler([
1565
+ "versions",
1566
+ "view",
1567
+ versionId,
1568
+ ...commonArgs,
1569
+ "--name",
1570
+ options.workerName,
1571
+ "--json"
1572
+ ], { capture: true }));
1573
+ if (isCleanCommandExit(inspection)) {
1574
+ const inspected = JSON.parse(inspection.stdout);
1575
+ if (isRecord(inspected) && inspected.id === versionId) draft = inspected;
1576
+ }
1577
+ }
1518
1578
  return {
1519
1579
  metadata: typeof message === "string" ? parseCloudflareVersionMessage(message) : null,
1520
- isSecretOnlyDraft: isWranglerSecretOnlyDraft(entry)
1580
+ isSecretOnlyDraft: isWranglerSecretOnlyDraft(draft)
1521
1581
  };
1522
1582
  }
1523
1583
  } catch {
@@ -1543,7 +1603,8 @@ async function readRecentCloudflareVersionIds(options, commonArgs) {
1543
1603
  if (!Array.isArray(versions) || !versions.every((version) => isRecord(version) && typeof version.id === "string" && typeof version.number === "number" && Number.isSafeInteger(version.number))) return null;
1544
1604
  return versions.map((version) => ({
1545
1605
  id: version.id,
1546
- number: version.number
1606
+ number: version.number,
1607
+ inspection: version
1547
1608
  }));
1548
1609
  } catch {
1549
1610
  return null;
@@ -1593,23 +1654,6 @@ async function readCloudflareVersionSecretBindings(options, commonArgs, versionI
1593
1654
  }
1594
1655
  }
1595
1656
  /**
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
1657
  * The Versions API cannot create a Worker or its initial Durable Object migration history, and
1614
1658
  * some accounts do not expose a version preview until their first ordinary deployment. A
1615
1659
  * brand-new Worker can therefore need one ordinary deploy before later releases use preview-first
@@ -1923,4 +1967,108 @@ function defaultSleep(milliseconds) {
1923
1967
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
1924
1968
  }
1925
1969
  //#endregion
1926
- export { activateCloudflareVersion, assertCloudflareBindingsPreserved, assertCloudflareDurableObjectLifecycleSupported, assertExistingCloudflareConfig, deployCloudflareVersionTraffic, deployCloudflareVersionTriggers, encodeCloudflareVersionMessage, executeCloudflareDeploy, extractWorkersDevSubdomain, normalizeWorkersDevSubdomain, parseCloudflareVersionMessage, parseRootWranglerConfig, readBindingFieldPresence, readCloudflareTriggerConfig, readCloudflareVersionMigrationTag, readCloudflareWorkerMigrationTag, readConfigEntryString, readExistingCloudflareWorker, readStoredResourceName, readWranglerTopLevelString, renderCloudflareStaticEntry, resolveD1MigrateTarget, stabilizeCloudflareVersionTriggers, stageCloudflareStaticAssets, writeCloudflareFrameworkDeployConfig, writeCloudflareStaticDeployConfig };
1970
+ //#region src/metadata.ts
1971
+ /**
1972
+ * Share one authenticated transport for the activator's read-only metadata calls.
1973
+ * Writes and unsupported commands retain the pinned Wrangler runner. Responses
1974
+ * keep its JSON contract so activation and concurrency checks remain unchanged.
1975
+ */
1976
+ function createCloudflareMetadataRunner(options) {
1977
+ let tokenPromise;
1978
+ const common = [
1979
+ ...options.configPath ? ["-c", options.configPath] : [],
1980
+ "--env=",
1981
+ "--name",
1982
+ options.workerName,
1983
+ "--json"
1984
+ ];
1985
+ const prefix = `${options.apiBaseUrl.replace(/\/$/, "")}/accounts/${encodeURIComponent(options.accountId)}/workers/scripts/${encodeURIComponent(options.workerName)}`;
1986
+ const fetcher = options.fetch ?? globalThis.fetch;
1987
+ const proxyEnv = options.env ?? process.env;
1988
+ const requiresWranglerProxy = Boolean(proxyEnv.https_proxy || proxyEnv.HTTPS_PROXY || proxyEnv.http_proxy || proxyEnv.HTTP_PROXY);
1989
+ const refused = {
1990
+ status: 1,
1991
+ stdout: "",
1992
+ stderr: "Cloudflare uploader integrity check failed.",
1993
+ error: "uploader-changed"
1994
+ };
1995
+ const fallback = (args, runOptions) => options.verifyUploader() ? options.runWrangler(args, runOptions) : refused;
1996
+ return async (args, runOptions) => {
1997
+ let kind;
1998
+ let resource;
1999
+ let rest = [];
2000
+ if (args[0] === "deployments" && args[1] === "status") {
2001
+ kind = "deployment";
2002
+ resource = "/deployments";
2003
+ rest = args.slice(2);
2004
+ } else if (args[0] === "versions" && args[1] === "list") {
2005
+ kind = "versions";
2006
+ resource = "/versions?deployable=true";
2007
+ rest = args.slice(2);
2008
+ } else if (args[0] === "versions" && args[1] === "view" && /^[0-9a-f-]{36}$/i.test(args[2] ?? "")) {
2009
+ kind = "version";
2010
+ resource = `/versions/${encodeURIComponent(args[2])}`;
2011
+ rest = args.slice(3);
2012
+ }
2013
+ if (!kind || !runOptions.capture || rest.length !== common.length || rest.some((value, i) => value !== common[i])) return fallback(args, runOptions);
2014
+ if (requiresWranglerProxy) return fallback(args, runOptions);
2015
+ let response;
2016
+ try {
2017
+ if (!tokenPromise && !options.verifyUploader()) return refused;
2018
+ const token = await (tokenPromise ??= options.getToken());
2019
+ if (token) {
2020
+ response = await fetcher(prefix + resource, {
2021
+ headers: { Authorization: `Bearer ${token}` },
2022
+ redirect: "manual",
2023
+ signal: AbortSignal.timeout(1e4)
2024
+ });
2025
+ if (response.status === 401 || response.status === 403) tokenPromise = void 0;
2026
+ if (response.ok) {
2027
+ const body = await response.json();
2028
+ if (record(body) && body.success === true && record(body.result)) {
2029
+ const result = body.result;
2030
+ if (kind === "deployment" && Array.isArray(result.deployments)) {
2031
+ const latest = result.deployments[0];
2032
+ if (result.deployments.length === 0) return {
2033
+ status: 1,
2034
+ stdout: "",
2035
+ stderr: `The Worker ${options.workerName} has no deployments.`
2036
+ };
2037
+ if (record(latest) && Array.isArray(latest.versions)) return {
2038
+ status: 0,
2039
+ stdout: JSON.stringify(latest),
2040
+ stderr: ""
2041
+ };
2042
+ }
2043
+ if (kind === "version" && result.id === args[2]) return {
2044
+ status: 0,
2045
+ stdout: JSON.stringify(result),
2046
+ stderr: ""
2047
+ };
2048
+ if (kind === "versions" && Array.isArray(result.items)) {
2049
+ const items = result.items;
2050
+ if (items.every((item) => record(item) && record(item.metadata) && typeof item.metadata.created_on === "string")) {
2051
+ const sorted = items.sort((a, b) => a.metadata.created_on.localeCompare(b.metadata.created_on)).slice(-10);
2052
+ return {
2053
+ status: 0,
2054
+ stdout: JSON.stringify(sorted),
2055
+ stderr: ""
2056
+ };
2057
+ }
2058
+ }
2059
+ }
2060
+ }
2061
+ }
2062
+ } catch {
2063
+ tokenPromise = void 0;
2064
+ } finally {
2065
+ if (response && !response.bodyUsed) await response.body?.cancel().catch(() => {});
2066
+ }
2067
+ return fallback(args, runOptions);
2068
+ };
2069
+ }
2070
+ function record(value) {
2071
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2072
+ }
2073
+ //#endregion
2074
+ export { 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.20.0",
3
+ "version": "0.20.2",
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.2",
24
24
  "jsonc-parser": "3.3.1"
25
25
  },
26
26
  "devDependencies": {