@package-broker/cli 0.19.8 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/LICENSE +661 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/bin.d.ts +3 -0
  4. package/dist/bin.d.ts.map +1 -0
  5. package/dist/bin.js +12 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/build-artifacts.d.ts +27 -0
  8. package/dist/build-artifacts.d.ts.map +1 -0
  9. package/dist/build-artifacts.js +169 -0
  10. package/dist/build-artifacts.js.map +1 -0
  11. package/dist/build-assets.d.ts +36 -0
  12. package/dist/build-assets.d.ts.map +1 -0
  13. package/dist/build-assets.js +224 -0
  14. package/dist/build-assets.js.map +1 -0
  15. package/dist/build.d.ts +31 -0
  16. package/dist/build.d.ts.map +1 -0
  17. package/dist/build.js +277 -0
  18. package/dist/build.js.map +1 -0
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.d.ts.map +1 -0
  21. package/dist/cli.js +828 -0
  22. package/dist/cli.js.map +1 -0
  23. package/dist/cloudflare/d1-http-executor.d.ts +30 -0
  24. package/dist/cloudflare/d1-http-executor.d.ts.map +1 -0
  25. package/dist/cloudflare/d1-http-executor.js +230 -0
  26. package/dist/cloudflare/d1-http-executor.js.map +1 -0
  27. package/dist/cloudflare/d1-http-response.d.ts +4 -0
  28. package/dist/cloudflare/d1-http-response.d.ts.map +1 -0
  29. package/dist/cloudflare/d1-http-response.js +113 -0
  30. package/dist/cloudflare/d1-http-response.js.map +1 -0
  31. package/dist/commands/admin.d.ts +47 -0
  32. package/dist/commands/admin.d.ts.map +1 -0
  33. package/dist/commands/admin.js +447 -0
  34. package/dist/commands/admin.js.map +1 -0
  35. package/dist/commands/built-manifest.d.ts +13 -0
  36. package/dist/commands/built-manifest.d.ts.map +1 -0
  37. package/dist/commands/built-manifest.js +197 -0
  38. package/dist/commands/built-manifest.js.map +1 -0
  39. package/dist/commands/cloudflare/api.d.ts +132 -0
  40. package/dist/commands/cloudflare/api.d.ts.map +1 -0
  41. package/dist/commands/cloudflare/api.js +502 -0
  42. package/dist/commands/cloudflare/api.js.map +1 -0
  43. package/dist/commands/cloudflare/deploy.d.ts +111 -0
  44. package/dist/commands/cloudflare/deploy.d.ts.map +1 -0
  45. package/dist/commands/cloudflare/deploy.js +2170 -0
  46. package/dist/commands/cloudflare/deploy.js.map +1 -0
  47. package/dist/commands/cloudflare/inventory.d.ts +257 -0
  48. package/dist/commands/cloudflare/inventory.d.ts.map +1 -0
  49. package/dist/commands/cloudflare/inventory.js +842 -0
  50. package/dist/commands/cloudflare/inventory.js.map +1 -0
  51. package/dist/commands/cloudflare/janitor.d.ts +10 -0
  52. package/dist/commands/cloudflare/janitor.d.ts.map +1 -0
  53. package/dist/commands/cloudflare/janitor.js +84 -0
  54. package/dist/commands/cloudflare/janitor.js.map +1 -0
  55. package/dist/commands/cloudflare/migrations-check.d.ts +5 -0
  56. package/dist/commands/cloudflare/migrations-check.d.ts.map +1 -0
  57. package/dist/commands/cloudflare/migrations-check.js +10 -0
  58. package/dist/commands/cloudflare/migrations-check.js.map +1 -0
  59. package/dist/commands/cloudflare/providers-file.d.ts +17 -0
  60. package/dist/commands/cloudflare/providers-file.d.ts.map +1 -0
  61. package/dist/commands/cloudflare/providers-file.js +155 -0
  62. package/dist/commands/cloudflare/providers-file.js.map +1 -0
  63. package/dist/commands/cloudflare/provision.d.ts +79 -0
  64. package/dist/commands/cloudflare/provision.d.ts.map +1 -0
  65. package/dist/commands/cloudflare/provision.js +961 -0
  66. package/dist/commands/cloudflare/provision.js.map +1 -0
  67. package/dist/commands/cloudflare/queue-consumer.d.ts +4 -0
  68. package/dist/commands/cloudflare/queue-consumer.d.ts.map +1 -0
  69. package/dist/commands/cloudflare/queue-consumer.js +57 -0
  70. package/dist/commands/cloudflare/queue-consumer.js.map +1 -0
  71. package/dist/commands/cloudflare/routes.d.ts +3 -0
  72. package/dist/commands/cloudflare/routes.d.ts.map +1 -0
  73. package/dist/commands/cloudflare/routes.js +26 -0
  74. package/dist/commands/cloudflare/routes.js.map +1 -0
  75. package/dist/commands/cloudflare/setup.d.ts +50 -0
  76. package/dist/commands/cloudflare/setup.d.ts.map +1 -0
  77. package/dist/commands/cloudflare/setup.js +836 -0
  78. package/dist/commands/cloudflare/setup.js.map +1 -0
  79. package/dist/commands/cloudflare/smoke.d.ts +29 -0
  80. package/dist/commands/cloudflare/smoke.d.ts.map +1 -0
  81. package/dist/commands/cloudflare/smoke.js +701 -0
  82. package/dist/commands/cloudflare/smoke.js.map +1 -0
  83. package/dist/commands/cloudflare/teardown.d.ts +65 -0
  84. package/dist/commands/cloudflare/teardown.d.ts.map +1 -0
  85. package/dist/commands/cloudflare/teardown.js +984 -0
  86. package/dist/commands/cloudflare/teardown.js.map +1 -0
  87. package/dist/commands/cloudflare/worker-settings.d.ts +4 -0
  88. package/dist/commands/cloudflare/worker-settings.d.ts.map +1 -0
  89. package/dist/commands/cloudflare/worker-settings.js +93 -0
  90. package/dist/commands/cloudflare/worker-settings.js.map +1 -0
  91. package/dist/commands/cloudflare/zone.d.ts +3 -0
  92. package/dist/commands/cloudflare/zone.d.ts.map +1 -0
  93. package/dist/commands/cloudflare/zone.js +73 -0
  94. package/dist/commands/cloudflare/zone.js.map +1 -0
  95. package/dist/commands/cloudflare.d.ts +55 -0
  96. package/dist/commands/cloudflare.d.ts.map +1 -0
  97. package/dist/commands/cloudflare.js +393 -0
  98. package/dist/commands/cloudflare.js.map +1 -0
  99. package/dist/commands/import-cutover.d.ts +5 -0
  100. package/dist/commands/import-cutover.d.ts.map +1 -0
  101. package/dist/commands/import-cutover.js +25 -0
  102. package/dist/commands/import-cutover.js.map +1 -0
  103. package/dist/commands/import-private-packagist.d.ts +38 -0
  104. package/dist/commands/import-private-packagist.d.ts.map +1 -0
  105. package/dist/commands/import-private-packagist.js +232 -0
  106. package/dist/commands/import-private-packagist.js.map +1 -0
  107. package/dist/commands/import-satis.d.ts +11 -0
  108. package/dist/commands/import-satis.d.ts.map +1 -0
  109. package/dist/commands/import-satis.js +112 -0
  110. package/dist/commands/import-satis.js.map +1 -0
  111. package/dist/commands/import.d.ts +22 -0
  112. package/dist/commands/import.d.ts.map +1 -0
  113. package/dist/commands/import.js +343 -0
  114. package/dist/commands/import.js.map +1 -0
  115. package/dist/commands/openapi-specs.d.ts +4 -0
  116. package/dist/commands/openapi-specs.d.ts.map +1 -0
  117. package/dist/commands/openapi-specs.js +94 -0
  118. package/dist/commands/openapi-specs.js.map +1 -0
  119. package/dist/commands/openapi.d.ts +12 -0
  120. package/dist/commands/openapi.d.ts.map +1 -0
  121. package/dist/commands/openapi.js +44 -0
  122. package/dist/commands/openapi.js.map +1 -0
  123. package/dist/commands/source.d.ts +8 -0
  124. package/dist/commands/source.d.ts.map +1 -0
  125. package/dist/commands/source.js +61 -0
  126. package/dist/commands/source.js.map +1 -0
  127. package/dist/commands/sqlite.d.ts +11 -0
  128. package/dist/commands/sqlite.d.ts.map +1 -0
  129. package/dist/commands/sqlite.js +76 -0
  130. package/dist/commands/sqlite.js.map +1 -0
  131. package/dist/import/client.d.ts +71 -0
  132. package/dist/import/client.d.ts.map +1 -0
  133. package/dist/import/client.js +348 -0
  134. package/dist/import/client.js.map +1 -0
  135. package/dist/import/credential-refs.d.ts +2 -0
  136. package/dist/import/credential-refs.d.ts.map +1 -0
  137. package/dist/import/credential-refs.js +22 -0
  138. package/dist/import/credential-refs.js.map +1 -0
  139. package/dist/import/cutover.d.ts +64 -0
  140. package/dist/import/cutover.d.ts.map +1 -0
  141. package/dist/import/cutover.js +544 -0
  142. package/dist/import/cutover.js.map +1 -0
  143. package/dist/import/private-packagist/api.d.ts +57 -0
  144. package/dist/import/private-packagist/api.d.ts.map +1 -0
  145. package/dist/import/private-packagist/api.js +246 -0
  146. package/dist/import/private-packagist/api.js.map +1 -0
  147. package/dist/import/private-packagist/dates.d.ts +30 -0
  148. package/dist/import/private-packagist/dates.d.ts.map +1 -0
  149. package/dist/import/private-packagist/dates.js +189 -0
  150. package/dist/import/private-packagist/dates.js.map +1 -0
  151. package/dist/import/private-packagist/map.d.ts +48 -0
  152. package/dist/import/private-packagist/map.d.ts.map +1 -0
  153. package/dist/import/private-packagist/map.js +162 -0
  154. package/dist/import/private-packagist/map.js.map +1 -0
  155. package/dist/import/private-packagist/plan.d.ts +61 -0
  156. package/dist/import/private-packagist/plan.d.ts.map +1 -0
  157. package/dist/import/private-packagist/plan.js +936 -0
  158. package/dist/import/private-packagist/plan.js.map +1 -0
  159. package/dist/import/private-packagist/snapshot.d.ts +31 -0
  160. package/dist/import/private-packagist/snapshot.d.ts.map +1 -0
  161. package/dist/import/private-packagist/snapshot.js +406 -0
  162. package/dist/import/private-packagist/snapshot.js.map +1 -0
  163. package/dist/import/satis.d.ts +93 -0
  164. package/dist/import/satis.d.ts.map +1 -0
  165. package/dist/import/satis.js +873 -0
  166. package/dist/import/satis.js.map +1 -0
  167. package/dist/index.d.ts +3 -2
  168. package/dist/index.d.ts.map +1 -1
  169. package/dist/index.js +3 -96
  170. package/dist/index.js.map +1 -1
  171. package/dist/manifest.d.ts +56 -0
  172. package/dist/manifest.d.ts.map +1 -0
  173. package/dist/manifest.js +137 -0
  174. package/dist/manifest.js.map +1 -0
  175. package/dist/migrations-collect.d.ts +6 -0
  176. package/dist/migrations-collect.d.ts.map +1 -0
  177. package/dist/migrations-collect.js +39 -0
  178. package/dist/migrations-collect.js.map +1 -0
  179. package/dist/prebuilt-expected.json +7 -0
  180. package/dist/prebuilt.d.ts +27 -0
  181. package/dist/prebuilt.d.ts.map +1 -0
  182. package/dist/prebuilt.js +391 -0
  183. package/dist/prebuilt.js.map +1 -0
  184. package/package.json +22 -25
  185. package/README.md +0 -29
  186. package/dist/__tests__/migrate.test.d.ts +0 -2
  187. package/dist/__tests__/migrate.test.d.ts.map +0 -1
  188. package/dist/__tests__/migrate.test.js +0 -116
  189. package/dist/__tests__/migrate.test.js.map +0 -1
  190. package/dist/migrate.d.ts +0 -40
  191. package/dist/migrate.d.ts.map +0 -1
  192. package/dist/migrate.js +0 -335
  193. package/dist/migrate.js.map +0 -1
