@volter/twin-cloudflare 0.1.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.
Files changed (58) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +223 -0
  3. package/dist/r2/src/endpoint.d.ts +14 -0
  4. package/dist/r2/src/endpoint.js +30 -0
  5. package/dist/r2/src/fetch.d.ts +7 -0
  6. package/dist/r2/src/fetch.js +230 -0
  7. package/dist/r2/src/generated/surface.gen.json +1 -0
  8. package/dist/r2/src/manifest.d.ts +22 -0
  9. package/dist/r2/src/manifest.js +69 -0
  10. package/dist/r2/src/semantics/buckets.d.ts +18 -0
  11. package/dist/r2/src/semantics/buckets.js +145 -0
  12. package/dist/r2/src/semantics/objects.d.ts +8 -0
  13. package/dist/r2/src/semantics/objects.js +252 -0
  14. package/dist/r2/src/wire.d.ts +54 -0
  15. package/dist/r2/src/wire.js +120 -0
  16. package/dist/src/cli.d.ts +2 -0
  17. package/dist/src/cli.js +26 -0
  18. package/dist/src/cloudflare-budget.d.ts +22 -0
  19. package/dist/src/cloudflare-budget.js +40 -0
  20. package/dist/src/cloudflare-capabilities.d.ts +6 -0
  21. package/dist/src/cloudflare-capabilities.js +1161 -0
  22. package/dist/src/cloudflare-common.d.ts +59 -0
  23. package/dist/src/cloudflare-common.js +142 -0
  24. package/dist/src/cloudflare-conformance.d.ts +6 -0
  25. package/dist/src/cloudflare-conformance.js +251 -0
  26. package/dist/src/cloudflare-connector.d.ts +53 -0
  27. package/dist/src/cloudflare-connector.js +418 -0
  28. package/dist/src/cloudflare-openapi-operations.d.ts +10 -0
  29. package/dist/src/cloudflare-openapi-operations.js +3336 -0
  30. package/dist/src/cloudflare-provisioning.d.ts +5 -0
  31. package/dist/src/cloudflare-provisioning.js +2097 -0
  32. package/dist/src/cloudflare-r2.d.ts +121 -0
  33. package/dist/src/cloudflare-r2.js +138 -0
  34. package/dist/src/cloudflare-server.d.ts +17 -0
  35. package/dist/src/cloudflare-server.js +101 -0
  36. package/dist/src/cloudflare-twin.d.ts +14 -0
  37. package/dist/src/cloudflare-twin.js +762 -0
  38. package/dist/src/cloudflare-worker-assets.d.ts +25 -0
  39. package/dist/src/cloudflare-worker-assets.js +192 -0
  40. package/dist/src/index.d.ts +9 -0
  41. package/dist/src/index.js +69 -0
  42. package/dist/src/screens/dashboard.d.ts +3 -0
  43. package/dist/src/screens/dashboard.js +265 -0
  44. package/package.json +56 -0
  45. package/src/cli.ts +25 -0
  46. package/src/cloudflare-budget.ts +61 -0
  47. package/src/cloudflare-capabilities.ts +1107 -0
  48. package/src/cloudflare-common.ts +183 -0
  49. package/src/cloudflare-conformance.ts +204 -0
  50. package/src/cloudflare-connector.ts +443 -0
  51. package/src/cloudflare-openapi-operations.ts +3344 -0
  52. package/src/cloudflare-provisioning.ts +1969 -0
  53. package/src/cloudflare-r2.ts +173 -0
  54. package/src/cloudflare-server.ts +109 -0
  55. package/src/cloudflare-twin.ts +714 -0
  56. package/src/cloudflare-worker-assets.ts +207 -0
  57. package/src/index.ts +112 -0
  58. package/src/screens/dashboard.tsx +297 -0
