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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) 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 +801 -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 +129 -0
  40. package/dist/commands/cloudflare/api.d.ts.map +1 -0
  41. package/dist/commands/cloudflare/api.js +480 -0
  42. package/dist/commands/cloudflare/api.js.map +1 -0
  43. package/dist/commands/cloudflare/deploy.d.ts +81 -0
  44. package/dist/commands/cloudflare/deploy.d.ts.map +1 -0
  45. package/dist/commands/cloudflare/deploy.js +1848 -0
  46. package/dist/commands/cloudflare/deploy.js.map +1 -0
  47. package/dist/commands/cloudflare/inventory.d.ts +243 -0
  48. package/dist/commands/cloudflare/inventory.d.ts.map +1 -0
  49. package/dist/commands/cloudflare/inventory.js +786 -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/provision.d.ts +79 -0
  60. package/dist/commands/cloudflare/provision.d.ts.map +1 -0
  61. package/dist/commands/cloudflare/provision.js +961 -0
  62. package/dist/commands/cloudflare/provision.js.map +1 -0
  63. package/dist/commands/cloudflare/queue-consumer.d.ts +4 -0
  64. package/dist/commands/cloudflare/queue-consumer.d.ts.map +1 -0
  65. package/dist/commands/cloudflare/queue-consumer.js +57 -0
  66. package/dist/commands/cloudflare/queue-consumer.js.map +1 -0
  67. package/dist/commands/cloudflare/setup.d.ts +48 -0
  68. package/dist/commands/cloudflare/setup.d.ts.map +1 -0
  69. package/dist/commands/cloudflare/setup.js +836 -0
  70. package/dist/commands/cloudflare/setup.js.map +1 -0
  71. package/dist/commands/cloudflare/smoke.d.ts +29 -0
  72. package/dist/commands/cloudflare/smoke.d.ts.map +1 -0
  73. package/dist/commands/cloudflare/smoke.js +715 -0
  74. package/dist/commands/cloudflare/smoke.js.map +1 -0
  75. package/dist/commands/cloudflare/teardown.d.ts +62 -0
  76. package/dist/commands/cloudflare/teardown.d.ts.map +1 -0
  77. package/dist/commands/cloudflare/teardown.js +906 -0
  78. package/dist/commands/cloudflare/teardown.js.map +1 -0
  79. package/dist/commands/cloudflare/worker-settings.d.ts +4 -0
  80. package/dist/commands/cloudflare/worker-settings.d.ts.map +1 -0
  81. package/dist/commands/cloudflare/worker-settings.js +93 -0
  82. package/dist/commands/cloudflare/worker-settings.js.map +1 -0
  83. package/dist/commands/cloudflare.d.ts +55 -0
  84. package/dist/commands/cloudflare.d.ts.map +1 -0
  85. package/dist/commands/cloudflare.js +393 -0
  86. package/dist/commands/cloudflare.js.map +1 -0
  87. package/dist/commands/import-cutover.d.ts +5 -0
  88. package/dist/commands/import-cutover.d.ts.map +1 -0
  89. package/dist/commands/import-cutover.js +25 -0
  90. package/dist/commands/import-cutover.js.map +1 -0
  91. package/dist/commands/import-private-packagist.d.ts +38 -0
  92. package/dist/commands/import-private-packagist.d.ts.map +1 -0
  93. package/dist/commands/import-private-packagist.js +232 -0
  94. package/dist/commands/import-private-packagist.js.map +1 -0
  95. package/dist/commands/import-satis.d.ts +11 -0
  96. package/dist/commands/import-satis.d.ts.map +1 -0
  97. package/dist/commands/import-satis.js +112 -0
  98. package/dist/commands/import-satis.js.map +1 -0
  99. package/dist/commands/import.d.ts +22 -0
  100. package/dist/commands/import.d.ts.map +1 -0
  101. package/dist/commands/import.js +343 -0
  102. package/dist/commands/import.js.map +1 -0
  103. package/dist/commands/openapi-specs.d.ts +4 -0
  104. package/dist/commands/openapi-specs.d.ts.map +1 -0
  105. package/dist/commands/openapi-specs.js +94 -0
  106. package/dist/commands/openapi-specs.js.map +1 -0
  107. package/dist/commands/openapi.d.ts +12 -0
  108. package/dist/commands/openapi.d.ts.map +1 -0
  109. package/dist/commands/openapi.js +44 -0
  110. package/dist/commands/openapi.js.map +1 -0
  111. package/dist/commands/source.d.ts +8 -0
  112. package/dist/commands/source.d.ts.map +1 -0
  113. package/dist/commands/source.js +61 -0
  114. package/dist/commands/source.js.map +1 -0
  115. package/dist/commands/sqlite.d.ts +11 -0
  116. package/dist/commands/sqlite.d.ts.map +1 -0
  117. package/dist/commands/sqlite.js +76 -0
  118. package/dist/commands/sqlite.js.map +1 -0
  119. package/dist/import/client.d.ts +71 -0
  120. package/dist/import/client.d.ts.map +1 -0
  121. package/dist/import/client.js +348 -0
  122. package/dist/import/client.js.map +1 -0
  123. package/dist/import/credential-refs.d.ts +2 -0
  124. package/dist/import/credential-refs.d.ts.map +1 -0
  125. package/dist/import/credential-refs.js +22 -0
  126. package/dist/import/credential-refs.js.map +1 -0
  127. package/dist/import/cutover.d.ts +64 -0
  128. package/dist/import/cutover.d.ts.map +1 -0
  129. package/dist/import/cutover.js +544 -0
  130. package/dist/import/cutover.js.map +1 -0
  131. package/dist/import/private-packagist/api.d.ts +57 -0
  132. package/dist/import/private-packagist/api.d.ts.map +1 -0
  133. package/dist/import/private-packagist/api.js +246 -0
  134. package/dist/import/private-packagist/api.js.map +1 -0
  135. package/dist/import/private-packagist/dates.d.ts +30 -0
  136. package/dist/import/private-packagist/dates.d.ts.map +1 -0
  137. package/dist/import/private-packagist/dates.js +189 -0
  138. package/dist/import/private-packagist/dates.js.map +1 -0
  139. package/dist/import/private-packagist/map.d.ts +48 -0
  140. package/dist/import/private-packagist/map.d.ts.map +1 -0
  141. package/dist/import/private-packagist/map.js +162 -0
  142. package/dist/import/private-packagist/map.js.map +1 -0
  143. package/dist/import/private-packagist/plan.d.ts +61 -0
  144. package/dist/import/private-packagist/plan.d.ts.map +1 -0
  145. package/dist/import/private-packagist/plan.js +936 -0
  146. package/dist/import/private-packagist/plan.js.map +1 -0
  147. package/dist/import/private-packagist/snapshot.d.ts +31 -0
  148. package/dist/import/private-packagist/snapshot.d.ts.map +1 -0
  149. package/dist/import/private-packagist/snapshot.js +406 -0
  150. package/dist/import/private-packagist/snapshot.js.map +1 -0
  151. package/dist/import/satis.d.ts +93 -0
  152. package/dist/import/satis.d.ts.map +1 -0
  153. package/dist/import/satis.js +873 -0
  154. package/dist/import/satis.js.map +1 -0
  155. package/dist/index.d.ts +3 -2
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +3 -96
  158. package/dist/index.js.map +1 -1
  159. package/dist/manifest.d.ts +55 -0
  160. package/dist/manifest.d.ts.map +1 -0
  161. package/dist/manifest.js +125 -0
  162. package/dist/manifest.js.map +1 -0
  163. package/dist/migrations-collect.d.ts +6 -0
  164. package/dist/migrations-collect.d.ts.map +1 -0
  165. package/dist/migrations-collect.js +39 -0
  166. package/dist/migrations-collect.js.map +1 -0
  167. package/dist/prebuilt-expected.json +7 -0
  168. package/dist/prebuilt.d.ts +27 -0
  169. package/dist/prebuilt.d.ts.map +1 -0
  170. package/dist/prebuilt.js +391 -0
  171. package/dist/prebuilt.js.map +1 -0
  172. package/package.json +21 -25
  173. package/README.md +0 -29
  174. package/dist/__tests__/migrate.test.d.ts +0 -2
  175. package/dist/__tests__/migrate.test.d.ts.map +0 -1
  176. package/dist/__tests__/migrate.test.js +0 -116
  177. package/dist/__tests__/migrate.test.js.map +0 -1
  178. package/dist/migrate.d.ts +0 -40
  179. package/dist/migrate.d.ts.map +0 -1
  180. package/dist/migrate.js +0 -335
  181. package/dist/migrate.js.map +0 -1
