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

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