@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,1848 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { lstat, readFile, realpath } from 'node:fs/promises';
3
+ import { dirname, extname, isAbsolute, relative, resolve, sep } from 'node:path';
4
+ import { createOtlpExporter } from '@package-broker/core/platform/otlp';
5
+ import { parseUiManifest } from '@package-broker/core/platform/ui-manifest';
6
+ import { TRACE_FIELDS, validatePlatformPorts, } from '@package-broker/core/platform/port-config';
7
+ import { PLUGIN_BINDABLE_PORTS } from '@package-broker/core/plugin';
8
+ import { assertPrebuiltConfiguration, materializePrebuilt, prebuiltEnvelope, removePrebuilt, } from '../../prebuilt.js';
9
+ import { builtRouteProblems, loadBuiltMigrations } from '../built-manifest.js';
10
+ import { normalizeQueueConsumer } from './queue-consumer.js';
11
+ import { assertMigrationsApplied } from './migrations-check.js';
12
+ import { d1BindingId, workerSettingsDifferences } from './worker-settings.js';
13
+ import { CloudflareApi, CloudflareError, cloudflareCredentials, commandEnvelope, failCommand, object, wasNotSent, } from './api.js';
14
+ import { InventoryStore, InventoryTransaction, digest, installIdentity, recordDeploymentHistory, DeploymentHistoryCapacityError, safeBuildInfo, } from './inventory.js';
15
+ export function tracesMetadata(traces) {
16
+ if (traces === undefined)
17
+ return undefined;
18
+ return Object.fromEntries(Object.entries(TRACE_FIELDS)
19
+ .filter(([key]) => traces[key] !== undefined)
20
+ .map(([key, field]) => [field, traces[key]]));
21
+ }
22
+ const COMPATIBILITY = { compatibility_date: '2026-10-01', compatibility_flags: ['nodejs_compat'] };
23
+ const ASSET_CONFIG = { run_worker_first: true, html_handling: 'none', not_found_handling: 'none' };
24
+ const uuid = (v) => typeof v === 'string' &&
25
+ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(v);
26
+ const safeId = (v) => typeof v === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(v);
27
+ const invalid = () => {
28
+ throw new CloudflareError('invalid_input', 2, false, 'deploy');
29
+ };
30
+ const conflict = (field, ...fields) => {
31
+ throw new CloudflareError('configuration_conflict', 8, false, 'deploy', null, null, null, null, [
32
+ field,
33
+ ...fields,
34
+ ]);
35
+ };
36
+ const unknown = () => {
37
+ throw new CloudflareError('outcome_unknown', 6, false, 'deploy');
38
+ };
39
+ function canonical(value) {
40
+ if (Array.isArray(value))
41
+ return `[${value.map(canonical).join(',')}]`;
42
+ if (value && typeof value === 'object')
43
+ return `{${Object.entries(value)
44
+ .sort(([a], [b]) => a.localeCompare(b))
45
+ .map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`)
46
+ .join(',')}}`;
47
+ return JSON.stringify(value);
48
+ }
49
+ const equal = (a, b) => canonical(a) === canonical(b);
50
+ function definitiveRejection(error) {
51
+ return (error instanceof CloudflareError &&
52
+ error.status !== null &&
53
+ error.status >= 400 &&
54
+ error.status < 500 &&
55
+ ![408, 409, 429].includes(error.status));
56
+ }
57
+ /** The provider refused it, or the client never sent it: either way nothing was applied. */
58
+ const knownNotApplied = (error) => definitiveRejection(error) || wasNotSent(error);
59
+ function validateTelemetry(endpoint, rawHeaders) {
60
+ try {
61
+ const headers = rawHeaders === undefined ? {} : JSON.parse(rawHeaders);
62
+ if (!headers ||
63
+ typeof headers !== 'object' ||
64
+ Array.isArray(headers) ||
65
+ Object.values(headers).some((v) => typeof v !== 'string'))
66
+ return invalid();
67
+ createOtlpExporter({ endpoint, headers: headers });
68
+ }
69
+ catch {
70
+ return invalid();
71
+ }
72
+ }
73
+ function origin(raw) {
74
+ try {
75
+ const u = new URL(raw);
76
+ if (u.protocol !== 'https:' ||
77
+ u.username ||
78
+ u.password ||
79
+ u.search ||
80
+ u.hash ||
81
+ u.pathname !== '/' ||
82
+ u.port ||
83
+ u.hostname === 'localhost' ||
84
+ !u.hostname.includes('.'))
85
+ return invalid();
86
+ return u.origin;
87
+ }
88
+ catch {
89
+ return invalid();
90
+ }
91
+ }
92
+ function hostname(raw) {
93
+ if (raw.length > 253 ||
94
+ !raw.split('.').every((p) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(p)) ||
95
+ !raw.includes('.'))
96
+ return invalid();
97
+ return raw;
98
+ }
99
+ async function artifactFile(root, name) {
100
+ const file = resolve(root, name);
101
+ const rel = relative(root, file);
102
+ if (!rel || rel.startsWith(`..${sep}`) || isAbsolute(rel))
103
+ return invalid();
104
+ let current = root;
105
+ for (const part of rel.split(sep)) {
106
+ current = resolve(current, part);
107
+ const stat = await lstat(current);
108
+ if (stat.isSymbolicLink() || (current === file ? !stat.isFile() : !stat.isDirectory()))
109
+ return invalid();
110
+ }
111
+ if ((await realpath(file)) !== file)
112
+ return invalid();
113
+ return readFile(file);
114
+ }
115
+ export async function validateDeploymentArtifact(out) {
116
+ try {
117
+ const root = await realpath(resolve(out));
118
+ const files = new Map();
119
+ const read = async (name) => {
120
+ const b = await artifactFile(root, name);
121
+ files.set(name, b);
122
+ return b;
123
+ };
124
+ const module = await read('worker.js');
125
+ if (module.length === 0)
126
+ return invalid();
127
+ const info = safeBuildInfo(JSON.parse((await read('build-info.json')).toString('utf8')));
128
+ const manifest = object(JSON.parse((await read('generated/manifest.json')).toString('utf8')));
129
+ if (manifest.schemaVersion !== 1 ||
130
+ manifest.platform !== 'cloudflare' ||
131
+ !Array.isArray(manifest.plugins) ||
132
+ !Array.isArray(manifest.bindings) ||
133
+ manifest.bindings.some((b) => !PLUGIN_BINDABLE_PORTS.includes(object(b).port)))
134
+ return invalid();
135
+ const ports = manifest.platformPorts;
136
+ if (ports !== null &&
137
+ (!ports || validatePlatformPorts({ cloudflare: ports }, 'cloudflare').length))
138
+ return invalid();
139
+ if (ports &&
140
+ (ports.blob.adapter !== 'r2' ||
141
+ ports.cache.adapter !== 'cache-api' ||
142
+ ports.jobs.adapter !== 'queues' ||
143
+ ports.observability.adapter !== 'workers'))
144
+ return invalid();
145
+ const entries = manifest.migrations;
146
+ if (!Array.isArray(entries))
147
+ return invalid();
148
+ for (const entry of entries) {
149
+ const e = object(entry);
150
+ if (typeof e.id !== 'string' ||
151
+ !/^[a-z0-9-]+:\d{4}_[a-z0-9_]+$/.test(e.id) ||
152
+ typeof e.name !== 'string' ||
153
+ !/^\d{4}_[a-z0-9_]+\.sql$/.test(e.name))
154
+ return invalid();
155
+ await read(`migrations/${e.id.split(':')[0]}/${e.name}`);
156
+ }
157
+ const migrations = await loadBuiltMigrations(root);
158
+ const routes = await builtRouteProblems(root);
159
+ if (routes === null || routes.length)
160
+ return invalid();
161
+ // T811's exact record contains the relative deployment descriptor; no second asset schema.
162
+ const record = object(JSON.parse((await read('assets.json')).toString('utf8')));
163
+ const descriptor = object(record.descriptor);
164
+ if (record.schemaVersion !== 1 ||
165
+ descriptor.directory !== './assets' ||
166
+ descriptor.binding !== 'ASSETS' ||
167
+ !equal(descriptor, { directory: './assets', binding: 'ASSETS', ...ASSET_CONFIG }) ||
168
+ typeof record.digest !== 'string')
169
+ return invalid();
170
+ const provider = object(record.provider);
171
+ if (typeof provider.specifier !== 'string' || typeof provider.version !== 'string')
172
+ return invalid();
173
+ const manifestBytes = await read('assets/manifest.json');
174
+ const ui = parseUiManifest(manifestBytes.toString('utf8'));
175
+ if (Object.hasOwn(ui.files, 'manifest.json'))
176
+ return invalid();
177
+ const assets = [
178
+ { name: 'manifest.json', content: manifestBytes, contentType: 'application/json' },
179
+ ];
180
+ for (const name of Object.keys(ui.files).sort()) {
181
+ const content = await read(`assets/${name}`);
182
+ if (content.length !== ui.files[name].bytes)
183
+ return invalid();
184
+ assets.push({ name, content, contentType: ui.files[name].contentType });
185
+ }
186
+ // Asset staging hashes bytes, including non-UTF8 files.
187
+ const byteHash = (b) => createHash('sha256').update(b).digest('hex');
188
+ const exactAssetsHash = digest(JSON.stringify(assets.map((f) => [f.name, byteHash(f.content)])));
189
+ if (record.digest !== `sha256:${exactAssetsHash}`)
190
+ return invalid();
191
+ const artifactHash = digest(canonical({
192
+ files: [...files].sort(([a], [b]) => a.localeCompare(b)).map(([n, b]) => [n, byteHash(b)]),
193
+ compatibility: COMPATIBILITY,
194
+ assets: ASSET_CONFIG,
195
+ ports,
196
+ }));
197
+ return { module, info, ports, assets, assetsHash: exactAssetsHash, artifactHash, migrations };
198
+ }
199
+ catch {
200
+ return invalid();
201
+ }
202
+ }
203
+ export const unverifiedDeployContract = {
204
+ versionsCreateScript: false,
205
+ secretInheritance: false,
206
+ };
207
+ /** Cloudflare's direct-upload example hashes base64 bytes plus the extension without its dot. */
208
+ export function cloudflareAssetHash(bytes, name) {
209
+ return createHash('sha256')
210
+ .update(bytes.toString('base64') + extname(name).substring(1))
211
+ .digest('hex')
212
+ .slice(0, 32);
213
+ }
214
+ export const cloudflareDeployContract = {
215
+ versionsCreateScript: false,
216
+ secretInheritance: true,
217
+ assetHash: cloudflareAssetHash,
218
+ createScript: async (api, script, form) => {
219
+ const result = object((await api.request(`/workers/scripts/${script}`, 'PUT', form)).result);
220
+ if (result.id !== script)
221
+ return unknown();
222
+ },
223
+ };
224
+ export async function active(api, base) {
225
+ const raw = object((await api.request(`${base}/deployments`)).result);
226
+ // Cloudflare documents deployments[0] as the deployment actively serving traffic.
227
+ if (!Array.isArray(raw.deployments))
228
+ return conflict('deployment');
229
+ if (!raw.deployments.length)
230
+ return { id: null, versions: [] };
231
+ const d = object(raw.deployments[0]);
232
+ if (!safeId(d.id) || !Array.isArray(d.versions) || d.versions.length < 1 || d.versions.length > 2)
233
+ return conflict('deployment');
234
+ const versions = d.versions.map((v) => {
235
+ const x = object(v);
236
+ if (!uuid(x.version_id) ||
237
+ typeof x.percentage !== 'number' ||
238
+ !Number.isInteger(x.percentage) ||
239
+ x.percentage <= 0 ||
240
+ x.percentage > 100)
241
+ return conflict('deployment');
242
+ return { version_id: x.version_id, percentage: x.percentage };
243
+ });
244
+ if (versions.reduce((n, v) => n + v.percentage, 0) !== 100 ||
245
+ new Set(versions.map((v) => v.version_id)).size !== versions.length)
246
+ return conflict('deployment');
247
+ return { id: d.id, versions: versions.sort((a, b) => a.version_id.localeCompare(b.version_id)) };
248
+ }
249
+ function distribution(a) {
250
+ if (!a.version_id)
251
+ return unknown();
252
+ return [
253
+ { version_id: a.version_id, percentage: a.requested_rollout },
254
+ ...(a.requested_rollout === 100
255
+ ? []
256
+ : [{ version_id: a.previous_version_id, percentage: 100 - a.requested_rollout }]),
257
+ ].sort((x, y) => x.version_id.localeCompare(y.version_id));
258
+ }
259
+ async function endpoints(api, base) {
260
+ const r = object((await api.request(`${base}/subdomain`)).result);
261
+ if (typeof r.enabled !== 'boolean' || typeof r.previews_enabled !== 'boolean')
262
+ throw new CloudflareError('capability_unknown');
263
+ return { enabled: r.enabled, previews_enabled: r.previews_enabled };
264
+ }
265
+ function routeInput(raw, script) {
266
+ if (raw === undefined)
267
+ return [];
268
+ const lines = raw.split('\n');
269
+ if (!lines.length || lines.some((l) => !l))
270
+ return invalid();
271
+ const routes = lines.map((line) => {
272
+ const match = /^([a-f0-9]{32}):([a-zA-Z0-9*./:-]{1,256})$/.exec(line);
273
+ if (!match)
274
+ return invalid();
275
+ return { id: null, zone_id: match[1], pattern: match[2], script };
276
+ });
277
+ if (new Set(routes.map((r) => `${r.zone_id}:${r.pattern}`)).size !== routes.length)
278
+ return invalid();
279
+ return routes.sort((a, b) => a.pattern.localeCompare(b.pattern));
280
+ }
281
+ function secretNames(o) {
282
+ const names = [...(o.secret ?? []), ...(o.secrets === undefined ? [] : o.secrets.split('\n'))];
283
+ if (new Set(names).size !== names.length ||
284
+ names.some((n) => !/^[A-Z][A-Z0-9_]{0,127}$/.test(n) || n === 'PB_TOKEN_PEPPER'))
285
+ return invalid();
286
+ return names.sort();
287
+ }
288
+ function bindingField(b, field) {
289
+ const type = ['d1', 'plain_text', 'r2_bucket', 'queue', 'analytics_engine', 'assets'].includes(String(b.type))
290
+ ? String(b.type)
291
+ : 'unknown';
292
+ const name = typeof b.name === 'string' && /^[A-Z][A-Z0-9_]{0,127}$/.test(b.name) ? b.name : 'unknown';
293
+ return `binding:${type}:${name}:${field}`;
294
+ }
295
+ function normalizedBinding(b) {
296
+ if (b.type === 'd1') {
297
+ if (b.database_id !== undefined && b.id !== undefined && b.database_id !== b.id)
298
+ return conflict('binding:d1:DB:id');
299
+ const normalized = { ...b, id: d1BindingId(b) };
300
+ delete normalized.database_id;
301
+ return normalized;
302
+ }
303
+ if (b.type === 'r2_bucket' && b.jurisdiction === undefined)
304
+ return { ...b, jurisdiction: 'default' };
305
+ return b;
306
+ }
307
+ function bindingDifferences(actual, expected) {
308
+ const fields = [];
309
+ for (const b of expected) {
310
+ const matches = actual.filter((x) => x.name === b.name);
311
+ if (matches.length !== 1) {
312
+ fields.push(bindingField(b, 'presence'));
313
+ continue;
314
+ }
315
+ const x = normalizedBinding(matches[0]);
316
+ for (const key of Object.keys(b))
317
+ if (!equal(x[key], b[key]))
318
+ fields.push(bindingField(b, key));
319
+ if (Object.keys(x).some((key) => !Object.hasOwn(b, key)))
320
+ fields.push(bindingField(b, 'configuration'));
321
+ }
322
+ if (actual.some((x) => !expected.some((b) => b.name === x.name)))
323
+ fields.push('bindings:unexpected');
324
+ return fields;
325
+ }
326
+ function verifyAssets(value) {
327
+ if (!value || typeof value !== 'object' || Array.isArray(value))
328
+ return conflict('assets:config');
329
+ const config = value;
330
+ for (const [key, expected] of Object.entries(ASSET_CONFIG)) {
331
+ const actual = key === 'run_worker_first' && equal(config[key], ['/*']) ? true : config[key];
332
+ if (!equal(actual, expected))
333
+ return conflict(`assets:${key}`);
334
+ }
335
+ if (config.base_path !== undefined && config.base_path !== '/')
336
+ return conflict('assets:base_path');
337
+ if (Object.keys(config).some((k) => !Object.hasOwn(ASSET_CONFIG, k) && k !== 'base_path'))
338
+ return conflict('assets:config');
339
+ }
340
+ async function fullVersion(api, base, id) {
341
+ const script = base.split('/').at(-1);
342
+ const workers = (await api.snapshot('/workers/scripts')).filter((w) => w.id === script);
343
+ if (workers.length !== 1)
344
+ return conflict('worker:identity');
345
+ // The live scripts listing may carry no worker_id; /workers/workers/{id} also accepts the name.
346
+ const listed = workers[0].worker_id;
347
+ const workerId = typeof listed === 'string' && listed !== '' ? listed : script;
348
+ if (!safeId(workerId))
349
+ return conflict('worker:identity');
350
+ const path = `/workers/workers/${workerId}`;
351
+ const v = object((await api.request(`${path}/versions/${id}`)).result);
352
+ if (v.id !== id)
353
+ return conflict('version_id');
354
+ return v;
355
+ }
356
+ async function completeR2Bindings(api, base, raw, j) {
357
+ if (j.identity.jurisdiction === 'default' ||
358
+ !raw.some((b) => b.type === 'r2_bucket' && b.jurisdiction === undefined))
359
+ return raw;
360
+ // An omitted jurisdiction is not evidence of an EU binding. Read the exact active version.
361
+ const current = await active(api, base);
362
+ if (current.versions.length !== 1 || current.versions[0].percentage !== 100)
363
+ return conflict('r2:jurisdiction');
364
+ const full = await fullVersion(api, base, current.versions[0].version_id);
365
+ if (!Array.isArray(full.bindings))
366
+ return conflict('r2:jurisdiction');
367
+ return raw.map((b) => {
368
+ if (b.type !== 'r2_bucket' || b.jurisdiction !== undefined)
369
+ return b;
370
+ const matches = full.bindings
371
+ .map(object)
372
+ .filter((x) => x.type === b.type && x.name === b.name && x.bucket_name === b.bucket_name);
373
+ if (matches.length !== 1 || matches[0].jurisdiction !== j.identity.jurisdiction)
374
+ return conflict('r2:jurisdiction');
375
+ return { ...b, jurisdiction: matches[0].jurisdiction };
376
+ });
377
+ }
378
+ async function ownedWorkerSettings(api, base, j) {
379
+ const s = object((await api.request(`${base}/settings`)).result);
380
+ if (!Array.isArray(s.tags) ||
381
+ !s.tags.includes('pb-install-v1') ||
382
+ !s.tags.includes(`pb-owner-${j.identity.owner_digest}`) ||
383
+ !Array.isArray(s.bindings))
384
+ throw new CloudflareError('ownership_conflict', 8, false, 'deploy');
385
+ s.bindings = await completeR2Bindings(api, base, s.bindings.map(object), j);
386
+ for (const raw of s.bindings) {
387
+ if (raw.type === 'd1' &&
388
+ raw.database_id !== undefined &&
389
+ raw.id !== undefined &&
390
+ raw.database_id !== raw.id)
391
+ return conflict('worker_settings:binding:DB:id');
392
+ const b = normalizedBinding(raw);
393
+ if (b.type === 'secret_text' && (Object.hasOwn(b, 'text') || Object.hasOwn(b, 'value')))
394
+ return conflict('worker_settings:bindings:secret_text');
395
+ if (b.type === 'd1') {
396
+ if (b.name !== 'DB')
397
+ return conflict('worker_settings:binding:DB:presence');
398
+ if (b.id !== j.resources.d1?.id)
399
+ return conflict('worker_settings:binding:DB:id');
400
+ }
401
+ if (b.type === 'r2_bucket') {
402
+ if (b.bucket_name !== j.resources.r2?.name)
403
+ return conflict('worker_settings:r2:bucket_name');
404
+ if (b.jurisdiction !== j.identity.jurisdiction)
405
+ return conflict('worker_settings:r2:jurisdiction');
406
+ }
407
+ if (b.type === 'queue' && b.queue_name !== j.resources.jobs?.name)
408
+ return conflict('worker_settings:binding:JOBS:queue_name');
409
+ if (b.type === 'analytics_engine' && b.dataset !== j.resources.analytics?.name)
410
+ return conflict('worker_settings:analytics_engine:dataset');
411
+ if (b.name === 'DEPLOYMENT_ID' &&
412
+ (b.type !== 'plain_text' || b.text !== j.identity.deployment_id))
413
+ return conflict('worker_settings:binding:DEPLOYMENT_ID:text');
414
+ }
415
+ return s;
416
+ }
417
+ async function liveResources(api, j, ports) {
418
+ const resources = [
419
+ ['d1', '/d1/database'],
420
+ ...(ports?.blob.adapter === 'r2' ? [['r2', '/r2/buckets']] : []),
421
+ ...(ports?.jobs.adapter === 'queues'
422
+ ? [
423
+ ['jobs', '/queues'],
424
+ ['dlq', '/queues'],
425
+ ]
426
+ : []),
427
+ ];
428
+ for (const [key, path] of resources) {
429
+ const r = j.resources[key];
430
+ if (!r?.id || r.state !== 'complete' || !r.evidence)
431
+ return invalid();
432
+ const remote = object((await api.request(`${path}/${r.id}`, 'GET', undefined, key === 'r2' ? { 'cf-r2-jurisdiction': j.identity.jurisdiction } : undefined)).result);
433
+ if ((key === 'r2' ? remote.name : (remote.uuid ?? remote.id ?? remote.queue_id)) !== r.id ||
434
+ (key === 'jobs' || key === 'dlq' ? remote.queue_name : remote.name) !== r.name)
435
+ return conflict(`${key}:identity`);
436
+ if ((key === 'r2' || key === 'd1') && remote.jurisdiction !== j.identity.jurisdiction)
437
+ throw new CloudflareError('jurisdiction_conflict', 8);
438
+ }
439
+ }
440
+ export async function deploymentDomain(api, script, domain) {
441
+ let foundDomain = null;
442
+ const domains = await api.snapshot('/workers/domains');
443
+ for (const d of domains) {
444
+ if (d.hostname === domain || d.service === script) {
445
+ if (foundDomain ||
446
+ d.service !== script ||
447
+ d.hostname !== domain ||
448
+ d.environment !== 'production' ||
449
+ !safeId(d.id) ||
450
+ typeof d.zone_id !== 'string' ||
451
+ !/^[a-f0-9]{32}$/.test(d.zone_id))
452
+ return conflict('domain');
453
+ foundDomain = {
454
+ id: d.id,
455
+ zone_id: d.zone_id,
456
+ hostname: hostname(String(d.hostname)),
457
+ service: d.service,
458
+ environment: 'production',
459
+ };
460
+ }
461
+ }
462
+ return foundDomain;
463
+ }
464
+ export async function workersDevOrigin(api, script) {
465
+ const registration = await api.request('/workers/subdomain').catch((error) => {
466
+ if (error instanceof CloudflareError && error.status === 404)
467
+ return { result: {} };
468
+ throw error;
469
+ });
470
+ const sub = object(registration.result).subdomain;
471
+ if (typeof sub !== 'string' || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(sub))
472
+ throw new CloudflareError('workers_dev_subdomain_missing', 2, false, 'deploy');
473
+ return `https://${script}.${sub}.workers.dev`;
474
+ }
475
+ async function readSettings(api, base, j, routes, domain, workerExists = true, managedZones = []) {
476
+ const policy = workerExists
477
+ ? await endpoints(api, base)
478
+ : { enabled: false, previews_enabled: false };
479
+ let schedules = [];
480
+ if (workerExists) {
481
+ const schedule = object((await api.request(`${base}/schedules`)).result);
482
+ if (!Array.isArray(schedule.schedules) ||
483
+ schedule.schedules.some((s) => object(s).cron !== '* * * * *') ||
484
+ schedule.schedules.length > 1)
485
+ return conflict('schedules');
486
+ schedules = schedule.schedules.map((s) => String(object(s).cron));
487
+ }
488
+ let consumer = null;
489
+ if (j.resources.jobs?.id) {
490
+ const consumers = await api.snapshot(`/queues/${j.resources.jobs.id}/consumers`);
491
+ if (consumers.length > 1)
492
+ return conflict('queue_consumer');
493
+ if (j.resources.dlq?.id &&
494
+ (await api.snapshot(`/queues/${j.resources.dlq.id}/consumers`)).length)
495
+ return conflict('queue_consumer');
496
+ if (consumers[0]) {
497
+ const c = normalizeQueueConsumer(consumers[0]);
498
+ if (!c ||
499
+ c.script_name !== j.identity.script_name ||
500
+ c.dead_letter_queue !== j.resources.dlq?.name)
501
+ return conflict('queue_consumer');
502
+ consumer = c;
503
+ }
504
+ }
505
+ const foundRoutes = [];
506
+ for (const zone of new Set([...routes.map((r) => r.zone_id), ...managedZones])) {
507
+ const listed = await zoneRoutes(api, zone);
508
+ for (const r of listed) {
509
+ if (r.script !== j.identity.script_name && typeof r.pattern === 'string') {
510
+ const attachedHost = r.pattern.replace(/^https?:\/\//, '').split('/')[0];
511
+ if (routes.some((x) => {
512
+ const wanted = x.pattern.replace(/^https?:\/\//, '').split('/')[0];
513
+ return (attachedHost === '*' ||
514
+ attachedHost === wanted ||
515
+ (attachedHost.startsWith('*.') && wanted.endsWith(attachedHost.slice(1))));
516
+ }))
517
+ return conflict('routes');
518
+ }
519
+ if (r.script === j.identity.script_name ||
520
+ routes.some((x) => x.zone_id === zone && x.pattern === r.pattern)) {
521
+ if (r.script !== j.identity.script_name ||
522
+ !safeId(r.id) ||
523
+ typeof r.pattern !== 'string' ||
524
+ !/^[a-zA-Z0-9*./:-]{1,256}$/.test(r.pattern))
525
+ return conflict('routes');
526
+ foundRoutes.push({ id: r.id, zone_id: zone, pattern: r.pattern, script: r.script });
527
+ }
528
+ }
529
+ }
530
+ const foundDomain = await deploymentDomain(api, j.identity.script_name, domain);
531
+ return {
532
+ endpoints: policy,
533
+ schedules,
534
+ consumer,
535
+ routes: foundRoutes.sort((a, b) => a.pattern.localeCompare(b.pattern)),
536
+ domain: foundDomain,
537
+ };
538
+ }
539
+ async function zoneRoutes(api, zone) {
540
+ // Zone APIs must never be sent beneath the accounts prefix.
541
+ const all = [];
542
+ for (let page = 1; page <= 10000; page++) {
543
+ const r = await api.requestZone(zone, `/workers/routes?page=${page}&per_page=100`);
544
+ if (!Array.isArray(r.result))
545
+ return conflict('routes');
546
+ all.push(...r.result.map(object));
547
+ if (r.result_info?.total_pages === page || r.result_info?.total_count === all.length)
548
+ return all;
549
+ if (typeof r.result_info?.total_pages !== 'number' || r.result_info.total_pages < page)
550
+ return conflict('routes');
551
+ }
552
+ return conflict('routes');
553
+ }
554
+ function bindings(j, ports, url) {
555
+ const b = [
556
+ { type: 'd1', name: 'DB', id: j.resources.d1.id },
557
+ { type: 'plain_text', name: 'DEPLOYMENT_ID', text: j.identity.deployment_id },
558
+ { type: 'plain_text', name: 'PB_PUBLIC_URL', text: url },
559
+ ];
560
+ if (ports) {
561
+ b.push({
562
+ type: 'r2_bucket',
563
+ name: ports.blob.binding,
564
+ bucket_name: j.resources.r2.name,
565
+ jurisdiction: j.identity.jurisdiction,
566
+ }, { type: 'queue', name: ports.jobs.binding, queue_name: j.resources.jobs.name });
567
+ if (ports.observability.analyticsBinding)
568
+ b.push({
569
+ type: 'analytics_engine',
570
+ name: ports.observability.analyticsBinding,
571
+ dataset: j.resources.analytics.name,
572
+ });
573
+ }
574
+ b.push({ type: 'assets', name: 'ASSETS' });
575
+ if (new Set(b.map((x) => x.name)).size !== b.length)
576
+ return invalid();
577
+ return b;
578
+ }
579
+ async function uploadAssets(api, base, artifact, hash) {
580
+ const byHash = new Map();
581
+ const manifest = {};
582
+ for (const f of artifact.assets) {
583
+ const h = hash(f.content, f.name);
584
+ if (!/^[a-f0-9]{32,64}$/.test(h))
585
+ throw new CloudflareError('capability_unknown');
586
+ if (byHash.has(h) &&
587
+ (!byHash.get(h).content.equals(f.content) || byHash.get(h).contentType !== f.contentType))
588
+ throw new CloudflareError('capability_unknown');
589
+ byHash.set(h, f);
590
+ manifest[`/${f.name}`] = { hash: h, size: f.content.length };
591
+ }
592
+ const session = object((await api.request(`${base}/assets-upload-session`, 'POST', { manifest })).result);
593
+ if (typeof session.jwt !== 'string' ||
594
+ !/^[A-Za-z0-9_.-]+$/.test(session.jwt) ||
595
+ !Array.isArray(session.buckets))
596
+ throw new CloudflareError('provider_failure');
597
+ let completion = session.jwt;
598
+ const requested = new Set();
599
+ for (const bucket of session.buckets) {
600
+ if (!Array.isArray(bucket) || bucket.length === 0)
601
+ throw new CloudflareError('provider_failure');
602
+ const form = new FormData();
603
+ for (const h of bucket) {
604
+ if (typeof h !== 'string' || !byHash.has(h) || requested.has(h))
605
+ throw new CloudflareError('provider_failure');
606
+ requested.add(h);
607
+ form.set(h, new Blob([byHash.get(h).content.toString('base64')], { type: byHash.get(h).contentType }), h);
608
+ }
609
+ const result = object((await api.uploadAssets(form, session.jwt)).result);
610
+ if (result.jwt !== undefined) {
611
+ if (typeof result.jwt !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(result.jwt))
612
+ throw new CloudflareError('provider_failure');
613
+ completion = result.jwt;
614
+ }
615
+ else if (requested.size === session.buckets.flat().length)
616
+ throw new CloudflareError('provider_failure');
617
+ }
618
+ if (session.buckets.length > 0 && completion === session.jwt)
619
+ throw new CloudflareError('provider_failure');
620
+ return completion;
621
+ }
622
+ function versionAnnotations(a, creation = false) {
623
+ return {
624
+ 'workers/tag': `pb-${a.operation_id}${creation ? '-create' : ''}`,
625
+ 'workers/message': `pb-artifact-sha256:${a.artifact_sha256}`,
626
+ };
627
+ }
628
+ export function versionAnnotationsMatch(annotations, a, creation = a.script_creation?.state === 'complete' &&
629
+ a.script_creation.version_id === a.version_id) {
630
+ const expected = versionAnnotations(a, creation);
631
+ return ((annotations['workers/tag'] === expected['workers/tag'] &&
632
+ annotations['workers/message'] === expected['workers/message']) ||
633
+ (!creation &&
634
+ annotations['pb-operation'] === a.operation_id &&
635
+ annotations['pb-artifact-sha256'] === a.artifact_sha256));
636
+ }
637
+ function beforeCandidate(a) {
638
+ const created = a.script_creation;
639
+ return created?.state === 'complete'
640
+ ? {
641
+ id: created.deployment_id,
642
+ versions: [{ version_id: created.version_id, percentage: 100 }],
643
+ }
644
+ : { id: a.previous_deployment_id, versions: a.previous_distribution };
645
+ }
646
+ async function reconcileScriptCreation(api, store, j, requiredSecrets, expectedBindings) {
647
+ const a = j.deployment.attempt;
648
+ const base = `/workers/scripts/${j.identity.script_name}`;
649
+ await enforceEndpoints(api, store, j, restrictivePolicy(a));
650
+ const versions = await api.list(`${base}/versions`, 'items');
651
+ const matches = versions.filter((v) => versionAnnotationsMatch(object(v.annotations), a, true));
652
+ if (matches.length !== 1 || !uuid(matches[0].id))
653
+ return unknown();
654
+ const id = matches[0].id;
655
+ const created = await active(api, base);
656
+ if (!created.id || created.versions.length !== 1 || created.versions[0].percentage !== 100)
657
+ return unknown();
658
+ if (created.versions[0].version_id !== id)
659
+ return conflict('deployment:version_id');
660
+ await version(api, base, id, a, requiredSecrets, expectedBindings, true);
661
+ a.script_creation = {
662
+ state: 'complete',
663
+ deployment_id: created.id,
664
+ version_id: id,
665
+ };
666
+ a.version_id = id;
667
+ a.phase = 'uploaded';
668
+ recordDeploymentHistory(j.deployment, { versionId: id });
669
+ await save(store, j, api.now);
670
+ }
671
+ async function save(store, j, now) {
672
+ j.revision++;
673
+ j.updated_at = new Date(now()).toISOString();
674
+ await store.write(j);
675
+ }
676
+ export async function reconcileCandidatePreview(api, store, j) {
677
+ const a = j.deployment?.attempt;
678
+ if (!a || a.preview === 'disabled')
679
+ return;
680
+ api.reserveRecovery(1);
681
+ const base = `/workers/scripts/${j.identity.script_name}`;
682
+ await ownedWorkerSettings(api, base, j);
683
+ a.preview = 'cleanup_pending';
684
+ await save(store, j, api.now);
685
+ const live = await endpoints(api, base);
686
+ const expected = {
687
+ enabled: live.enabled && a.intended_settings.endpoints.enabled,
688
+ previews_enabled: false,
689
+ };
690
+ if (!equal(live, expected))
691
+ await api.request(`${base}/subdomain`, 'POST', expected);
692
+ if (!equal(await endpoints(api, base), expected))
693
+ throw new CloudflareError('configuration_conflict', 10, false, 'preview_cleanup');
694
+ a.preview = 'disabled';
695
+ await save(store, j, api.now);
696
+ }
697
+ /** Smoke owns this finally block. Persisted intent also covers process interruption. */
698
+ export async function withCandidatePreview(api, store, j, contract, probe) {
699
+ const a = j.deployment?.attempt;
700
+ if (!a?.version_id || a.requested_rollout === 100 || !contract.preview)
701
+ return invalid();
702
+ await reconcileCandidatePreview(api, store, j);
703
+ const base = `/workers/scripts/${j.identity.script_name}`;
704
+ await ownedWorkerSettings(api, base, j);
705
+ if (!equal((await active(api, base)).versions, distribution(a)))
706
+ return conflict('preview');
707
+ a.preview = 'enable_pending';
708
+ await save(store, j, api.now);
709
+ try {
710
+ await api.request(`${base}/subdomain`, 'POST', {
711
+ ...a.intended_settings.endpoints,
712
+ previews_enabled: true,
713
+ });
714
+ if (!equal(await endpoints(api, base), {
715
+ ...a.intended_settings.endpoints,
716
+ previews_enabled: true,
717
+ }))
718
+ return conflict('preview');
719
+ a.preview = 'enabled';
720
+ await save(store, j, api.now);
721
+ const p = await contract.preview(api, j.identity.script_name, a.version_id);
722
+ if (p.version_id !== a.version_id ||
723
+ !p.bindings_verified ||
724
+ origin(p.url) !== a.candidate_probe_url)
725
+ return conflict('preview');
726
+ return await probe(origin(p.url));
727
+ }
728
+ finally {
729
+ await reconcileCandidatePreview(api, store, j);
730
+ }
731
+ }
732
+ async function writeSetting(api, base, j, kind, value) {
733
+ if (kind === 'endpoints')
734
+ await api.request(`${base}/subdomain`, 'POST', value.endpoints);
735
+ if (kind === 'schedules')
736
+ await api.request(`${base}/schedules`, 'PUT', value.schedules.map((cron) => ({ cron })));
737
+ if (kind === 'consumer' && value.consumer) {
738
+ const { consumer_id, script_name, dead_letter_queue, ...settings } = value.consumer;
739
+ await api.request(`/queues/${j.resources.jobs.id}/consumers${consumer_id ? `/${consumer_id}` : ''}`, consumer_id ? 'PUT' : 'POST', { type: 'worker', script_name, dead_letter_queue, settings });
740
+ }
741
+ if (kind === 'routes')
742
+ for (const r of value.routes)
743
+ await api.requestZone(r.zone_id, `/workers/routes${r.id ? `/${r.id}` : ''}`, r.id ? 'PUT' : 'POST', { pattern: r.pattern, script: r.script });
744
+ if (kind === 'domain' && value.domain) {
745
+ const { id, ...body } = value.domain;
746
+ void id;
747
+ await api.request('/workers/domains', 'PUT', body);
748
+ }
749
+ }
750
+ function restrictivePolicy(a) {
751
+ return {
752
+ enabled: a.prior_settings.endpoints.enabled && a.intended_settings.endpoints.enabled,
753
+ previews_enabled: false,
754
+ };
755
+ }
756
+ async function applyEndpointPolicy(api, store, j, policy) {
757
+ const a = j.deployment.attempt;
758
+ const base = `/workers/scripts/${j.identity.script_name}`;
759
+ if (!equal(await endpoints(api, base), policy)) {
760
+ const change = {
761
+ kind: 'endpoints',
762
+ state: 'pending',
763
+ restoration: 'not_needed',
764
+ };
765
+ a.changes.push(change);
766
+ await save(store, j, api.now);
767
+ try {
768
+ await api.request(`${base}/subdomain`, 'POST', policy);
769
+ if (!equal(await endpoints(api, base), policy))
770
+ return conflict('endpoint_policy');
771
+ change.state = 'applied';
772
+ }
773
+ catch (error) {
774
+ change.state = 'unknown';
775
+ await save(store, j, api.now);
776
+ throw error;
777
+ }
778
+ await save(store, j, api.now);
779
+ }
780
+ }
781
+ /** The only way deploy and rollback change endpoint flags: ownership read, journalled write, read-back. */
782
+ async function enforceEndpoints(api, store, j, policy) {
783
+ await ownedWorkerSettings(api, `/workers/scripts/${j.identity.script_name}`, j);
784
+ await applyEndpointPolicy(api, store, j, policy);
785
+ }
786
+ /** Exit guard: every failure or rollback exit restricts and verifies endpoints, never throwing. */
787
+ async function secureEndpoints(api, store, j) {
788
+ const a = j.deployment.attempt;
789
+ try {
790
+ await enforceEndpoints(api, store, j, restrictivePolicy(a));
791
+ const unresolved = a.changes.filter((c) => c.kind === 'endpoints' && (c.restoration === 'unknown' || c.restoration === 'failed'));
792
+ for (const c of unresolved)
793
+ c.restoration = 'restored';
794
+ if (unresolved.length)
795
+ await save(store, j, api.now);
796
+ return 'verified';
797
+ }
798
+ catch (error) {
799
+ return error instanceof CloudflareError && error.code === 'ownership_conflict'
800
+ ? 'ownership_conflict'
801
+ : 'unverified';
802
+ }
803
+ }
804
+ function recoveryMutations(a) {
805
+ return 4 + a.changes.length + a.prior_settings.routes.length + a.intended_settings.routes.length;
806
+ }
807
+ const routeKey = (r) => `${r.zone_id}:${r.pattern}`;
808
+ function routesWithin(inner, outer) {
809
+ const keys = new Set(outer.map(routeKey));
810
+ return inner.every((r) => keys.has(routeKey(r)));
811
+ }
812
+ /** Live attachments match a managed snapshot; routes are only ever added, so a subset matches. */
813
+ function matchesManaged(live, managed, required = []) {
814
+ const rest = (s) => ({
815
+ ...withoutIds({ ...s, endpoints: managed.endpoints }),
816
+ routes: null,
817
+ });
818
+ return (equal(rest(live), rest(managed)) &&
819
+ routesWithin(live.routes, managed.routes) &&
820
+ routesWithin(required, live.routes));
821
+ }
822
+ /** Settings a failed or rolled-back attempt left behind: prior values, plus retained first-install attachments. */
823
+ function restoredSnapshot(a, observed) {
824
+ const snapshot = structuredClone(a.prior_settings);
825
+ for (const change of a.changes) {
826
+ if (change.kind === 'endpoints')
827
+ continue;
828
+ if (change.restoration === 'retained_first_install')
829
+ Object.assign(snapshot, { [change.kind]: a.intended_settings[change.kind] });
830
+ else if (change.restoration !== 'restored') {
831
+ if (!observed)
832
+ return null;
833
+ const part = (s) => withoutIds(s)[change.kind];
834
+ const prior = a.prior_settings[change.kind];
835
+ if (equal(part(observed), part(a.prior_settings)))
836
+ continue;
837
+ // A formerly unreadable first-install attachment needs exact read-back evidence.
838
+ if ((prior === null || (Array.isArray(prior) && prior.length === 0)) &&
839
+ equal(part(observed), part(a.intended_settings)))
840
+ Object.assign(snapshot, { [change.kind]: a.intended_settings[change.kind] });
841
+ else
842
+ return null;
843
+ }
844
+ }
845
+ return snapshot;
846
+ }
847
+ function withoutIds(s) {
848
+ return {
849
+ ...s,
850
+ consumer: s.consumer && { ...s.consumer, consumer_id: null },
851
+ routes: s.routes.map((r) => ({ ...r, id: null })),
852
+ domain: s.domain && { ...s.domain, id: null },
853
+ };
854
+ }
855
+ async function restoreSettings(api, store, j, routes, domain) {
856
+ const a = j.deployment.attempt, base = `/workers/scripts/${j.identity.script_name}`;
857
+ for (const change of [...a.changes].reverse()) {
858
+ if (change.restoration === 'restored' || change.restoration === 'retained_first_install')
859
+ continue;
860
+ try {
861
+ const live = await readSettings(api, base, j, routes, domain);
862
+ const prior = a.prior_settings[change.kind];
863
+ const safePrior = { ...a.prior_settings, endpoints: restrictivePolicy(a) };
864
+ const part = (v) => withoutIds(v);
865
+ const settle = async (restoration) => {
866
+ change.restoration = restoration;
867
+ await save(store, j, api.now);
868
+ };
869
+ if (equal(part(live)[change.kind], part(safePrior)[change.kind])) {
870
+ await settle('restored');
871
+ continue;
872
+ }
873
+ if (change.kind === 'endpoints') {
874
+ await settle('unknown');
875
+ continue;
876
+ }
877
+ if (change.kind === 'routes') {
878
+ const intendedRoutes = a.intended_settings.routes;
879
+ await settle(routesWithin(a.prior_settings.routes, live.routes) &&
880
+ routesWithin(live.routes, intendedRoutes)
881
+ ? 'retained_first_install'
882
+ : 'unknown');
883
+ continue;
884
+ }
885
+ if (!equal(part(live)[change.kind], part(a.intended_settings)[change.kind])) {
886
+ await settle('unknown');
887
+ continue;
888
+ }
889
+ if ((Array.isArray(prior) && prior.length === 0) || prior === null) {
890
+ await settle('retained_first_install');
891
+ continue;
892
+ }
893
+ await writeSetting(api, base, j, change.kind, safePrior);
894
+ const after = await readSettings(api, base, j, routes, domain);
895
+ change.restoration = equal(part(after)[change.kind], part(safePrior)[change.kind])
896
+ ? 'restored'
897
+ : 'failed';
898
+ }
899
+ catch {
900
+ change.restoration = 'unknown';
901
+ }
902
+ await save(store, j, api.now);
903
+ }
904
+ }
905
+ async function version(api, base, id, a, requiredSecrets, expectedBindings, creation) {
906
+ const v = object((await api.request(`${base}/versions/${id}`)).result);
907
+ const annotations = object(v.annotations);
908
+ const resources = object(v.resources);
909
+ if (v.id !== id)
910
+ return conflict('version_id');
911
+ if (!versionAnnotationsMatch(annotations, a, creation))
912
+ return conflict('version_annotations');
913
+ if (!Array.isArray(resources.bindings))
914
+ return conflict('bindings');
915
+ const secrets = resources.bindings.map(object).filter((b) => b.type === 'secret_text');
916
+ if (secrets.some((b) => Object.hasOwn(b, 'text') || Object.hasOwn(b, 'value')))
917
+ return conflict('bindings:secret_text');
918
+ const names = secrets.map((b) => b.name);
919
+ if (requiredSecrets.some((n) => !names.includes(n)) || new Set(names).size !== names.length)
920
+ return conflict('bindings:secret_text');
921
+ const actualBindings = resources.bindings.map(object).filter((b) => b.type !== 'secret_text');
922
+ let assets = resources.assets;
923
+ let runtime = resources.script_runtime;
924
+ if (assets === undefined ||
925
+ actualBindings.some((b) => b.type === 'r2_bucket' && b.jurisdiction === undefined)) {
926
+ // The legacy version response does not document assets; the Version API does.
927
+ const full = await fullVersion(api, base, id);
928
+ if (!versionAnnotationsMatch(object(full.annotations), a, creation))
929
+ return conflict('version_annotations');
930
+ if (assets === undefined)
931
+ assets = full.assets;
932
+ for (const b of actualBindings) {
933
+ if (b.type !== 'r2_bucket' || b.jurisdiction !== undefined)
934
+ continue;
935
+ if (!Array.isArray(full.bindings))
936
+ return conflict('r2:jurisdiction');
937
+ const matches = full.bindings
938
+ .map(object)
939
+ .filter((x) => x.type === b.type && x.name === b.name && x.bucket_name === b.bucket_name);
940
+ if (matches.length !== 1)
941
+ return conflict('r2:jurisdiction');
942
+ b.jurisdiction = matches[0].jurisdiction;
943
+ }
944
+ runtime ??= {
945
+ compatibility_date: full.compatibility_date,
946
+ compatibility_flags: full.compatibility_flags,
947
+ };
948
+ }
949
+ const differences = bindingDifferences(actualBindings, expectedBindings);
950
+ if (differences.length)
951
+ return conflict(differences[0], ...differences.slice(1));
952
+ const config = runtime && typeof runtime === 'object' && !Array.isArray(runtime)
953
+ ? runtime
954
+ : {};
955
+ for (const [key, value] of Object.entries(COMPATIBILITY))
956
+ if (!equal(config[key], value))
957
+ return conflict(key);
958
+ if (!assets || typeof assets !== 'object' || Array.isArray(assets))
959
+ return conflict('assets:config');
960
+ verifyAssets(assets.config);
961
+ }
962
+ async function finalizeActivation(api, store, j, ports) {
963
+ const a = j.deployment.attempt;
964
+ const base = `/workers/scripts/${j.identity.script_name}`;
965
+ const intended = a.intended_settings;
966
+ const settings = () => readSettings(api, base, j, intended.routes, intended.domain?.hostname).then(withoutIds);
967
+ await ownedWorkerSettings(api, base, j);
968
+ await liveResources(api, j, ports);
969
+ const live = (await settings());
970
+ if (!equal({ ...live, endpoints: intended.endpoints }, withoutIds(intended)))
971
+ return conflict('managed_settings');
972
+ // A recovery guard may have narrowed endpoints; anything wider than intended is concurrent drift.
973
+ const widened = (live.endpoints.enabled && !intended.endpoints.enabled) || live.endpoints.previews_enabled;
974
+ await enforceEndpoints(api, store, j, intended.endpoints);
975
+ if (widened || !equal(await settings(), withoutIds(intended)))
976
+ return conflict('managed_settings');
977
+ a.phase = 'active';
978
+ a.activated_at = new Date(api.now()).toISOString();
979
+ j.resources.worker = {
980
+ name: j.identity.script_name,
981
+ id: j.identity.script_name,
982
+ state: 'complete',
983
+ evidence: { source: 'worker_binding', operation_id: null, verified_at: a.activated_at },
984
+ };
985
+ if (j.resources.analytics) {
986
+ j.resources.analytics.state = 'bound';
987
+ j.resources.analytics.evidence = {
988
+ source: 'worker_binding',
989
+ operation_id: null,
990
+ verified_at: a.activated_at,
991
+ };
992
+ }
993
+ if (a.secret_names.includes('PB_MASTER_KEY'))
994
+ j.secret_delivery.master_key = 'installed';
995
+ await save(store, j, api.now);
996
+ }
997
+ function outputAttempt(output, a) {
998
+ output.version_id = a.version_id;
999
+ output.previous_version_id = a.previous_version_id;
1000
+ output.artifact_sha256 = a.artifact_sha256;
1001
+ output.build_info = a.build_info;
1002
+ output.worker_url = a.worker_url;
1003
+ output.rollback = {
1004
+ status: a.rollback,
1005
+ target_version_id: a.rollback === 'not_needed' ? null : a.previous_version_id,
1006
+ };
1007
+ }
1008
+ export async function deploy(o, operations = {}) {
1009
+ let prebuiltOut;
1010
+ const output = commandEnvelope('deploy', o.dryRun);
1011
+ const result = { exitCode: 1, output };
1012
+ const succeed = () => {
1013
+ result.exitCode = 0;
1014
+ return result;
1015
+ };
1016
+ let release;
1017
+ let journal = null, store, api;
1018
+ let requestedRoutes = [];
1019
+ let attemptStarted = false;
1020
+ let uploadSent = false;
1021
+ let creatingWorker = false;
1022
+ try {
1023
+ if (o.prebuilt)
1024
+ await assertPrebuiltConfiguration(resolve(o.project ?? dirname(resolve(o.configPath ?? 'broker.config.ts'))), o.configPath);
1025
+ const auth = cloudflareCredentials(o.env);
1026
+ const identity = installIdentity({
1027
+ ...auth,
1028
+ workerName: o.workerName,
1029
+ ...(o.environment === undefined ? {} : { environment: o.environment }),
1030
+ ...(o.deploymentId === undefined ? {} : { deploymentId: o.deploymentId }),
1031
+ ...(o.jurisdiction === undefined ? {} : { jurisdiction: o.jurisdiction }),
1032
+ ...(o.runIdentity === undefined ? {} : { runIdentity: o.runIdentity }),
1033
+ });
1034
+ const project = resolve(o.project ?? dirname(resolve(o.configPath ?? 'broker.config.ts')));
1035
+ let out = resolve(project, o.outDir ?? (o.prebuilt ? '.pb/prebuilt/cloudflare' : '.pb/cloudflare'));
1036
+ if (o.prebuilt) {
1037
+ const materialized = await (operations.materializePrebuilt ?? materializePrebuilt)({
1038
+ project,
1039
+ outDir: out,
1040
+ });
1041
+ out = prebuiltOut = materialized.outDir;
1042
+ output.prebuilt = prebuiltEnvelope(materialized);
1043
+ output.artifact_sha256 = materialized.artifactSha256;
1044
+ }
1045
+ const rollout = o.rollout ?? 100;
1046
+ if (!Number.isInteger(rollout) || rollout < 1 || rollout > 100)
1047
+ return invalid();
1048
+ const selectedSecrets = secretNames(o);
1049
+ if (o.domain)
1050
+ hostname(o.domain);
1051
+ requestedRoutes = routeInput(o.routes, identity.script_name);
1052
+ if (requestedRoutes.length && !o.publicUrl)
1053
+ return invalid();
1054
+ let url = o.publicUrl ? origin(o.publicUrl) : o.domain ? `https://${o.domain}` : null;
1055
+ if (o.domain && url !== `https://${o.domain}`)
1056
+ return invalid();
1057
+ if (requestedRoutes.some((r) => r.pattern.replace(/^https:\/\//, '').split('/')[0] !== new URL(url).hostname))
1058
+ return invalid();
1059
+ const artifact = await validateDeploymentArtifact(out);
1060
+ const endpointEnv = artifact.ports?.observability.endpointEnv;
1061
+ const headersEnv = artifact.ports?.observability.headersEnv;
1062
+ const generatedNames = [
1063
+ 'DB',
1064
+ 'DEPLOYMENT_ID',
1065
+ 'PB_PUBLIC_URL',
1066
+ 'ASSETS',
1067
+ ...(artifact.ports
1068
+ ? [
1069
+ artifact.ports.blob.binding,
1070
+ artifact.ports.jobs.binding,
1071
+ artifact.ports.observability.analyticsBinding,
1072
+ ]
1073
+ : []),
1074
+ ];
1075
+ if ([endpointEnv, headersEnv].some((n) => n && generatedNames.includes(n)))
1076
+ return invalid();
1077
+ const telemetryEndpoint = endpointEnv ? o.env[endpointEnv] : undefined;
1078
+ if (endpointEnv) {
1079
+ if (!telemetryEndpoint)
1080
+ return invalid();
1081
+ const suppliedHeaders = headersEnv && selectedSecrets.includes(headersEnv) ? o.env[headersEnv] : undefined;
1082
+ validateTelemetry(telemetryEndpoint, suppliedHeaders || undefined);
1083
+ }
1084
+ output.build_info = artifact.info;
1085
+ output.artifact_sha256 = artifact.artifactHash;
1086
+ store = await InventoryStore.create({
1087
+ project,
1088
+ outDir: out,
1089
+ identity,
1090
+ ...(o.inventory === undefined ? {} : { inventory: o.inventory }),
1091
+ });
1092
+ if (o.transaction)
1093
+ o.transaction.assertStore(store);
1094
+ const now = o.now ?? Date.now;
1095
+ if (!o.dryRun && !o.transaction)
1096
+ release = await store.lock(now());
1097
+ journal = await store.read();
1098
+ if (!journal ||
1099
+ journal.operations.some((op) => op.state === 'pending' || op.state === 'unknown'))
1100
+ return invalid();
1101
+ if (o.prebuilt) {
1102
+ const migrations = await loadBuiltMigrations(out);
1103
+ if (migrations.some((m) => !journal.migrations.ledger.some((entry) => entry.id === m.id && entry.checksum === m.checksum)))
1104
+ throw new CloudflareError('migrations_pending', 2, false, 'deploy');
1105
+ }
1106
+ if (journal.deployment) {
1107
+ outputAttempt(output, journal.deployment.attempt);
1108
+ output.artifact_sha256 = artifact.artifactHash;
1109
+ output.build_info = artifact.info;
1110
+ }
1111
+ output.database_id = journal.resources.d1?.id ?? null;
1112
+ output.r2_bucket = journal.resources.r2?.id ?? null;
1113
+ output.queue_names = [journal.resources.jobs?.name, journal.resources.dlq?.name].filter((n) => n !== undefined);
1114
+ const requiredKeys = ['d1', ...(artifact.ports ? ['r2', 'jobs', 'dlq'] : [])];
1115
+ if (requiredKeys.some((k) => {
1116
+ const r = journal.resources[k];
1117
+ return !r?.id || r.state !== 'complete' || !r.evidence;
1118
+ }))
1119
+ return invalid();
1120
+ if (artifact.ports?.observability.analyticsBinding &&
1121
+ !['binding_pending', 'bound'].includes(journal.resources.analytics?.state ?? ''))
1122
+ return invalid();
1123
+ const appliedIds = new Set(journal.migrations.ledger.map(({ id }) => id));
1124
+ const pendingMigrations = artifact.migrations.filter(({ id }) => !appliedIds.has(id));
1125
+ assertMigrationsApplied(o.dryRun ? artifact.migrations.filter(({ id }) => appliedIds.has(id)) : artifact.migrations, journal.migrations.ledger, 'pb cloudflare migrate');
1126
+ output.pending_migrations = pendingMigrations.map(({ id }) => id);
1127
+ for (const migration of pendingMigrations) {
1128
+ if (migration.phase === 'contract')
1129
+ output.checks.push({
1130
+ name: migration.id,
1131
+ status: 'warning',
1132
+ source: `Pending contract migration requires pb cloudflare migrate --allow-contract ${migration.id}.`,
1133
+ });
1134
+ if (o.dryRun)
1135
+ output.plan.push({
1136
+ stage: 'migrate',
1137
+ action: 'apply',
1138
+ name: migration.id,
1139
+ kind: migration.phase,
1140
+ });
1141
+ }
1142
+ const ledgerHash = digest(canonical(journal.migrations.ledger));
1143
+ const contract = o.contract ?? cloudflareDeployContract;
1144
+ api = new CloudflareApi({
1145
+ ...auth,
1146
+ ...(o.fetch ? { fetch: o.fetch } : {}),
1147
+ ...(o.now ? { now: o.now } : {}),
1148
+ ...(o.sleep ? { sleep: o.sleep } : {}),
1149
+ });
1150
+ const base = `/workers/scripts/${identity.script_name}`;
1151
+ const workers = await api.snapshot('/workers/scripts');
1152
+ const matches = workers.filter((w) => w.id === identity.script_name);
1153
+ if (matches.length > 1)
1154
+ return conflict('worker:identity');
1155
+ const exists = matches.length === 1;
1156
+ creatingWorker = !exists;
1157
+ if (exists) {
1158
+ const tags = matches[0].tags;
1159
+ if (!Array.isArray(tags) ||
1160
+ !tags.includes('pb-install-v1') ||
1161
+ !tags.includes(`pb-owner-${identity.owner_digest}`))
1162
+ throw new CloudflareError('ownership_conflict', 8, false, 'deploy');
1163
+ }
1164
+ if (journal.deployment && journal.deployment.attempt.preview !== 'disabled') {
1165
+ if (o.dryRun) {
1166
+ output.plan.push({ stage: 'deploy', action: 'reconcile_preview_cleanup' });
1167
+ output.status = 'planned';
1168
+ return succeed();
1169
+ }
1170
+ await reconcileCandidatePreview(api, store, journal);
1171
+ return conflict('preview_cleanup'); // Re-read the complete configuration on a fresh run after interrupted cleanup.
1172
+ }
1173
+ const workerSettings = exists ? await ownedWorkerSettings(api, base, journal) : null;
1174
+ if (workerSettings) {
1175
+ for (const raw of workerSettings.bindings.map(object)) {
1176
+ const b = normalizedBinding(raw);
1177
+ if (b.type === 'secret_text') {
1178
+ if (Object.hasOwn(b, 'text') || Object.hasOwn(b, 'value'))
1179
+ return conflict('bindings:secret_text');
1180
+ continue;
1181
+ }
1182
+ if (b.name === 'DB' && (b.type !== 'd1' || b.id !== journal.resources.d1.id))
1183
+ return conflict('binding:d1:DB:id');
1184
+ if (b.name === 'DEPLOYMENT_ID' &&
1185
+ (b.type !== 'plain_text' || b.text !== identity.deployment_id))
1186
+ return conflict('binding:plain_text:DEPLOYMENT_ID:text');
1187
+ if (b.type === 'r2_bucket' && b.name !== artifact.ports?.blob.binding)
1188
+ return conflict('r2:binding');
1189
+ if (b.type === 'queue' && b.name !== artifact.ports?.jobs.binding)
1190
+ return conflict('queue:binding');
1191
+ if (b.type === 'analytics_engine' &&
1192
+ b.name !== artifact.ports?.observability.analyticsBinding)
1193
+ return conflict('analytics_engine:binding');
1194
+ }
1195
+ }
1196
+ await liveResources(api, journal, artifact.ports);
1197
+ let current = exists ? await active(api, base) : { id: null, versions: [] };
1198
+ const recordedVerified = journal.deployment?.attempt.phase === 'verified' &&
1199
+ journal.deployment.attempt.smoke === 'passed'
1200
+ ? journal.deployment.attempt
1201
+ : journal.deployment?.last_verified;
1202
+ if (current.versions.length > 1 &&
1203
+ journal.deployment?.attempt.phase !== 'activation_pending' &&
1204
+ !(recordedVerified && equal(current.versions, distribution(recordedVerified))))
1205
+ return conflict('deployment');
1206
+ if (!current.versions.length && rollout !== 100)
1207
+ return invalid();
1208
+ if (current.versions.length === 1) {
1209
+ const prior = object((await api.request(`${base}/versions/${current.versions[0].version_id}`)).result);
1210
+ if (prior.id !== current.versions[0].version_id)
1211
+ return conflict('version_id');
1212
+ const annotations = object(prior.annotations);
1213
+ if (annotations['pb-maintenance-shell'] === 'true')
1214
+ throw new CloudflareError('capability_unknown', 1, false, 'deploy');
1215
+ }
1216
+ if (!url)
1217
+ url = await workersDevOrigin(api, identity.script_name);
1218
+ if (!exists && !contract.versionsCreateScript && !contract.createScript)
1219
+ throw new CloudflareError('capability_unknown');
1220
+ if (!contract.assetHash)
1221
+ throw new CloudflareError('capability_unknown');
1222
+ if (rollout < 100 && !contract.preview)
1223
+ throw new CloudflareError('capability_unknown');
1224
+ const custom = !!(o.domain || requestedRoutes.length || o.publicUrl);
1225
+ const policy = { enabled: !custom, previews_enabled: false };
1226
+ const journalled = journal.deployment;
1227
+ const managedZones = [
1228
+ ...new Set([
1229
+ journalled?.attempt.prior_settings,
1230
+ journalled?.attempt.intended_settings,
1231
+ journalled?.last_verified?.intended_settings,
1232
+ ].flatMap((settings) => settings?.routes.map((r) => r.zone_id) ?? [])),
1233
+ ];
1234
+ const live = await readSettings(api, base, journal, requestedRoutes, o.domain, exists, managedZones);
1235
+ const installed = exists ? await api.snapshot(`${base}/secrets`) : [];
1236
+ if (installed.some((s) => s.type !== 'secret_text' ||
1237
+ typeof s.name !== 'string' ||
1238
+ !/^[A-Z][A-Z0-9_]{0,127}$/.test(s.name)))
1239
+ return conflict('bindings:secret_text');
1240
+ const installedNames = installed.map((s) => s.name).sort();
1241
+ if (installedNames.length !== new Set(installedNames).size ||
1242
+ installedNames.includes('PB_TOKEN_PEPPER'))
1243
+ return invalid();
1244
+ if (installedNames.length && !contract.secretInheritance)
1245
+ throw new CloudflareError('capability_unknown');
1246
+ const plainBindings = bindings(journal, artifact.ports, url);
1247
+ if (endpointEnv) {
1248
+ if (selectedSecrets.includes(endpointEnv) || installedNames.includes(endpointEnv))
1249
+ return invalid();
1250
+ plainBindings.push({ type: 'plain_text', name: endpointEnv, text: telemetryEndpoint });
1251
+ }
1252
+ const collisionNames = new Set(plainBindings.map((b) => b.name));
1253
+ if ([...selectedSecrets, ...installedNames].some((n) => collisionNames.has(n)))
1254
+ return invalid();
1255
+ const updates = [];
1256
+ for (const name of selectedSecrets) {
1257
+ if ((name === 'PB_MASTER_KEY' || name === 'PB_MASTER_KEY_PREVIOUS') &&
1258
+ installedNames.includes(name)) {
1259
+ output.checks.push({ name, status: 'existing_secret_retained' });
1260
+ continue;
1261
+ }
1262
+ const text = o.env[name];
1263
+ if (text)
1264
+ updates.push({ type: 'secret_text', name, text });
1265
+ else if (!installedNames.includes(name))
1266
+ return invalid();
1267
+ }
1268
+ const pending = journal.deployment?.attempt;
1269
+ const pendingNames = pending &&
1270
+ ['upload_pending', 'uploaded', 'configuring', 'activation_pending'].includes(pending.phase)
1271
+ ? pending.secret_names
1272
+ : [];
1273
+ const allNames = [
1274
+ ...new Set([...installedNames, ...pendingNames, ...updates.map((b) => String(b.name))]),
1275
+ ].sort();
1276
+ if (!allNames.includes('PB_MASTER_KEY'))
1277
+ return invalid();
1278
+ if (headersEnv && !allNames.includes(headersEnv))
1279
+ return invalid();
1280
+ let domain = live.domain;
1281
+ if (o.domain && !domain) {
1282
+ if (!contract.domainZone)
1283
+ throw new CloudflareError('capability_unknown');
1284
+ const zone = await contract.domainZone(api, o.domain);
1285
+ if (!/^[a-f0-9]{32}$/.test(zone))
1286
+ throw new CloudflareError('capability_unknown');
1287
+ domain = {
1288
+ id: null,
1289
+ zone_id: zone,
1290
+ hostname: o.domain,
1291
+ service: identity.script_name,
1292
+ environment: 'production',
1293
+ };
1294
+ }
1295
+ const intended = {
1296
+ endpoints: policy,
1297
+ schedules: ['* * * * *'],
1298
+ consumer: artifact.ports
1299
+ ? {
1300
+ consumer_id: live.consumer?.consumer_id ?? null,
1301
+ script_name: identity.script_name,
1302
+ batch_size: 1,
1303
+ max_wait_time_ms: 1000,
1304
+ max_retries: 4,
1305
+ max_concurrency: 2,
1306
+ dead_letter_queue: journal.resources.dlq.name,
1307
+ }
1308
+ : null,
1309
+ routes: requestedRoutes.map((r) => ({
1310
+ ...r,
1311
+ id: live.routes.find((x) => x.zone_id === r.zone_id && x.pattern === r.pattern)?.id ?? null,
1312
+ })),
1313
+ domain,
1314
+ };
1315
+ const settingsHash = digest(canonical({
1316
+ settings: withoutIds(intended),
1317
+ bindings: plainBindings,
1318
+ secrets: allNames,
1319
+ ledgerHash,
1320
+ }));
1321
+ const priorJournal = journal.deployment;
1322
+ const priorAttempt = priorJournal?.attempt;
1323
+ const verified = priorAttempt?.phase === 'verified' && priorAttempt.smoke === 'passed'
1324
+ ? priorAttempt
1325
+ : (priorJournal?.last_verified ?? null);
1326
+ const livePlain = workerSettings &&
1327
+ workerSettings.bindings
1328
+ .map(object)
1329
+ .map(normalizedBinding)
1330
+ .filter((b) => b.type !== 'secret_text');
1331
+ const plainMatch = livePlain !== null &&
1332
+ livePlain !== undefined &&
1333
+ plainBindings.every((b) => livePlain.some((x) => equal(x, b))) &&
1334
+ livePlain.length === plainBindings.length;
1335
+ if (verified &&
1336
+ plainMatch &&
1337
+ verified.artifact_sha256 === artifact.artifactHash &&
1338
+ verified.settings_sha256 === settingsHash &&
1339
+ verified.requested_rollout === rollout &&
1340
+ verified.migration_ledger_sha256 === ledgerHash &&
1341
+ equal(current.versions, distribution(verified)) &&
1342
+ equal(withoutIds(live), withoutIds(intended)) &&
1343
+ !updates.some((b) => b.name !== 'PB_MASTER_KEY' && b.name !== 'PB_MASTER_KEY_PREVIOUS')) {
1344
+ outputAttempt(output, verified);
1345
+ output.plan.push({ stage: 'deploy', action: 'unchanged' });
1346
+ output.checks.push({ name: 'endpoint_policy', status: 'verified', ...policy });
1347
+ output.status = o.dryRun ? 'planned' : 'ok';
1348
+ return succeed();
1349
+ }
1350
+ const resuming = priorAttempt &&
1351
+ [
1352
+ 'preparing_upload',
1353
+ 'upload_pending',
1354
+ 'uploaded',
1355
+ 'configuring',
1356
+ 'activation_pending',
1357
+ ].includes(priorAttempt.phase);
1358
+ if (resuming &&
1359
+ (priorAttempt.phase !== 'preparing_upload' || priorAttempt.script_creation !== undefined) &&
1360
+ updates.some((b) => b.name !== 'PB_MASTER_KEY' && b.name !== 'PB_MASTER_KEY_PREVIOUS'))
1361
+ return conflict('bindings:secret_text');
1362
+ if (resuming) {
1363
+ if (priorAttempt.artifact_sha256 !== artifact.artifactHash)
1364
+ return conflict('artifact_sha256');
1365
+ if (priorAttempt.settings_sha256 !== settingsHash)
1366
+ return conflict('settings_sha256');
1367
+ if (priorAttempt.requested_rollout !== rollout)
1368
+ return conflict('requested_rollout');
1369
+ }
1370
+ if (!resuming) {
1371
+ if (current.versions.length > 1)
1372
+ return conflict('deployment');
1373
+ let managed = priorAttempt?.intended_settings ?? verified?.intended_settings;
1374
+ if (priorAttempt?.phase === 'failed' || priorAttempt?.phase === 'rolled_back')
1375
+ managed =
1376
+ restoredSnapshot(priorAttempt, priorAttempt.phase === 'failed' && equal(current, beforeCandidate(priorAttempt))
1377
+ ? live
1378
+ : undefined) ?? conflict('managed_settings');
1379
+ if (live.routes.some((r) => !intended.routes.some((x) => x.zone_id === r.zone_id && x.pattern === r.pattern)))
1380
+ return conflict('routes');
1381
+ // Never take over pre-existing attachments without a previous managed receipt.
1382
+ if ((live.consumer || live.schedules.length || live.routes.length || live.domain) &&
1383
+ (!managed || !matchesManaged(live, managed)))
1384
+ return conflict('managed_settings');
1385
+ }
1386
+ if (priorJournal) {
1387
+ const pendingHistory = structuredClone(priorJournal);
1388
+ recordDeploymentHistory(pendingHistory, {
1389
+ plainBinding: { name: 'PB_PUBLIC_URL', text: url },
1390
+ });
1391
+ recordDeploymentHistory(pendingHistory, {
1392
+ reserveVersion: !resuming || !priorAttempt.version_id,
1393
+ ...(endpointEnv ? { plainBinding: { name: endpointEnv, text: telemetryEndpoint } } : {}),
1394
+ });
1395
+ }
1396
+ output.worker_url = url;
1397
+ output.previous_version_id = resuming
1398
+ ? priorAttempt.previous_version_id
1399
+ : (current.versions[0]?.version_id ?? null);
1400
+ output.checks.push({
1401
+ name: 'provider_semantics',
1402
+ status: contract === cloudflareDeployContract ? 'documented' : 'injected',
1403
+ });
1404
+ output.plan.push({ stage: 'deploy', action: resuming ? 'reconcile' : 'upload_activate' });
1405
+ if (o.dryRun) {
1406
+ output.status = 'planned';
1407
+ return succeed();
1408
+ }
1409
+ let a;
1410
+ if (resuming)
1411
+ a = priorAttempt;
1412
+ else {
1413
+ a = {
1414
+ operation_id: randomUUID(),
1415
+ artifact_sha256: artifact.artifactHash,
1416
+ assets_sha256: artifact.assetsHash,
1417
+ settings_sha256: settingsHash,
1418
+ secret_names: allNames,
1419
+ build_info: artifact.info,
1420
+ version_id: null,
1421
+ previous_version_id: current.versions[0]?.version_id ?? null,
1422
+ previous_deployment_id: current.id,
1423
+ previous_distribution: current.versions,
1424
+ requested_rollout: rollout,
1425
+ worker_url: url,
1426
+ candidate_probe_url: null,
1427
+ migration_ledger_sha256: ledgerHash,
1428
+ rollback_compatible: journal.migrations.rollback_compatible,
1429
+ contract_ids: [...journal.migrations.contract_ids],
1430
+ phase: 'preparing_upload',
1431
+ activated_at: null,
1432
+ smoke: 'pending',
1433
+ rollback: 'not_needed',
1434
+ preview: 'disabled',
1435
+ prior_settings: structuredClone(live),
1436
+ intended_settings: structuredClone(intended),
1437
+ changes: [],
1438
+ };
1439
+ if (priorJournal)
1440
+ recordDeploymentHistory(priorJournal, {});
1441
+ journal.deployment = {
1442
+ attempt: a,
1443
+ last_verified: verified,
1444
+ ...(priorJournal?.history ? { history: structuredClone(priorJournal.history) } : {}),
1445
+ };
1446
+ recordDeploymentHistory(journal.deployment, {
1447
+ ...(priorAttempt?.version_id ? { versionId: priorAttempt.version_id } : {}),
1448
+ ...(endpointEnv ? { plainBinding: { name: endpointEnv, text: telemetryEndpoint } } : {}),
1449
+ });
1450
+ await save(store, journal, now);
1451
+ }
1452
+ attemptStarted = true;
1453
+ if (endpointEnv && journal.deployment)
1454
+ recordDeploymentHistory(journal.deployment, {
1455
+ plainBinding: { name: endpointEnv, text: telemetryEndpoint },
1456
+ });
1457
+ outputAttempt(output, a);
1458
+ if (a.script_creation?.state === 'pending' ||
1459
+ (a.script_creation?.state === 'complete' && a.phase === 'preparing_upload')) {
1460
+ uploadSent = true;
1461
+ await reconcileScriptCreation(api, store, journal, allNames, plainBindings);
1462
+ }
1463
+ if (a.phase === 'preparing_upload' || a.phase === 'upload_pending') {
1464
+ if (a.phase === 'upload_pending') {
1465
+ if (exists)
1466
+ await enforceEndpoints(api, store, journal, restrictivePolicy(a));
1467
+ const versions = await api.list(`${base}/versions`, 'items');
1468
+ const matches = versions.filter((v) => {
1469
+ const n = object(v.annotations);
1470
+ return versionAnnotationsMatch(n, a);
1471
+ });
1472
+ if (matches.length !== 1 || !uuid(matches[0].id))
1473
+ return unknown();
1474
+ a.version_id = matches[0].id;
1475
+ recordDeploymentHistory(journal.deployment, { versionId: a.version_id });
1476
+ await version(api, base, a.version_id, a, allNames, plainBindings);
1477
+ }
1478
+ else {
1479
+ if (!equal(await activeOrAbsent(api, base, exists), beforeCandidate(a)))
1480
+ return conflict('deployment');
1481
+ if (exists)
1482
+ await enforceEndpoints(api, store, journal, restrictivePolicy(a));
1483
+ const token = await uploadAssets(api, base, artifact, contract.assetHash);
1484
+ const makeForm = (creation = false) => {
1485
+ const form = new FormData();
1486
+ form.set('metadata', JSON.stringify({
1487
+ ...COMPATIBILITY,
1488
+ main_module: 'worker.js',
1489
+ bindings: [...plainBindings, ...updates],
1490
+ ...(installedNames.length ? { keep_bindings: ['secret_text'] } : {}),
1491
+ assets: { jwt: token, config: ASSET_CONFIG },
1492
+ tags: ['pb-install-v1', `pb-owner-${identity.owner_digest}`],
1493
+ annotations: {
1494
+ ...versionAnnotations(a, creation),
1495
+ },
1496
+ observability: {
1497
+ enabled: true,
1498
+ logs: { invocation_logs: false },
1499
+ traces: tracesMetadata(artifact.ports?.observability.traces),
1500
+ },
1501
+ }));
1502
+ form.set('worker.js', new Blob([new Uint8Array(artifact.module)], { type: 'application/javascript+module' }), 'worker.js');
1503
+ return form;
1504
+ };
1505
+ if (!exists && !contract.versionsCreateScript) {
1506
+ if (!contract.createScript || a.script_creation)
1507
+ return unknown();
1508
+ a.script_creation = { state: 'pending', deployment_id: null, version_id: null };
1509
+ await save(store, journal, now);
1510
+ uploadSent = true;
1511
+ try {
1512
+ await contract.createScript(api, identity.script_name, makeForm(true));
1513
+ }
1514
+ catch (error) {
1515
+ if (knownNotApplied(error)) {
1516
+ delete a.script_creation;
1517
+ uploadSent = false;
1518
+ await save(store, journal, now);
1519
+ }
1520
+ throw error;
1521
+ }
1522
+ await reconcileScriptCreation(api, store, journal, allNames, plainBindings);
1523
+ }
1524
+ else {
1525
+ const form = makeForm();
1526
+ a.phase = 'upload_pending';
1527
+ await save(store, journal, now);
1528
+ uploadSent = true;
1529
+ let response;
1530
+ try {
1531
+ response = await api.request(`${base}/versions`, 'POST', form);
1532
+ }
1533
+ catch (error) {
1534
+ if (knownNotApplied(error)) {
1535
+ if (wasNotSent(error))
1536
+ uploadSent = false;
1537
+ a.phase = 'preparing_upload';
1538
+ await save(store, journal, now);
1539
+ }
1540
+ throw error;
1541
+ }
1542
+ if (!exists)
1543
+ await enforceEndpoints(api, store, journal, restrictivePolicy(a));
1544
+ const uploaded = object(response.result);
1545
+ if (!uuid(uploaded.id))
1546
+ return unknown();
1547
+ a.version_id = uploaded.id;
1548
+ recordDeploymentHistory(journal.deployment, { versionId: a.version_id });
1549
+ await save(store, journal, now);
1550
+ await version(api, base, a.version_id, a, allNames, plainBindings);
1551
+ }
1552
+ }
1553
+ a.phase = 'uploaded';
1554
+ await save(store, journal, now);
1555
+ outputAttempt(output, a);
1556
+ }
1557
+ if (!a.version_id)
1558
+ return unknown();
1559
+ await version(api, base, a.version_id, a, allNames, plainBindings);
1560
+ if (rollout < 100) {
1561
+ const preview = await contract.preview(api, identity.script_name, a.version_id);
1562
+ if (preview.version_id !== a.version_id || !preview.bindings_verified)
1563
+ return conflict('preview');
1564
+ const probeUrl = origin(preview.url);
1565
+ if (!probeUrl.endsWith('.workers.dev') ||
1566
+ probeUrl === url ||
1567
+ !new URL(probeUrl).hostname.startsWith(`${a.version_id}-`))
1568
+ return conflict('preview');
1569
+ a.candidate_probe_url = probeUrl;
1570
+ await save(store, journal, now);
1571
+ }
1572
+ if (a.phase === 'activation_pending') {
1573
+ const observed = await active(api, base);
1574
+ if (equal(observed.versions, distribution(a))) {
1575
+ await finalizeActivation(api, store, journal, artifact.ports);
1576
+ output.checks.push({ name: 'endpoint_policy', status: 'verified', ...policy });
1577
+ outputAttempt(output, a);
1578
+ return succeed();
1579
+ }
1580
+ return unknown(); // An old distribution does not prove that an activation was never accepted.
1581
+ }
1582
+ a.phase = 'configuring';
1583
+ await save(store, journal, now);
1584
+ let expected = await readSettings(api, base, journal, requestedRoutes, o.domain, true, managedZones);
1585
+ for (const kind of ['endpoints', 'consumer', 'schedules', 'routes', 'domain']) {
1586
+ const part = (v) => withoutIds(v)[kind];
1587
+ if (!equal(part(expected), part(a.prior_settings)) &&
1588
+ !equal(part(expected), part(a.intended_settings)) &&
1589
+ !(kind === 'endpoints' && equal(expected.endpoints, restrictivePolicy(a))))
1590
+ return conflict(kind === 'consumer' ? 'queue_consumer' : kind);
1591
+ }
1592
+ for (const kind of ['consumer', 'schedules', 'routes', 'domain']) {
1593
+ const before = await readSettings(api, base, journal, requestedRoutes, o.domain, true, managedZones);
1594
+ const part = (v) => withoutIds(v)[kind];
1595
+ if (!equal(part(before), part(expected)) && !equal(part(before), part(a.intended_settings)))
1596
+ return conflict(kind === 'consumer' ? 'queue_consumer' : kind);
1597
+ if (!equal(part(before), part(a.intended_settings))) {
1598
+ const change = {
1599
+ kind,
1600
+ state: 'pending',
1601
+ restoration: 'not_needed',
1602
+ };
1603
+ a.changes.push(change);
1604
+ await save(store, journal, now);
1605
+ try {
1606
+ await writeSetting(api, base, journal, kind, a.intended_settings);
1607
+ change.state = 'applied';
1608
+ }
1609
+ catch (error) {
1610
+ change.state = 'unknown';
1611
+ await save(store, journal, now);
1612
+ throw error;
1613
+ }
1614
+ await save(store, journal, now);
1615
+ }
1616
+ const observed = await readSettings(api, base, journal, requestedRoutes, o.domain, true, managedZones);
1617
+ if (!equal(part(observed), part(a.intended_settings)))
1618
+ return conflict(kind === 'consumer' ? 'queue_consumer' : kind);
1619
+ expected = { ...expected, [kind]: observed[kind] };
1620
+ a.intended_settings = { ...a.intended_settings, [kind]: observed[kind] };
1621
+ if (!equal(withoutIds(observed), withoutIds(expected)))
1622
+ return conflict('managed_settings');
1623
+ await save(store, journal, now);
1624
+ }
1625
+ current = await active(api, base);
1626
+ if (!equal(current, beforeCandidate(a)))
1627
+ return conflict('deployment');
1628
+ if (!equal(withoutIds(await readSettings(api, base, journal, requestedRoutes, o.domain, true, managedZones)), withoutIds(expected)))
1629
+ return conflict('managed_settings');
1630
+ if (workerSettings) {
1631
+ const differences = workerSettingsDifferences(await ownedWorkerSettings(api, base, journal), workerSettings, [...plainBindings.map((b) => String(b.name)), ...allNames]);
1632
+ if (differences.length)
1633
+ return conflict(differences[0], ...differences.slice(1));
1634
+ }
1635
+ await liveResources(api, journal, artifact.ports);
1636
+ await enforceEndpoints(api, store, journal, policy);
1637
+ if (!equal(withoutIds(await readSettings(api, base, journal, requestedRoutes, o.domain, true, managedZones)), withoutIds(a.intended_settings)))
1638
+ return conflict('managed_settings');
1639
+ if (!equal(await active(api, base), current))
1640
+ return conflict('deployment');
1641
+ a.phase = 'activation_pending';
1642
+ await save(store, journal, now);
1643
+ let activationFailure;
1644
+ try {
1645
+ await api.request(`${base}/deployments`, 'POST', {
1646
+ strategy: 'percentage',
1647
+ versions: distribution(a),
1648
+ });
1649
+ }
1650
+ catch (error) {
1651
+ activationFailure = error;
1652
+ }
1653
+ const refused = knownNotApplied(activationFailure);
1654
+ if (refused) {
1655
+ // Journalled before the fallible read-back; a resumed configuration re-verifies the prior distribution.
1656
+ a.phase = 'configuring';
1657
+ await save(store, journal, now);
1658
+ }
1659
+ const activated = await active(api, base);
1660
+ if (!equal(activated.versions, distribution(a))) {
1661
+ if (refused && equal(activated, beforeCandidate(a)))
1662
+ throw activationFailure;
1663
+ a.phase = 'activation_pending';
1664
+ await save(store, journal, now);
1665
+ return unknown();
1666
+ }
1667
+ if (refused) {
1668
+ a.phase = 'activation_pending';
1669
+ await save(store, journal, now);
1670
+ }
1671
+ await finalizeActivation(api, store, journal, artifact.ports);
1672
+ output.checks.push({ name: 'endpoint_policy', status: 'verified', ...policy });
1673
+ outputAttempt(output, a);
1674
+ return succeed();
1675
+ }
1676
+ catch (error) {
1677
+ let failure = error;
1678
+ if (uploadSent &&
1679
+ (journal?.deployment?.attempt.phase === 'upload_pending' ||
1680
+ journal?.deployment?.attempt.script_creation?.state === 'pending') &&
1681
+ (!(error instanceof CloudflareError) ||
1682
+ (error.code === 'provider_failure' &&
1683
+ (error.status === null || [408, 409, 429].includes(error.status) || error.status >= 500))))
1684
+ failure = new CloudflareError('outcome_unknown', 6, false, 'deploy');
1685
+ if (attemptStarted && journal?.deployment && store && api && (!creatingWorker || uploadSent)) {
1686
+ const a = journal.deployment.attempt, base = `/workers/scripts/${journal.identity.script_name}`;
1687
+ outputAttempt(output, a);
1688
+ api.reserveRecovery(recoveryMutations(a));
1689
+ await secureEndpoints(api, store, journal);
1690
+ try {
1691
+ if (a.phase === 'configuring') {
1692
+ await ownedWorkerSettings(api, base, journal);
1693
+ const observed = await active(api, base);
1694
+ if (!equal(observed, beforeCandidate(a))) {
1695
+ output.checks.push({ name: 'configuration_restoration', status: 'conflict' });
1696
+ throw new CloudflareError('configuration_conflict', 8, false, 'restore');
1697
+ }
1698
+ await restoreSettings(api, store, journal, requestedRoutes, o.domain);
1699
+ a.phase = 'failed';
1700
+ await save(store, journal, api.now);
1701
+ }
1702
+ }
1703
+ catch {
1704
+ output.checks.push({ name: 'configuration_restoration', status: 'unknown' });
1705
+ }
1706
+ output.checks.push({
1707
+ name: 'endpoint_policy',
1708
+ status: await secureEndpoints(api, store, journal),
1709
+ });
1710
+ if (a.changes.some((c) => c.restoration === 'unknown' || c.restoration === 'failed'))
1711
+ output.checks.push({ name: 'configuration_restoration', status: 'failed_or_unknown' });
1712
+ if (a.changes.some((c) => c.restoration === 'retained_first_install'))
1713
+ output.checks.push({
1714
+ name: 'configuration_restoration',
1715
+ status: 'incomplete_attachments_retained',
1716
+ });
1717
+ }
1718
+ else if (attemptStarted && journal?.deployment)
1719
+ outputAttempt(output, journal.deployment.attempt);
1720
+ Object.assign(result, failCommand(output, failure instanceof DeploymentHistoryCapacityError
1721
+ ? new DeploymentHistoryCapacityError()
1722
+ : failure instanceof CloudflareError && failure.stage === 'prebuilt'
1723
+ ? failure
1724
+ : failure instanceof CloudflareError
1725
+ ? new CloudflareError(failure.code, failure.exitCode, failure.retryable, 'deploy', failure.status, failure.check, failure.providerCode, failure.source, failure.fields)
1726
+ : new CloudflareError('provider_failure', 1, false, 'deploy')));
1727
+ return result;
1728
+ }
1729
+ finally {
1730
+ if (prebuiltOut)
1731
+ await removePrebuilt(prebuiltOut).catch(() => undefined);
1732
+ if (release) {
1733
+ try {
1734
+ await release();
1735
+ }
1736
+ catch {
1737
+ result.exitCode = failCommand(output, new CloudflareError('install_locked', 4, false, 'deploy')).exitCode;
1738
+ }
1739
+ }
1740
+ }
1741
+ }
1742
+ async function activeOrAbsent(api, base, exists) {
1743
+ if (exists)
1744
+ return active(api, base);
1745
+ const script = base.split('/').at(-1);
1746
+ if ((await api.snapshot('/workers/scripts')).some((w) => w.id === script))
1747
+ return conflict('deployment');
1748
+ return { id: null, versions: [] };
1749
+ }
1750
+ export async function runCloudflareDeploy(o) {
1751
+ const result = await deploy(o);
1752
+ if (o.json || !result.output.error)
1753
+ console.log(JSON.stringify(result.output));
1754
+ else
1755
+ console.error(`${result.output.error.code}: ${result.output.error.message}`);
1756
+ return result.exitCode;
1757
+ }
1758
+ /** Caller holds the install lease; smoke uses this after disabling any temporary preview. */
1759
+ export async function rollbackDeployment(api, store, journal) {
1760
+ const a = journal.deployment?.attempt;
1761
+ if (!a?.version_id)
1762
+ return invalid();
1763
+ api.reserveRecovery(recoveryMutations(a));
1764
+ await reconcileCandidatePreview(api, store, journal);
1765
+ if (!a.rollback_compatible ||
1766
+ a.contract_ids.length ||
1767
+ !journal.migrations.rollback_compatible ||
1768
+ journal.migrations.contract_ids.length)
1769
+ throw new CloudflareError('configuration_conflict', 11, false, 'rollback');
1770
+ if (!a.previous_version_id)
1771
+ throw new CloudflareError('configuration_conflict', 12, false, 'rollback');
1772
+ await ownedWorkerSettings(api, `/workers/scripts/${journal.identity.script_name}`, journal);
1773
+ let outcome;
1774
+ try {
1775
+ outcome = await rollbackOwned(api, store, journal, a, a.previous_version_id);
1776
+ }
1777
+ catch (error) {
1778
+ await secureEndpoints(api, store, journal);
1779
+ throw error;
1780
+ }
1781
+ const guard = await secureEndpoints(api, store, journal);
1782
+ if (outcome !== 'succeeded')
1783
+ return outcome;
1784
+ if (guard !== 'verified')
1785
+ a.rollback = 'unknown';
1786
+ else {
1787
+ a.phase = 'rolled_back';
1788
+ a.rollback = 'succeeded';
1789
+ }
1790
+ await save(store, journal, api.now);
1791
+ return guard === 'verified' ? 'succeeded' : 'unknown';
1792
+ }
1793
+ async function rollbackOwned(api, store, journal, a, previous) {
1794
+ const base = `/workers/scripts/${journal.identity.script_name}`;
1795
+ const prior = [{ version_id: previous, percentage: 100 }];
1796
+ const live = await active(api, base);
1797
+ const recovering = a.phase === 'rollback_pending';
1798
+ if (!equal(live.versions, distribution(a)) && !(recovering && equal(live.versions, prior)))
1799
+ return conflict('deployment');
1800
+ if ((await secureEndpoints(api, store, journal)) === 'ownership_conflict')
1801
+ throw new CloudflareError('ownership_conflict', 8, false, 'rollback');
1802
+ let rollbackFailure;
1803
+ const previousPhase = a.phase;
1804
+ if (!recovering) {
1805
+ a.phase = 'rollback_pending';
1806
+ a.rollback = 'pending';
1807
+ await save(store, journal, api.now);
1808
+ try {
1809
+ await api.request(`${base}/deployments`, 'POST', { strategy: 'percentage', versions: prior });
1810
+ }
1811
+ catch (error) {
1812
+ rollbackFailure = error;
1813
+ }
1814
+ }
1815
+ const refused = knownNotApplied(rollbackFailure);
1816
+ if (refused) {
1817
+ // Journalled before the fallible read-back; a retry re-verifies the live distribution first.
1818
+ a.phase = previousPhase;
1819
+ a.rollback = 'failed';
1820
+ await save(store, journal, api.now);
1821
+ }
1822
+ const observed = await active(api, base);
1823
+ if (!equal(observed.versions, prior)) {
1824
+ if (refused && equal(observed, live))
1825
+ return 'failed';
1826
+ a.phase = 'rollback_pending';
1827
+ a.rollback = 'unknown';
1828
+ await save(store, journal, api.now);
1829
+ return 'unknown';
1830
+ }
1831
+ if (refused) {
1832
+ a.phase = 'rollback_pending';
1833
+ a.rollback = 'pending';
1834
+ await save(store, journal, api.now);
1835
+ }
1836
+ await ownedWorkerSettings(api, base, journal);
1837
+ await restoreSettings(api, store, journal, a.intended_settings.routes, a.intended_settings.domain?.hostname);
1838
+ const expected = restoredSnapshot(a);
1839
+ const restored = expected &&
1840
+ matchesManaged(await readSettings(api, base, journal, a.intended_settings.routes, a.intended_settings.domain?.hostname), expected, a.prior_settings.routes);
1841
+ if (!restored) {
1842
+ a.rollback = 'failed';
1843
+ await save(store, journal, api.now);
1844
+ return 'failed';
1845
+ }
1846
+ return 'succeeded';
1847
+ }
1848
+ //# sourceMappingURL=deploy.js.map