@@ -0,0 +1,2097 @@
1
+ // Cloudflare Workers / R2 / Hyperdrive / Queues PROVISIONING control plane — the surface a
2
+ // wrangler-shaped or raw-API deploy script drives when standing up a Workers launch plane:
3
+ // Worker script upload (metadata + bindings recorded, content fingerprinted, NEVER executed),
4
+ // cron triggers, workers.dev subdomains, zone routes and custom domains, R2 bucket control
5
+ // plane (no object data plane), Hyperdrive configuration records (origins recorded, never
6
+ // dialed), Queues and queue consumers, and Durable Object namespace records materialized from
7
+ // script migrations.
8
+ //
9
+ // Grounding (re-read 2026-08-23 from the SAME pinned sources as the core slice): Cloudflare's
10
+ // first-party api-schemas OpenAPI 4.0.0 (commit 2ac8369e9b63dccacee1a2284e95bb819f05b307 —
11
+ // paths, envelopes, request schemas, script-name/bucket-name patterns, the delete?force
12
+ // contract) and the official cloudflare-typescript SDK (npm cloudflare@7.1.0 — response
13
+ // shapes, binding-kind union, consumer union, multipart upload encoding). The pinned OpenAPI
14
+ // declares only a GENERIC 4XX error envelope for these operations, so specific numeric error
15
+ // codes are not pinned by it: this file uses Cloudflare's widely observed codes where they are
16
+ // well known (10007 script not found, 10006 R2 bucket missing, 10004 R2 bucket exists) and the
17
+ // pack's generic conventions (1004 validation, 1001 not found) everywhere else — see README
18
+ // "Official grounding". No live probe was possible (no Cloudflare account); the OpenAPI + SDK
19
+ // are the honest grounding ceiling for this slice.
20
+ import { createHash } from 'node:crypto';
21
+ import { deterministicHex, failure, headers as lowerHeaders, isResponse, mint, noRoute, paginate, parseBody, readOnly, row, rows, callerEmail, audit, scopeMissing, success, uuidFrom, write, } from "./cloudflare-common.js";
22
+ import { catchUpR2CustomDomains, connectR2CustomDomain, createR2TempCredential, r2BearerToken, r2Credential, r2ParentCovers, removeR2CustomDomain, r2CustomDomains, TEMP_PERMISSIONS, updateR2CustomDomain } from "./cloudflare-r2.js";
23
+ import { createAssetsUploadSession, redeemAssetsCompletion, uploadAssets } from "./cloudflare-worker-assets.js";
24
+ const SCRIPT_NAME_PATTERN = /^[a-z0-9_][a-z0-9-_]*$/; // OpenAPI workers_script_name
25
+ const BUCKET_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/; // OpenAPI r2 create schema (3-64 chars)
26
+ const R2_LOCATIONS = new Set(['apac', 'eeur', 'enam', 'weur', 'wnam', 'oc']);
27
+ const R2_STORAGE_CLASSES = new Set(['Standard', 'InfrequentAccess']);
28
+ const R2_JURISDICTIONS = new Set(['default', 'eu', 'fedramp']);
29
+ const USAGE_MODELS = new Set(['standard', 'bundled', 'unbound']);
30
+ const HYPERDRIVE_SCHEMES = new Set(['postgres', 'postgresql', 'mysql']);
31
+ // The closed binding-kind union from the official SDK (cloudflare@7.1.0 WorkersBindingKind*).
32
+ // An unknown `type` fails 400 — the union is closed in both the OpenAPI and the SDK.
33
+ const BINDING_KINDS = new Set([
34
+ 'ai', 'ai_search', 'ai_search_namespace', 'messaging', 'analytics_engine', 'assets', 'browser',
35
+ 'd1', 'data_blob', 'dispatch_namespace', 'durable_object_namespace', 'hyperdrive', 'inherit',
36
+ 'images', 'json', 'kv_namespace', 'media', 'mtls_certificate', 'plain_text', 'pipelines',
37
+ 'queue', 'ratelimit', 'r2_bucket', 'secret_text', 'send_email', 'service', 'text_blob',
38
+ 'vectorize', 'version_metadata', 'secrets_store_secret', 'flagship', 'secret_key', 'workflow',
39
+ 'wasm_module', 'vpc_service', 'vpc_network',
40
+ // Dynamic Worker Loaders: wrangler's `worker_loaders` config uploads `{type: "worker_loader",
41
+ // name}` — newer than the pinned SDK union; recorded as configuration like every kind below
42
+ // the deploy-critical ones.
43
+ 'worker_loader',
44
+ ]);
45
+ function str(value) {
46
+ return typeof value === 'string' && value.length > 0;
47
+ }
48
+ const CRON_FIELDS = [
49
+ { min: 0, max: 59 },
50
+ { min: 0, max: 23 },
51
+ { min: 1, max: 31, special: /^(l|lw|[0-9]{1,2}w)$/ }, // day-of-month: last / last-weekday / nth-weekday
52
+ { min: 1, max: 12, names: /^(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)$/ },
53
+ { min: 0, max: 7, names: /^(sun|mon|tue|wed|thu|fri|sat)$/, special: /^([0-7]#[1-5]|[0-7]?l)$/ }, // day-of-week: nth / last
54
+ ];
55
+ function cronToken(token, f) {
56
+ const lower = token.toLowerCase();
57
+ if (f.names?.test(lower))
58
+ return true;
59
+ if (f.special?.test(lower))
60
+ return true;
61
+ return /^[0-9]+$/.test(token) && Number(token) >= f.min && Number(token) <= f.max;
62
+ }
63
+ function validCronField(field, f) {
64
+ if (field === '?' || field === '*')
65
+ return true;
66
+ return field.split(',').every((part) => {
67
+ if (part === '')
68
+ return false;
69
+ const slash = part.split('/');
70
+ if (slash.length > 2)
71
+ return false; // reject */5/5 and the like
72
+ const [base, step] = slash;
73
+ if (step !== undefined && !/^[1-9][0-9]*$/.test(step))
74
+ return false;
75
+ if (base === '*')
76
+ return true;
77
+ const dash = base.split('-');
78
+ if (dash.length > 2)
79
+ return false;
80
+ if (dash.length === 2) {
81
+ // A numeric range must be low<=high; special/named endpoints are accepted as-is.
82
+ if (!cronToken(dash[0], f) || !cronToken(dash[1], f))
83
+ return false;
84
+ if (/^[0-9]+$/.test(dash[0]) && /^[0-9]+$/.test(dash[1]) && Number(dash[0]) > Number(dash[1]))
85
+ return false;
86
+ return true;
87
+ }
88
+ return cronToken(base, f);
89
+ });
90
+ }
91
+ function validCron(cron) {
92
+ const fields = cron.trim().split(/\s+/);
93
+ return fields.length === 5 && fields.every((field, index) => validCronField(field, CRON_FIELDS[index]));
94
+ }
95
+ function scriptID(accountID, name) {
96
+ return `${accountID}:${name}`;
97
+ }
98
+ function scriptRows(accountID, root) {
99
+ return rows('worker_script', root).filter((item) => item.account_id === accountID);
100
+ }
101
+ function scriptRow(accountID, name, root) {
102
+ return row('worker_script', scriptID(accountID, name), root);
103
+ }
104
+ function scriptNotFound(name) {
105
+ return failure(404, 10007, `workers.api.error.script_not_found: script "${name}" not found`);
106
+ }
107
+ /** Kernel writes here fold a per-subject `rev` ordinal into every action so a write returning
108
+ * a subject to a previously held value in the same millisecond can never collide with the
109
+ * earlier action's content hash and be dropped as a replay (the kernel's content+timestamp
110
+ * dedupe — see ADDING_A_TWIN.md section 5; upstash-store.ts `rev` is the precedent). */
111
+ async function pwrite(type, id, operation, fields, req) {
112
+ const rev = Number(row(type, id, req.root, true)?.rev ?? 0) + 1;
113
+ await write(type, id, operation, { ...fields, rev }, req);
114
+ }
115
+ function namespaceRows(accountID, root) {
116
+ return rows('durable_object_namespace', root).filter((item) => item.account_id === accountID);
117
+ }
118
+ // ── views (response shapes per the official SDK) ────────────────────────────────────────────
119
+ function scriptView(value) {
120
+ return {
121
+ id: value.name,
122
+ etag: value.etag,
123
+ created_on: value.created_on,
124
+ modified_on: value.modified_on,
125
+ usage_model: value.usage_model ?? 'standard',
126
+ logpush: value.logpush ?? false,
127
+ has_assets: value.assets !== null && value.assets !== undefined,
128
+ has_modules: value.has_modules === true,
129
+ tags: value.tags ?? [],
130
+ tail_consumers: value.tail_consumers ?? null,
131
+ ...(str(value.compatibility_date) ? { compatibility_date: value.compatibility_date } : {}),
132
+ ...(Array.isArray(value.compatibility_flags) ? { compatibility_flags: value.compatibility_flags } : {}),
133
+ ...(str(value.migration_tag) ? { migration_tag: value.migration_tag } : {}),
134
+ };
135
+ }
136
+ /** A script's tag: the vendor's opaque 32-hex identity for one Worker, stable across uploads and
137
+ * new after a delete/recreate. Minted against every tag already in state (tombstones included). */
138
+ function mintScriptTag(accountID, scriptName, root) {
139
+ const taken = new Set(rows('worker_script', root, true).map((item) => String(item.tag ?? '')));
140
+ for (let salt = 0;; salt++) {
141
+ const tag = deterministicHex(`${accountID}:${scriptName}:tag:${salt}`, 32);
142
+ if (!taken.has(tag))
143
+ return tag;
144
+ }
145
+ }
146
+ /** The client a script was last uploaded from, as the vendor reports it (`last_deployed_from`):
147
+ * wrangler identifies itself in its User-Agent (`wrangler/<version>`); anything else is the API. */
148
+ function deploySource(req) {
149
+ return /^wrangler(\/|$)/i.test(lowerHeaders(req.headers)['user-agent'] ?? '') ? 'wrangler' : 'api';
150
+ }
151
+ /** `GET /accounts/{account_id}/workers/services/{service_name}` — the Worker-as-service view
152
+ * wrangler reads before every deploy (tag, tags, migration tag, last deploy source). This path
153
+ * is not in the pinned OpenAPI; its shape is what Cloudflare's own client (wrangler, the
154
+ * cloudflare/workers-sdk deploy helpers) reads from it: `default_environment.environment` and
155
+ * `default_environment.script.{tag,tags,migration_tag,last_deployed_from}`. A Worker that does
156
+ * not exist answers 10007, the not-found code wrangler treats as "first deploy". */
157
+ function serviceView(value) {
158
+ const script = {
159
+ ...scriptView(value),
160
+ tag: value.tag,
161
+ last_deployed_from: value.last_deployed_from ?? 'api',
162
+ };
163
+ return {
164
+ id: value.name,
165
+ default_environment: {
166
+ environment: 'production',
167
+ created_on: value.created_on,
168
+ modified_on: value.modified_on,
169
+ script,
170
+ },
171
+ environments: [{ environment: 'production', created_on: value.created_on, modified_on: value.modified_on }],
172
+ created_on: value.created_on,
173
+ modified_on: value.modified_on,
174
+ };
175
+ }
176
+ function routeView(value) {
177
+ return { id: value.id, pattern: value.pattern, ...(str(value.script) ? { script: value.script } : {}) };
178
+ }
179
+ function domainView(value) {
180
+ return {
181
+ id: value.id,
182
+ cert_id: value.cert_id,
183
+ hostname: value.hostname,
184
+ service: value.service,
185
+ zone_id: value.zone_id,
186
+ zone_name: value.zone_name,
187
+ environment: 'production',
188
+ };
189
+ }
190
+ function bucketView(value) {
191
+ return {
192
+ name: value.name,
193
+ creation_date: value.creation_date,
194
+ ...(str(value.location) ? { location: value.location } : {}),
195
+ storage_class: value.storage_class ?? 'Standard',
196
+ jurisdiction: value.jurisdiction ?? 'default',
197
+ };
198
+ }
199
+ function hyperdriveView(value) {
200
+ return {
201
+ id: value.id,
202
+ name: value.name,
203
+ origin: value.origin,
204
+ ...(value.caching !== undefined && value.caching !== null ? { caching: value.caching } : {}),
205
+ ...(value.mtls !== undefined && value.mtls !== null ? { mtls: value.mtls } : {}),
206
+ ...(typeof value.origin_connection_limit === 'number' ? { origin_connection_limit: value.origin_connection_limit } : {}),
207
+ created_on: value.created_on,
208
+ modified_on: value.modified_on,
209
+ };
210
+ }
211
+ function consumerView(value, root) {
212
+ const queue = row('queue', String(value.queue_id), root);
213
+ return {
214
+ consumer_id: value.id,
215
+ queue_name: queue?.queue_name ?? null,
216
+ created_on: value.created_on,
217
+ type: value.consumer_type,
218
+ ...(value.consumer_type === 'worker' && str(value.script_name) ? { script_name: value.script_name } : {}),
219
+ ...(str(value.dead_letter_queue) ? { dead_letter_queue: value.dead_letter_queue } : {}),
220
+ settings: value.settings ?? {},
221
+ };
222
+ }
223
+ function queueProducers(accountID, queueName, root) {
224
+ return scriptRows(accountID, root)
225
+ .filter((script) => (Array.isArray(script.bindings) ? script.bindings : [])
226
+ .some((binding) => binding?.type === 'queue' && binding?.queue_name === queueName))
227
+ .map((script) => ({ script: script.name, type: 'worker' }));
228
+ }
229
+ function queueView(value, root) {
230
+ const consumers = rows('queue_consumer', root)
231
+ .filter((item) => item.queue_id === value.id)
232
+ .map((item) => consumerView(item, root));
233
+ const producers = queueProducers(String(value.account_id), String(value.queue_name), root);
234
+ return {
235
+ queue_id: value.id,
236
+ queue_name: value.queue_name,
237
+ created_on: value.created_on,
238
+ modified_on: value.modified_on,
239
+ producers,
240
+ producers_total_count: producers.length,
241
+ consumers,
242
+ consumers_total_count: consumers.length,
243
+ ...(value.settings !== undefined && value.settings !== null ? { settings: value.settings } : {}),
244
+ };
245
+ }
246
+ function namespaceView(value) {
247
+ return { id: value.id, class: value.class_name, script: value.script, use_sqlite: value.use_sqlite === true };
248
+ }
249
+ /** Parse a multipart/form-data body already decoded as UTF-8 text. Text module parts survive
250
+ * intact; a binary part (e.g. wasm) survives only as its lossy UTF-8 decoding — the content is
251
+ * still recorded and fingerprinted deterministically, but byte-exact wasm round-tripping is a
252
+ * documented non-goal of this control-plane twin. */
253
+ function parseMultipart(body, boundary) {
254
+ const delimiter = `--${boundary}`;
255
+ const segments = body.split(delimiter);
256
+ if (segments.length < 3)
257
+ return null;
258
+ const parts = [];
259
+ for (const segment of segments.slice(1, -1)) {
260
+ const cleaned = segment.replace(/^\r?\n/, '');
261
+ const headerEnd = cleaned.indexOf('\r\n\r\n') >= 0 ? cleaned.indexOf('\r\n\r\n') : cleaned.indexOf('\n\n');
262
+ if (headerEnd < 0)
263
+ return null;
264
+ const rawHeaders = cleaned.slice(0, headerEnd);
265
+ const data = cleaned.slice(headerEnd).replace(/^(\r\n\r\n|\n\n)/, '').replace(/\r?\n$/, '');
266
+ const disposition = rawHeaders.split(/\r?\n/).find((line) => /^content-disposition:/i.test(line)) ?? '';
267
+ const nameMatch = /name="([^"]*)"/i.exec(disposition);
268
+ const filenameMatch = /filename="([^"]*)"/i.exec(disposition);
269
+ const typeLine = rawHeaders.split(/\r?\n/).find((line) => /^content-type:/i.test(line));
270
+ if (!nameMatch)
271
+ return null;
272
+ parts.push({
273
+ name: nameMatch[1],
274
+ ...(filenameMatch ? { filename: filenameMatch[1] } : {}),
275
+ ...(typeLine ? { contentType: typeLine.split(':')[1].trim() } : {}),
276
+ data,
277
+ });
278
+ }
279
+ return parts;
280
+ }
281
+ // ── Worker script upload ────────────────────────────────────────────────────────────────────
282
+ function validateBindings(accountID, scriptName, bindings, req, projectedClasses) {
283
+ if (bindings === undefined)
284
+ return null;
285
+ if (!Array.isArray(bindings))
286
+ return failure(400, 1004, 'metadata.bindings must be an array');
287
+ for (const value of bindings) {
288
+ if (!value || typeof value !== 'object' || Array.isArray(value))
289
+ return failure(400, 1004, 'Each binding must be an object');
290
+ const binding = value;
291
+ if (!str(binding.type) || !BINDING_KINDS.has(binding.type)) {
292
+ return failure(400, 1004, `Binding type "${String(binding.type)}" is not a valid Workers binding kind`);
293
+ }
294
+ if (!str(binding.name))
295
+ return failure(400, 1004, 'Each binding requires a nonempty name');
296
+ if ((binding.type === 'plain_text' || binding.type === 'secret_text') && !str(binding.text)) {
297
+ return failure(400, 1004, `${binding.type} binding "${binding.name}" requires text`);
298
+ }
299
+ if (binding.type === 'json' && binding.json === undefined) {
300
+ return failure(400, 1004, `json binding "${binding.name}" requires json`);
301
+ }
302
+ if (binding.type === 'kv_namespace' && !str(binding.namespace_id)) {
303
+ return failure(400, 1004, `kv_namespace binding "${binding.name}" requires namespace_id`);
304
+ }
305
+ if (binding.type === 'service') {
306
+ if (!str(binding.service))
307
+ return failure(400, 1004, `service binding "${binding.name}" requires service`);
308
+ // A service binding must target an existing Worker (or this script itself) — the delete
309
+ // path treats service bindings as real dependents, so a dangling one must not be recordable
310
+ // (round-two N4, same cross-resource-ghost class as the route→script check).
311
+ if (binding.service !== scriptName && !scriptRow(accountID, binding.service, req.root)) {
312
+ return failure(400, 1004, `service binding "${binding.name}" references unknown Worker "${binding.service}"`);
313
+ }
314
+ }
315
+ if (binding.type === 'r2_bucket') {
316
+ if (!str(binding.bucket_name))
317
+ return failure(400, 1004, `r2_bucket binding "${binding.name}" requires bucket_name`);
318
+ if (!row('r2_bucket', `${accountID}:${binding.bucket_name}`, req.root)) {
319
+ return failure(400, 1004, `r2_bucket binding "${binding.name}" references unknown bucket "${binding.bucket_name}"`);
320
+ }
321
+ }
322
+ if (binding.type === 'queue') {
323
+ if (!str(binding.queue_name))
324
+ return failure(400, 1004, `queue binding "${binding.name}" requires queue_name`);
325
+ if (!rows('queue', req.root).some((item) => item.account_id === accountID && item.queue_name === binding.queue_name)) {
326
+ return failure(400, 1004, `queue binding "${binding.name}" references unknown queue "${binding.queue_name}"`);
327
+ }
328
+ }
329
+ if (binding.type === 'hyperdrive') {
330
+ if (!str(binding.id))
331
+ return failure(400, 1004, `hyperdrive binding "${binding.name}" requires id`);
332
+ const config = row('hyperdrive_config', binding.id, req.root);
333
+ if (!config || config.account_id !== accountID) {
334
+ return failure(400, 1004, `hyperdrive binding "${binding.name}" references unknown Hyperdrive configuration "${binding.id}"`);
335
+ }
336
+ }
337
+ if (binding.type === 'durable_object_namespace') {
338
+ const owner = str(binding.script_name) ? binding.script_name : scriptName;
339
+ if (!str(binding.class_name) && !str(binding.namespace_id)) {
340
+ return failure(400, 1004, `durable_object_namespace binding "${binding.name}" requires class_name or namespace_id`);
341
+ }
342
+ if (str(binding.namespace_id) && !namespaceRows(accountID, req.root).some((item) => item.id === binding.namespace_id)) {
343
+ return failure(400, 1004, `durable_object_namespace binding "${binding.name}" references unknown namespace "${binding.namespace_id}"`);
344
+ }
345
+ if (str(binding.class_name)) {
346
+ // Same-script class refs are validated against the PROJECTED post-migration class set
347
+ // (this upload's own migrations may create the class); cross-script refs against the
348
+ // other script's already-live classes.
349
+ const known = owner === scriptName
350
+ ? projectedClasses.has(binding.class_name)
351
+ : namespaceRows(accountID, req.root).some((item) => item.script === owner && item.class_name === binding.class_name);
352
+ if (!known)
353
+ return failure(400, 1004, `durable_object_namespace binding "${binding.name}" references unknown class "${binding.class_name}" on script "${owner}"`);
354
+ }
355
+ }
356
+ }
357
+ return null;
358
+ }
359
+ function classList(value, label) {
360
+ if (value === undefined)
361
+ return [];
362
+ if (!Array.isArray(value) || value.some((item) => !str(item)))
363
+ return failure(400, 1004, `migrations.${label} must be an array of class names`);
364
+ return value;
365
+ }
366
+ /** Validate migrations and compute the ordered write-plan + the resulting live-class set —
367
+ * WITHOUT writing anything. Splitting plan from apply is what makes a Worker upload atomic:
368
+ * nothing is written until ALL validation (metadata, bindings against the projected classes,
369
+ * and migrations) has passed, so a rejected upload can never leave orphan Durable Object
370
+ * namespaces behind (round-two N5). */
371
+ function planMigrations(accountID, scriptName, migrations, currentTag, root) {
372
+ if (str(migrations.old_tag) && migrations.old_tag !== currentTag) {
373
+ return failure(400, 1004, `migrations.old_tag "${migrations.old_tag}" does not match the script's current migration tag`);
374
+ }
375
+ const steps = Array.isArray(migrations.steps) ? migrations.steps : [migrations];
376
+ const ops = [];
377
+ // Simulate the evolving live-class map (className -> existing id, or null for a planned create).
378
+ const live = new Map();
379
+ for (const item of namespaceRows(accountID, root).filter((row) => row.script === scriptName)) {
380
+ live.set(String(item.class_name), item.id);
381
+ }
382
+ for (const step of steps) {
383
+ if (!step || typeof step !== 'object')
384
+ return failure(400, 1004, 'Each migration step must be an object');
385
+ if (step.transferred_classes !== undefined) {
386
+ return failure(400, 1004, 'migrations.transferred_classes is not modeled by this twin');
387
+ }
388
+ const created = classList(step.new_classes, 'new_classes');
389
+ if (isResponse(created))
390
+ return created;
391
+ const createdSqlite = classList(step.new_sqlite_classes, 'new_sqlite_classes');
392
+ if (isResponse(createdSqlite))
393
+ return createdSqlite;
394
+ const deleted = classList(step.deleted_classes, 'deleted_classes');
395
+ if (isResponse(deleted))
396
+ return deleted;
397
+ const renames = step.renamed_classes === undefined ? [] : step.renamed_classes;
398
+ if (!Array.isArray(renames))
399
+ return failure(400, 1004, 'migrations.renamed_classes must be an array');
400
+ for (const [list, useSqlite] of [[created, false], [createdSqlite, true]]) {
401
+ for (const className of list) {
402
+ if (live.has(className))
403
+ return failure(400, 1004, `migrations: class "${className}" already has a Durable Object namespace on this script`);
404
+ live.set(className, null);
405
+ ops.push({ kind: 'create', className, useSqlite });
406
+ }
407
+ }
408
+ for (const className of deleted) {
409
+ const id = live.get(className);
410
+ if (id === undefined)
411
+ return failure(400, 1004, `migrations: cannot delete unknown class "${className}"`);
412
+ live.delete(className);
413
+ if (id !== null)
414
+ ops.push({ kind: 'delete', id });
415
+ // A class created by an EARLIER step of this same upload never materializes: drop its
416
+ // planned create (e.g. steps v1 new WorldDO → v2 delete WorldDO on a first deploy).
417
+ else {
418
+ const planned = ops.findIndex((op) => op.kind === 'create' && op.className === className);
419
+ if (planned >= 0)
420
+ ops.splice(planned, 1);
421
+ }
422
+ }
423
+ for (const rename of renames) {
424
+ const pair = rename;
425
+ if (!pair || typeof pair !== 'object' || !str(pair.from) || !str(pair.to)) {
426
+ return failure(400, 1004, 'migrations.renamed_classes entries require from and to');
427
+ }
428
+ const id = live.get(pair.from);
429
+ if (id === undefined)
430
+ return failure(400, 1004, `migrations: cannot rename unknown class "${pair.from}"`);
431
+ if (live.has(pair.to))
432
+ return failure(400, 1004, `migrations: rename target class "${pair.to}" already exists`);
433
+ live.delete(pair.from);
434
+ live.set(pair.to, id);
435
+ if (id !== null)
436
+ ops.push({ kind: 'rename', id, to: pair.to });
437
+ // Renaming a class this upload creates: the planned create takes the new name.
438
+ else {
439
+ const planned = ops.find((op) => op.kind === 'create' && op.className === pair.from);
440
+ if (planned && planned.kind === 'create')
441
+ planned.className = pair.to;
442
+ }
443
+ }
444
+ }
445
+ return { ...(str(migrations.new_tag) ? { migrationTag: migrations.new_tag } : {}), ops, classes: new Set(live.keys()) };
446
+ }
447
+ async function applyMigrationPlan(accountID, scriptName, ops, req) {
448
+ for (const op of ops) {
449
+ if (op.kind === 'create') {
450
+ const id = mint('durable_object_namespace', `${accountID}:${scriptName}:${op.className}`, req.root);
451
+ await pwrite('durable_object_namespace', id, 'durable_object_namespace.create', {
452
+ account_id: accountID, script: scriptName, class_name: op.className, use_sqlite: op.useSqlite, deleted: false,
453
+ }, req);
454
+ }
455
+ else if (op.kind === 'delete') {
456
+ await pwrite('durable_object_namespace', op.id, 'durable_object_namespace.delete', { account_id: accountID, script: scriptName, deleted: true }, req);
457
+ }
458
+ else {
459
+ await pwrite('durable_object_namespace', op.id, 'durable_object_namespace.rename', { account_id: accountID, script: scriptName, class_name: op.to }, req);
460
+ }
461
+ }
462
+ }
463
+ /** Fold Rails-style bracket keys (the encoding the official SDK's FormData serializer emits:
464
+ * `metadata[main_module]`, `metadata[bindings][][type]`, …) into a metadata object. An empty
465
+ * `[]` segment appends to an array; a repeated direct key inside the trailing array element
466
+ * starts a new element, matching the serializer's sequential emission order. Values arrive as
467
+ * strings; the boolean fields this twin validates are coerced afterwards. */
468
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
469
+ function foldBracketMetadata(entries) {
470
+ const root = Object.create(null);
471
+ for (const { path, value } of entries) {
472
+ const tokens = [...path.matchAll(/\[([^\]]*)\]/g)].map((match) => match[1]);
473
+ if (tokens.length === 0)
474
+ return null;
475
+ // Refuse prototype-pollution keys outright — a bracket path like metadata[__proto__][x]
476
+ // must never reach into Object.prototype (Object.create(null) already denies the write, but
477
+ // rejecting loudly is clearer and covers constructor/prototype too).
478
+ if (tokens.some((token) => FORBIDDEN_KEYS.has(token)))
479
+ return null;
480
+ let node = root;
481
+ for (let i = 0; i < tokens.length; i++) {
482
+ const token = tokens[i];
483
+ const last = i === tokens.length - 1;
484
+ if (token === '') {
485
+ if (!Array.isArray(node))
486
+ return null;
487
+ if (last) {
488
+ node.push(value);
489
+ break;
490
+ }
491
+ const nextToken = tokens[i + 1];
492
+ let element = node[node.length - 1];
493
+ const needNew = element === undefined || typeof element !== 'object' || element === null
494
+ || (nextToken !== '' && !Array.isArray(element) && Object.hasOwn(element, nextToken));
495
+ if (needNew) {
496
+ element = nextToken === '' ? [] : {};
497
+ node.push(element);
498
+ }
499
+ node = element;
500
+ }
501
+ else {
502
+ if (typeof node !== 'object' || node === null || Array.isArray(node))
503
+ return null;
504
+ const object = node;
505
+ if (last) {
506
+ object[token] = value;
507
+ break;
508
+ }
509
+ const nextToken = tokens[i + 1];
510
+ if (object[token] === undefined)
511
+ object[token] = nextToken === '' ? [] : {};
512
+ node = object[token];
513
+ }
514
+ }
515
+ }
516
+ for (const key of ['logpush', 'keep_assets']) {
517
+ if (root[key] === 'true')
518
+ root[key] = true;
519
+ if (root[key] === 'false')
520
+ root[key] = false;
521
+ }
522
+ return root;
523
+ }
524
+ function parseUpload(req) {
525
+ const contentType = lowerHeaders(req.headers)['content-type'] ?? '';
526
+ const body = req.body ?? '';
527
+ // The wire can be multipart under EITHER content type: wrangler/raw API deploys send
528
+ // multipart/form-data with a boundary parameter, while the official SDK's runtime keeps the
529
+ // operation's application/javascript content type on a FormData-serialized body (measured
530
+ // against cloudflare@7.1.0 under Bun). Accept the header boundary when present, otherwise
531
+ // sniff the body's own delimiter line.
532
+ const headerBoundary = /boundary\s*=\s*(?:"([^"]+)"|([^\s;]+))/i.exec(contentType);
533
+ const sniffedBoundary = /^--([^\r\n]+)\r?\n/.exec(body);
534
+ const boundary = headerBoundary?.[1] ?? headerBoundary?.[2]
535
+ ?? (/content-disposition:\s*form-data/i.test(body) ? sniffedBoundary?.[1] : undefined);
536
+ if (/^multipart\/form-data/i.test(contentType) || boundary !== undefined) {
537
+ if (!boundary)
538
+ return failure(400, 1004, 'Invalid multipart/form-data body');
539
+ const parts = parseMultipart(body, boundary);
540
+ if (!parts)
541
+ return failure(400, 1004, 'Invalid multipart/form-data body');
542
+ const modules = parts.filter((part) => part.filename !== undefined).map((part) => ({
543
+ name: part.filename,
544
+ ...(part.contentType ? { content_type: part.contentType } : {}),
545
+ content: part.data,
546
+ }));
547
+ const fields = parts.filter((part) => part.filename === undefined);
548
+ const jsonPart = fields.find((part) => part.name === 'metadata');
549
+ const bracketParts = fields.filter((part) => part.name.startsWith('metadata['));
550
+ const unexpected = fields.find((part) => part !== jsonPart && !bracketParts.includes(part));
551
+ if (unexpected)
552
+ return failure(400, 1004, `Unexpected multipart part "${unexpected.name}"`);
553
+ if (jsonPart && bracketParts.length > 0)
554
+ return failure(400, 1004, 'metadata must be a single JSON part or bracket-encoded fields, not both');
555
+ let metadata;
556
+ if (jsonPart) {
557
+ try {
558
+ metadata = JSON.parse(jsonPart.data);
559
+ }
560
+ catch {
561
+ return failure(400, 1004, 'metadata part must be valid JSON');
562
+ }
563
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
564
+ return failure(400, 1004, 'metadata part must be a JSON object');
565
+ }
566
+ else if (bracketParts.length > 0) {
567
+ metadata = foldBracketMetadata(bracketParts.map((part) => ({ path: part.name.slice('metadata'.length), value: part.data })));
568
+ if (!metadata)
569
+ return failure(400, 1004, 'Invalid bracket-encoded metadata fields');
570
+ }
571
+ else {
572
+ return failure(400, 1004, 'multipart upload requires a metadata part');
573
+ }
574
+ return { metadata: metadata, modules };
575
+ }
576
+ // The official SDK serializes a no-files update as JSON {"metadata": {...}} under the
577
+ // operation's application/javascript content type; a raw non-JSON body is the classic
578
+ // service-worker-syntax script upload (OpenAPI application/javascript | text/javascript).
579
+ try {
580
+ const parsed = JSON.parse(body);
581
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && 'metadata' in parsed) {
582
+ const metadata = parsed.metadata;
583
+ if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata))
584
+ return failure(400, 1004, 'metadata must be a JSON object');
585
+ return { metadata: metadata, modules: [] };
586
+ }
587
+ }
588
+ catch { /* not JSON — treat as raw service-worker script */ }
589
+ if (!body)
590
+ return failure(400, 1004, 'Script upload requires content');
591
+ return { metadata: {}, modules: [], serviceWorkerBody: body };
592
+ }
593
+ const VERSIONED_FIELDS = ['etag', 'entry_module', 'has_modules', 'bindings', 'compatibility_date', 'compatibility_flags', 'usage_model', 'assets'];
594
+ function versionRows(script, root) {
595
+ return rows('worker_version', root)
596
+ .filter((item) => item.account_id === script.account_id && item.script_name === script.name && item.script_tag === script.tag)
597
+ .sort((a, b) => Number(b.number) - Number(a.number));
598
+ }
599
+ /** The deployed modules: the live version's, or — for a record written before versions were
600
+ * modeled — the script row's own copy. */
601
+ function liveModules(script, root) {
602
+ if (Array.isArray(script.modules) && script.modules.length > 0)
603
+ return script.modules;
604
+ const version = str(script.version_id) ? row('worker_version', script.version_id, root) : undefined;
605
+ return Array.isArray(version?.modules) ? version.modules : [];
606
+ }
607
+ /** Validate one multipart Worker upload and compute the version it makes — WITHOUT writing.
608
+ * `mode: 'deploy'` is the script PUT (creates the Worker, may migrate Durable Objects);
609
+ * `mode: 'version'` is the versions upload (the Worker must exist; migrations are refused —
610
+ * Cloudflare applies Durable Object migrations only through a deploy). */
611
+ function buildUpload(req, url, accountID, scriptName, mode) {
612
+ const upload = parseUpload(req);
613
+ if (isResponse(upload))
614
+ return upload;
615
+ const { metadata, modules, serviceWorkerBody } = upload;
616
+ if (str(metadata.main_module) && str(metadata.body_part)) {
617
+ return failure(400, 1004, 'metadata cannot declare both main_module and body_part');
618
+ }
619
+ const entry = str(metadata.main_module) ? metadata.main_module : str(metadata.body_part) ? metadata.body_part : undefined;
620
+ if (modules.length > 0) {
621
+ if (!entry)
622
+ return failure(400, 1004, 'metadata requires main_module or body_part naming an uploaded file');
623
+ if (!modules.some((module) => module.name === entry)) {
624
+ return failure(400, 1004, `metadata references "${entry}" but no uploaded file has that name`);
625
+ }
626
+ }
627
+ else if (entry) {
628
+ return failure(400, 1004, `metadata references "${entry}" but the upload contains no files`);
629
+ }
630
+ if (metadata.compatibility_date !== undefined && (!str(metadata.compatibility_date) || !/^\d{4}-\d{2}-\d{2}$/.test(metadata.compatibility_date))) {
631
+ return failure(400, 1004, 'metadata.compatibility_date must be a YYYY-MM-DD date');
632
+ }
633
+ if (metadata.compatibility_flags !== undefined && (!Array.isArray(metadata.compatibility_flags) || metadata.compatibility_flags.some((flag) => !str(flag)))) {
634
+ return failure(400, 1004, 'metadata.compatibility_flags must be an array of strings');
635
+ }
636
+ if (metadata.usage_model !== undefined && (!str(metadata.usage_model) || !USAGE_MODELS.has(metadata.usage_model))) {
637
+ return failure(400, 1004, 'metadata.usage_model must be standard, bundled, or unbound');
638
+ }
639
+ if (metadata.logpush !== undefined && typeof metadata.logpush !== 'boolean') {
640
+ return failure(400, 1004, 'metadata.logpush must be a boolean');
641
+ }
642
+ if (metadata.tags !== undefined && (!Array.isArray(metadata.tags) || metadata.tags.some((tag) => !str(tag)))) {
643
+ return failure(400, 1004, 'metadata.tags must be an array of strings');
644
+ }
645
+ if (metadata.annotations !== undefined && (!metadata.annotations || typeof metadata.annotations !== 'object' || Array.isArray(metadata.annotations))) {
646
+ return failure(400, 1004, 'metadata.annotations must be an object');
647
+ }
648
+ const existing = scriptRow(accountID, scriptName, req.root);
649
+ if (mode === 'version' && !existing)
650
+ return scriptNotFound(scriptName);
651
+ const hasContent = modules.length > 0 || serviceWorkerBody !== undefined;
652
+ if (!existing && !hasContent)
653
+ return failure(400, 1004, 'Script upload requires content');
654
+ // Plan migrations FIRST (pure — no writes) to learn the projected post-migration class set,
655
+ // then validate every binding against it. All validation completes before any write, so a
656
+ // rejected upload leaves no orphan Durable Object namespaces (round-two N5).
657
+ let migrationTag = str(existing?.migration_tag) ? String(existing?.migration_tag) : undefined;
658
+ let migrationOps = [];
659
+ let projectedClasses = new Set(namespaceRows(accountID, req.root).filter((item) => item.script === scriptName).map((item) => String(item.class_name)));
660
+ if (metadata.migrations !== undefined) {
661
+ if (mode === 'version')
662
+ return failure(400, 1004, 'Durable Object migrations cannot be applied by a version upload; deploy the Worker to migrate');
663
+ if (!metadata.migrations || typeof metadata.migrations !== 'object' || Array.isArray(metadata.migrations)) {
664
+ return failure(400, 1004, 'metadata.migrations must be an object');
665
+ }
666
+ const plan = planMigrations(accountID, scriptName, metadata.migrations, migrationTag, req.root);
667
+ if (isResponse(plan))
668
+ return plan;
669
+ migrationOps = plan.ops;
670
+ projectedClasses = plan.classes;
671
+ if (plan.migrationTag)
672
+ migrationTag = plan.migrationTag;
673
+ }
674
+ const invalidBinding = validateBindings(accountID, scriptName, metadata.bindings, req, projectedClasses);
675
+ if (invalidBinding)
676
+ return invalidBinding;
677
+ // `inherit` bindings take the deployed version's binding of the same name (or `old_name`);
678
+ // under `bindings_inherit=strict` a binding with nothing to inherit is refused.
679
+ let bindings = metadata.bindings ?? existing?.bindings ?? [];
680
+ if (Array.isArray(metadata.bindings)) {
681
+ const previous = Array.isArray(existing?.bindings) ? existing.bindings : [];
682
+ const strict = url.searchParams.get('bindings_inherit') === 'strict';
683
+ const resolved = [];
684
+ for (const binding of metadata.bindings) {
685
+ if (binding.type !== 'inherit') {
686
+ resolved.push(binding);
687
+ continue;
688
+ }
689
+ const from = str(binding.old_name) ? binding.old_name : binding.name;
690
+ const found = previous.find((item) => item.name === from);
691
+ if (found)
692
+ resolved.push({ ...found, name: binding.name });
693
+ else if (strict)
694
+ return failure(400, 1004, `inherit binding "${String(binding.name)}" has no binding named "${String(from)}" on the deployed version to inherit`);
695
+ }
696
+ bindings = resolved;
697
+ }
698
+ // Static assets: metadata.assets redeems an asset-upload completion token (the manifest the
699
+ // session recorded plus the router/asset config the upload carries); keep_assets carries the
700
+ // previous upload's assets forward; otherwise the Worker has none. An `assets` binding needs
701
+ // assets to bind.
702
+ let assets = null;
703
+ if (metadata.assets !== undefined) {
704
+ if (!metadata.assets || typeof metadata.assets !== 'object' || Array.isArray(metadata.assets)) {
705
+ return failure(400, 1004, 'metadata.assets must be an object');
706
+ }
707
+ const declared = metadata.assets;
708
+ if (declared.config !== undefined && (!declared.config || typeof declared.config !== 'object' || Array.isArray(declared.config))) {
709
+ return failure(400, 1004, 'metadata.assets.config must be an object');
710
+ }
711
+ const redeemed = redeemAssetsCompletion(req, accountID, scriptName, declared.jwt);
712
+ if (isResponse(redeemed))
713
+ return redeemed;
714
+ assets = { session_id: redeemed.session_id, manifest: redeemed.manifest, config: declared.config ?? {} };
715
+ }
716
+ else if (metadata.keep_assets === true && existing?.assets) {
717
+ assets = existing.assets;
718
+ }
719
+ const assetsBinding = (Array.isArray(bindings) ? bindings : []).find((binding) => binding.type === 'assets');
720
+ if (assetsBinding && !assets) {
721
+ return failure(400, 1004, `assets binding "${String(assetsBinding.name)}" requires the upload to carry assets (metadata.assets or keep_assets)`);
722
+ }
723
+ const storedModules = hasContent
724
+ ? (serviceWorkerBody !== undefined ? [{ name: 'script', content_type: 'application/javascript', content: serviceWorkerBody }] : modules)
725
+ : liveModules(existing, req.root);
726
+ const entryModule = hasContent ? (serviceWorkerBody !== undefined ? 'script' : entry) : existing?.entry_module ?? null;
727
+ const hasModules = serviceWorkerBody !== undefined ? false : (hasContent ? modules.length > 0 : existing?.has_modules === true);
728
+ // Compute the COMPLETE effective record (metadata wins, else prior live value, else the
729
+ // vendor default). Writing every field explicitly — never a conditional spread — is what
730
+ // stops a tombstone's stale value from surviving the kernel's field merge on delete→recreate
731
+ // (finding-3), and the etag hashing the SAME effective values is what keeps the etag honest
732
+ // about what is stored (finding-10). `existing` is a LIVE row only (rows() filters tombstones),
733
+ // so on recreate every field falls through to metadata-or-default and overwrites the tombstone.
734
+ const compatibilityDate = metadata.compatibility_date ?? existing?.compatibility_date ?? null;
735
+ const compatibilityFlags = metadata.compatibility_flags ?? existing?.compatibility_flags ?? null;
736
+ const usageModel = metadata.usage_model ?? existing?.usage_model ?? 'standard';
737
+ const logpush = metadata.logpush ?? existing?.logpush ?? false;
738
+ const tags = metadata.tags ?? existing?.tags ?? [];
739
+ const etag = createHash('sha256')
740
+ .update(JSON.stringify({
741
+ modules: storedModules.map((module) => ({ name: module.name, content: module.content })),
742
+ entry: entryModule,
743
+ bindings,
744
+ compatibility_date: compatibilityDate,
745
+ compatibility_flags: compatibilityFlags,
746
+ usage_model: usageModel,
747
+ logpush,
748
+ tags,
749
+ // what the assets ARE (manifest + config), not which upload session delivered them
750
+ assets: assets ? { manifest: assets.manifest, config: assets.config } : null,
751
+ }))
752
+ .digest('hex');
753
+ const annotations = Object.fromEntries(Object.entries(metadata.annotations ?? {}).filter(([, value]) => typeof value === 'string'));
754
+ return {
755
+ existing,
756
+ content: {
757
+ etag, modules: storedModules, entry_module: entryModule, has_modules: hasModules, bindings,
758
+ compatibility_date: compatibilityDate, compatibility_flags: compatibilityFlags, usage_model: usageModel, assets,
759
+ },
760
+ migrationOps,
761
+ migrationTag,
762
+ logpush,
763
+ tags,
764
+ annotations,
765
+ };
766
+ }
767
+ /** Write one version row: its content, number, source and annotations. The version's id is a
768
+ * UUID (the shape wrangler prints as "Current Version ID"). */
769
+ async function writeVersion(req, accountID, scriptName, scriptTag, number, content, annotations) {
770
+ const id = mint('worker_version', `${accountID}:${scriptName}:${scriptTag}:${number}`, req.root);
771
+ const at = req.occurredAt ?? new Date().toISOString();
772
+ await pwrite('worker_version', id, 'worker_version.upload', {
773
+ account_id: accountID, script_name: scriptName, script_tag: scriptTag, number, ...content,
774
+ source: deploySource(req), annotations, author_email: callerEmail(req) || null, created_on: at, modified_on: at, deleted: false,
775
+ }, req);
776
+ return row('worker_version', id, req.root);
777
+ }
778
+ function deploymentRecord(req, script, versions, annotations) {
779
+ const count = Array.isArray(script.deployments) ? script.deployments.length : 0;
780
+ return {
781
+ id: uuidFrom(`${String(script.tag)}:deployment:${count}`),
782
+ created_on: req.occurredAt ?? new Date().toISOString(),
783
+ source: deploySource(req),
784
+ strategy: 'percentage',
785
+ versions,
786
+ annotations,
787
+ // Cloudflare names the user whose token made the deployment.
788
+ author_email: callerEmail(req) || null,
789
+ };
790
+ }
791
+ /** `PUT /accounts/{account_id}/workers/scripts/{script_name}` — upload AND deploy: every upload
792
+ * is a new version deployed at 100% (the vendor's `deployment_id` in the answer is that version's
793
+ * id, the value wrangler prints as "Current Version ID"). */
794
+ async function uploadScript(req, url, accountID, scriptName) {
795
+ const blocked = readOnly(req);
796
+ if (blocked)
797
+ return blocked;
798
+ if (!SCRIPT_NAME_PATTERN.test(scriptName))
799
+ return failure(400, 1004, 'script_name must match ^[a-z0-9_][a-z0-9-_]*$');
800
+ const built = buildUpload(req, url, accountID, scriptName, 'deploy');
801
+ if (isResponse(built))
802
+ return built;
803
+ const { existing, content } = built;
804
+ const tag = String(existing?.tag ?? mintScriptTag(accountID, scriptName, req.root));
805
+ const number = Number(existing?.version_count ?? 0) + 1;
806
+ const at = req.occurredAt ?? new Date().toISOString();
807
+ // All validation passed — now perform the writes (migrations, the version, then the script).
808
+ await applyMigrationPlan(accountID, scriptName, built.migrationOps, req);
809
+ const version = await writeVersion(req, accountID, scriptName, tag, number, content, built.annotations);
810
+ const deployments = Array.isArray(existing?.deployments) ? existing.deployments : [];
811
+ const deployment = deploymentRecord(req, { tag, deployments }, [{ version_id: version.id, percentage: 100 }], { ...built.annotations, 'workers/triggered_by': 'upload' });
812
+ const { modules: _modules, ...live } = content;
813
+ await audit(req, accountID, { type: existing ? 'update' : 'create', resource: { type: 'script', id: scriptName }, newValue: { version_id: version.id, deployment_id: deployment.id }, metadata: { script: scriptName, deployment_id: deployment.id, version_id: version.id, annotations: built.annotations } });
814
+ await pwrite('worker_script', scriptID(accountID, scriptName), existing ? 'worker_script.update' : 'worker_script.upload', {
815
+ account_id: accountID,
816
+ name: scriptName,
817
+ ...live,
818
+ modules: null,
819
+ version_id: version.id,
820
+ version_count: number,
821
+ deployments: [...deployments, deployment],
822
+ logpush: built.logpush,
823
+ tags: built.tags,
824
+ migration_tag: built.migrationTag ?? null,
825
+ tag,
826
+ last_deployed_from: deploySource(req),
827
+ created_on: existing?.created_on ?? at,
828
+ modified_on: at,
829
+ schedules: existing?.schedules ?? [],
830
+ subdomain_enabled: existing?.subdomain_enabled ?? false,
831
+ subdomain_previews_enabled: existing?.subdomain_previews_enabled ?? false,
832
+ tail_consumers: existing?.tail_consumers ?? null,
833
+ observability: existing?.observability ?? null,
834
+ deleted: false,
835
+ }, req);
836
+ const script = scriptRow(accountID, scriptName, req.root);
837
+ return success({ ...scriptView(script), deployment_id: version.id, startup_time_ms: 5 });
838
+ }
839
+ function downloadScript(script, root) {
840
+ const modules = liveModules(script, root);
841
+ // A module-syntax Worker downloads as multipart/form-data regardless of module count (the
842
+ // pinned spec lists both content types for the download); a service-worker script downloads
843
+ // as raw application/javascript.
844
+ if (script.has_modules === true && modules.length > 0) {
845
+ const boundary = `twin-${deterministicHex(String(script.etag ?? script.id), 16)}`;
846
+ const body = modules
847
+ .map((module) => `--${boundary}\r\nContent-Disposition: form-data; name="${module.name}"; filename="${module.name}"\r\nContent-Type: ${module.content_type ?? 'application/javascript+module'}\r\n\r\n${module.content}\r\n`)
848
+ .join('') + `--${boundary}--\r\n`;
849
+ return { status: 200, body, raw: true, headers: { 'content-type': `multipart/form-data; boundary=${boundary}` } };
850
+ }
851
+ return {
852
+ status: 200,
853
+ body: modules[0]?.content ?? '',
854
+ raw: true,
855
+ headers: { 'content-type': 'application/javascript' },
856
+ };
857
+ }
858
+ // ── versions, deployments, script settings ──────────────────────────────────────────────────
859
+ /** Version shape per the official SDK (VersionGetResponse): id, number, metadata, annotations,
860
+ * and the resources it carries. The modules themselves are served by the script download. */
861
+ function versionView(version) {
862
+ return {
863
+ id: version.id,
864
+ number: version.number,
865
+ metadata: {
866
+ author_email: version.author_email ?? null,
867
+ author_id: null,
868
+ created_on: version.created_on,
869
+ modified_on: version.modified_on,
870
+ source: version.source ?? 'api',
871
+ hasPreview: false,
872
+ },
873
+ annotations: version.annotations ?? {},
874
+ resources: {
875
+ bindings: bindingsView(version.bindings),
876
+ script: {
877
+ etag: version.etag,
878
+ handlers: [],
879
+ last_deployed_from: version.source ?? 'api',
880
+ },
881
+ script_runtime: {
882
+ compatibility_date: version.compatibility_date ?? null,
883
+ compatibility_flags: version.compatibility_flags ?? [],
884
+ usage_model: version.usage_model ?? 'standard',
885
+ },
886
+ },
887
+ };
888
+ }
889
+ /** The script's secrets as the vendor lists them (SDK SecretListResponse): name and type of each
890
+ * secret_text / secret_key binding — never the value. */
891
+ function scriptSecrets(script) {
892
+ return (Array.isArray(script.bindings) ? script.bindings : [])
893
+ .filter((binding) => binding.type === 'secret_text' || binding.type === 'secret_key')
894
+ .map((binding) => binding.type === 'secret_key'
895
+ ? { name: binding.name, type: 'secret_key', ...(binding.format !== undefined ? { format: binding.format } : {}), ...(binding.usages !== undefined ? { usages: binding.usages } : {}) }
896
+ : { name: binding.name, type: 'secret_text' });
897
+ }
898
+ /** Bindings as the vendor reads them back: a secret's value is write-only, so secret_text
899
+ * bindings come back without `text` (and secret_key without its key material). */
900
+ function bindingsView(bindings) {
901
+ return (Array.isArray(bindings) ? bindings : []).map((binding) => {
902
+ if (binding.type === 'secret_text') {
903
+ const { text: _text, ...rest } = binding;
904
+ return rest;
905
+ }
906
+ if (binding.type === 'secret_key') {
907
+ const { key_base64: _key, key_jwk: _jwk, ...rest } = binding;
908
+ return rest;
909
+ }
910
+ return binding;
911
+ });
912
+ }
913
+ /** `GET …/scripts/{script_name}/settings` (SDK ScriptSettings): the deployed version's bindings
914
+ * and runtime, plus the non-versioned settings. */
915
+ function scriptSettingsView(script) {
916
+ return {
917
+ bindings: bindingsView(script.bindings),
918
+ compatibility_date: script.compatibility_date ?? null,
919
+ compatibility_flags: script.compatibility_flags ?? [],
920
+ logpush: script.logpush ?? false,
921
+ observability: script.observability ?? null,
922
+ placement: {},
923
+ tags: script.tags ?? [],
924
+ tail_consumers: script.tail_consumers ?? null,
925
+ usage_model: script.usage_model ?? 'standard',
926
+ };
927
+ }
928
+ function settingsView(script) {
929
+ return {
930
+ logpush: script.logpush ?? false,
931
+ tail_consumers: script.tail_consumers ?? null,
932
+ observability: script.observability ?? null,
933
+ tags: script.tags ?? [],
934
+ };
935
+ }
936
+ /** POST /accounts/{account_id}/workers/scripts/{script_name}/versions — upload a version without deploying it. */
937
+ async function uploadVersion(req, url, accountID, scriptName) {
938
+ const blocked = readOnly(req);
939
+ if (blocked)
940
+ return blocked;
941
+ const built = buildUpload(req, url, accountID, scriptName, 'version');
942
+ if (isResponse(built))
943
+ return built;
944
+ const script = built.existing;
945
+ const number = Number(script.version_count ?? 0) + 1;
946
+ const version = await writeVersion(req, accountID, scriptName, String(script.tag), number, built.content, built.annotations);
947
+ await pwrite('worker_script', scriptID(accountID, scriptName), 'worker_script.version', { account_id: accountID, version_count: number }, req);
948
+ return success({ ...versionView(version), startup_time_ms: 5 });
949
+ }
950
+ /** POST /accounts/{account_id}/workers/scripts/{script_name}/deployments — route traffic to
951
+ * versions. The version at the larger share becomes the Worker's live content; the twin records
952
+ * the split, it never routes a request. */
953
+ async function createDeployment(req, accountID, script) {
954
+ const blocked = readOnly(req);
955
+ if (blocked)
956
+ return blocked;
957
+ const body = parseBody(req);
958
+ if (isResponse(body))
959
+ return body;
960
+ if (body.strategy !== 'percentage')
961
+ return failure(400, 1004, 'strategy must be "percentage"');
962
+ if (!Array.isArray(body.versions) || body.versions.length === 0 || body.versions.length > 2) {
963
+ return failure(400, 1004, 'versions must list one or two {version_id, percentage} entries');
964
+ }
965
+ const known = new Map(versionRows(script, req.root).map((version) => [version.id, version]));
966
+ const split = [];
967
+ for (const value of body.versions) {
968
+ if (!value || !str(value.version_id) || typeof value.percentage !== 'number' || value.percentage < 0 || value.percentage > 100) {
969
+ return failure(400, 1004, 'each deployment version requires version_id and a percentage between 0 and 100');
970
+ }
971
+ if (!known.has(value.version_id))
972
+ return failure(400, 1004, `version "${value.version_id}" is not a version of Worker "${String(script.name)}"`);
973
+ if (split.some((item) => item.version_id === value.version_id))
974
+ return failure(400, 1004, `version "${value.version_id}" is listed twice`);
975
+ split.push({ version_id: value.version_id, percentage: value.percentage });
976
+ }
977
+ if (Math.abs(split.reduce((sum, item) => sum + item.percentage, 0) - 100) > 1e-9)
978
+ return failure(400, 1004, 'version percentages must sum to 100');
979
+ if (body.annotations !== undefined && (!body.annotations || typeof body.annotations !== 'object' || Array.isArray(body.annotations))) {
980
+ return failure(400, 1004, 'annotations must be an object');
981
+ }
982
+ const annotations = Object.fromEntries(Object.entries(body.annotations ?? {}).filter(([, value]) => typeof value === 'string'));
983
+ const deployments = Array.isArray(script.deployments) ? script.deployments : [];
984
+ const deployment = deploymentRecord(req, { tag: script.tag, deployments }, split, annotations);
985
+ const live = known.get([...split].sort((a, b) => b.percentage - a.percentage)[0].version_id);
986
+ await pwrite('worker_script', script.id, 'worker_script.deploy', {
987
+ account_id: accountID,
988
+ ...Object.fromEntries(VERSIONED_FIELDS.map((field) => [field, live[field] ?? null])),
989
+ modules: null,
990
+ version_id: live.id,
991
+ deployments: [...deployments, deployment],
992
+ modified_on: req.occurredAt ?? new Date().toISOString(),
993
+ }, req);
994
+ return success(deployment);
995
+ }
996
+ async function patchScriptSettings(req, accountID, script) {
997
+ const blocked = readOnly(req);
998
+ if (blocked)
999
+ return blocked;
1000
+ const body = parseBody(req);
1001
+ if (isResponse(body))
1002
+ return body;
1003
+ if (body.logpush !== undefined && typeof body.logpush !== 'boolean')
1004
+ return failure(400, 1004, 'logpush must be a boolean');
1005
+ if (body.tags !== undefined && body.tags !== null && (!Array.isArray(body.tags) || body.tags.some((tag) => !str(tag)))) {
1006
+ return failure(400, 1004, 'tags must be an array of strings');
1007
+ }
1008
+ if (body.tail_consumers !== undefined && body.tail_consumers !== null
1009
+ && (!Array.isArray(body.tail_consumers) || body.tail_consumers.some((item) => !item || typeof item !== 'object' || !str(item.service)))) {
1010
+ return failure(400, 1004, 'tail_consumers must be an array of {service} objects');
1011
+ }
1012
+ if (body.observability !== undefined && body.observability !== null) {
1013
+ const observability = body.observability;
1014
+ if (typeof observability !== 'object' || Array.isArray(observability) || typeof observability.enabled !== 'boolean') {
1015
+ return failure(400, 1004, 'observability requires enabled (boolean)');
1016
+ }
1017
+ if (observability.head_sampling_rate !== undefined && observability.head_sampling_rate !== null
1018
+ && (typeof observability.head_sampling_rate !== 'number' || observability.head_sampling_rate < 0 || observability.head_sampling_rate > 1)) {
1019
+ return failure(400, 1004, 'observability.head_sampling_rate must be between 0 and 1');
1020
+ }
1021
+ }
1022
+ await pwrite('worker_script', script.id, 'worker_script.settings', {
1023
+ account_id: accountID,
1024
+ ...(body.logpush !== undefined ? { logpush: body.logpush } : {}),
1025
+ ...(body.tags !== undefined ? { tags: body.tags ?? [] } : {}),
1026
+ ...(body.tail_consumers !== undefined ? { tail_consumers: body.tail_consumers } : {}),
1027
+ ...(body.observability !== undefined ? { observability: body.observability } : {}),
1028
+ modified_on: req.occurredAt ?? new Date().toISOString(),
1029
+ }, req);
1030
+ return success(settingsView(scriptRow(accountID, String(script.name), req.root)));
1031
+ }
1032
+ // ── the route table ─────────────────────────────────────────────────────────────────────────
1033
+ export const CLOUDFLARE_PROVISIONING_SURFACE = [
1034
+ 'GET /accounts/{account_id}/workers/scripts',
1035
+ 'PUT /accounts/{account_id}/workers/scripts/{script_name}',
1036
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}',
1037
+ 'DELETE /accounts/{account_id}/workers/scripts/{script_name}',
1038
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/schedules',
1039
+ 'PUT /accounts/{account_id}/workers/scripts/{script_name}/schedules',
1040
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
1041
+ 'POST /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
1042
+ 'DELETE /accounts/{account_id}/workers/scripts/{script_name}/subdomain',
1043
+ 'POST /accounts/{account_id}/workers/scripts/{script_name}/assets-upload-session',
1044
+ 'POST /accounts/{account_id}/workers/assets/upload',
1045
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/versions',
1046
+ 'POST /accounts/{account_id}/workers/scripts/{script_name}/versions',
1047
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/versions/{version_id}',
1048
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/deployments',
1049
+ 'POST /accounts/{account_id}/workers/scripts/{script_name}/deployments',
1050
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/script-settings',
1051
+ 'PATCH /accounts/{account_id}/workers/scripts/{script_name}/script-settings',
1052
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/settings',
1053
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/secrets',
1054
+ 'GET /accounts/{account_id}/workers/scripts/{script_name}/secrets/{secret_name}',
1055
+ 'GET /accounts/{account_id}/workers/subdomain',
1056
+ 'PUT /accounts/{account_id}/workers/subdomain',
1057
+ 'DELETE /accounts/{account_id}/workers/subdomain',
1058
+ 'GET /accounts/{account_id}/workers/domains',
1059
+ 'PUT /accounts/{account_id}/workers/domains',
1060
+ 'GET /accounts/{account_id}/workers/domains/{domain_id}',
1061
+ 'DELETE /accounts/{account_id}/workers/domains/{domain_id}',
1062
+ 'GET /accounts/{account_id}/workers/durable_objects/namespaces',
1063
+ 'GET /accounts/{account_id}/workers/services/{service_name}',
1064
+ 'GET /zones/{zone_id}/workers/routes',
1065
+ 'POST /zones/{zone_id}/workers/routes',
1066
+ 'GET /zones/{zone_id}/workers/routes/{route_id}',
1067
+ 'PUT /zones/{zone_id}/workers/routes/{route_id}',
1068
+ 'DELETE /zones/{zone_id}/workers/routes/{route_id}',
1069
+ 'GET /accounts/{account_id}/r2/buckets',
1070
+ 'POST /accounts/{account_id}/r2/buckets',
1071
+ 'GET /accounts/{account_id}/r2/buckets/{bucket_name}',
1072
+ 'DELETE /accounts/{account_id}/r2/buckets/{bucket_name}',
1073
+ 'GET /accounts/{account_id}/r2/buckets/{bucket_name}/domains/custom',
1074
+ 'POST /accounts/{account_id}/r2/buckets/{bucket_name}/domains/custom',
1075
+ 'GET /accounts/{account_id}/r2/buckets/{bucket_name}/domains/custom/{domain}',
1076
+ 'PUT /accounts/{account_id}/r2/buckets/{bucket_name}/domains/custom/{domain}',
1077
+ 'DELETE /accounts/{account_id}/r2/buckets/{bucket_name}/domains/custom/{domain}',
1078
+ 'POST /accounts/{account_id}/r2/temp-access-credentials',
1079
+ 'GET /accounts/{account_id}/hyperdrive/configs',
1080
+ 'POST /accounts/{account_id}/hyperdrive/configs',
1081
+ 'GET /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
1082
+ 'PUT /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
1083
+ 'PATCH /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
1084
+ 'DELETE /accounts/{account_id}/hyperdrive/configs/{hyperdrive_id}',
1085
+ 'GET /accounts/{account_id}/queues',
1086
+ 'POST /accounts/{account_id}/queues',
1087
+ 'GET /accounts/{account_id}/queues/{queue_id}',
1088
+ 'DELETE /accounts/{account_id}/queues/{queue_id}',
1089
+ 'GET /accounts/{account_id}/queues/{queue_id}/consumers',
1090
+ 'POST /accounts/{account_id}/queues/{queue_id}/consumers',
1091
+ 'GET /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
1092
+ 'PUT /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
1093
+ 'DELETE /accounts/{account_id}/queues/{queue_id}/consumers/{consumer_id}',
1094
+ ];
1095
+ /** Route the provisioning slice. Returns null when the path belongs to another slice; once a
1096
+ * provisioning-owned prefix matches, an unmodeled method/subresource fails loudly (noRoute). */
1097
+ export async function handleProvisioningRoute(req, method, url, parts) {
1098
+ if (parts[0] === 'zones' && parts[2] === 'workers') {
1099
+ if (parts[3] !== 'routes')
1100
+ return noRoute();
1101
+ const zoneID = parts[1];
1102
+ const zone = row('zone', zoneID, req.root);
1103
+ if (!zone)
1104
+ return scopeMissing('zone', zoneID);
1105
+ return routesRoute(req, method, parts, zoneID, String(zone.account_id));
1106
+ }
1107
+ if (parts[0] !== 'accounts')
1108
+ return null;
1109
+ const area = parts[2];
1110
+ if (area !== 'workers' && area !== 'r2' && area !== 'hyperdrive' && area !== 'queues')
1111
+ return null;
1112
+ const accountID = parts[1];
1113
+ if (!row('account', accountID, req.root))
1114
+ return scopeMissing('account', accountID);
1115
+ if (area === 'workers')
1116
+ return workersRoute(req, method, url, parts, accountID);
1117
+ if (area === 'r2')
1118
+ return r2Route(req, method, url, parts, accountID);
1119
+ if (area === 'hyperdrive')
1120
+ return hyperdriveRoute(req, method, url, parts, accountID);
1121
+ return queuesRoute(req, method, parts, accountID);
1122
+ }
1123
+ async function workersRoute(req, method, url, parts, accountID) {
1124
+ if (parts[3] === 'scripts') {
1125
+ if (parts.length === 4 && method === 'GET') {
1126
+ const page = paginate(scriptRows(accountID, req.root).map(scriptView), url, 100, 1000);
1127
+ return success(page.result, page.info);
1128
+ }
1129
+ const scriptName = parts[4];
1130
+ if (!scriptName || parts.length > 7)
1131
+ return noRoute();
1132
+ if (parts.length === 7) {
1133
+ if ((parts[5] !== 'versions' && parts[5] !== 'secrets') || method !== 'GET')
1134
+ return noRoute();
1135
+ const owner = scriptRow(accountID, scriptName, req.root);
1136
+ if (!owner)
1137
+ return scriptNotFound(scriptName);
1138
+ if (parts[5] === 'secrets') {
1139
+ const secret = scriptSecrets(owner).find((item) => item.name === parts[6]);
1140
+ return secret ? success(secret) : failure(404, 1001, `secret "${parts[6]}" not found on Worker "${scriptName}"`);
1141
+ }
1142
+ const version = versionRows(owner, req.root).find((item) => item.id === parts[6]);
1143
+ return version ? success(versionView(version)) : failure(404, 1001, `version "${parts[6]}" not found on Worker "${scriptName}"`);
1144
+ }
1145
+ if (parts.length === 5 && method === 'PUT')
1146
+ return uploadScript(req, url, accountID, scriptName);
1147
+ // The asset upload session precedes the script's first upload, so it is not gated on the
1148
+ // script existing.
1149
+ if (parts.length === 6 && parts[5] === 'assets-upload-session' && method === 'POST') {
1150
+ const blocked = readOnly(req);
1151
+ if (blocked)
1152
+ return blocked;
1153
+ if (!SCRIPT_NAME_PATTERN.test(scriptName))
1154
+ return failure(400, 1004, 'script_name must match ^[a-z0-9_][a-z0-9-_]*$');
1155
+ const body = parseBody(req);
1156
+ if (isResponse(body))
1157
+ return body;
1158
+ return createAssetsUploadSession(req, accountID, scriptName, body);
1159
+ }
1160
+ const script = scriptRow(accountID, scriptName, req.root);
1161
+ if (parts.length === 5) {
1162
+ if (method === 'GET')
1163
+ return script ? downloadScript(script, req.root) : scriptNotFound(scriptName);
1164
+ if (method === 'DELETE') {
1165
+ if (!script)
1166
+ return scriptNotFound(scriptName);
1167
+ const blocked = readOnly(req);
1168
+ if (blocked)
1169
+ return blocked;
1170
+ const force = url.searchParams.get('force') === 'true';
1171
+ const ownNamespaces = namespaceRows(accountID, req.root).filter((item) => item.script === scriptName);
1172
+ const dependents = scriptRows(accountID, req.root).filter((other) => other.name !== scriptName
1173
+ && (Array.isArray(other.bindings) ? other.bindings : []).some((binding) => binding?.type === 'service' && binding?.service === scriptName));
1174
+ if (!force && (ownNamespaces.length > 0 || dependents.length > 0)) {
1175
+ // Grounded in the pinned OpenAPI's own `force` description: without force=true, delete
1176
+ // IS stopped by an associated service binding or durable object.
1177
+ return failure(400, 1004, `Could not delete script "${scriptName}": it has associated Durable Object namespaces or service bindings; retry with ?force=true`);
1178
+ }
1179
+ for (const namespace of ownNamespaces) {
1180
+ await pwrite('durable_object_namespace', namespace.id, 'durable_object_namespace.delete', {
1181
+ account_id: accountID, script: scriptName, deleted: true,
1182
+ }, req);
1183
+ }
1184
+ await pwrite('worker_script', scriptID(accountID, scriptName), 'worker_script.delete', { account_id: accountID, deleted: true }, req);
1185
+ return success(null);
1186
+ }
1187
+ return noRoute();
1188
+ }
1189
+ // parts.length === 6 — script subresources
1190
+ if (!script)
1191
+ return scriptNotFound(scriptName);
1192
+ if (parts[5] === 'versions') {
1193
+ if (method === 'POST')
1194
+ return uploadVersion(req, url, accountID, scriptName);
1195
+ if (method === 'GET') {
1196
+ const page = paginate(versionRows(script, req.root).map(versionView), url, 10, 100);
1197
+ return success({ items: page.result }, page.info);
1198
+ }
1199
+ return noRoute();
1200
+ }
1201
+ if (parts[5] === 'deployments') {
1202
+ if (method === 'GET') {
1203
+ const deployments = Array.isArray(script.deployments) ? [...script.deployments].reverse() : [];
1204
+ return success({ deployments });
1205
+ }
1206
+ if (method === 'POST')
1207
+ return createDeployment(req, accountID, script);
1208
+ return noRoute();
1209
+ }
1210
+ if (parts[5] === 'secrets' && method === 'GET') {
1211
+ // Secrets are the script's secret bindings; their values are write-only and never listed.
1212
+ return success(scriptSecrets(script));
1213
+ }
1214
+ if (parts[5] === 'settings' && method === 'GET')
1215
+ return success(scriptSettingsView(script));
1216
+ if (parts[5] === 'script-settings') {
1217
+ if (method === 'GET')
1218
+ return success(settingsView(script));
1219
+ if (method === 'PATCH')
1220
+ return patchScriptSettings(req, accountID, script);
1221
+ return noRoute();
1222
+ }
1223
+ if (parts[5] === 'schedules') {
1224
+ if (method === 'GET') {
1225
+ return success({ schedules: Array.isArray(script.schedules) ? script.schedules : [] });
1226
+ }
1227
+ if (method === 'PUT') {
1228
+ const blocked = readOnly(req);
1229
+ if (blocked)
1230
+ return blocked;
1231
+ let body;
1232
+ try {
1233
+ body = req.body ? JSON.parse(req.body) : undefined;
1234
+ }
1235
+ catch {
1236
+ return failure(400, 1004, 'Invalid request body');
1237
+ }
1238
+ if (!Array.isArray(body))
1239
+ return failure(400, 1004, 'Cron trigger update requires an array of {cron} objects');
1240
+ const at = req.occurredAt ?? new Date().toISOString();
1241
+ const schedules = [];
1242
+ for (const value of body) {
1243
+ const item = value;
1244
+ if (!item || typeof item !== 'object' || !str(item.cron))
1245
+ return failure(400, 1004, 'Each cron trigger requires a nonempty cron expression');
1246
+ if (!validCron(item.cron))
1247
+ return failure(400, 1004, `Invalid cron expression "${item.cron}"`);
1248
+ const previous = (Array.isArray(script.schedules) ? script.schedules : []).find((existing) => existing.cron === item.cron);
1249
+ schedules.push({ cron: item.cron, created_on: previous?.created_on ?? at, modified_on: at });
1250
+ }
1251
+ await pwrite('worker_script', scriptID(accountID, scriptName), 'worker_script.schedules', { account_id: accountID, schedules }, req);
1252
+ return success({ schedules });
1253
+ }
1254
+ return noRoute();
1255
+ }
1256
+ if (parts[5] === 'subdomain') {
1257
+ const view = () => ({
1258
+ enabled: scriptRow(accountID, scriptName, req.root)?.subdomain_enabled === true,
1259
+ previews_enabled: scriptRow(accountID, scriptName, req.root)?.subdomain_previews_enabled === true,
1260
+ });
1261
+ if (method === 'GET')
1262
+ return success(view());
1263
+ if (method === 'POST') {
1264
+ const blocked = readOnly(req);
1265
+ if (blocked)
1266
+ return blocked;
1267
+ const body = parseBody(req);
1268
+ if (isResponse(body))
1269
+ return body;
1270
+ if (typeof body.enabled !== 'boolean')
1271
+ return failure(400, 1004, 'enabled is required and must be a boolean');
1272
+ if (body.previews_enabled !== undefined && typeof body.previews_enabled !== 'boolean') {
1273
+ return failure(400, 1004, 'previews_enabled must be a boolean');
1274
+ }
1275
+ await pwrite('worker_script', scriptID(accountID, scriptName), 'worker_script.subdomain', {
1276
+ account_id: accountID,
1277
+ subdomain_enabled: body.enabled,
1278
+ subdomain_previews_enabled: body.previews_enabled ?? (body.enabled ? script.subdomain_previews_enabled === true : false),
1279
+ }, req);
1280
+ return success(view());
1281
+ }
1282
+ if (method === 'DELETE') {
1283
+ const blocked = readOnly(req);
1284
+ if (blocked)
1285
+ return blocked;
1286
+ await pwrite('worker_script', scriptID(accountID, scriptName), 'worker_script.subdomain', {
1287
+ account_id: accountID, subdomain_enabled: false, subdomain_previews_enabled: false,
1288
+ }, req);
1289
+ return success(view());
1290
+ }
1291
+ return noRoute();
1292
+ }
1293
+ return noRoute();
1294
+ }
1295
+ if (parts[3] === 'assets' && parts[4] === 'upload' && parts.length === 5 && method === 'POST') {
1296
+ const blocked = readOnly(req);
1297
+ if (blocked)
1298
+ return blocked;
1299
+ return uploadAssets(req, accountID, url, parseMultipart);
1300
+ }
1301
+ if (parts[3] === 'services') {
1302
+ const serviceName = parts[4];
1303
+ if (!serviceName || parts.length !== 5 || method !== 'GET')
1304
+ return noRoute();
1305
+ const script = scriptRow(accountID, serviceName, req.root);
1306
+ return script ? success(serviceView(script)) : scriptNotFound(serviceName);
1307
+ }
1308
+ if (parts[3] === 'subdomain' && parts.length === 4) {
1309
+ const existing = row('workers_subdomain', accountID, req.root);
1310
+ if (method === 'GET') {
1311
+ // An account that never registered a workers.dev subdomain answers 10007 — the code
1312
+ // wrangler's getWorkersDevSubdomain keys its "register one now?" branch on.
1313
+ return existing ? success({ subdomain: existing.subdomain }) : failure(404, 10007, 'workers.api.error.subdomain_not_found: This account does not have a workers.dev subdomain');
1314
+ }
1315
+ if (method === 'PUT') {
1316
+ const blocked = readOnly(req);
1317
+ if (blocked)
1318
+ return blocked;
1319
+ const body = parseBody(req);
1320
+ if (isResponse(body))
1321
+ return body;
1322
+ if (!str(body.subdomain))
1323
+ return failure(400, 1004, 'subdomain is required');
1324
+ const subdomain = body.subdomain.toLowerCase();
1325
+ // workers.dev subdomains are a single DNS label (RFC-1035-shaped).
1326
+ if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/.test(subdomain)) {
1327
+ return failure(400, 1004, 'subdomain must be a valid DNS label (letters, digits, hyphens; 1-63 chars)');
1328
+ }
1329
+ await pwrite('workers_subdomain', accountID, 'workers_subdomain.update', { account_id: accountID, subdomain, deleted: false }, req);
1330
+ return success({ subdomain });
1331
+ }
1332
+ if (method === 'DELETE') {
1333
+ const blocked = readOnly(req);
1334
+ if (blocked)
1335
+ return blocked;
1336
+ if (existing)
1337
+ await pwrite('workers_subdomain', accountID, 'workers_subdomain.delete', { account_id: accountID, deleted: true }, req);
1338
+ return { status: 204, body: null, raw: true };
1339
+ }
1340
+ return noRoute();
1341
+ }
1342
+ if (parts[3] === 'domains') {
1343
+ if (parts.length === 4 && method === 'GET') {
1344
+ let values = rows('worker_domain', req.root).filter((item) => item.account_id === accountID);
1345
+ for (const [param, field] of [['hostname', 'hostname'], ['service', 'service'], ['zone_id', 'zone_id'], ['zone_name', 'zone_name']]) {
1346
+ const filter = url.searchParams.get(param);
1347
+ if (filter)
1348
+ values = values.filter((item) => item[field] === filter);
1349
+ }
1350
+ // All modeled domains are production; honor the documented `environment` filter.
1351
+ const environment = url.searchParams.get('environment');
1352
+ if (environment)
1353
+ values = environment === 'production' ? values : [];
1354
+ return success(values.map(domainView));
1355
+ }
1356
+ if (parts.length === 4 && method === 'PUT') {
1357
+ const blocked = readOnly(req);
1358
+ if (blocked)
1359
+ return blocked;
1360
+ const body = parseBody(req);
1361
+ if (isResponse(body))
1362
+ return body;
1363
+ if (!str(body.hostname) || !str(body.service))
1364
+ return failure(400, 1004, 'hostname and service are required');
1365
+ const hostname = body.hostname.toLowerCase();
1366
+ const zone = rows('zone', req.root)
1367
+ .filter((item) => item.account_id === accountID && (hostname === item.name || hostname.endsWith(`.${item.name}`)))
1368
+ .sort((a, b) => String(b.name).length - String(a.name).length)[0];
1369
+ if (!zone)
1370
+ return failure(400, 1004, `hostname "${hostname}" is not within a zone on this account`);
1371
+ if (str(body.zone_id) && body.zone_id !== zone.id)
1372
+ return failure(400, 1004, 'zone_id does not match the zone containing hostname');
1373
+ if (str(body.zone_name) && body.zone_name.toLowerCase() !== zone.name)
1374
+ return failure(400, 1004, 'zone_name does not match the zone containing hostname');
1375
+ if (!scriptRow(accountID, body.service, req.root))
1376
+ return failure(400, 1004, `service "${body.service}" is not a Worker on this account`);
1377
+ const existing = rows('worker_domain', req.root).find((item) => item.account_id === accountID && item.hostname === hostname);
1378
+ const id = existing?.id ?? mint('worker_domain', `${accountID}:${hostname}`, req.root);
1379
+ await pwrite('worker_domain', id, existing ? 'worker_domain.update' : 'worker_domain.attach', {
1380
+ account_id: accountID,
1381
+ hostname,
1382
+ service: body.service,
1383
+ zone_id: zone.id,
1384
+ zone_name: zone.name,
1385
+ cert_id: existing?.cert_id ?? uuidFrom(`${accountID}:${hostname}:cert`),
1386
+ deleted: false,
1387
+ }, req);
1388
+ return success(domainView(row('worker_domain', id, req.root)));
1389
+ }
1390
+ const domainID = parts[3] === 'domains' && parts.length === 5 ? parts[4] : undefined;
1391
+ if (!domainID)
1392
+ return noRoute();
1393
+ const domain = row('worker_domain', domainID, req.root);
1394
+ if (!domain || domain.account_id !== accountID)
1395
+ return failure(404, 1001, 'Worker domain not found');
1396
+ if (method === 'GET')
1397
+ return success(domainView(domain));
1398
+ if (method === 'DELETE') {
1399
+ const blocked = readOnly(req);
1400
+ if (blocked)
1401
+ return blocked;
1402
+ await pwrite('worker_domain', domainID, 'worker_domain.detach', { account_id: accountID, deleted: true }, req);
1403
+ // OpenAPI: 200 with the bare success envelope and no result member.
1404
+ return { status: 200, body: { success: true, errors: [], messages: [] } };
1405
+ }
1406
+ return noRoute();
1407
+ }
1408
+ if (parts[3] === 'durable_objects' && parts[4] === 'namespaces' && parts.length === 5 && method === 'GET') {
1409
+ const page = paginate(namespaceRows(accountID, req.root).map(namespaceView), url, 20, 1000);
1410
+ return success(page.result, page.info);
1411
+ }
1412
+ return noRoute();
1413
+ }
1414
+ async function routesRoute(req, method, parts, zoneID, accountID) {
1415
+ const zoneRoutes = () => rows('worker_route', req.root).filter((item) => item.zone_id === zoneID);
1416
+ const scriptMissing = (script) => str(script) && !scriptRow(accountID, script, req.root)
1417
+ ? failure(400, 10007, `script "${String(script)}" is not a Worker on this account`) : null;
1418
+ if (parts.length === 4 && method === 'GET')
1419
+ return success(zoneRoutes().map(routeView));
1420
+ if (parts.length === 4 && method === 'POST') {
1421
+ const blocked = readOnly(req);
1422
+ if (blocked)
1423
+ return blocked;
1424
+ const body = parseBody(req);
1425
+ if (isResponse(body))
1426
+ return body;
1427
+ if (!str(body.pattern))
1428
+ return failure(400, 1004, 'pattern is required');
1429
+ if (body.script !== undefined && !str(body.script))
1430
+ return failure(400, 1004, 'script must be a nonempty string');
1431
+ const missing = scriptMissing(body.script);
1432
+ if (missing)
1433
+ return missing;
1434
+ if (zoneRoutes().some((item) => item.pattern === body.pattern))
1435
+ return failure(409, 1003, 'A route with this pattern already exists');
1436
+ const id = mint('worker_route', `${zoneID}:${body.pattern}`, req.root);
1437
+ await pwrite('worker_route', id, 'worker_route.create', {
1438
+ zone_id: zoneID, pattern: body.pattern, ...(str(body.script) ? { script: body.script } : {}), deleted: false,
1439
+ }, req);
1440
+ return success(routeView(row('worker_route', id, req.root)));
1441
+ }
1442
+ const routeID = parts[4];
1443
+ if (!routeID || parts.length !== 5)
1444
+ return noRoute();
1445
+ const route = row('worker_route', routeID, req.root);
1446
+ if (!route || route.zone_id !== zoneID)
1447
+ return failure(404, 1001, 'Route not found');
1448
+ if (method === 'GET')
1449
+ return success(routeView(route));
1450
+ if (method === 'PUT') {
1451
+ const blocked = readOnly(req);
1452
+ if (blocked)
1453
+ return blocked;
1454
+ const body = parseBody(req);
1455
+ if (isResponse(body))
1456
+ return body;
1457
+ if (!str(body.pattern))
1458
+ return failure(400, 1004, 'pattern is required');
1459
+ if (body.script !== undefined && !str(body.script))
1460
+ return failure(400, 1004, 'script must be a nonempty string');
1461
+ const missing = scriptMissing(body.script);
1462
+ if (missing)
1463
+ return missing;
1464
+ if (zoneRoutes().some((item) => item.id !== routeID && item.pattern === body.pattern)) {
1465
+ return failure(409, 1003, 'A route with this pattern already exists');
1466
+ }
1467
+ await pwrite('worker_route', routeID, 'worker_route.update', {
1468
+ zone_id: zoneID, pattern: body.pattern, script: str(body.script) ? body.script : null,
1469
+ }, req);
1470
+ return success(routeView(row('worker_route', routeID, req.root)));
1471
+ }
1472
+ if (method === 'DELETE') {
1473
+ const blocked = readOnly(req);
1474
+ if (blocked)
1475
+ return blocked;
1476
+ await pwrite('worker_route', routeID, 'worker_route.delete', { zone_id: zoneID, deleted: true }, req);
1477
+ return success({ id: routeID });
1478
+ }
1479
+ return noRoute();
1480
+ }
1481
+ // Who may call R2's REST routes the lane relies on (temporary credentials, custom domains). R2's troubleshooting page
1482
+ // (https://developers.cloudflare.com/r2/platform/troubleshooting/): an R2 API token with Object Read & Write or Object
1483
+ // Read only "against the Cloudflare REST API ... return one of the following: When the token applies to all buckets:
1484
+ // {"code":10002,"message":"Unauthorized"} (HTTP 401). When the token is scoped to specific buckets:
1485
+ // {"code":10000,"message":"Authentication error"} (HTTP 403)", and "To use the REST API, authenticate with an Admin Read &
1486
+ // Write or Admin Read only token". A user API token of an Administrator or Super Administrator member serves too ("User API
1487
+ // tokens ... inherit your personal permissions", R2 API tokens). Where Cloudflare's pages are silent, the twin's choice:
1488
+ // any other bearer, and an Admin Read only token on a write, answer 10000 Authentication error (HTTP 403). The rest of the
1489
+ // pack's routes keep accepting any bearer: a cloudflare-pack defect, fixed when that pack migrates.
1490
+ const R2_API_ROLES = new Set([deterministicHex('cf-role:Super Administrator - All Privileges', 32), deterministicHex('cf-role:Administrator', 32)]);
1491
+ function r2ApiRefusal(req, accountID, write) {
1492
+ const bearer = /^Bearer\s+(\S+)$/i.exec(lowerHeaders(req.headers).authorization ?? '')?.[1] ?? '';
1493
+ const denied = failure(403, 10000, 'Authentication error');
1494
+ const token = bearer ? r2BearerToken(accountID, bearer, req.root) : undefined;
1495
+ if (token) {
1496
+ if (token.permission === 'object-rw' || token.permission === 'object-ro')
1497
+ return token.buckets === '*' ? failure(401, 10002, 'Unauthorized') : denied;
1498
+ return write && token.permission === 'admin-ro' ? denied : null;
1499
+ }
1500
+ const user = rows('user_token', req.root).find((u) => u.token === bearer);
1501
+ const member = user ? rows('account_member', req.root).find((m) => m.account_id === accountID && m.email === user.email && m.status !== 'pending') : undefined;
1502
+ return member && (member.role_ids ?? []).some((r) => R2_API_ROLES.has(r)) ? null : denied;
1503
+ }
1504
+ async function r2Route(req, method, url, parts, accountID) {
1505
+ if (parts[3] === 'temp-access-credentials' && parts.length === 4 && method === 'POST')
1506
+ return r2ApiRefusal(req, accountID, true) ?? r2TempCredentialsRoute(req, accountID);
1507
+ if (parts[3] !== 'buckets')
1508
+ return noRoute();
1509
+ const jurisdictionHeader = lowerHeaders(req.headers)['cf-r2-jurisdiction'];
1510
+ if (jurisdictionHeader !== undefined && !R2_JURISDICTIONS.has(jurisdictionHeader)) {
1511
+ return failure(400, 1004, 'cf-r2-jurisdiction must be default, eu, or fedramp');
1512
+ }
1513
+ const accountBuckets = () => rows('r2_bucket', req.root).filter((item) => item.account_id === accountID);
1514
+ if (parts.length === 4 && method === 'GET') {
1515
+ let values = accountBuckets();
1516
+ const nameContains = url.searchParams.get('name_contains');
1517
+ if (nameContains)
1518
+ values = values.filter((item) => String(item.name).includes(nameContains));
1519
+ values = values.sort((a, b) => String(a.name).localeCompare(String(b.name)));
1520
+ if (url.searchParams.get('direction') === 'desc')
1521
+ values = values.reverse();
1522
+ const startAfter = url.searchParams.get('start_after');
1523
+ if (startAfter) {
1524
+ const index = values.findIndex((item) => item.name === startAfter);
1525
+ values = index >= 0 ? values.slice(index + 1) : values;
1526
+ }
1527
+ const perPage = Math.min(1000, Math.max(1, Number(url.searchParams.get('per_page') ?? 20) || 20));
1528
+ return success({ buckets: values.slice(0, perPage).map(bucketView) });
1529
+ }
1530
+ if (parts.length === 4 && method === 'POST') {
1531
+ const blocked = readOnly(req);
1532
+ if (blocked)
1533
+ return blocked;
1534
+ const body = parseBody(req);
1535
+ if (isResponse(body))
1536
+ return body;
1537
+ if (!str(body.name) || body.name.length < 3 || body.name.length > 64 || !BUCKET_NAME_PATTERN.test(body.name)) {
1538
+ return failure(400, 1004, 'name must be 3-64 characters of lowercase letters, numbers, and hyphens');
1539
+ }
1540
+ if (body.locationHint !== undefined && (!str(body.locationHint) || !R2_LOCATIONS.has(body.locationHint))) {
1541
+ return failure(400, 1004, 'locationHint must be one of apac, eeur, enam, weur, wnam, oc');
1542
+ }
1543
+ if (body.storageClass !== undefined && (!str(body.storageClass) || !R2_STORAGE_CLASSES.has(body.storageClass))) {
1544
+ return failure(400, 1004, 'storageClass must be Standard or InfrequentAccess');
1545
+ }
1546
+ if (accountBuckets().some((item) => item.name === body.name)) {
1547
+ return failure(409, 10004, 'The bucket you tried to create already exists, and you own it.');
1548
+ }
1549
+ const at = req.occurredAt ?? new Date().toISOString();
1550
+ await pwrite('r2_bucket', `${accountID}:${body.name}`, 'r2_bucket.create', {
1551
+ account_id: accountID,
1552
+ name: body.name,
1553
+ creation_date: at,
1554
+ ...(str(body.locationHint) ? { location: body.locationHint } : { location: null }),
1555
+ storage_class: body.storageClass ?? 'Standard',
1556
+ jurisdiction: jurisdictionHeader ?? 'default',
1557
+ deleted: false,
1558
+ }, req);
1559
+ return success(bucketView(row('r2_bucket', `${accountID}:${body.name}`, req.root)));
1560
+ }
1561
+ const bucketName = parts[4];
1562
+ if (!bucketName)
1563
+ return noRoute();
1564
+ const bucket = row('r2_bucket', `${accountID}:${bucketName}`, req.root);
1565
+ if (!bucket)
1566
+ return failure(404, 10006, 'The specified bucket does not exist.');
1567
+ if (parts[5] === 'domains' && parts[6] === 'custom')
1568
+ return r2ApiRefusal(req, accountID, method !== 'GET') ?? r2CustomDomainRoute(req, method, parts, accountID, bucketName);
1569
+ if (parts.length !== 5)
1570
+ return noRoute();
1571
+ if (method === 'GET')
1572
+ return success(bucketView(bucket));
1573
+ if (method === 'DELETE') {
1574
+ const blocked = readOnly(req);
1575
+ if (blocked)
1576
+ return blocked;
1577
+ // one bucket, whether the S3 API or this API made it (cloudflare-r2.ts): a bucket that holds objects is not deleted
1578
+ // (https://developers.cloudflare.com/r2/api/error-codes/: "10008 BucketNotEmpty 409 Cannot delete bucket that
1579
+ // contains objects.")
1580
+ if (rows('r2_object', req.root).some((o) => o.bucket_id === `${accountID}:${bucketName}`))
1581
+ return failure(409, 10008, 'Cannot delete bucket that contains objects.');
1582
+ await pwrite('r2_bucket', `${accountID}:${bucketName}`, 'r2_bucket.delete', { account_id: accountID, deleted: true }, req);
1583
+ // "The specified domain will also remain connected to R2 until you remove it or delete the bucket." (public buckets)
1584
+ for (const d of r2CustomDomains(req.root).filter((x) => x.account_id === accountID && x.bucket === bucketName))
1585
+ await removeR2CustomDomain(d.id, req.root, req.occurredAt ?? new Date().toISOString());
1586
+ return success({});
1587
+ }
1588
+ return noRoute();
1589
+ }
1590
+ // R2's Temporary Credentials API (https://developers.cloudflare.com/api/resources/r2/subresources/temporary_credentials/methods/create/):
1591
+ // POST /accounts/{account_id}/r2/temp-access-credentials {bucket, parentAccessKeyId, permission, ttlSeconds (up to
1592
+ // 604800), prefixes?, objects?}, answering {accessKeyId, secretAccessKey, sessionToken}. The reference declares a generic
1593
+ // error envelope only, so a refusal takes the pack's 1004 validation convention.
1594
+ async function r2TempCredentialsRoute(req, accountID) {
1595
+ const blocked = readOnly(req);
1596
+ if (blocked)
1597
+ return blocked;
1598
+ const body = parseBody(req);
1599
+ if (isResponse(body))
1600
+ return body;
1601
+ if (!str(body.bucket) || !row('r2_bucket', `${accountID}:${body.bucket}`, req.root))
1602
+ return failure(404, 10006, 'The specified bucket does not exist.');
1603
+ const permission = TEMP_PERMISSIONS[String(body.permission)];
1604
+ if (!permission)
1605
+ return failure(400, 1004, 'permission must be one of admin-read-write, admin-read-only, object-read-write, object-read-only');
1606
+ const ttl = Number(body.ttlSeconds);
1607
+ if (!Number.isInteger(ttl) || ttl < 1 || ttl > 604800)
1608
+ return failure(400, 1004, 'ttlSeconds must be a whole number of seconds up to 604800');
1609
+ const parent = str(body.parentAccessKeyId) ? r2Credential(accountID, body.parentAccessKeyId, req.root) : undefined;
1610
+ if (!parent)
1611
+ return failure(400, 1004, 'parentAccessKeyId must name a live R2 API token of the account');
1612
+ // "A temporary credential cannot exceed the permissions of its parent token"
1613
+ if (!r2ParentCovers(parent, permission, body.bucket))
1614
+ return failure(400, 1004, 'The temporary credentials cannot exceed the parent token\'s permissions');
1615
+ const list = (v) => (v === undefined ? [] : Array.isArray(v) && v.every((x) => typeof x === 'string') ? v : undefined);
1616
+ const prefixes = list(body.prefixes);
1617
+ const objects = list(body.objects);
1618
+ if (!prefixes || !objects)
1619
+ return failure(400, 1004, 'prefixes and objects must be lists of strings');
1620
+ const c = await createR2TempCredential({ accountId: accountID, bucket: body.bucket, parent, permission, ttlSeconds: ttl, prefixes, objects }, req.root, req.occurredAt ?? new Date().toISOString());
1621
+ return success({ accessKeyId: c.access_key_id, secretAccessKey: c.secret_access_key, sessionToken: c.session_token });
1622
+ }
1623
+ // R2 custom domains (https://developers.cloudflare.com/api/resources/r2/subresources/buckets/subresources/domains/subresources/custom/):
1624
+ // attach (POST {domain, enabled, zoneId, minTLS?, ciphers?}), list, get, configure (PUT) and remove, each answered in the
1625
+ // shapes the reference gives. A domain belongs to a zone of the bucket's account (public buckets: "The domain being used
1626
+ // must have been added as a zone in the same account as the R2 bucket"); its status turns Active by Cloudflare's own move
1627
+ // (cloudflare-r2.ts). The reference declares only a generic error envelope, so a refusal takes the pack's 1004 validation
1628
+ // and 1001 not-found conventions.
1629
+ const TLS_VERSIONS = new Set(['1.0', '1.1', '1.2', '1.3']);
1630
+ async function r2CustomDomainRoute(req, method, parts, accountID, bucketName) {
1631
+ const at = req.occurredAt ?? new Date().toISOString();
1632
+ await catchUpR2CustomDomains(req.root, at);
1633
+ const mine = () => r2CustomDomains(req.root).filter((d) => d.account_id === accountID && d.bucket === bucketName);
1634
+ const full = (d) => ({ domain: d.domain, enabled: d.enabled, status: { ownership: d.ownership, ssl: d.ssl }, minTLS: d.min_tls, ...(d.ciphers ? { ciphers: d.ciphers } : {}), zoneId: d.zone_id, zoneName: d.zone_name });
1635
+ if (parts.length === 7 && method === 'GET')
1636
+ return success({ domains: mine().map(full) });
1637
+ if (parts.length === 7 && method === 'POST') {
1638
+ const blocked = readOnly(req);
1639
+ if (blocked)
1640
+ return blocked;
1641
+ const body = parseBody(req);
1642
+ if (isResponse(body))
1643
+ return body;
1644
+ if (!str(body.domain) || !str(body.zoneId))
1645
+ return failure(400, 1004, 'domain and zoneId are required');
1646
+ const domain = body.domain.toLowerCase();
1647
+ const zone = row('zone', body.zoneId, req.root);
1648
+ if (!zone || zone.account_id !== accountID)
1649
+ return failure(400, 1004, 'zoneId must name a zone in the same account as the bucket');
1650
+ const zoneName = String(zone.name);
1651
+ if (domain !== zoneName && !domain.endsWith(`.${zoneName}`))
1652
+ return failure(400, 1004, `domain must be ${zoneName} or a subdomain of it`);
1653
+ if (body.minTLS !== undefined && !TLS_VERSIONS.has(String(body.minTLS)))
1654
+ return failure(400, 1004, 'minTLS must be one of 1.0, 1.1, 1.2, 1.3');
1655
+ if (r2CustomDomains(req.root).some((d) => d.domain === domain))
1656
+ return failure(409, 1004, `${domain} is already connected to a bucket`);
1657
+ const d = await connectR2CustomDomain({ accountId: accountID, bucket: bucketName, domain, enabled: body.enabled !== false, zoneId: body.zoneId, zoneName, ...(body.minTLS !== undefined ? { minTLS: String(body.minTLS) } : {}), ...(Array.isArray(body.ciphers) ? { ciphers: body.ciphers.map(String) } : {}) }, req.root, at);
1658
+ return success({ domain: d.domain, enabled: d.enabled, zoneId: d.zone_id, minTLS: d.min_tls, ...(d.ciphers ? { ciphers: d.ciphers } : {}) });
1659
+ }
1660
+ if (parts.length !== 8)
1661
+ return noRoute();
1662
+ const d = mine().find((x) => x.domain === parts[7].toLowerCase());
1663
+ if (!d)
1664
+ return failure(404, 1001, `The custom domain ${parts[7]} is not connected to this bucket`);
1665
+ if (method === 'GET')
1666
+ return success(full(d));
1667
+ const blocked = readOnly(req);
1668
+ if (blocked)
1669
+ return blocked;
1670
+ if (method === 'PUT') {
1671
+ const body = parseBody(req);
1672
+ if (isResponse(body))
1673
+ return body;
1674
+ if (body.minTLS !== undefined && !TLS_VERSIONS.has(String(body.minTLS)))
1675
+ return failure(400, 1004, 'minTLS must be one of 1.0, 1.1, 1.2, 1.3');
1676
+ await updateR2CustomDomain(d.id, { ...(typeof body.enabled === 'boolean' ? { enabled: body.enabled } : {}), ...(body.minTLS !== undefined ? { minTLS: String(body.minTLS) } : {}), ...(Array.isArray(body.ciphers) ? { ciphers: body.ciphers.map(String) } : {}) }, req.root, at);
1677
+ const now = mine().find((x) => x.id === d.id);
1678
+ return success({ domain: now.domain, enabled: now.enabled, minTLS: now.min_tls, ...(now.ciphers ? { ciphers: now.ciphers } : {}) });
1679
+ }
1680
+ if (method === 'DELETE') {
1681
+ await removeR2CustomDomain(d.id, req.root, at);
1682
+ return success({ domain: d.domain });
1683
+ }
1684
+ return noRoute();
1685
+ }
1686
+ function validHyperdriveOrigin(origin) {
1687
+ if (!origin || typeof origin !== 'object' || Array.isArray(origin))
1688
+ return failure(400, 1004, 'origin is required and must be an object');
1689
+ const value = origin;
1690
+ if (!str(value.scheme) || !HYPERDRIVE_SCHEMES.has(value.scheme))
1691
+ return failure(400, 1004, 'origin.scheme must be postgres, postgresql, or mysql');
1692
+ if (!str(value.database) || !str(value.user) || !str(value.password)) {
1693
+ return failure(400, 1004, 'origin requires database, user, and password');
1694
+ }
1695
+ const isAccess = str(value.access_client_id) || str(value.access_client_secret);
1696
+ const isVpc = str(value.service_id);
1697
+ if (isAccess && isVpc)
1698
+ return failure(400, 1004, 'origin cannot mix Access and VPC fields');
1699
+ if (isVpc) {
1700
+ // Database reachable through a Workers VPC — recorded, never dialed.
1701
+ return { origin: { database: value.database, scheme: value.scheme, service_id: value.service_id, user: value.user } };
1702
+ }
1703
+ if (isAccess) {
1704
+ if (!str(value.access_client_id) || !str(value.access_client_secret)) {
1705
+ return failure(400, 1004, 'Access-protected origin requires access_client_id and access_client_secret');
1706
+ }
1707
+ if (!str(value.host))
1708
+ return failure(400, 1004, 'origin.host is required');
1709
+ // access_client_secret is writeOnly in the vendor schema: recorded config, never echoed.
1710
+ return { origin: { access_client_id: value.access_client_id, database: value.database, host: value.host, scheme: value.scheme, user: value.user } };
1711
+ }
1712
+ if (!str(value.host))
1713
+ return failure(400, 1004, 'origin.host is required');
1714
+ if (!Number.isInteger(value.port) || Number(value.port) < 1 || Number(value.port) > 65535) {
1715
+ return failure(400, 1004, 'origin.port must be an integer between 1 and 65535');
1716
+ }
1717
+ return { origin: { database: value.database, host: value.host, port: value.port, scheme: value.scheme, user: value.user } };
1718
+ }
1719
+ // PATCH accepts the vendor's TYPED PARTIAL edit-origin (SDK ConfigEditParams): any subset of the
1720
+ // non-secret origin fields, each validated if present. Secrets (password/access_client_secret)
1721
+ // are writeOnly — accepted-if-string but never stored/echoed. The caller merges the returned
1722
+ // partial over the existing origin.
1723
+ function validPartialHyperdriveOrigin(origin) {
1724
+ if (!origin || typeof origin !== 'object' || Array.isArray(origin))
1725
+ return failure(400, 1004, 'origin must be an object');
1726
+ const value = origin;
1727
+ if (value.scheme !== undefined && (!str(value.scheme) || !HYPERDRIVE_SCHEMES.has(value.scheme))) {
1728
+ return failure(400, 1004, 'origin.scheme must be postgres, postgresql, or mysql');
1729
+ }
1730
+ if (value.port !== undefined && (!Number.isInteger(value.port) || Number(value.port) < 1 || Number(value.port) > 65535)) {
1731
+ return failure(400, 1004, 'origin.port must be an integer between 1 and 65535');
1732
+ }
1733
+ for (const key of ['database', 'user', 'host', 'access_client_id', 'service_id']) {
1734
+ if (value[key] !== undefined && !str(value[key]))
1735
+ return failure(400, 1004, `origin.${key} must be a nonempty string`);
1736
+ }
1737
+ for (const secret of ['password', 'access_client_secret']) {
1738
+ if (value[secret] !== undefined && !str(value[secret]))
1739
+ return failure(400, 1004, `origin.${secret} must be a nonempty string`);
1740
+ }
1741
+ const partial = {};
1742
+ for (const key of ['scheme', 'database', 'user', 'host', 'port', 'access_client_id', 'service_id']) {
1743
+ if (value[key] !== undefined)
1744
+ partial[key] = value[key];
1745
+ }
1746
+ return { origin: partial };
1747
+ }
1748
+ // After a PATCH merges a typed partial over the stored origin, the RESULT must still be exactly
1749
+ // one of the vendor's origin variants — a partial edit must not produce a hybrid the create path
1750
+ // could never make (e.g. an Access origin that grew a `port`, or a VPC origin with a host)
1751
+ // (round-two N1). Secrets are absent from a stored origin by design, so this is password-exempt.
1752
+ function validMergedOrigin(origin) {
1753
+ if (!str(origin.scheme) || !HYPERDRIVE_SCHEMES.has(String(origin.scheme)))
1754
+ return failure(400, 1004, 'origin.scheme must be postgres, postgresql, or mysql');
1755
+ if (!str(origin.database) || !str(origin.user))
1756
+ return failure(400, 1004, 'origin requires database and user');
1757
+ const hasService = str(origin.service_id);
1758
+ const hasAccess = str(origin.access_client_id);
1759
+ const hasHost = str(origin.host);
1760
+ const hasPort = origin.port !== undefined;
1761
+ if (hasService) {
1762
+ if (hasHost || hasPort || hasAccess)
1763
+ return failure(400, 1004, 'a VPC origin cannot also carry host, port, or Access fields');
1764
+ return null;
1765
+ }
1766
+ if (hasAccess) {
1767
+ if (!hasHost)
1768
+ return failure(400, 1004, 'an Access-protected origin requires host');
1769
+ if (hasPort)
1770
+ return failure(400, 1004, 'an Access-protected origin cannot carry a port');
1771
+ return null;
1772
+ }
1773
+ if (!hasHost || !hasPort)
1774
+ return failure(400, 1004, 'a public origin requires host and port');
1775
+ if (!Number.isInteger(origin.port) || Number(origin.port) < 1 || Number(origin.port) > 65535)
1776
+ return failure(400, 1004, 'origin.port must be an integer between 1 and 65535');
1777
+ return null;
1778
+ }
1779
+ // caching/mtls are closed vendor sub-schemas — validate known keys' types and REJECT unknown
1780
+ // keys so the twin never records-and-echoes a shape the SDK's own types cannot hold (finding-9).
1781
+ function validCaching(caching) {
1782
+ if (caching === undefined)
1783
+ return null;
1784
+ if (!caching || typeof caching !== 'object' || Array.isArray(caching))
1785
+ return failure(400, 1004, 'caching must be an object');
1786
+ for (const [key, value] of Object.entries(caching)) {
1787
+ if (key === 'disabled') {
1788
+ if (typeof value !== 'boolean')
1789
+ return failure(400, 1004, 'caching.disabled must be a boolean');
1790
+ }
1791
+ else if (key === 'max_age' || key === 'stale_while_revalidate') {
1792
+ if (typeof value !== 'number')
1793
+ return failure(400, 1004, `caching.${key} must be a number`);
1794
+ }
1795
+ else
1796
+ return failure(400, 1004, `caching.${key} is not a valid caching option`);
1797
+ }
1798
+ return null;
1799
+ }
1800
+ function validMtls(mtls) {
1801
+ if (mtls === undefined)
1802
+ return null;
1803
+ if (!mtls || typeof mtls !== 'object' || Array.isArray(mtls))
1804
+ return failure(400, 1004, 'mtls must be an object');
1805
+ for (const [key, value] of Object.entries(mtls)) {
1806
+ if (key === 'ca_certificate_id' || key === 'mtls_certificate_id' || key === 'sslmode') {
1807
+ if (typeof value !== 'string')
1808
+ return failure(400, 1004, `mtls.${key} must be a string`);
1809
+ }
1810
+ else
1811
+ return failure(400, 1004, `mtls.${key} is not a valid mtls option`);
1812
+ }
1813
+ return null;
1814
+ }
1815
+ async function hyperdriveRoute(req, method, url, parts, accountID) {
1816
+ if (parts[3] !== 'configs')
1817
+ return noRoute();
1818
+ const accountConfigs = () => rows('hyperdrive_config', req.root).filter((item) => item.account_id === accountID);
1819
+ const validateWrite = (body) => {
1820
+ if (!str(body.name))
1821
+ return failure(400, 1004, 'name is required');
1822
+ const caching = validCaching(body.caching);
1823
+ if (caching)
1824
+ return caching;
1825
+ const mtls = validMtls(body.mtls);
1826
+ if (mtls)
1827
+ return mtls;
1828
+ if (body.origin_connection_limit !== undefined && (!Number.isInteger(body.origin_connection_limit) || Number(body.origin_connection_limit) < 5)) {
1829
+ return failure(400, 1004, 'origin_connection_limit must be an integer >= 5');
1830
+ }
1831
+ return validHyperdriveOrigin(body.origin);
1832
+ };
1833
+ if (parts.length === 4 && method === 'GET') {
1834
+ const page = paginate(accountConfigs().map(hyperdriveView), url, 20, 100);
1835
+ return success(page.result, page.info);
1836
+ }
1837
+ if (parts.length === 4 && method === 'POST') {
1838
+ const blocked = readOnly(req);
1839
+ if (blocked)
1840
+ return blocked;
1841
+ const body = parseBody(req);
1842
+ if (isResponse(body))
1843
+ return body;
1844
+ const checked = validateWrite(body);
1845
+ if (isResponse(checked))
1846
+ return checked;
1847
+ const at = req.occurredAt ?? new Date().toISOString();
1848
+ const id = mint('hyperdrive_config', `${accountID}:${body.name}`, req.root);
1849
+ await pwrite('hyperdrive_config', id, 'hyperdrive_config.create', {
1850
+ account_id: accountID,
1851
+ name: body.name,
1852
+ origin: checked.origin,
1853
+ ...(body.caching !== undefined ? { caching: body.caching } : {}),
1854
+ ...(body.mtls !== undefined ? { mtls: body.mtls } : {}),
1855
+ ...(body.origin_connection_limit !== undefined ? { origin_connection_limit: body.origin_connection_limit } : {}),
1856
+ created_on: at,
1857
+ modified_on: at,
1858
+ deleted: false,
1859
+ }, req);
1860
+ return success(hyperdriveView(row('hyperdrive_config', id, req.root)));
1861
+ }
1862
+ const hyperdriveID = parts[4];
1863
+ if (!hyperdriveID || parts.length !== 5)
1864
+ return noRoute();
1865
+ const config = row('hyperdrive_config', hyperdriveID, req.root);
1866
+ if (!config || config.account_id !== accountID)
1867
+ return failure(404, 1001, 'Hyperdrive configuration not found');
1868
+ if (method === 'GET')
1869
+ return success(hyperdriveView(config));
1870
+ if (method === 'PUT') {
1871
+ const blocked = readOnly(req);
1872
+ if (blocked)
1873
+ return blocked;
1874
+ const body = parseBody(req);
1875
+ if (isResponse(body))
1876
+ return body;
1877
+ const checked = validateWrite(body);
1878
+ if (isResponse(checked))
1879
+ return checked;
1880
+ await pwrite('hyperdrive_config', hyperdriveID, 'hyperdrive_config.replace', {
1881
+ account_id: accountID,
1882
+ name: body.name,
1883
+ origin: checked.origin,
1884
+ caching: body.caching ?? null,
1885
+ mtls: body.mtls ?? null,
1886
+ origin_connection_limit: typeof body.origin_connection_limit === 'number' ? body.origin_connection_limit : null,
1887
+ modified_on: req.occurredAt ?? new Date().toISOString(),
1888
+ }, req);
1889
+ return success(hyperdriveView(row('hyperdrive_config', hyperdriveID, req.root)));
1890
+ }
1891
+ if (method === 'PATCH') {
1892
+ const blocked = readOnly(req);
1893
+ if (blocked)
1894
+ return blocked;
1895
+ const body = parseBody(req);
1896
+ if (isResponse(body))
1897
+ return body;
1898
+ if (body.name !== undefined && !str(body.name))
1899
+ return failure(400, 1004, 'name must be a nonempty string');
1900
+ const caching = validCaching(body.caching);
1901
+ if (caching)
1902
+ return caching;
1903
+ const mtls = validMtls(body.mtls);
1904
+ if (mtls)
1905
+ return mtls;
1906
+ if (body.origin_connection_limit !== undefined && (!Number.isInteger(body.origin_connection_limit) || Number(body.origin_connection_limit) < 5)) {
1907
+ return failure(400, 1004, 'origin_connection_limit must be an integer >= 5');
1908
+ }
1909
+ let origin;
1910
+ if (body.origin !== undefined) {
1911
+ // PATCH merges the typed partial edit-origin over the existing origin (the vendor's edit
1912
+ // union is partial; requiring the full create-shape here spuriously 400s the SDK's own
1913
+ // typed partial — finding-4).
1914
+ const checked = validPartialHyperdriveOrigin(body.origin);
1915
+ if (isResponse(checked))
1916
+ return checked;
1917
+ origin = { ...config.origin, ...checked.origin };
1918
+ const coherent = validMergedOrigin(origin);
1919
+ if (coherent)
1920
+ return coherent;
1921
+ }
1922
+ await pwrite('hyperdrive_config', hyperdriveID, 'hyperdrive_config.update', {
1923
+ account_id: accountID,
1924
+ ...(body.name !== undefined ? { name: body.name } : {}),
1925
+ ...(origin !== undefined ? { origin } : {}),
1926
+ ...(body.caching !== undefined ? { caching: body.caching } : {}),
1927
+ ...(body.mtls !== undefined ? { mtls: body.mtls } : {}),
1928
+ ...(body.origin_connection_limit !== undefined ? { origin_connection_limit: body.origin_connection_limit } : {}),
1929
+ modified_on: req.occurredAt ?? new Date().toISOString(),
1930
+ }, req);
1931
+ return success(hyperdriveView(row('hyperdrive_config', hyperdriveID, req.root)));
1932
+ }
1933
+ if (method === 'DELETE') {
1934
+ const blocked = readOnly(req);
1935
+ if (blocked)
1936
+ return blocked;
1937
+ await pwrite('hyperdrive_config', hyperdriveID, 'hyperdrive_config.delete', { account_id: accountID, deleted: true }, req);
1938
+ return success(null);
1939
+ }
1940
+ return noRoute();
1941
+ }
1942
+ function validConsumerBody(body) {
1943
+ if (body.type !== 'worker' && body.type !== 'http_pull')
1944
+ return failure(400, 1004, 'type must be worker or http_pull');
1945
+ if (body.type === 'worker' && !str(body.script_name))
1946
+ return failure(400, 1004, 'worker consumers require script_name');
1947
+ if (body.dead_letter_queue !== undefined && !str(body.dead_letter_queue))
1948
+ return failure(400, 1004, 'dead_letter_queue must be a nonempty string');
1949
+ if (body.settings !== undefined && (body.settings === null || typeof body.settings !== 'object' || Array.isArray(body.settings))) {
1950
+ return failure(400, 1004, 'settings must be an object');
1951
+ }
1952
+ const allowed = body.type === 'worker'
1953
+ ? new Set(['batch_size', 'max_concurrency', 'max_retries', 'max_wait_time_ms', 'retry_delay'])
1954
+ : new Set(['batch_size', 'max_retries', 'retry_delay', 'visibility_timeout_ms']);
1955
+ // ONLY max_concurrency is documented nullable ("set to null to opt into the platform maximum");
1956
+ // every other setting is a plain number — accepting null for them would record-and-echo a shape
1957
+ // the SDK's own types cannot hold (round-two N2).
1958
+ for (const [key, value] of Object.entries(body.settings ?? {})) {
1959
+ if (!allowed.has(key))
1960
+ return failure(400, 1004, `settings.${key} is not a valid ${String(body.type)} consumer setting`);
1961
+ if (key === 'max_concurrency') {
1962
+ if (value !== null && typeof value !== 'number')
1963
+ return failure(400, 1004, 'settings.max_concurrency must be a number or null');
1964
+ }
1965
+ else if (typeof value !== 'number') {
1966
+ return failure(400, 1004, `settings.${key} must be a number`);
1967
+ }
1968
+ }
1969
+ return null;
1970
+ }
1971
+ async function queuesRoute(req, method, parts, accountID) {
1972
+ const accountQueues = () => rows('queue', req.root).filter((item) => item.account_id === accountID);
1973
+ if (parts.length === 3 && method === 'GET')
1974
+ return success(accountQueues().map((item) => queueView(item, req.root)));
1975
+ if (parts.length === 3 && method === 'POST') {
1976
+ const blocked = readOnly(req);
1977
+ if (blocked)
1978
+ return blocked;
1979
+ const body = parseBody(req);
1980
+ if (isResponse(body))
1981
+ return body;
1982
+ if (!str(body.queue_name))
1983
+ return failure(400, 1004, 'queue_name is required');
1984
+ if (accountQueues().some((item) => item.queue_name === body.queue_name)) {
1985
+ return failure(400, 1004, `A queue named "${body.queue_name}" already exists on this account`);
1986
+ }
1987
+ const at = req.occurredAt ?? new Date().toISOString();
1988
+ const id = mint('queue', `${accountID}:${body.queue_name}`, req.root);
1989
+ await pwrite('queue', id, 'queue.create', {
1990
+ account_id: accountID, queue_name: body.queue_name, created_on: at, modified_on: at, deleted: false,
1991
+ }, req);
1992
+ return success(queueView(row('queue', id, req.root), req.root));
1993
+ }
1994
+ const queueID = parts[3];
1995
+ if (!queueID)
1996
+ return noRoute();
1997
+ const queue = row('queue', queueID, req.root);
1998
+ if (!queue || queue.account_id !== accountID)
1999
+ return failure(404, 1001, 'Queue not found');
2000
+ if (parts.length === 4) {
2001
+ if (method === 'GET')
2002
+ return success(queueView(queue, req.root));
2003
+ if (method === 'DELETE') {
2004
+ const blocked = readOnly(req);
2005
+ if (blocked)
2006
+ return blocked;
2007
+ for (const consumer of rows('queue_consumer', req.root).filter((item) => item.queue_id === queueID)) {
2008
+ await pwrite('queue_consumer', consumer.id, 'queue_consumer.delete', { queue_id: queueID, deleted: true }, req);
2009
+ }
2010
+ await pwrite('queue', queueID, 'queue.delete', { account_id: accountID, deleted: true }, req);
2011
+ // Pinned OpenAPI QueueDeleteResponse: bare {success,errors,messages}, no result member.
2012
+ return { status: 200, body: { success: true, errors: [], messages: [] } };
2013
+ }
2014
+ return noRoute();
2015
+ }
2016
+ if (parts[4] !== 'consumers')
2017
+ return noRoute();
2018
+ const queueConsumers = () => rows('queue_consumer', req.root).filter((item) => item.queue_id === queueID);
2019
+ if (parts.length === 5 && method === 'GET')
2020
+ return success(queueConsumers().map((item) => consumerView(item, req.root)));
2021
+ if (parts.length === 5 && method === 'POST') {
2022
+ const blocked = readOnly(req);
2023
+ if (blocked)
2024
+ return blocked;
2025
+ const body = parseBody(req);
2026
+ if (isResponse(body))
2027
+ return body;
2028
+ const invalid = validConsumerBody(body);
2029
+ if (invalid)
2030
+ return invalid;
2031
+ if (body.type === 'worker' && !scriptRow(accountID, String(body.script_name), req.root)) {
2032
+ return failure(400, 1004, `script_name "${body.script_name}" is not a Worker on this account`);
2033
+ }
2034
+ if (str(body.dead_letter_queue) && !rows('queue', req.root).some((item) => item.account_id === accountID && item.queue_name === body.dead_letter_queue)) {
2035
+ return failure(400, 1004, `dead_letter_queue "${body.dead_letter_queue}" is not a queue on this account`);
2036
+ }
2037
+ // A Cloudflare queue has at most one consumer (worker and http_pull are mutually exclusive).
2038
+ if (queueConsumers().length > 0)
2039
+ return failure(400, 1004, 'This queue already has a consumer; delete it before adding another');
2040
+ const at = req.occurredAt ?? new Date().toISOString();
2041
+ const id = mint('queue_consumer', `${queueID}:${String(body.script_name ?? 'http_pull')}`, req.root);
2042
+ await pwrite('queue_consumer', id, 'queue_consumer.create', {
2043
+ account_id: accountID,
2044
+ queue_id: queueID,
2045
+ consumer_type: body.type,
2046
+ ...(body.type === 'worker' ? { script_name: body.script_name } : {}),
2047
+ ...(str(body.dead_letter_queue) ? { dead_letter_queue: body.dead_letter_queue } : {}),
2048
+ settings: body.settings ?? {},
2049
+ created_on: at,
2050
+ deleted: false,
2051
+ }, req);
2052
+ return success(consumerView(row('queue_consumer', id, req.root), req.root));
2053
+ }
2054
+ const consumerID = parts[5];
2055
+ if (!consumerID || parts.length !== 6)
2056
+ return noRoute();
2057
+ const consumer = row('queue_consumer', consumerID, req.root);
2058
+ if (!consumer || consumer.queue_id !== queueID)
2059
+ return failure(404, 1001, 'Consumer not found');
2060
+ if (method === 'GET')
2061
+ return success(consumerView(consumer, req.root));
2062
+ if (method === 'PUT') {
2063
+ const blocked = readOnly(req);
2064
+ if (blocked)
2065
+ return blocked;
2066
+ const body = parseBody(req);
2067
+ if (isResponse(body))
2068
+ return body;
2069
+ const invalid = validConsumerBody(body);
2070
+ if (invalid)
2071
+ return invalid;
2072
+ if (body.type === 'worker' && !scriptRow(accountID, String(body.script_name), req.root)) {
2073
+ return failure(400, 1004, `script_name "${body.script_name}" is not a Worker on this account`);
2074
+ }
2075
+ if (str(body.dead_letter_queue) && !rows('queue', req.root).some((item) => item.account_id === accountID && item.queue_name === body.dead_letter_queue)) {
2076
+ return failure(400, 1004, `dead_letter_queue "${body.dead_letter_queue}" is not a queue on this account`);
2077
+ }
2078
+ await pwrite('queue_consumer', consumerID, 'queue_consumer.update', {
2079
+ account_id: accountID,
2080
+ queue_id: queueID,
2081
+ consumer_type: body.type,
2082
+ script_name: body.type === 'worker' ? body.script_name : null,
2083
+ dead_letter_queue: str(body.dead_letter_queue) ? body.dead_letter_queue : null,
2084
+ settings: body.settings ?? {},
2085
+ }, req);
2086
+ return success(consumerView(row('queue_consumer', consumerID, req.root), req.root));
2087
+ }
2088
+ if (method === 'DELETE') {
2089
+ const blocked = readOnly(req);
2090
+ if (blocked)
2091
+ return blocked;
2092
+ await pwrite('queue_consumer', consumerID, 'queue_consumer.delete', { queue_id: queueID, deleted: true }, req);
2093
+ // Pinned OpenAPI ConsumerDeleteResponse: bare {success,errors,messages}, no result member.
2094
+ return { status: 200, body: { success: true, errors: [], messages: [] } };
2095
+ }
2096
+ return noRoute();
2097
+ }