@package-broker/cli 2.0.0-rc.1 → 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 (47) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +29 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/cloudflare/api.d.ts +5 -2
  6. package/dist/commands/cloudflare/api.d.ts.map +1 -1
  7. package/dist/commands/cloudflare/api.js +35 -13
  8. package/dist/commands/cloudflare/api.js.map +1 -1
  9. package/dist/commands/cloudflare/deploy.d.ts +32 -2
  10. package/dist/commands/cloudflare/deploy.d.ts.map +1 -1
  11. package/dist/commands/cloudflare/deploy.js +398 -76
  12. package/dist/commands/cloudflare/deploy.js.map +1 -1
  13. package/dist/commands/cloudflare/inventory.d.ts +16 -2
  14. package/dist/commands/cloudflare/inventory.d.ts.map +1 -1
  15. package/dist/commands/cloudflare/inventory.js +61 -5
  16. package/dist/commands/cloudflare/inventory.js.map +1 -1
  17. package/dist/commands/cloudflare/providers-file.d.ts +17 -0
  18. package/dist/commands/cloudflare/providers-file.d.ts.map +1 -0
  19. package/dist/commands/cloudflare/providers-file.js +155 -0
  20. package/dist/commands/cloudflare/providers-file.js.map +1 -0
  21. package/dist/commands/cloudflare/routes.d.ts +3 -0
  22. package/dist/commands/cloudflare/routes.d.ts.map +1 -0
  23. package/dist/commands/cloudflare/routes.js +26 -0
  24. package/dist/commands/cloudflare/routes.js.map +1 -0
  25. package/dist/commands/cloudflare/setup.d.ts +2 -0
  26. package/dist/commands/cloudflare/setup.d.ts.map +1 -1
  27. package/dist/commands/cloudflare/setup.js +8 -8
  28. package/dist/commands/cloudflare/setup.js.map +1 -1
  29. package/dist/commands/cloudflare/smoke.d.ts.map +1 -1
  30. package/dist/commands/cloudflare/smoke.js +10 -24
  31. package/dist/commands/cloudflare/smoke.js.map +1 -1
  32. package/dist/commands/cloudflare/teardown.d.ts +6 -3
  33. package/dist/commands/cloudflare/teardown.d.ts.map +1 -1
  34. package/dist/commands/cloudflare/teardown.js +122 -44
  35. package/dist/commands/cloudflare/teardown.js.map +1 -1
  36. package/dist/commands/cloudflare/zone.d.ts +3 -0
  37. package/dist/commands/cloudflare/zone.d.ts.map +1 -0
  38. package/dist/commands/cloudflare/zone.js +73 -0
  39. package/dist/commands/cloudflare/zone.js.map +1 -0
  40. package/dist/commands/cloudflare.js +1 -1
  41. package/dist/commands/cloudflare.js.map +1 -1
  42. package/dist/manifest.d.ts +2 -1
  43. package/dist/manifest.d.ts.map +1 -1
  44. package/dist/manifest.js +12 -0
  45. package/dist/manifest.js.map +1 -1
  46. package/dist/prebuilt-expected.json +3 -3
  47. package/package.json +7 -6
@@ -4,13 +4,24 @@ import { dirname, extname, isAbsolute, relative, resolve, sep } from 'node:path'
4
4
  import { createOtlpExporter } from '@package-broker/core/platform/otlp';
5
5
  import { parseUiManifest } from '@package-broker/core/platform/ui-manifest';
6
6
  import { TRACE_FIELDS, validatePlatformPorts, } from '@package-broker/core/platform/port-config';
7
- import { PLUGIN_BINDABLE_PORTS } from '@package-broker/core/plugin';
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';
8
16
  import { assertPrebuiltConfiguration, materializePrebuilt, prebuiltEnvelope, removePrebuilt, } from '../../prebuilt.js';
9
17
  import { builtRouteProblems, loadBuiltMigrations } from '../built-manifest.js';
18
+ import { validateProvidersDocument } from './providers-file.js';
10
19
  import { normalizeQueueConsumer } from './queue-consumer.js';
11
20
  import { assertMigrationsApplied } from './migrations-check.js';
12
21
  import { d1BindingId, workerSettingsDifferences } from './worker-settings.js';
13
22
  import { CloudflareApi, CloudflareError, cloudflareCredentials, commandEnvelope, failCommand, object, wasNotSent, } from './api.js';