@@ -0,0 +1,715 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { normalizeQueueConsumer } from './queue-consumer.js';
4
+ import { CloudflareApi, CloudflareError, cloudflareCredentials, commandEnvelope, failCommand, object, } from './api.js';
5
+ import { reconcileCandidatePreview, rollbackDeployment, active, cloudflareDeployContract, versionAnnotationsMatch, withCandidatePreview, } from './deploy.js';
6
+ import { installIdentity, InventoryStore, } from './inventory.js';
7
+ const invalid = () => {
8
+ throw new CloudflareError('invalid_input', 2, false, 'smoke');
9
+ };
10
+ const conflict = () => {
11
+ throw new CloudflareError('ownership_conflict', 8, false, 'smoke');
12
+ };
13
+ const failed = () => {
14
+ throw new CloudflareError('smoke_failed', 9, false, 'smoke');
15
+ };
16
+ const record = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
17
+ function origin(raw) {
18
+ try {
19
+ const u = new URL(raw);
20
+ if (u.protocol !== 'https:' ||
21
+ u.origin !== raw ||
22
+ u.port ||
23
+ u.username ||
24
+ u.password ||
25
+ u.hostname === 'localhost' ||
26
+ !u.hostname.includes('.'))
27
+ return invalid();
28
+ return u.origin;
29
+ }
30
+ catch {
31
+ return invalid();
32
+ }
33
+ }
34
+ const sorted = (d) => [...d].sort((a, b) => a.version_id.localeCompare(b.version_id));
35
+ const equalDistribution = (a, b) => JSON.stringify(sorted(a)) === JSON.stringify(sorted(b));
36
+ function candidate(a) {
37
+ return [
38
+ { version_id: a.version_id, percentage: a.requested_rollout },
39
+ ...(a.requested_rollout === 100
40
+ ? []
41
+ : [{ version_id: a.previous_version_id, percentage: 100 - a.requested_rollout }]),
42
+ ];
43
+ }
44
+ async function liveDistribution(api, base) {
45
+ const live = await active(api, base);
46
+ if (!live.versions.length)
47
+ return conflict();
48
+ return live.versions;
49
+ }
50
+ async function ownership(api, base, j) {
51
+ const settings = object((await api.request(`${base}/settings`)).result);
52
+ if (!Array.isArray(settings.tags) ||
53
+ !settings.tags.includes('pb-install-v1') ||
54
+ !settings.tags.includes(`pb-owner-${j.identity.owner_digest}`) ||
55
+ !Array.isArray(settings.bindings))
56
+ return conflict();
57
+ const bindings = settings.bindings.map(object);
58
+ for (const b of bindings) {
59
+ if (b.type === 'secret_text' && (Object.hasOwn(b, 'text') || Object.hasOwn(b, 'value')))
60
+ return conflict();
61
+ if (b.name === 'DEPLOYMENT_ID' &&
62
+ (b.type !== 'plain_text' || b.text !== j.identity.deployment_id))
63
+ return conflict();
64
+ if (b.type === 'd1' && (b.name !== 'DB' || b.id !== j.resources.d1?.id))
65
+ return conflict();
66
+ if (b.type === 'r2_bucket' &&
67
+ (b.bucket_name !== j.resources.r2?.name || b.jurisdiction !== j.identity.jurisdiction))
68
+ return conflict();
69
+ if (b.type === 'queue' && b.queue_name !== j.resources.jobs?.name)
70
+ return conflict();
71
+ if (b.type === 'analytics_engine' && b.dataset !== j.resources.analytics?.name)
72
+ return conflict();
73
+ }
74
+ const urls = bindings.filter((b) => b.name === 'PB_PUBLIC_URL');
75
+ if (urls.length !== 1 || urls[0].type !== 'plain_text' || typeof urls[0].text !== 'string')
76
+ return conflict();
77
+ return origin(urls[0].text);
78
+ }
79
+ async function endpointPolicy(api, base, canonical, a, restored = false) {
80
+ const p = object((await api.request(`${base}/subdomain`)).result);
81
+ if (typeof p.enabled !== 'boolean' || typeof p.previews_enabled !== 'boolean')
82
+ return failed();
83
+ const enabled = a && a.intended_settings.endpoints.enabled && (!restored || a.prior_settings.endpoints.enabled);
84
+ const reconciling = a?.phase === 'rollback_pending' &&
85
+ p.enabled === (a.prior_settings.endpoints.enabled && a.intended_settings.endpoints.enabled);
86
+ if (p.previews_enabled || (a && p.enabled !== enabled && !reconciling))
87
+ return failed();
88
+ if (a && (a.intended_settings.domain || a.intended_settings.routes.length) && p.enabled)
89
+ return failed();
90
+ if (!a) {
91
+ const domains = await api.list('/workers/domains');
92
+ const custom = !new URL(canonical).hostname.endsWith('.workers.dev') ||
93
+ domains.some((d) => d.service === base.slice('/workers/scripts/'.length));
94
+ if (custom && p.enabled)
95
+ return failed();
96
+ }
97
+ }
98
+ async function verifyRestoredSettings(api, base, j) {
99
+ const a = j.deployment.attempt;
100
+ const expected = structuredClone(a.prior_settings);
101
+ for (const change of a.changes) {
102
+ if (change.kind === 'endpoints')
103
+ continue;
104
+ if (change.restoration === 'retained_first_install')
105
+ Object.assign(expected, { [change.kind]: a.intended_settings[change.kind] });
106
+ else if (change.restoration !== 'restored')
107
+ return failed();
108
+ }
109
+ const schedules = object((await api.request(`${base}/schedules`)).result).schedules;
110
+ if (!Array.isArray(schedules) ||
111
+ !isDeepStrictEqual(schedules.map((s) => object(s).cron).sort(), [...expected.schedules].sort()))
112
+ return failed();
113
+ const consumers = j.resources.jobs?.id
114
+ ? await api.snapshot(`/queues/${j.resources.jobs.id}/consumers`)
115
+ : [];
116
+ if (j.resources.dlq?.id && (await api.snapshot(`/queues/${j.resources.dlq.id}/consumers`)).length)
117
+ return failed();
118
+ if (consumers.length !== (expected.consumer ? 1 : 0))
119
+ return failed();
120
+ if (expected.consumer) {
121
+ const c = normalizeQueueConsumer(consumers[0]);
122
+ if (!c ||
123
+ !isDeepStrictEqual({ ...c, consumer_id: null }, { ...expected.consumer, consumer_id: null }))
124
+ return failed();
125
+ }
126
+ const routeKey = (r) => JSON.stringify([r.zone_id, r.pattern, r.script]);
127
+ const routes = [];
128
+ const zones = new Set([...a.prior_settings.routes, ...a.intended_settings.routes].map((r) => r.zone_id));
129
+ for (const zone of zones) {
130
+ let complete = false;
131
+ for (let page = 1; page <= 10_000; page++) {
132
+ const response = await api.requestZone(zone, `/workers/routes?page=${page}&per_page=100`);
133
+ if (!Array.isArray(response.result))
134
+ return failed();
135
+ for (const raw of response.result) {
136
+ const r = object(raw);
137
+ if (r.script === j.identity.script_name)
138
+ routes.push(routeKey({ zone_id: zone, pattern: r.pattern, script: r.script }));
139
+ }
140
+ const pages = response.result_info?.total_pages;
141
+ if (pages === page || (pages === 0 && response.result.length === 0)) {
142
+ complete = true;
143
+ break;
144
+ }
145
+ if (typeof pages !== 'number' || !Number.isSafeInteger(pages) || pages < page)
146
+ return failed();
147
+ }
148
+ if (!complete)
149
+ return failed();
150
+ }
151
+ const allowed = new Set(expected.routes.map(routeKey));
152
+ if (routes.some((r) => !allowed.has(r)) ||
153
+ a.prior_settings.routes.some((r) => !routes.includes(routeKey(r))))
154
+ return failed();
155
+ const domains = (await api.list('/workers/domains')).filter((d) => d.service === j.identity.script_name || d.hostname === expected.domain?.hostname);
156
+ if (domains.length !== (expected.domain ? 1 : 0))
157
+ return failed();
158
+ if (expected.domain &&
159
+ !['zone_id', 'hostname', 'service', 'environment'].every((key) => domains[0][key] === expected.domain[key]))
160
+ return failed();
161
+ }
162
+ async function candidateArtifact(api, base, a) {
163
+ const v = object((await api.request(`${base}/versions/${a.version_id}`)).result);
164
+ const annotations = object(v.annotations);
165
+ if (v.id !== a.version_id || !versionAnnotationsMatch(annotations, a))
166
+ return failed();
167
+ }
168
+ function errorCode(body) {
169
+ return record(body) &&
170
+ record(body.error) &&
171
+ typeof body.error.code === 'string' &&
172
+ typeof body.error.message === 'string' &&
173
+ typeof body.error.requestId === 'string'
174
+ ? body.error.code
175
+ : null;
176
+ }
177
+ function npmErrorCode(body) {
178
+ return record(body) &&
179
+ typeof body.error === 'string' &&
180
+ typeof body.code === 'string' &&
181
+ typeof body.requestId === 'string'
182
+ ? body.code
183
+ : null;
184
+ }
185
+ function composer(body, project, canonical) {
186
+ if (!record(body.packages) && !(Array.isArray(body.packages) && body.packages.length === 0))
187
+ return false;
188
+ const metadata = body['metadata-url'];
189
+ if (typeof metadata === 'string') {
190
+ // Validate the protocol reference without fetching it or following another origin.
191
+ return (metadata === `/${project}/p2/%package%.json` ||
192
+ metadata === `${canonical}/${project}/p2/%package%.json`);
193
+ }
194
+ return (record(body['provider-includes']) &&
195
+ Object.keys(body['provider-includes']).length > 0 &&
196
+ typeof body['providers-url'] === 'string' &&
197
+ body['providers-url'].startsWith(`/${project}/`));
198
+ }
199
+ async function assertionAttempts(ctx, url, name, kind, project, canonical, authorization) {
200
+ for (let attempt = 1;; attempt++) {
201
+ const started = ctx.now();
202
+ let status = 'failed';
203
+ let delayHeader = null;
204
+ let response;
205
+ const controller = new AbortController();
206
+ const remaining = ctx.deadline - started;
207
+ if (remaining <= 0)
208
+ return failed();
209
+ const timer = setTimeout(() => controller.abort(), Math.min(5000, remaining));
210
+ const aborted = new Promise((_, reject) => controller.signal.addEventListener('abort', () => reject(new Error()), { once: true }));
211
+ try {
212
+ try {
213
+ response = await Promise.race([
214
+ ctx.fetch(url, {
215
+ method: 'GET',
216
+ redirect: 'manual',
217
+ credentials: 'omit',
218
+ cache: 'no-store',
219
+ signal: controller.signal,
220
+ headers: { accept: 'application/json', ...(authorization ? { authorization } : {}) },
221
+ }),
222
+ aborted,
223
+ ]);
224
+ }
225
+ catch {
226
+ status = controller.signal.aborted ? 'timeout' : 'connection';
227
+ if (kind !== 'ready')
228
+ return failed();
229
+ }
230
+ if (response) {
231
+ if (response.redirected || (response.url && response.url !== url))
232
+ return failed();
233
+ if (response.status === 429) {
234
+ status = 'rate_limited';
235
+ delayHeader = response.headers.get('retry-after');
236
+ }
237
+ else {
238
+ if (!/^application\/json(?:\s*;|$)/i.test(response.headers.get('content-type') ?? ''))
239
+ return failed();
240
+ const reader = response.body?.getReader();
241
+ if (!reader)
242
+ return failed();
243
+ const chunks = [];
244
+ let bytes = 0;
245
+ try {
246
+ for (;;) {
247
+ const part = await Promise.race([reader.read(), aborted]);
248
+ if (part.done)
249
+ break;
250
+ bytes += part.value.byteLength;
251
+ if (bytes > 262_144)
252
+ return failed();
253
+ chunks.push(part.value);
254
+ }
255
+ }
256
+ finally {
257
+ void reader.cancel().catch(() => undefined);
258
+ reader.releaseLock();
259
+ }
260
+ let body;
261
+ try {
262
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
263
+ }
264
+ catch {
265
+ return failed();
266
+ }
267
+ if (!record(body))
268
+ return failed();
269
+ if (kind === 'ready' && response.status === 503 && errorCode(body) === 'not_ready') {
270
+ status = 'not_ready';
271
+ }
272
+ else {
273
+ const valid = kind === 'ready'
274
+ ? response.status === 200 && body.status === 'ready'
275
+ : kind === 'negative'
276
+ ? response.status === 401 && errorCode(body) === 'unauthorized'
277
+ : kind === 'npm_negative'
278
+ ? response.status === 401 && npmErrorCode(body) === 'unauthorized'
279
+ : kind === 'composer'
280
+ ? response.status === 200 && composer(body, project, canonical)
281
+ : response.status === 200;
282
+ if (!valid)
283
+ return failed();
284
+ status = 'passed';
285
+ }
286
+ }
287
+ }
288
+ if (ctx.now() >= ctx.deadline) {
289
+ status = 'timeout';
290
+ return failed();
291
+ }
292
+ }
293
+ catch {
294
+ if (controller.signal.aborted)
295
+ status = 'timeout';
296
+ return failed();
297
+ }
298
+ finally {
299
+ clearTimeout(timer);
300
+ controller.abort();
301
+ void response?.body?.cancel().catch(() => undefined);
302
+ ctx.checks.push({
303
+ name,
304
+ status,
305
+ attempt,
306
+ elapsed_ms: Math.max(0, Math.ceil(ctx.now() - started)),
307
+ });
308
+ }
309
+ if (status === 'passed')
310
+ return;
311
+ let delay = Math.min(10_000, 1000 * 2 ** Math.min(attempt - 1, 4));
312
+ if (delayHeader) {
313
+ const requested = /^\d+$/.test(delayHeader)
314
+ ? Number(delayHeader) * 1000
315
+ : Date.parse(delayHeader) - ctx.now();
316
+ if (Number.isFinite(requested) && requested >= 0)
317
+ delay = Math.max(delay, requested);
318
+ }
319
+ if (ctx.now() + delay >= ctx.deadline)
320
+ return failed();
321
+ await ctx.sleep(delay);
322
+ }
323
+ }
324
+ async function assertion(...args) {
325
+ try {
326
+ await assertionAttempts(...args);
327
+ }
328
+ catch (error) {
329
+ await args[0].onFailure?.();
330
+ throw error;
331
+ }
332
+ }
333
+ async function probes(ctx, url, prefix, project, canonical, token) {
334
+ await assertion(ctx, `${url}/health/ready`, `${prefix}_ready`, 'ready', project, canonical);
335
+ await assertion(ctx, `${url}/${project}/packages.json`, `${prefix}_composer_unauthorized`, 'negative', project, canonical);
336
+ await assertion(ctx, `${url}/${project}/npm/-/ping`, `${prefix}_npm_unauthorized`, 'npm_negative', project, canonical);
337
+ if (token) {
338
+ await assertion(ctx, `${url}/${project}/packages.json`, `${prefix}_composer_authorized`, 'composer', project, canonical, `Basic ${Buffer.from(`token:${token}`).toString('base64')}`);
339
+ await assertion(ctx, `${url}/${project}/npm/-/ping`, `${prefix}_npm_authorized`, 'npm', project, canonical, `Bearer ${token}`);
340
+ }
341
+ }
342
+ export async function smoke(o) {
343
+ const output = commandEnvelope('smoke', o.dryRun);
344
+ let release;
345
+ let result = { exitCode: 1, output };
346
+ try {
347
+ const seconds = o.deadlineSeconds ?? 120;
348
+ if (!Number.isInteger(seconds) || seconds < 1 || seconds > 300)
349
+ return invalid();
350
+ const tokenName = o.smokeTokenEnv ?? 'PB_SMOKE_TOKEN';
351
+ if (!/^[A-Z][A-Z0-9_]{0,127}$/.test(tokenName) ||
352
+ ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID'].includes(tokenName))
353
+ return invalid();
354
+ const token = o.env[tokenName];
355
+ const project = o.project ?? 'pb-smoke-unassigned';
356
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(project) ||
357
+ ['health', '_pb', 'auth', 'setup', 'callback', 'login'].includes(project) ||
358
+ (token && (!o.project || o.project === 'pb-smoke-unassigned')) ||
359
+ (o.smokeTokenEnv !== undefined && !token) ||
360
+ (token && (!/^[\x21-\x7e]+$/.test(token) || token === o.env.CLOUDFLARE_API_TOKEN)))
361
+ return invalid();
362
+ if (o.publicUrl)
363
+ origin(o.publicUrl);
364
+ const auth = cloudflareCredentials(o.env);
365
+ const identity = installIdentity({
366
+ ...auth,
367
+ workerName: o.workerName,
368
+ ...(o.environment === undefined ? {} : { environment: o.environment }),
369
+ ...(o.deploymentId === undefined ? {} : { deploymentId: o.deploymentId }),
370
+ ...(o.jurisdiction === undefined ? {} : { jurisdiction: o.jurisdiction }),
371
+ });
372
+ const root = resolve(o.projectRoot ?? dirname(resolve(o.configPath ?? 'broker.config.ts')));
373
+ const store = await InventoryStore.create({
374
+ project: root,
375
+ outDir: resolve(root, o.outDir ?? '.pb/cloudflare'),
376
+ identity,
377
+ ...(o.inventory === undefined ? {} : { inventory: o.inventory }),
378
+ });
379
+ if (o.transaction)
380
+ o.transaction.assertStore(store);
381
+ const now = o.now ?? Date.now;
382
+ const sleep = o.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
383
+ const transport = o.fetch ?? fetch;
384
+ const end = now() + seconds * 1000;
385
+ const apiOptions = {
386
+ ...auth,
387
+ fetch: transport,
388
+ now,
389
+ sleep,
390
+ jitter: () => 0,
391
+ minimumMutationBudgetMs: 1,
392
+ };
393
+ const api = new CloudflareApi({ ...apiOptions, deadline: end });
394
+ if (!o.dryRun && !o.transaction)
395
+ release = await store.lock(now());
396
+ const j = await store.read();
397
+ if (!j ||
398
+ !j.resources.d1?.id ||
399
+ j.resources.d1.state !== 'complete' ||
400
+ j.operations.some((op) => ['pending', 'unknown'].includes(op.state)))
401
+ return invalid();
402
+ const a = j.deployment?.attempt;
403
+ if (a &&
404
+ (!a.version_id ||
405
+ !a.activated_at ||
406
+ !['active', 'verified', 'failed', 'rollback_pending', 'rolled_back'].includes(a.phase)))
407
+ return invalid();
408
+ if (a &&
409
+ a.requested_rollout < 100 &&
410
+ (!a.candidate_probe_url || !a.previous_version_id || a.candidate_probe_url === a.worker_url))
411
+ return invalid();
412
+ if (a &&
413
+ (a.intended_settings.endpoints.previews_enabled ||
414
+ ((a.intended_settings.domain || a.intended_settings.routes.length) &&
415
+ a.intended_settings.endpoints.enabled)))
416
+ return invalid();
417
+ const previousReceipt = j.deployment?.last_verified;
418
+ const previousOrigin = a && previousReceipt?.version_id === a.previous_version_id
419
+ ? previousReceipt?.worker_url
420
+ : undefined;
421
+ output.database_id = j.resources.d1.id;
422
+ output.r2_bucket = j.resources.r2?.id ?? null;
423
+ output.queue_names = [j.resources.jobs?.name, j.resources.dlq?.name].filter((n) => n !== undefined);
424
+ output.migrations_applied = o.transaction?.recordAppliedMigrations([]) ?? [];
425
+ if (a) {
426
+ origin(a.worker_url);
427
+ if (a.candidate_probe_url)
428
+ origin(a.candidate_probe_url);
429
+ output.version_id = a.version_id;
430
+ output.previous_version_id = a.previous_version_id;
431
+ output.build_info = a.build_info;
432
+ output.artifact_sha256 = a.artifact_sha256;
433
+ }
434
+ const save = async () => {
435
+ j.revision++;
436
+ j.updated_at = new Date(now()).toISOString();
437
+ await store.write(j);
438
+ };
439
+ const base = `/workers/scripts/${identity.script_name}`;
440
+ const liveOrigin = await ownership(api, base, j);
441
+ const canonical = a?.worker_url ?? liveOrigin;
442
+ if (o.publicUrl && o.publicUrl !== canonical)
443
+ return conflict();
444
+ output.worker_url = canonical;
445
+ const prior = a?.previous_version_id
446
+ ? [{ version_id: a.previous_version_id, percentage: 100 }]
447
+ : [];
448
+ const initialDistribution = await liveDistribution(api, base);
449
+ const assertDistribution = async (client, observed) => {
450
+ const live = observed ?? (await liveDistribution(client, base));
451
+ if (a
452
+ ? !equalDistribution(live, candidate(a)) &&
453
+ !(['rollback_pending', 'rolled_back'].includes(a.phase) &&
454
+ equalDistribution(live, prior))
455
+ : !equalDistribution(live, initialDistribution)) {
456
+ output.rollback.status = 'conflict';
457
+ return conflict();
458
+ }
459
+ return live;
460
+ };
461
+ const current = await assertDistribution(api, initialDistribution);
462
+ const restored = a &&
463
+ ['rollback_pending', 'rolled_back'].includes(a.phase) &&
464
+ equalDistribution(current, prior);
465
+ const expectedOrigin = restored ? previousOrigin : canonical;
466
+ if (expectedOrigin && liveOrigin !== expectedOrigin)
467
+ return conflict();
468
+ const blocked = a &&
469
+ (!a.rollback_compatible ||
470
+ a.contract_ids.length ||
471
+ !j.migrations.rollback_compatible ||
472
+ j.migrations.contract_ids.length);
473
+ if (o.dryRun) {
474
+ await endpointPolicy(api, base, canonical, a, !!restored);
475
+ output.status = 'planned';
476
+ for (const prefix of a && a.requested_rollout < 100
477
+ ? ['canonical', 'candidate']
478
+ : ['canonical'])
479
+ for (const name of [
480
+ 'ready',
481
+ 'composer_unauthorized',
482
+ 'npm_unauthorized',
483
+ ...(token ? ['composer_authorized', 'npm_authorized'] : []),
484
+ ])
485
+ output.plan.push({
486
+ stage: 'smoke',
487
+ action: `${prefix}_${name}`,
488
+ name: `GET ${prefix === 'candidate' ? a?.candidate_probe_url : canonical}${name === 'ready' ? '/health/ready' : name.startsWith('composer') ? `/${project}/packages.json` : `/${project}/npm/-/ping`}`,
489
+ });
490
+ output.rollback = {
491
+ status: !a
492
+ ? 'unavailable_no_receipt'
493
+ : !a.previous_version_id
494
+ ? 'unavailable_first_deploy'
495
+ : o.noRollback
496
+ ? 'disabled'
497
+ : blocked
498
+ ? 'blocked_contract'
499
+ : 'eligible',
500
+ target_version_id: a?.previous_version_id ?? null,
501
+ };
502
+ result = { exitCode: 0, output };
503
+ return result;
504
+ }
505
+ const ctx = { fetch: transport, now, sleep, deadline: end, checks: [] };
506
+ if (a)
507
+ ctx.onFailure = async () => {
508
+ a.smoke = 'failed';
509
+ a.smoke_checks = structuredClone(ctx.checks);
510
+ await save();
511
+ };
512
+ let recoveryEnd;
513
+ const recoveryDeadline = () => (recoveryEnd ??= now() + 60_000);
514
+ let probeError;
515
+ if (a?.smoke === 'failed') {
516
+ output.checks.push(...(a.smoke_checks ?? []));
517
+ probeError = new CloudflareError('smoke_failed', 9, false, 'smoke');
518
+ }
519
+ else {
520
+ try {
521
+ if (a?.preview !== undefined && a.preview !== 'disabled') {
522
+ const cleanup = new CloudflareApi({
523
+ ...apiOptions,
524
+ deadline: Math.min(end, now() + 60_000),
525
+ });
526
+ await reconcileCandidatePreview(cleanup, store, j);
527
+ }
528
+ await endpointPolicy(api, base, canonical, a);
529
+ if (a)
530
+ await candidateArtifact(api, base, a);
531
+ await probes(ctx, canonical, 'canonical', project, canonical, token);
532
+ if (a && a.requested_rollout < 100) {
533
+ if (!a.candidate_probe_url || !(o.contract ?? cloudflareDeployContract).preview)
534
+ throw new CloudflareError('capability_unknown', 2, false, 'smoke');
535
+ const previewApi = new CloudflareApi({
536
+ ...apiOptions,
537
+ deadline: end,
538
+ recoveryDeadline,
539
+ });
540
+ await withCandidatePreview(previewApi, store, j, o.contract ?? cloudflareDeployContract, async (url) => {
541
+ await probes(ctx, origin(url), 'candidate', project, canonical, token);
542
+ });
543
+ }
544
+ await assertDistribution(api);
545
+ await endpointPolicy(api, base, canonical, a);
546
+ }
547
+ catch (error) {
548
+ probeError = error;
549
+ }
550
+ output.checks.push(...ctx.checks);
551
+ if (a) {
552
+ a.smoke_checks = ctx.checks;
553
+ a.smoke = probeError ? 'failed' : 'passed';
554
+ if (!probeError) {
555
+ a.rollback_compatible &&= j.migrations.rollback_compatible;
556
+ a.contract_ids = [...new Set([...a.contract_ids, ...j.migrations.contract_ids])];
557
+ a.phase = 'verified';
558
+ j.deployment.last_verified = structuredClone(a);
559
+ j.migrations.rollback_compatible = true;
560
+ j.migrations.contract_ids = [];
561
+ }
562
+ await save();
563
+ }
564
+ }
565
+ if (!probeError) {
566
+ result = { exitCode: 0, output };
567
+ return result;
568
+ }
569
+ if (probeError instanceof CloudflareError && probeError.exitCode === 8) {
570
+ output.rollback.status = 'conflict';
571
+ throw probeError;
572
+ }
573
+ if (!a) {
574
+ await assertDistribution(new CloudflareApi({ ...apiOptions, deadline: recoveryDeadline() }));
575
+ output.rollback.status = o.noRollback ? 'disabled' : 'unavailable_no_receipt';
576
+ result = failCommand(output, new CloudflareError('smoke_failed', 9, false, 'smoke'));
577
+ return result;
578
+ }
579
+ const recoveryApi = new CloudflareApi({ ...apiOptions, deadline: recoveryDeadline() });
580
+ try {
581
+ await ownership(recoveryApi, base, j);
582
+ await assertDistribution(recoveryApi);
583
+ await reconcileCandidatePreview(recoveryApi, store, j);
584
+ try {
585
+ await endpointPolicy(recoveryApi, base, canonical, a, a.phase === 'rolled_back');
586
+ }
587
+ catch {
588
+ a.preview = 'cleanup_pending';
589
+ await save();
590
+ await reconcileCandidatePreview(recoveryApi, store, j);
591
+ await endpointPolicy(recoveryApi, base, canonical, a, a.phase === 'rolled_back');
592
+ }
593
+ if (!a.previous_version_id) {
594
+ output.rollback.status = 'unavailable_first_deploy';
595
+ throw new CloudflareError('rollback_unavailable_first_deploy', 12, false, 'rollback');
596
+ }
597
+ if (blocked && !o.noRollback) {
598
+ output.rollback = { status: 'blocked_contract', target_version_id: a.previous_version_id };
599
+ output.pending_migrations = [...new Set([...a.contract_ids, ...j.migrations.contract_ids])];
600
+ throw new CloudflareError('rollback_blocked_contract', 11, false, 'rollback');
601
+ }
602
+ output.rollback.target_version_id = a.previous_version_id;
603
+ if (o.noRollback) {
604
+ output.rollback.status = 'disabled';
605
+ await endpointPolicy(recoveryApi, base, canonical, a, a.phase === 'rolled_back');
606
+ }
607
+ else {
608
+ let outcome = a.rollback;
609
+ if (outcome === 'not_needed' || (a.phase === 'rollback_pending' && outcome !== 'failed')) {
610
+ outcome = await rollbackDeployment(recoveryApi, store, j);
611
+ }
612
+ output.rollback.status = outcome;
613
+ if (outcome !== 'succeeded')
614
+ throw new CloudflareError('rollback_failed', 10, false, 'rollback');
615
+ if (!equalDistribution(await liveDistribution(recoveryApi, base), prior)) {
616
+ output.rollback.status = 'conflict';
617
+ return conflict();
618
+ }
619
+ const restoredOrigin = await ownership(recoveryApi, base, j);
620
+ if (previousOrigin && restoredOrigin !== previousOrigin)
621
+ return conflict();
622
+ const ready = {
623
+ ...ctx,
624
+ onFailure: undefined,
625
+ deadline: Math.min(recoveryDeadline(), now() + 30_000),
626
+ checks: [],
627
+ };
628
+ try {
629
+ await assertion(ready, `${restoredOrigin}/health/ready`, 'rollback_ready', 'ready', project, canonical);
630
+ }
631
+ catch {
632
+ a.rollback = 'failed';
633
+ output.rollback.status = 'failed';
634
+ throw new CloudflareError('rollback_failed', 10, false, 'rollback');
635
+ }
636
+ finally {
637
+ output.checks.push(...ready.checks);
638
+ const candidateChecks = (a.smoke_checks ?? []).filter((c) => c.name !== 'rollback_ready');
639
+ const recoveryCapacity = 4000 - candidateChecks.length;
640
+ a.smoke_checks = [
641
+ ...candidateChecks,
642
+ ...ready.checks.slice(Math.max(0, ready.checks.length - recoveryCapacity)),
643
+ ];
644
+ await save();
645
+ }
646
+ await endpointPolicy(recoveryApi, base, canonical, a, true);
647
+ try {
648
+ await verifyRestoredSettings(recoveryApi, base, j);
649
+ }
650
+ catch {
651
+ a.rollback = 'failed';
652
+ output.rollback.status = 'failed';
653
+ await save();
654
+ throw new CloudflareError('rollback_failed', 10, false, 'rollback');
655
+ }
656
+ if (!equalDistribution(await liveDistribution(recoveryApi, base), prior)) {
657
+ output.rollback.status = 'conflict';
658
+ return conflict();
659
+ }
660
+ }
661
+ result = failCommand(output, new CloudflareError('smoke_failed', 9, false, 'smoke'));
662
+ }
663
+ catch (error) {
664
+ const safe = error instanceof CloudflareError ? error : null;
665
+ if (safe?.exitCode === 8)
666
+ output.rollback.status = 'conflict';
667
+ else if (![11, 12].includes(safe?.exitCode ?? 0)) {
668
+ if (!['failed', 'unknown'].includes(output.rollback.status))
669
+ output.rollback.status = 'unknown';
670
+ result = failCommand(output, new CloudflareError('rollback_failed', 10, false, 'rollback'));
671
+ return result;
672
+ }
673
+ result = failCommand(output, error);
674
+ }
675
+ return result;
676
+ }
677
+ catch (error) {
678
+ if (error instanceof CloudflareError && error.exitCode === 8)
679
+ output.rollback.status = 'conflict';
680
+ result = failCommand(output, error);
681
+ return result;
682
+ }
683
+ finally {
684
+ try {
685
+ await release?.();
686
+ }
687
+ catch (error) {
688
+ Object.assign(result, failCommand(output, error));
689
+ }
690
+ }
691
+ }
692
+ export async function runCloudflareSmoke(o) {
693
+ const result = await smoke(o);
694
+ if (o.json)
695
+ console.log(JSON.stringify(result.output));
696
+ else {
697
+ const info = result.output.build_info;
698
+ if (info) {
699
+ console.log(`revision: ${info.gitSha ?? 'null'}; dirty: ${info.dirty ?? 'null'}`);
700
+ console.log(`artifact_sha256: ${result.output.artifact_sha256 ?? 'null'}`);
701
+ }
702
+ for (const entry of result.output.plan)
703
+ console.error(`${entry.action}: ${entry.name}`);
704
+ for (const check of result.output.checks)
705
+ console.error(`${check.name}: ${check.status} (attempt ${check.attempt ?? 1}, ${check.elapsed_ms ?? 0}ms)`);
706
+ if (result.exitCode !== 0 || o.dryRun)
707
+ console.error(`rollback: ${result.output.rollback.status}`);
708
+ if (result.output.error)
709
+ console.error(result.output.error.message);
710
+ else
711
+ console.log(o.dryRun ? 'Cloudflare smoke planned.' : 'Cloudflare smoke passed.');
712
+ }
713
+ return result.exitCode;
714
+ }
715
+ //# sourceMappingURL=smoke.js.map