@@ -0,0 +1,984 @@
1
+ import { normalizeQueueConsumer, queueConsumerScript } from './queue-consumer.js';
2
+ import { d1BindingId } from './worker-settings.js';
3
+ import { constants } from 'node:fs';
4
+ import { lstat, open, realpath } from 'node:fs/promises';
5
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
+ import { CloudflareError, cloudflareCredentials, commandEnvelope, failCommand, object, paginatedList, } from './api.js';
7
+ import { InventoryStore, abortOnlyRule, digest, hashedBindingNames, installIdentity, lifecycleRule, validateInventory, } from './inventory.js';
8
+ // R2 attaches this rule to every new bucket.
9
+ const PROVIDER_DEFAULT_LIFECYCLE_RULE = {
10
+ id: 'Default Multipart Abort Rule',
11
+ enabled: true,
12
+ conditions: { prefix: '' },
13
+ abortMultipartUploadsTransition: { condition: { type: 'Age', maxAge: 604800 } },
14
+ };
15
+ export class ForeignAccountInventoryError extends CloudflareError {
16
+ constructor() {
17
+ super('ownership_conflict', 8);
18
+ }
19
+ }
20
+ class MultipartCleanupError extends CloudflareError {
21
+ constructor() {
22
+ super('ownership_conflict', 8);
23
+ }
24
+ }
25
+ class WorkerOwnershipError extends CloudflareError {
26
+ constructor() {
27
+ super('ownership_conflict', 8);
28
+ }
29
+ }
30
+ export function cleanupFailure(error, stage) {
31
+ const safe = error instanceof CloudflareError
32
+ ? new CloudflareError(error.code, error.exitCode, error.retryable, stage, error.status)
33
+ : new CloudflareError('state_io_failure', 1, false, stage);
34
+ const messages = {
35
+ invalid_input: 'Invalid cleanup input. Production teardown requires --i-understand-this-deletes and --inventory-id equal to identity.owner_digest. Adopted D1 and R2 resources always require --i-understand-this-deletes.',
36
+ ownership_conflict: 'Inventory ownership or live attachments differ from the selected account, run or resource.',
37
+ outcome_unknown: 'A resource has no resolved inventory ID or a deletion response was lost. Retain the inventory, inspect exact IDs and rerun cleanup.',
38
+ deadline_exceeded: 'Cleanup deadline exceeded. Retain the inventory and rerun to finish.',
39
+ state_io_failure: 'Cleanup inventory or lock I/O failed. Resolve local state and retain the private inventory.',
40
+ permission_denied: 'The provider denied cleanup. Verify token permissions and retain the private inventory.',
41
+ };
42
+ if (Object.hasOwn(messages, safe.code))
43
+ safe.message = messages[safe.code];
44
+ if (error instanceof MultipartCleanupError)
45
+ safe.message =
46
+ 'R2 remains nonempty after an empty object drain; unrecorded multipart uploads may remain. Inspect and abort them with S3 credentials, retain the inventory and rerun cleanup. Trigger attachments may already be removed.';
47
+ if (error instanceof WorkerOwnershipError)
48
+ safe.message =
49
+ 'Worker ownership tags do not match the broker installation. Refusing to delete the Worker or its resources; retain the inventory and inspect the replacement Worker.';
50
+ return safe;
51
+ }
52
+ export async function cleanupPath(project, path) {
53
+ const root = join(project, '.pb/state/cloudflare');
54
+ const part = relative(root, path);
55
+ if (isAbsolute(part) || part === '..' || part.startsWith(`..${sep}`))
56
+ throw new CloudflareError('unsafe_path', 2);
57
+ let current = project;
58
+ for (const component of relative(project, path).split(sep)) {
59
+ current = join(current, component);
60
+ try {
61
+ const entry = await lstat(current);
62
+ if (entry.isSymbolicLink() || (current !== path && !entry.isDirectory()))
63
+ throw new CloudflareError('unsafe_path', 2);
64
+ }
65
+ catch (error) {
66
+ if (error.code === 'ENOENT')
67
+ return;
68
+ throw error;
69
+ }
70
+ }
71
+ }
72
+ export async function cleanupInventory(o) {
73
+ const auth = cloudflareCredentials(o.env);
74
+ const project = await realpath(resolve(o.project ?? dirname(resolve(o.configPath ?? 'broker.config.ts'))));
75
+ const path = resolve(project, o.inventory);
76
+ await cleanupPath(project, path);
77
+ const entry = await lstat(path).catch((error) => {
78
+ if (error.code === 'ENOENT')
79
+ throw new CloudflareError('invalid_inventory', 2);
80
+ throw error;
81
+ });
82
+ if (!entry.isFile() || entry.size > 4_194_304)
83
+ throw new CloudflareError('unsafe_path', 2);
84
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
85
+ let journal;
86
+ try {
87
+ const raw = JSON.parse(await file.readFile('utf8'));
88
+ const identity = object(object(raw).identity);
89
+ if (typeof identity.account_id !== 'string' || !/^[a-f0-9]{32}$/.test(identity.account_id))
90
+ throw new CloudflareError('invalid_inventory', 2);
91
+ if (identity.account_id !== auth.accountId)
92
+ throw new ForeignAccountInventoryError();
93
+ const expected = installIdentity({
94
+ accountId: auth.accountId,
95
+ workerName: identity.worker_name,
96
+ environment: identity.environment,
97
+ deploymentId: identity.deployment_id,
98
+ jurisdiction: identity.jurisdiction,
99
+ ...(identity.run_identity === null ? {} : { runIdentity: identity.run_identity }),
100
+ });
101
+ journal = validateInventory(raw, expected);
102
+ }
103
+ catch (error) {
104
+ if (error instanceof CloudflareError && error.code !== 'provider_failure')
105
+ throw error;
106
+ throw new CloudflareError('invalid_inventory', 2);
107
+ }
108
+ finally {
109
+ await file.close();
110
+ }
111
+ const store = await InventoryStore.create({
112
+ project,
113
+ inventory: path,
114
+ outDir: o.outDir ?? '.pb/cloudflare',
115
+ identity: journal.identity,
116
+ });
117
+ return { auth, store, journal };
118
+ }
119
+ export function expiredRun(journal, repositoryId, now) {
120
+ if (!/^[1-9][0-9]{0,19}$/.test(repositoryId))
121
+ throw new CloudflareError('invalid_input', 2);
122
+ const run = journal.identity.run_identity;
123
+ const match = run?.match(/^pb-e2e-([1-9][0-9]{0,19})-([0-9]{10})(?:-[a-zA-Z0-9_.-]+)?$/);
124
+ if (!match || match[1] !== repositoryId)
125
+ return false;
126
+ const stem = `pb-e2e-${repositoryId}-${match[2]}`;
127
+ const compact = `pb-e2e-${BigInt(repositoryId).toString(36)}-${Number(match[2]).toString(36)}`;
128
+ const worker = journal.identity.worker_name;
129
+ return ([stem, compact].some((name) => worker === name || worker.startsWith(`${name}-`)) &&
130
+ now - Number(match[2]) * 1000 > 6 * 3600_000 &&
131
+ now - Date.parse(journal.updated_at) > 6 * 3600_000);
132
+ }
133
+ function errorCodes(text) {
134
+ try {
135
+ const parsed = object(JSON.parse(text));
136
+ if (parsed.success !== false || parsed.result !== null || !Array.isArray(parsed.errors))
137
+ return [];
138
+ const codes = parsed.errors.map((entry) => object(entry).code);
139
+ return codes.every((code) => Number.isInteger(code)) ? codes : [];
140
+ }
141
+ catch {
142
+ return [];
143
+ }
144
+ }
145
+ class CleanupApi {
146
+ o;
147
+ auth;
148
+ now;
149
+ deadline;
150
+ constructor(o, auth) {
151
+ this.o = o;
152
+ this.auth = auth;
153
+ this.now = o.now ?? Date.now;
154
+ this.deadline = this.now() + 600_000;
155
+ }
156
+ async request(path, method = 'DELETE', body, r2 = false, notFoundCodes = false, deniedCodes = false) {
157
+ if ((!/^\/(?:accounts|zones)\/[a-f0-9]{32}\/[a-zA-Z0-9_/?=&%.-]+$/.test(path) &&
158
+ !/^\/zones\?account\.id=[a-f0-9]{32}&(?:page=\d+&per_page=100|cursor=[a-zA-Z0-9_%.-]+)$/.test(path)) ||
159
+ path.includes('..'))
160
+ throw new CloudflareError('invalid_input', 2);
161
+ for (let attempt = 0; attempt < 5; attempt++) {
162
+ const remaining = this.deadline - this.now();
163
+ if (remaining <= 0)
164
+ throw new CloudflareError('deadline_exceeded', 1, true);
165
+ const controller = new AbortController();
166
+ const timer = setTimeout(() => controller.abort(), Math.min(35_000, remaining));
167
+ const aborted = new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(new Error()), { once: true }));
168
+ let response;
169
+ try {
170
+ response = await Promise.race([
171
+ (this.o.fetch ?? fetch)(`https://api.cloudflare.com/client/v4${path}`, {
172
+ method,
173
+ redirect: 'manual',
174
+ signal: controller.signal,
175
+ headers: {
176
+ authorization: `Bearer ${this.auth.apiToken}`,
177
+ 'content-type': 'application/json',
178
+ ...(r2 ? { 'cf-r2-jurisdiction': this.jurisdiction } : {}),
179
+ },
180
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
181
+ }),
182
+ aborted,
183
+ ]);
184
+ const read = async (source) => {
185
+ const reader = source.body?.getReader();
186
+ if (!reader)
187
+ throw new Error();
188
+ const chunks = [];
189
+ let bytes = 0;
190
+ try {
191
+ for (;;) {
192
+ const { done, value } = await Promise.race([reader.read(), aborted]);
193
+ if (done)
194
+ break;
195
+ bytes += value.byteLength;
196
+ if (bytes > 1_048_576)
197
+ throw new Error();
198
+ chunks.push(value);
199
+ }
200
+ }
201
+ finally {
202
+ void reader.cancel().catch(() => undefined);
203
+ reader.releaseLock();
204
+ }
205
+ return Buffer.concat(chunks).toString('utf8');
206
+ };
207
+ if (response.status === 404) {
208
+ if (!notFoundCodes)
209
+ return { gone: true, result: null };
210
+ return { gone: true, result: null, codes: errorCodes(await read(response)) };
211
+ }
212
+ if (response.status === 403 && deniedCodes)
213
+ return {
214
+ gone: false,
215
+ denied: true,
216
+ result: null,
217
+ codes: errorCodes(await read(response)),
218
+ };
219
+ if (!response.ok)
220
+ throw new CloudflareError([401, 403].includes(response.status) ? 'permission_denied' : 'provider_failure', 1, [408, 429, 500, 502, 503, 504].includes(response.status), 'teardown', response.status);
221
+ const text = await read(response);
222
+ try {
223
+ const parsed = object(JSON.parse(text));
224
+ if (parsed.success !== true || !Object.hasOwn(parsed, 'result'))
225
+ throw new Error();
226
+ return {
227
+ gone: false,
228
+ result: parsed.result,
229
+ ...(parsed.result_info == null ? {} : { result_info: object(parsed.result_info) }),
230
+ ...(Object.hasOwn(parsed, 'result_info') ? {} : { resultInfoAbsent: true }),
231
+ };
232
+ }
233
+ catch {
234
+ throw new CloudflareError('provider_failure', method === 'GET' ? 1 : 6, false, 'teardown');
235
+ }
236
+ }
237
+ catch (error) {
238
+ const safe = error instanceof CloudflareError
239
+ ? error
240
+ : new CloudflareError('provider_failure', method === 'GET' ? 1 : 6, false, 'teardown');
241
+ if (!safe.retryable || attempt === 4)
242
+ throw safe;
243
+ const header = response?.headers.get('retry-after');
244
+ const retry = header && /^\d+$/.test(header)
245
+ ? Number(header) * 1000
246
+ : header
247
+ ? Date.parse(header) - this.now()
248
+ : 0;
249
+ const delay = Math.max(1000 * 2 ** attempt, Number.isFinite(retry) ? retry : 0);
250
+ if (this.now() + delay >= this.deadline)
251
+ throw new CloudflareError('deadline_exceeded', 1, true);
252
+ await (this.o.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(delay);
253
+ }
254
+ finally {
255
+ clearTimeout(timer);
256
+ controller.abort();
257
+ void response?.body?.cancel().catch(() => undefined);
258
+ }
259
+ }
260
+ throw new CloudflareError('provider_failure');
261
+ }
262
+ async list(path, field, r2 = false, deniedMeansEmpty = false) {
263
+ const conflict = () => {
264
+ throw new CloudflareError('ownership_conflict', 8);
265
+ };
266
+ const empty = new Error();
267
+ let first = true;
268
+ try {
269
+ return await paginatedList(path, async (page) => {
270
+ const response = await this.request(page, 'GET', undefined, r2, false, deniedMeansEmpty && first);
271
+ first = false;
272
+ if (response.denied) {
273
+ if (response.codes?.length === 1 && response.codes[0] === 10003)
274
+ throw empty;
275
+ throw new CloudflareError('permission_denied', 1, false, 'teardown', 403);
276
+ }
277
+ // A missing child collection is not evidence of an empty live parent.
278
+ if (response.gone)
279
+ return conflict();
280
+ return response;
281
+ }, field, conflict, conflict);
282
+ }
283
+ catch (error) {
284
+ if (error === empty)
285
+ return [];
286
+ throw error;
287
+ }
288
+ }
289
+ async snapshot(path) {
290
+ const response = await this.request(path, 'GET');
291
+ if (response.gone || !Array.isArray(response.result))
292
+ throw new CloudflareError('ownership_conflict', 8);
293
+ completeSnapshot(response.result_info, response.result.length);
294
+ return response.result.map(object);
295
+ }
296
+ async configuration(path, field, emptyCode) {
297
+ const response = await this.request(path, 'GET', undefined, true, emptyCode !== undefined);
298
+ // Cloudflare answers 404 with a dedicated code for an existing bucket without this configuration.
299
+ if (emptyCode !== undefined &&
300
+ response.gone &&
301
+ response.codes?.length === 1 &&
302
+ response.codes[0] === emptyCode)
303
+ return [];
304
+ if (response.gone)
305
+ throw new CloudflareError('ownership_conflict', 8);
306
+ const records = object(response.result)[field];
307
+ if (!Array.isArray(records))
308
+ throw new CloudflareError('ownership_conflict', 8);
309
+ completeSnapshot(response.result_info, records.length);
310
+ return records.map(object);
311
+ }
312
+ jurisdiction = 'eu';
313
+ drained = 0;
314
+ async drain(path, readOnly = false) {
315
+ let cursor;
316
+ const seen = new Set();
317
+ for (let page = 0; page < 10_000; page++) {
318
+ // A 1024-byte key can occupy 6144 JSON bytes; leave room for object metadata.
319
+ const response = await this.request(`${path}/objects?per_page=100${cursor === undefined ? '' : `&cursor=${encodeURIComponent(cursor)}`}`, 'GET', undefined, true);
320
+ if (response.gone) {
321
+ if ((await this.request(path, 'GET', undefined, true)).gone)
322
+ return false;
323
+ throw new CloudflareError('capability_unknown');
324
+ }
325
+ const info = response.result_info;
326
+ // An empty bucket answers without result_info; only an empty listing is complete without it.
327
+ if (!Array.isArray(response.result) ||
328
+ (info === undefined
329
+ ? !response.resultInfoAbsent || response.result.length > 0
330
+ : typeof info.is_truncated !== 'boolean'))
331
+ throw new CloudflareError('provider_failure');
332
+ const keys = response.result.map((raw) => {
333
+ const key = object(raw).key;
334
+ if (typeof key !== 'string' || !key || Buffer.byteLength(key) > 1024)
335
+ throw new CloudflareError('provider_failure');
336
+ return key;
337
+ });
338
+ if (keys.length && !readOnly) {
339
+ const deleted = await this.request(`${path}/objects`, 'DELETE', keys, true);
340
+ if (deleted.gone || !Array.isArray(deleted.result))
341
+ throw new CloudflareError('outcome_unknown', 6);
342
+ const confirmed = new Set();
343
+ for (const entry of deleted.result) {
344
+ if (!entry ||
345
+ typeof entry !== 'object' ||
346
+ Array.isArray(entry) ||
347
+ typeof entry.key !== 'string')
348
+ throw new CloudflareError('outcome_unknown', 6);
349
+ confirmed.add(entry.key);
350
+ }
351
+ if (keys.some((key) => !confirmed.has(key)))
352
+ throw new CloudflareError('outcome_unknown', 6);
353
+ this.drained += keys.length;
354
+ }
355
+ if (info === undefined || !info.is_truncated)
356
+ return true;
357
+ if (typeof info.cursor !== 'string' || !info.cursor || seen.has(info.cursor))
358
+ throw new CloudflareError('provider_failure');
359
+ seen.add(info.cursor);
360
+ cursor = info.cursor;
361
+ }
362
+ throw new CloudflareError('provider_failure');
363
+ }
364
+ }
365
+ function matchesReceipt(live, receipt) {
366
+ if (live === null || typeof live !== 'object' || Array.isArray(live))
367
+ return false;
368
+ if (receipt.type === 'worker' && Object.hasOwn(receipt, 'consumer_id')) {
369
+ const actual = normalizeQueueConsumer(live);
370
+ const expected = normalizeQueueConsumer(receipt);
371
+ return actual !== null && expected !== null && matchesReceipt(actual, { ...expected });
372
+ }
373
+ return Object.entries(receipt).every(([key, value]) => {
374
+ const actual = live[key];
375
+ return value !== null && typeof value === 'object'
376
+ ? matchesReceipt(actual, value)
377
+ : actual === value;
378
+ });
379
+ }
380
+ function deletions(j) {
381
+ const base = `/accounts/${j.identity.account_id}`;
382
+ const script = `${base}/workers/scripts/${j.identity.script_name}`;
383
+ const entries = [];
384
+ let unresolved = j.operations.some((op) => ['pending', 'unknown'].includes(op.state) && op.resolved_id === null);
385
+ const add = (entry) => {
386
+ const existing = entries.find((x) => x.path === entry.path);
387
+ if (!existing)
388
+ entries.push(entry);
389
+ else {
390
+ if (entry.crons)
391
+ existing.crons = [...new Set([...(existing.crons ?? []), ...entry.crons])];
392
+ if (entry.receipts)
393
+ existing.receipts = [...(existing.receipts ?? []), ...entry.receipts];
394
+ }
395
+ };
396
+ const attempts = [j.deployment?.attempt, j.deployment?.last_verified].filter((x) => x !== null && x !== undefined);
397
+ const uploadedWorker = attempts.some((attempt) => attempt.version_id !== null);
398
+ const knownWorker = uploadedWorker || Boolean(j.resources.worker?.id);
399
+ if (!knownWorker && attempts.some((attempt) => attempt.phase !== 'preparing_upload'))
400
+ throw new CloudflareError('outcome_unknown', 6);
401
+ for (const attempt of attempts) {
402
+ for (const settings of [attempt.intended_settings, attempt.prior_settings]) {
403
+ const recorded = (kind) => settings === attempt.prior_settings ||
404
+ attempt.changes.some((change) => change.kind === kind && change.restoration !== 'restored');
405
+ for (const route of settings.routes) {
406
+ if (route.script !== j.identity.script_name)
407
+ throw new CloudflareError('ownership_conflict', 8);
408
+ if (!route.id && !recorded('routes'))
409
+ continue;
410
+ if (route.id)
411
+ add({
412
+ name: 'route',
413
+ path: `/zones/${route.zone_id}/workers/routes/${route.id}`,
414
+ receipts: [{ id: route.id, pattern: route.pattern, script: route.script }],
415
+ });
416
+ else
417
+ unresolved = true;
418
+ }
419
+ if (settings.domain && (settings.domain.id || recorded('domain'))) {
420
+ if (settings.domain.service !== j.identity.script_name)
421
+ throw new CloudflareError('ownership_conflict', 8);
422
+ if (settings.domain.id)
423
+ add({
424
+ name: 'domain',
425
+ path: `${base}/workers/domains/${settings.domain.id}`,
426
+ receipts: [{ ...settings.domain }],
427
+ });
428
+ else
429
+ unresolved = true;
430
+ }
431
+ if (settings.consumer && (settings.consumer.consumer_id || recorded('consumer'))) {
432
+ if (settings.consumer.script_name !== j.identity.script_name)
433
+ throw new CloudflareError('ownership_conflict', 8);
434
+ if (settings.consumer.consumer_id && j.resources.jobs?.id) {
435
+ const { consumer_id, script_name, dead_letter_queue, ...consumerSettings } = settings.consumer;
436
+ add({
437
+ name: 'consumer',
438
+ path: `${base}/queues/${j.resources.jobs.id}/consumers/${settings.consumer.consumer_id}`,
439
+ receipts: [
440
+ {
441
+ consumer_id,
442
+ type: 'worker',
443
+ script_name,
444
+ dead_letter_queue,
445
+ settings: consumerSettings,
446
+ },
447
+ ],
448
+ });
449
+ }
450
+ else
451
+ unresolved = true;
452
+ }
453
+ if (settings.schedules.length && recorded('schedules')) {
454
+ if (knownWorker)
455
+ add({
456
+ name: 'schedules',
457
+ path: `${script}/schedules`,
458
+ method: 'PUT',
459
+ body: [],
460
+ crons: settings.schedules,
461
+ });
462
+ else
463
+ unresolved = true;
464
+ }
465
+ }
466
+ }
467
+ // Deleting the inventoried script also removes its versions; never enumerate workers by prefix.
468
+ for (const key of ['worker', 'jobs', 'dlq', 'd1', 'r2']) {
469
+ if (key === 'worker' && uploadedWorker) {
470
+ add({ name: key, path: script });
471
+ continue;
472
+ }
473
+ const r = j.resources[key];
474
+ if (!r)
475
+ continue;
476
+ const operations = j.operations.filter((op) => op.name === r.name);
477
+ const ids = new Set([r.id, ...operations.map((op) => op.resolved_id)].filter((id) => id !== null));
478
+ if (!ids.size) {
479
+ if (r.state !== 'planned' &&
480
+ !(r.state === 'creating' &&
481
+ operations.length > 0 &&
482
+ operations.every((op) => op.state === 'failed')))
483
+ unresolved = true;
484
+ continue;
485
+ }
486
+ for (const id of ids) {
487
+ if (key === 'worker')
488
+ add({ name: key, path: script });
489
+ else if (key === 'r2')
490
+ add({ name: key, path: `${base}/r2/buckets/${id}`, bucket: true });
491
+ else if (key === 'd1')
492
+ add({
493
+ name: key,
494
+ path: `${base}/d1/database/${id}?jurisdiction=${j.identity.jurisdiction}`,
495
+ });
496
+ else
497
+ add({ name: key, path: `${base}/queues/${id}` });
498
+ }
499
+ }
500
+ return { entries, unresolved };
501
+ }
502
+ function sorted(value) {
503
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
504
+ ? Object.fromEntries(Object.entries(value)
505
+ .sort(([a], [b]) => (a < b ? -1 : 1))
506
+ .map(([k, v]) => [k, sorted(v)]))
507
+ : value;
508
+ }
509
+ function liveId(value, pattern = /^[a-zA-Z0-9_-]+$/) {
510
+ if (typeof value !== 'string' || !pattern.test(value))
511
+ throw new CloudflareError('ownership_conflict', 8);
512
+ return value;
513
+ }
514
+ function completeSnapshot(info, length) {
515
+ if (info &&
516
+ ((info.total_pages !== undefined &&
517
+ info.total_pages !== 1 &&
518
+ !(info.total_pages === 0 && length === 0)) ||
519
+ (info.cursor !== undefined && info.cursor !== '') ||
520
+ (length !== undefined && info.total_count !== undefined && info.total_count !== length)))
521
+ throw new CloudflareError('ownership_conflict', 8);
522
+ }
523
+ function recordedBinding(binding, j, queueIdsByName) {
524
+ const resources = Object.values(j.resources);
525
+ const attempts = [j.deployment?.attempt, j.deployment?.last_verified].filter((attempt) => attempt !== null && attempt !== undefined);
526
+ if (binding.type === 'd1')
527
+ return resources.some((r) => r === j.resources.d1 && r.id !== null && r.id === d1BindingId(binding));
528
+ if (binding.type === 'r2_bucket')
529
+ return Boolean(j.resources.r2?.id &&
530
+ binding.bucket_name === j.resources.r2.name &&
531
+ binding.jurisdiction === j.identity.jurisdiction);
532
+ if (binding.type === 'queue')
533
+ return [j.resources.jobs, j.resources.dlq].some((r) => r?.id &&
534
+ (binding.queue_id !== undefined || binding.queue_name !== undefined) &&
535
+ (binding.queue_id === undefined || binding.queue_id === r.id) &&
536
+ (binding.queue_name === undefined ||
537
+ (binding.queue_name === r.name && queueIdsByName.get(r.name) === r.id)));
538
+ if (binding.type === 'analytics_engine')
539
+ return Boolean(binding.dataset === j.resources.analytics?.name &&
540
+ attempts.some((a) => a.version_id !== null) &&
541
+ (j.resources.analytics?.state === 'binding_pending' ||
542
+ (j.resources.analytics?.state === 'bound' &&
543
+ j.resources.analytics.evidence?.source === 'worker_binding')));
544
+ if (binding.type === 'plain_text') {
545
+ const text = binding.text;
546
+ if (typeof binding.name === 'string' && hashedBindingNames.has(binding.name))
547
+ return (typeof text === 'string' &&
548
+ Boolean(j.deployment?.history?.hashed_bindings?.some((b) => b.name === binding.name && b.sha256.includes(digest(text)))));
549
+ return ((binding.name === 'DEPLOYMENT_ID' && binding.text === j.identity.deployment_id) ||
550
+ (binding.name === 'PB_PUBLIC_URL' && attempts.some((a) => binding.text === a.worker_url)) ||
551
+ Boolean(j.deployment?.history?.plain_bindings.some((b) => b.name === binding.name &&
552
+ typeof binding.text === 'string' &&
553
+ b.texts.includes(binding.text))));
554
+ }
555
+ if (binding.type === 'assets')
556
+ return binding.name === 'ASSETS' && attempts.some((a) => a.assets_sha256 !== null);
557
+ if (binding.type === 'secret_text')
558
+ return (!Object.hasOwn(binding, 'text') &&
559
+ !Object.hasOwn(binding, 'value') &&
560
+ ((binding.name === 'PB_MASTER_KEY' && j.secret_delivery.master_key !== 'not_started') ||
561
+ attempts.some((a) => a.secret_names.includes(String(binding.name)))));
562
+ return false;
563
+ }
564
+ function checkWorkerOwner(settings, j) {
565
+ const tags = object(settings).tags ?? [];
566
+ const deployed = Boolean(j.deployment?.history?.version_ids.length) ||
567
+ [j.deployment?.attempt, j.deployment?.last_verified].some((a) => a?.version_id != null);
568
+ if (!Array.isArray(tags) ||
569
+ (deployed &&
570
+ (!tags.includes('pb-install-v1') || !tags.includes(`pb-owner-${j.identity.owner_digest}`))) ||
571
+ tags.some((tag) => typeof tag !== 'string' ||
572
+ (tag.startsWith('pb-owner-') && tag !== `pb-owner-${j.identity.owner_digest}`)))
573
+ throw new WorkerOwnershipError();
574
+ }
575
+ export class WorkerReferenceScan {
576
+ removed = new Set();
577
+ accounts = new Map();
578
+ bindings = new Map();
579
+ async scripts(api, base) {
580
+ let result = this.accounts.get(base);
581
+ if (!result) {
582
+ result = api.snapshot(`${base}/workers/scripts`);
583
+ this.accounts.set(base, result);
584
+ }
585
+ return (await result).filter((worker) => !this.removed.has(`${base}/workers/scripts/${liveId(worker.id)}`));
586
+ }
587
+ deleted(path) {
588
+ this.removed.add(path);
589
+ }
590
+ references(api, path) {
591
+ let result = this.bindings.get(path);
592
+ if (!result) {
593
+ result = this.readReferences(api, path);
594
+ this.bindings.set(path, result);
595
+ }
596
+ return result;
597
+ }
598
+ async readReferences(api, path) {
599
+ const bindings = [];
600
+ const add = (raw) => {
601
+ if (!Array.isArray(raw))
602
+ throw new CloudflareError('ownership_conflict', 8);
603
+ bindings.push(...raw);
604
+ };
605
+ const settings = await api.request(`${path}/settings`, 'GET');
606
+ if (settings.gone)
607
+ throw new CloudflareError('ownership_conflict', 8);
608
+ completeSnapshot(settings.result_info);
609
+ add(object(settings.result).bindings);
610
+ for (const version of await api.list(`${path}/versions`, 'items')) {
611
+ const id = liveId(version.id);
612
+ const detail = await api.request(`${path}/versions/${id}`, 'GET');
613
+ if (detail.gone)
614
+ throw new CloudflareError('ownership_conflict', 8);
615
+ completeSnapshot(detail.result_info);
616
+ add(object(object(detail.result).resources).bindings);
617
+ }
618
+ return bindings;
619
+ }
620
+ }
621
+ async function preflight(api, entries, j, references) {
622
+ const absent = new Set();
623
+ const base = `/accounts/${j.identity.account_id}`;
624
+ const primary = entries.filter((entry) => ['worker', 'jobs', 'dlq', 'd1', 'r2'].includes(entry.name));
625
+ for (const entry of primary) {
626
+ const path = entry.name === 'worker' ? `${entry.path}/settings` : entry.path;
627
+ const live = await api.request(path, 'GET', undefined, entry.bucket);
628
+ if (live.gone)
629
+ absent.add(entry.path);
630
+ else if (entry.name === 'worker')
631
+ checkWorkerOwner(live.result, j);
632
+ }
633
+ const workers = new Set(primary
634
+ .filter((entry) => entry.name === 'worker' && !absent.has(entry.path))
635
+ .map((entry) => entry.path.split('/').at(-1)));
636
+ const livePrimary = primary.filter((entry) => !absent.has(entry.path));
637
+ const schedules = new Map();
638
+ for (const entry of entries) {
639
+ if (entry.crons !== undefined)
640
+ schedules.set(entry.path, entry.crons);
641
+ if (entry.name === 'worker' && !schedules.has(`${entry.path}/schedules`))
642
+ schedules.set(`${entry.path}/schedules`, []);
643
+ }
644
+ for (const [path, allowed] of schedules) {
645
+ const live = await api.request(path, 'GET');
646
+ if (live.gone) {
647
+ absent.add(path);
648
+ continue;
649
+ }
650
+ const crons = object(live.result).schedules;
651
+ if (!Array.isArray(crons))
652
+ throw new CloudflareError('provider_failure');
653
+ completeSnapshot(live.result_info, crons.length);
654
+ if (crons.some((raw) => !allowed.includes(String(object(raw).cron))))
655
+ throw new CloudflareError('ownership_conflict', 8);
656
+ }
657
+ for (const entry of entries.filter((entry) => ['route', 'domain'].includes(entry.name))) {
658
+ const live = await api.request(entry.path, 'GET');
659
+ if (live.gone)
660
+ absent.add(entry.path);
661
+ else {
662
+ completeSnapshot(live.result_info);
663
+ if (!entry.receipts?.some((receipt) => matchesReceipt(live.result, receipt)))
664
+ throw new CloudflareError('ownership_conflict', 8);
665
+ }
666
+ }
667
+ const queues = new Set(entries.filter((entry) => ['jobs', 'dlq'].includes(entry.name)).map((entry) => entry.path));
668
+ for (const entry of entries.filter((entry) => entry.name === 'consumer'))
669
+ queues.add(entry.path.slice(0, entry.path.lastIndexOf('/consumers/')));
670
+ for (const queue of queues) {
671
+ const consumers = entries.filter((entry) => entry.path.startsWith(`${queue}/consumers/`));
672
+ const records = absent.has(queue) ? [] : await api.snapshot(`${queue}/consumers`);
673
+ if (records.some((record) => !consumers.some((entry) => entry.receipts?.some((receipt) => matchesReceipt(record, receipt)))))
674
+ throw new CloudflareError('ownership_conflict', 8);
675
+ for (const entry of consumers)
676
+ if (!records.some((record) => entry.receipts?.some((r) => matchesReceipt(record, r))))
677
+ absent.add(entry.path);
678
+ }
679
+ if (!livePrimary.length)
680
+ return absent;
681
+ const owns = (name, record, path) => entries.some((entry) => entry.name === name &&
682
+ (path === undefined || entry.path === path) &&
683
+ entry.receipts?.some((receipt) => matchesReceipt(record, receipt)));
684
+ if (workers.size) {
685
+ for (const domain of await api.snapshot(`${base}/workers/domains`)) {
686
+ liveId(domain.service);
687
+ if (workers.has(String(domain.service)) && !owns('domain', domain))
688
+ throw new CloudflareError('ownership_conflict', 8);
689
+ }
690
+ for (const zone of await api.list(`/zones?account.id=${j.identity.account_id}`)) {
691
+ const zoneId = liveId(zone.id, /^[a-f0-9]{32}$/);
692
+ for (const route of await api.snapshot(`/zones/${zoneId}/workers/routes`))
693
+ if (workers.has(String(route.script)) &&
694
+ !owns('route', route, `/zones/${zoneId}/workers/routes/${liveId(route.id)}`))
695
+ throw new CloudflareError('ownership_conflict', 8);
696
+ }
697
+ }
698
+ const selectedQueues = new Map(livePrimary
699
+ .filter((entry) => ['jobs', 'dlq'].includes(entry.name))
700
+ .map((entry) => [
701
+ entry.path.split('/').at(-1),
702
+ j.resources[entry.name].name,
703
+ ]));
704
+ const queueIdsByName = new Map();
705
+ if (workers.size || selectedQueues.size) {
706
+ for (const queue of await api.list(`${base}/queues`)) {
707
+ const id = liveId(queue.queue_id, /^[a-f0-9]{32}$/);
708
+ if (typeof queue.queue_name === 'string') {
709
+ if (queueIdsByName.has(queue.queue_name))
710
+ throw new CloudflareError('ownership_conflict', 8);
711
+ queueIdsByName.set(queue.queue_name, id);
712
+ }
713
+ if (absent.has(`${base}/queues/${id}`))
714
+ throw new CloudflareError('ownership_conflict', 8);
715
+ if (queues.has(`${base}/queues/${id}`))
716
+ continue;
717
+ for (const consumer of await api.snapshot(`${base}/queues/${id}/consumers`)) {
718
+ const script = queueConsumerScript(consumer);
719
+ if ((consumer.type === 'worker' && script === null) ||
720
+ workers.has(script ?? '') ||
721
+ [...selectedQueues].some(([id, name]) => [id, name].includes(String(consumer.dead_letter_queue))))
722
+ throw new CloudflareError('ownership_conflict', 8);
723
+ }
724
+ }
725
+ }
726
+ const selectedBuckets = new Set(livePrimary.filter((entry) => entry.bucket).map((entry) => entry.path.split('/').at(-1)));
727
+ if (selectedQueues.size || selectedBuckets.size) {
728
+ const original = api.jurisdiction;
729
+ try {
730
+ for (const jurisdiction of ['default', 'eu', 'fedramp']) {
731
+ api.jurisdiction = jurisdiction;
732
+ const buckets = await api.list(`${base}/r2/buckets`, 'buckets', true,
733
+ // Accounts without FedRAMP answer 403/10003 to a FedRAMP-scoped bucket listing.
734
+ jurisdiction === 'fedramp' && j.identity.jurisdiction !== 'fedramp');
735
+ const names = new Set(buckets.map((bucket) => liveId(bucket.name)));
736
+ if (jurisdiction === j.identity.jurisdiction)
737
+ for (const name of selectedBuckets)
738
+ names.add(name);
739
+ for (const name of names) {
740
+ const notifications = await api.configuration(`${base}/event_notifications/r2/${name}/configuration`, 'queues', 11015);
741
+ for (const notification of notifications) {
742
+ liveId(notification.queueId, /^[a-f0-9]{32}$/);
743
+ if (!Array.isArray(notification.rules))
744
+ throw new CloudflareError('ownership_conflict', 8);
745
+ if (notification.rules.length &&
746
+ ((jurisdiction === j.identity.jurisdiction && selectedBuckets.has(name)) ||
747
+ selectedQueues.has(String(notification.queueId))))
748
+ throw new CloudflareError('ownership_conflict', 8);
749
+ }
750
+ }
751
+ }
752
+ }
753
+ finally {
754
+ api.jurisdiction = original;
755
+ }
756
+ }
757
+ // Both current settings and inactive versions can retain references to backing resources.
758
+ const scripts = new Set((await references.scripts(api, base)).map((w) => liveId(w.id)));
759
+ for (const worker of workers)
760
+ scripts.add(worker);
761
+ const affectedBinding = (b) => livePrimary.some((entry) => {
762
+ const id = entry.path.split('/').at(-1).split('?')[0];
763
+ return ((entry.name === 'd1' && b.type === 'd1' && d1BindingId(b) === id) ||
764
+ (entry.bucket &&
765
+ b.type === 'r2_bucket' &&
766
+ b.bucket_name === id &&
767
+ (!['default', 'eu', 'fedramp'].includes(String(b.jurisdiction)) ||
768
+ b.jurisdiction === j.identity.jurisdiction)) ||
769
+ (selectedQueues.has(id) &&
770
+ b.type === 'queue' &&
771
+ (b.queue_id === id || b.queue_name === selectedQueues.get(id))) ||
772
+ (entry.name === 'worker' && b.type === 'service' && b.service === id));
773
+ });
774
+ const checkBindings = (bindings, selected) => {
775
+ if (!Array.isArray(bindings))
776
+ throw new CloudflareError('ownership_conflict', 8);
777
+ for (const binding of bindings.map(object)) {
778
+ if (binding.type === 'd1' && d1BindingId(binding) === null)
779
+ throw new CloudflareError('ownership_conflict', 8);
780
+ if (selected ? !recordedBinding(binding, j, queueIdsByName) : affectedBinding(binding))
781
+ throw new CloudflareError('ownership_conflict', 8);
782
+ }
783
+ };
784
+ const attempts = [j.deployment?.attempt, j.deployment?.last_verified].filter((a) => a !== null && a !== undefined);
785
+ const versionIds = new Set(j.deployment?.history?.version_ids ?? []);
786
+ for (const id of attempts.flatMap((a) => [
787
+ a.version_id,
788
+ a.previous_version_id,
789
+ ...a.previous_distribution.map((v) => v.version_id),
790
+ ]))
791
+ versionIds.add(id);
792
+ for (const script of scripts) {
793
+ const path = `${base}/workers/scripts/${script}`;
794
+ const selected = workers.has(script);
795
+ if (!selected) {
796
+ checkBindings(await references.references(api, path), false);
797
+ continue;
798
+ }
799
+ const settings = await api.request(`${path}/settings`, 'GET');
800
+ if (settings.gone)
801
+ throw new CloudflareError('ownership_conflict', 8);
802
+ completeSnapshot(settings.result_info);
803
+ if (selected)
804
+ checkWorkerOwner(settings.result, j);
805
+ checkBindings(object(settings.result).bindings, selected);
806
+ for (const version of await api.list(`${path}/versions`, 'items')) {
807
+ const id = liveId(version.id);
808
+ if (selected && !versionIds.has(id))
809
+ throw new CloudflareError('ownership_conflict', 8);
810
+ const detail = await api.request(`${path}/versions/${id}`, 'GET');
811
+ if (detail.gone)
812
+ throw new CloudflareError('ownership_conflict', 8);
813
+ completeSnapshot(detail.result_info);
814
+ checkBindings(object(object(detail.result).resources).bindings, selected);
815
+ }
816
+ if (selected) {
817
+ for (const secret of await api.snapshot(`${path}/secrets`))
818
+ if (!recordedBinding(secret, j, queueIdsByName))
819
+ throw new CloudflareError('ownership_conflict', 8);
820
+ const endpoint = await api.request(`${path}/subdomain`, 'GET');
821
+ completeSnapshot(endpoint.result_info);
822
+ const candidate = j.deployment?.attempt;
823
+ const previewPolicies = candidate?.version_id &&
824
+ candidate.requested_rollout < 100 &&
825
+ candidate.preview !== 'disabled'
826
+ ? [{ ...candidate.intended_settings.endpoints, previews_enabled: true }]
827
+ : [];
828
+ if (endpoint.gone ||
829
+ ![
830
+ ...attempts.flatMap((a) => [a.prior_settings.endpoints, a.intended_settings.endpoints]),
831
+ ...previewPolicies,
832
+ { enabled: false, previews_enabled: false },
833
+ ].some((receipt) => matchesReceipt(endpoint.result, { ...receipt })))
834
+ throw new CloudflareError('ownership_conflict', 8);
835
+ }
836
+ }
837
+ const r2 = j.resources.r2;
838
+ const implicitBaseline = r2?.lifecycle_baseline === undefined &&
839
+ !!r2?.evidence &&
840
+ ['created', 'pending_create'].includes(r2.evidence.source);
841
+ for (const entry of livePrimary.filter((entry) => entry.bucket)) {
842
+ for (const [suffix, field] of [
843
+ ['domains/custom', 'domains'],
844
+ ['cors', 'rules'],
845
+ ['lifecycle', 'rules'],
846
+ ['lock', 'rules'],
847
+ ]) {
848
+ const rules = await api.configuration(`${entry.path}/${suffix}`, field, suffix === 'cors' ? 10059 : undefined);
849
+ const lifecycle = suffix === 'lifecycle';
850
+ const remaining = (lifecycle
851
+ ? (r2?.lifecycle_baseline ?? (implicitBaseline ? [PROVIDER_DEFAULT_LIFECYCLE_RULE] : []))
852
+ : []).map((expected) => JSON.stringify(sorted(expected)));
853
+ if (lifecycle && implicitBaseline && rules.length !== 1)
854
+ throw new CloudflareError('ownership_conflict', 8);
855
+ for (const rule of rules) {
856
+ const index = remaining.indexOf(JSON.stringify(sorted(lifecycleRule(rule) ? abortOnlyRule(rule) : rule)));
857
+ if (index < 0)
858
+ throw new CloudflareError('ownership_conflict', 8);
859
+ remaining.splice(index, 1);
860
+ }
861
+ }
862
+ for (const suffix of ['domains/managed', 'sippy']) {
863
+ const config = await api.request(`${entry.path}/${suffix}`, 'GET', undefined, true);
864
+ completeSnapshot(config.result_info);
865
+ if (config.gone || object(config.result).enabled !== false)
866
+ throw new CloudflareError('ownership_conflict', 8);
867
+ }
868
+ if (!(await api.drain(entry.path, true)))
869
+ absent.add(entry.path);
870
+ }
871
+ return absent;
872
+ }
873
+ export async function teardown(o, references = new WorkerReferenceScan()) {
874
+ const output = commandEnvelope('teardown', o.dryRun);
875
+ let release;
876
+ let result;
877
+ try {
878
+ const { auth, store } = await cleanupInventory(o);
879
+ if (!o.dryRun)
880
+ release = await store.lock((o.now ?? Date.now)());
881
+ const journal = await store.read();
882
+ if (!journal)
883
+ throw new CloudflareError('invalid_inventory', 2);
884
+ if (o.repositoryId !== undefined &&
885
+ !expiredRun(journal, o.repositoryId, (o.now ?? Date.now)())) {
886
+ output.checks.push({ name: 'inventory', status: 'skipped' });
887
+ }
888
+ else {
889
+ if (journal.identity.run_identity === null &&
890
+ (!o.iUnderstandThisDeletes || o.inventoryId !== journal.identity.owner_digest))
891
+ throw new CloudflareError('invalid_input', 2);
892
+ if (o.inventoryId !== undefined && o.inventoryId !== journal.identity.owner_digest)
893
+ throw new CloudflareError('invalid_input', 2);
894
+ if (!o.iUnderstandThisDeletes &&
895
+ [journal.resources.d1, journal.resources.r2].some((resource) => resource?.evidence?.source === 'adopted'))
896
+ throw new CloudflareError('invalid_input', 2);
897
+ const plan = deletions(journal);
898
+ const order = (entry) => ['schedules', 'consumer', 'route', 'domain'].includes(entry.name)
899
+ ? 0
900
+ : entry.name === 'worker'
901
+ ? 1
902
+ : entry.bucket
903
+ ? 2
904
+ : 3;
905
+ plan.entries.sort((a, b) => order(a) - order(b));
906
+ output.plan = plan.entries.map((entry) => ({
907
+ stage: 'teardown',
908
+ action: entry.bucket ? 'drain_then_delete' : 'delete',
909
+ kind: entry.name,
910
+ }));
911
+ const api = new CleanupApi(o, auth);
912
+ api.jurisdiction = journal.identity.jurisdiction;
913
+ const absent = o.dryRun
914
+ ? new Set()
915
+ : await preflight(api, plan.entries, journal, references);
916
+ for (const entry of plan.entries) {
917
+ if (o.dryRun)
918
+ continue;
919
+ if (absent.has(entry.path)) {
920
+ if (entry.name === 'worker')
921
+ references.deleted(entry.path);
922
+ output.checks.push({ name: entry.name, status: 'already_gone' });
923
+ continue;
924
+ }
925
+ let gone = false;
926
+ if (entry.bucket) {
927
+ let finished = false;
928
+ for (let attempt = 0; attempt < 4; attempt++) {
929
+ if (!(await api.drain(entry.path))) {
930
+ gone = true;
931
+ finished = true;
932
+ break;
933
+ }
934
+ try {
935
+ gone = (await api.request(entry.path, 'DELETE', undefined, true)).gone;
936
+ finished = true;
937
+ break;
938
+ }
939
+ catch (error) {
940
+ if (!(error instanceof CloudflareError) || error.status !== 409)
941
+ throw error;
942
+ if (attempt === 3) {
943
+ if (api.drained === 0)
944
+ throw new MultipartCleanupError();
945
+ throw error;
946
+ }
947
+ }
948
+ }
949
+ if (!finished)
950
+ throw new CloudflareError('provider_failure');
951
+ }
952
+ else
953
+ gone = (await api.request(entry.path, entry.method ?? 'DELETE', entry.body)).gone;
954
+ if (entry.name === 'worker')
955
+ references.deleted(entry.path);
956
+ output.checks.push({ name: entry.name, status: gone ? 'already_gone' : 'deleted' });
957
+ }
958
+ if (plan.unresolved)
959
+ throw new CloudflareError('outcome_unknown', 6);
960
+ }
961
+ output.status = o.dryRun ? 'planned' : 'ok';
962
+ result = { exitCode: 0, output };
963
+ }
964
+ catch (error) {
965
+ result = failCommand(output, cleanupFailure(error, 'teardown'));
966
+ }
967
+ try {
968
+ await release?.();
969
+ }
970
+ catch (error) {
971
+ if (result.exitCode === 0)
972
+ result = failCommand(output, cleanupFailure(error, 'teardown'));
973
+ }
974
+ return result;
975
+ }
976
+ export async function runCloudflareTeardown(o) {
977
+ const result = await teardown(o);
978
+ if (o.json || !result.output.error)
979
+ console.log(JSON.stringify(result.output));
980
+ else
981
+ console.error(`${result.output.error.code}: ${result.output.error.message}`);
982
+ return result.exitCode;
983
+ }
984
+ //# sourceMappingURL=teardown.js.map