23
+ import { resolveZone } from './zone.js';
24
+ import { routeConflict } from './routes.js';
14
25
  import { InventoryStore, InventoryTransaction, digest, installIdentity, recordDeploymentHistory, DeploymentHistoryCapacityError, safeBuildInfo, } from './inventory.js';
15
26
  export function tracesMetadata(traces) {
16
27
  if (traces === undefined)
@@ -21,11 +32,24 @@ export function tracesMetadata(traces) {
21
32
  }
22
33
  const COMPATIBILITY = { compatibility_date: '2026-10-01', compatibility_flags: ['nodejs_compat'] };
23
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
+ };
24
44
  const uuid = (v) => typeof v === 'string' &&
25
45
  /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(v);
26
46
  const safeId = (v) => typeof v === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(v);
27
- const invalid = () => {
28
- throw new CloudflareError('invalid_input', 2, false, 'deploy');
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;
29
53
  };
30
54
  const conflict = (field, ...fields) => {
31
55
  throw new CloudflareError('configuration_conflict', 8, false, 'deploy', null, null, null, null, [
@@ -36,12 +60,161 @@ const conflict = (field, ...fields) => {
36
60
  const unknown = () => {
37
61
  throw new CloudflareError('outcome_unknown', 6, false, 'deploy');
38
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
+ }
39
212
  function canonical(value) {
40
213
  if (Array.isArray(value))
41
214
  return `[${value.map(canonical).join(',')}]`;
42
215
  if (value && typeof value === 'object')
43
216
  return `{${Object.entries(value)
44
- .sort(([a], [b]) => a.localeCompare(b))
217
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
45
218
  .map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`)
46
219
  .join(',')}}`;
47
220
  return JSON.stringify(value);
@@ -126,6 +299,10 @@ export async function validateDeploymentArtifact(out) {
126
299
  return invalid();
127
300
  const info = safeBuildInfo(JSON.parse((await read('build-info.json')).toString('utf8')));
128
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();
129
306
  if (manifest.schemaVersion !== 1 ||
130
307
  manifest.platform !== 'cloudflare' ||
131
308
  !Array.isArray(manifest.plugins) ||
@@ -194,7 +371,16 @@ export async function validateDeploymentArtifact(out) {
194
371
  assets: ASSET_CONFIG,
195
372
  ports,
196
373
  }));
197
- return { module, info, ports, assets, assetsHash: exactAssetsHash, artifactHash, migrations };
374
+ return {
375
+ module,
376
+ info,
377
+ ports,
378
+ assets,
379
+ assetsHash: exactAssetsHash,
380
+ artifactHash,
381
+ migrations,
382
+ identity,
383
+ };
198
384
  }
199
385
  catch {
200
386
  return invalid();
@@ -203,6 +389,12 @@ export async function validateDeploymentArtifact(out) {
203
389
  export const unverifiedDeployContract = {
204
390
  versionsCreateScript: false,
205
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
+ },
206
398
  };
207
399
  /** Cloudflare's direct-upload example hashes base64 bytes plus the extension without its dot. */
208
400
  export function cloudflareAssetHash(bytes, name) {
@@ -214,7 +406,56 @@ export function cloudflareAssetHash(bytes, name) {
214
406
  export const cloudflareDeployContract = {
215
407
  versionsCreateScript: false,
216
408
  secretInheritance: true,
409
+ domainZone: resolveZone,
217
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
+ },
218
459
  createScript: async (api, script, form) => {
219
460
  const result = object((await api.request(`/workers/scripts/${script}`, 'PUT', form)).result);
220
461
  if (result.id !== script)
@@ -259,7 +500,7 @@ function distribution(a) {
259
500
  async function endpoints(api, base) {
260
501
  const r = object((await api.request(`${base}/subdomain`)).result);
261
502
  if (typeof r.enabled !== 'boolean' || typeof r.previews_enabled !== 'boolean')
262
- throw new CloudflareError('capability_unknown');
503
+ throw new CloudflareError('provider_failure');
263
504
  return { enabled: r.enabled, previews_enabled: r.previews_enabled };
264
505
  }
265
506
  function routeInput(raw, script) {
@@ -281,7 +522,7 @@ function routeInput(raw, script) {
281
522
  function secretNames(o) {
282
523
  const names = [...(o.secret ?? []), ...(o.secrets === undefined ? [] : o.secrets.split('\n'))];
283
524
  if (new Set(names).size !== names.length ||
284
- names.some((n) => !/^[A-Z][A-Z0-9_]{0,127}$/.test(n) || n === 'PB_TOKEN_PEPPER'))
525
+ names.some((n) => !/^[A-Z][A-Z0-9_]{0,127}$/.test(n) || ['PB_TOKEN_PEPPER', 'PB_IDP_PROVIDERS'].includes(n)))
285
526
  return invalid();
286
527
  return names.sort();
287
528
  }
@@ -506,23 +747,16 @@ async function readSettings(api, base, j, routes, domain, workerExists = true, m
506
747
  for (const zone of new Set([...routes.map((r) => r.zone_id), ...managedZones])) {
507
748
  const listed = await zoneRoutes(api, zone);
508
749
  for (const r of listed) {
509
- if (r.script !== j.identity.script_name && typeof r.pattern === 'string') {
510
- const attachedHost = r.pattern.replace(/^https?:\/\//, '').split('/')[0];
511
- if (routes.some((x) => {
512
- const wanted = x.pattern.replace(/^https?:\/\//, '').split('/')[0];
513
- return (attachedHost === '*' ||
514
- attachedHost === wanted ||
515
- (attachedHost.startsWith('*.') && wanted.endsWith(attachedHost.slice(1))));
516
- }))
517
- return conflict('routes');
518
- }
519
- if (r.script === j.identity.script_name ||
520
- routes.some((x) => x.zone_id === zone && x.pattern === r.pattern)) {
521
- if (r.script !== j.identity.script_name ||
522
- !safeId(r.id) ||
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) ||
523
757
  typeof r.pattern !== 'string' ||
524
758
  !/^[a-zA-Z0-9*./:-]{1,256}$/.test(r.pattern))
525
- return conflict('routes');
759
+ return conflict('routes', 'routes:own_invalid');
526
760
  foundRoutes.push({ id: r.id, zone_id: zone, pattern: r.pattern, script: r.script });
527
761
  }
528
762
  }
@@ -536,20 +770,8 @@ async function readSettings(api, base, j, routes, domain, workerExists = true, m
536
770
  domain: foundDomain,
537
771
  };
538
772
  }
539
- async function zoneRoutes(api, zone) {
540
- // Zone APIs must never be sent beneath the accounts prefix.
541
- const all = [];
542
- for (let page = 1; page <= 10000; page++) {
543
- const r = await api.requestZone(zone, `/workers/routes?page=${page}&per_page=100`);
544
- if (!Array.isArray(r.result))
545
- return conflict('routes');
546
- all.push(...r.result.map(object));
547
- if (r.result_info?.total_pages === page || r.result_info?.total_count === all.length)
548
- return all;
549
- if (typeof r.result_info?.total_pages !== 'number' || r.result_info.total_pages < page)
550
- return conflict('routes');
551
- }
552
- return conflict('routes');
773
+ export async function zoneRoutes(api, zone) {
774
+ return api.snapshotZone(zone, '/workers/routes');
553
775
  }
554
776
  function bindings(j, ports, url) {
555
777
  const b = [
@@ -697,7 +919,7 @@ export async function reconcileCandidatePreview(api, store, j) {
697
919
  /** Smoke owns this finally block. Persisted intent also covers process interruption. */
698
920
  export async function withCandidatePreview(api, store, j, contract, probe) {
699
921
  const a = j.deployment?.attempt;
700
- if (!a?.version_id || a.requested_rollout === 100 || !contract.preview)
922
+ if (!a?.version_id || a.requested_rollout === 100)
701
923
  return invalid();
702
924
  await reconcileCandidatePreview(api, store, j);
703
925
  const base = `/workers/scripts/${j.identity.script_name}`;
@@ -1057,12 +1279,15 @@ export async function deploy(o, operations = {}) {
1057
1279
  if (requestedRoutes.some((r) => r.pattern.replace(/^https:\/\//, '').split('/')[0] !== new URL(url).hostname))
1058
1280
  return invalid();
1059
1281
  const artifact = await validateDeploymentArtifact(out);
1282
+ if (o.providers?.kind === 'set')
1283
+ desiredProviders(o.providers, artifact.identity, null);
1060
1284
  const endpointEnv = artifact.ports?.observability.endpointEnv;
1061
1285
  const headersEnv = artifact.ports?.observability.headersEnv;
1062
1286
  const generatedNames = [
1063
1287
  'DB',
1064
1288
  'DEPLOYMENT_ID',
1065
1289
  'PB_PUBLIC_URL',
1290
+ 'PB_IDP_PROVIDERS',
1066
1291
  'ASSETS',
1067
1292
  ...(artifact.ports
1068
1293
  ? [
@@ -1164,6 +1389,12 @@ export async function deploy(o, operations = {}) {
1164
1389
  if (journal.deployment && journal.deployment.attempt.preview !== 'disabled') {
1165
1390
  if (o.dryRun) {
1166
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'] : []);
1167
1398
  output.status = 'planned';
1168
1399
  return succeed();
1169
1400
  }
@@ -1205,22 +1436,22 @@ export async function deploy(o, operations = {}) {
1205
1436
  return conflict('deployment');
1206
1437
  if (!current.versions.length && rollout !== 100)
1207
1438
  return invalid();
1439
+ let prior;
1208
1440
  if (current.versions.length === 1) {
1209
- const prior = object((await api.request(`${base}/versions/${current.versions[0].version_id}`)).result);
1441
+ prior = object((await api.request(`${base}/versions/${current.versions[0].version_id}`)).result);
1210
1442
  if (prior.id !== current.versions[0].version_id)
1211
1443
  return conflict('version_id');
1212
1444
  const annotations = object(prior.annotations);
1213
1445
  if (annotations['pb-maintenance-shell'] === 'true')
1214
1446
  throw new CloudflareError('capability_unknown', 1, false, 'deploy');
1215
1447
  }
1448
+ const workerOrigin = !url || rollout < 100 ? await workersDevOrigin(api, identity.script_name) : null;
1216
1449
  if (!url)
1217
- url = await workersDevOrigin(api, identity.script_name);
1450
+ url = workerOrigin;
1218
1451
  if (!exists && !contract.versionsCreateScript && !contract.createScript)
1219
1452
  throw new CloudflareError('capability_unknown');
1220
1453
  if (!contract.assetHash)
1221
1454
  throw new CloudflareError('capability_unknown');
1222
- if (rollout < 100 && !contract.preview)
1223
- throw new CloudflareError('capability_unknown');
1224
1455
  const custom = !!(o.domain || requestedRoutes.length || o.publicUrl);
1225
1456
  const policy = { enabled: !custom, previews_enabled: false };
1226
1457
  const journalled = journal.deployment;
@@ -1243,7 +1474,68 @@ export async function deploy(o, operations = {}) {
1243
1474
  return invalid();
1244
1475
  if (installedNames.length && !contract.secretInheritance)
1245
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);
1246
1536
  const plainBindings = bindings(journal, artifact.ports, url);
1537
+ if (providersText !== null)
1538
+ plainBindings.push({ type: 'plain_text', name: 'PB_IDP_PROVIDERS', text: providersText });
1247
1539
  if (endpointEnv) {
1248
1540
  if (selectedSecrets.includes(endpointEnv) || installedNames.includes(endpointEnv))
1249
1541
  return invalid();
@@ -1252,6 +1544,21 @@ export async function deploy(o, operations = {}) {
1252
1544
  const collisionNames = new Set(plainBindings.map((b) => b.name));
1253
1545
  if ([...selectedSecrets, ...installedNames].some((n) => collisionNames.has(n)))
1254
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' });
1255
1562
  const updates = [];
1256
1563
  for (const name of selectedSecrets) {
1257
1564
  if ((name === 'PB_MASTER_KEY' || name === 'PB_MASTER_KEY_PREVIOUS') &&
@@ -1265,25 +1572,26 @@ export async function deploy(o, operations = {}) {
1265
1572
  else if (!installedNames.includes(name))
1266
1573
  return invalid();
1267
1574
  }
1268
- const pending = journal.deployment?.attempt;
1269
- const pendingNames = pending &&
1270
- ['upload_pending', 'uploaded', 'configuring', 'activation_pending'].includes(pending.phase)
1271
- ? pending.secret_names
1272
- : [];
1273
1575
  const allNames = [
1274
1576
  ...new Set([...installedNames, ...pendingNames, ...updates.map((b) => String(b.name))]),
1275
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' });
1276
1586
  if (!allNames.includes('PB_MASTER_KEY'))
1277
1587
  return invalid();
1278
1588
  if (headersEnv && !allNames.includes(headersEnv))
1279
1589
  return invalid();
1280
1590
  let domain = live.domain;
1281
1591
  if (o.domain && !domain) {
1282
- if (!contract.domainZone)
1283
- throw new CloudflareError('capability_unknown');
1284
1592
  const zone = await contract.domainZone(api, o.domain);
1285
1593
  if (!/^[a-f0-9]{32}$/.test(zone))
1286
- throw new CloudflareError('capability_unknown');
1594
+ throw new CloudflareError('provider_failure', 1, false, 'deploy');
1287
1595
  domain = {
1288
1596
  id: null,
1289
1597
  zone_id: zone,
@@ -1319,7 +1627,6 @@ export async function deploy(o, operations = {}) {
1319
1627
  ledgerHash,
1320
1628
  }));
1321
1629
  const priorJournal = journal.deployment;
1322
- const priorAttempt = priorJournal?.attempt;
1323
1630
  const verified = priorAttempt?.phase === 'verified' && priorAttempt.smoke === 'passed'
1324
1631
  ? priorAttempt
1325
1632
  : (priorJournal?.last_verified ?? null);
@@ -1332,6 +1639,9 @@ export async function deploy(o, operations = {}) {
1332
1639
  livePlain !== undefined &&
1333
1640
  plainBindings.every((b) => livePlain.some((x) => equal(x, b))) &&
1334
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');
1335
1645
  if (verified &&
1336
1646
  plainMatch &&
1337
1647
  verified.artifact_sha256 === artifact.artifactHash &&
@@ -1347,37 +1657,36 @@ export async function deploy(o, operations = {}) {
1347
1657
  output.status = o.dryRun ? 'planned' : 'ok';
1348
1658
  return succeed();
1349
1659
  }
1350
- const resuming = priorAttempt &&
1351
- [
1352
- 'preparing_upload',
1353
- 'upload_pending',
1354
- 'uploaded',
1355
- 'configuring',
1356
- 'activation_pending',
1357
- ].includes(priorAttempt.phase);
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');
1358
1668
  if (resuming &&
1359
1669
  (priorAttempt.phase !== 'preparing_upload' || priorAttempt.script_creation !== undefined) &&
1360
1670
  updates.some((b) => b.name !== 'PB_MASTER_KEY' && b.name !== 'PB_MASTER_KEY_PREVIOUS'))
1361
1671
  return conflict('bindings:secret_text');
1362
1672
  if (resuming) {
1363
- if (priorAttempt.artifact_sha256 !== artifact.artifactHash)
1364
- return conflict('artifact_sha256');
1365
1673
  if (priorAttempt.settings_sha256 !== settingsHash)
1366
1674
  return conflict('settings_sha256');
1367
- if (priorAttempt.requested_rollout !== rollout)
1368
- return conflict('requested_rollout');
1369
1675
  }
1370
- if (!resuming) {
1371
- if (current.versions.length > 1)
1372
- return conflict('deployment');
1373
- let managed = priorAttempt?.intended_settings ?? verified?.intended_settings;
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;
1374
1683
  if (priorAttempt?.phase === 'failed' || priorAttempt?.phase === 'rolled_back')
1375
1684
  managed =
1376
1685
  restoredSnapshot(priorAttempt, priorAttempt.phase === 'failed' && equal(current, beforeCandidate(priorAttempt))
1377
1686
  ? live
1378
1687
  : undefined) ?? conflict('managed_settings');
1379
- if (live.routes.some((r) => !intended.routes.some((x) => x.zone_id === r.zone_id && x.pattern === r.pattern)))
1380
- return conflict('routes');
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');
1381
1690
  // Never take over pre-existing attachments without a previous managed receipt.
1382
1691
  if ((live.consumer || live.schedules.length || live.routes.length || live.domain) &&
1383
1692
  (!managed || !matchesManaged(live, managed)))
@@ -1407,14 +1716,19 @@ export async function deploy(o, operations = {}) {
1407
1716
  return succeed();
1408
1717
  }
1409
1718
  let a;
1410
- if (resuming)
1719
+ if (resuming) {
1411
1720
  a = priorAttempt;
1721
+ a.identity_sha256 = identityHash;
1722
+ }
1412
1723
  else {
1413
1724
  a = {
1414
1725
  operation_id: randomUUID(),
1415
1726
  artifact_sha256: artifact.artifactHash,
1416
1727
  assets_sha256: artifact.assetsHash,
1417
1728
  settings_sha256: settingsHash,
1729
+ providers_sha256: providersText === null ? null : digest(providersText),
1730
+ identity_sha256: identityHash,
1731
+ ...(providersText === null ? {} : { providers_text: providersText }),
1418
1732
  secret_names: allNames,
1419
1733
  build_info: artifact.info,
1420
1734
  version_id: null,
@@ -1557,14 +1871,20 @@ export async function deploy(o, operations = {}) {
1557
1871
  if (!a.version_id)
1558
1872
  return unknown();
1559
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
+ }
1560
1880
  if (rollout < 100) {
1561
1881
  const preview = await contract.preview(api, identity.script_name, a.version_id);
1562
1882
  if (preview.version_id !== a.version_id || !preview.bindings_verified)
1563
1883
  return conflict('preview');
1564
1884
  const probeUrl = origin(preview.url);
1565
- if (!probeUrl.endsWith('.workers.dev') ||
1566
- probeUrl === url ||
1567
- !new URL(probeUrl).hostname.startsWith(`${a.version_id}-`))
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}`)
1568
1888
  return conflict('preview');
1569
1889
  a.candidate_probe_url = probeUrl;
1570
1890
  await save(store, journal, now);
@@ -1587,13 +1907,13 @@ export async function deploy(o, operations = {}) {
1587
1907
  if (!equal(part(expected), part(a.prior_settings)) &&
1588
1908
  !equal(part(expected), part(a.intended_settings)) &&
1589
1909
  !(kind === 'endpoints' && equal(expected.endpoints, restrictivePolicy(a))))
1590
- return conflict(kind === 'consumer' ? 'queue_consumer' : kind);
1910
+ return conflict(kind === 'consumer' ? 'queue_consumer' : kind, ...(kind === 'routes' ? ['routes:drift'] : []));
1591
1911
  }
1592
1912
  for (const kind of ['consumer', 'schedules', 'routes', 'domain']) {
1593
1913
  const before = await readSettings(api, base, journal, requestedRoutes, o.domain, true, managedZones);
1594
1914
  const part = (v) => withoutIds(v)[kind];
1595
1915
  if (!equal(part(before), part(expected)) && !equal(part(before), part(a.intended_settings)))
1596
- return conflict(kind === 'consumer' ? 'queue_consumer' : kind);
1916
+ return conflict(kind === 'consumer' ? 'queue_consumer' : kind, ...(kind === 'routes' ? ['routes:drift'] : []));
1597
1917
  if (!equal(part(before), part(a.intended_settings))) {
1598
1918
  const change = {
1599
1919
  kind,
@@ -1615,7 +1935,7 @@ export async function deploy(o, operations = {}) {
1615
1935
  }
1616
1936
  const observed = await readSettings(api, base, journal, requestedRoutes, o.domain, true, managedZones);
1617
1937
  if (!equal(part(observed), part(a.intended_settings)))
1618
- return conflict(kind === 'consumer' ? 'queue_consumer' : kind);
1938
+ return conflict(kind === 'consumer' ? 'queue_consumer' : kind, ...(kind === 'routes' ? ['routes:drift'] : []));
1619
1939
  expected = { ...expected, [kind]: observed[kind] };
1620
1940
  a.intended_settings = { ...a.intended_settings, [kind]: observed[kind] };
1621
1941
  if (!equal(withoutIds(observed), withoutIds(expected)))
@@ -1722,7 +2042,9 @@ export async function deploy(o, operations = {}) {
1722
2042
  : failure instanceof CloudflareError && failure.stage === 'prebuilt'
1723
2043
  ? failure
1724
2044
  : failure instanceof CloudflareError
1725
- ? new CloudflareError(failure.code, failure.exitCode, failure.retryable, 'deploy', failure.status, failure.check, failure.providerCode, failure.source, failure.fields)
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
+ })
1726
2048
  : new CloudflareError('provider_failure', 1, false, 'deploy')));
1727
2049
  return result;
1728
2050
  }