@package-broker/cli 2.0.0-rc.3 → 2.0.0-rc.4

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 (50) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/build-artifacts.d.ts.map +1 -1
  3. package/dist/build-artifacts.js +1 -0
  4. package/dist/build-artifacts.js.map +1 -1
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +7 -2
  7. package/dist/build.js.map +1 -1
  8. package/dist/cli.d.ts.map +1 -1
  9. package/dist/cli.js +55 -7
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/admin.d.ts +6 -2
  12. package/dist/commands/admin.d.ts.map +1 -1
  13. package/dist/commands/admin.js +59 -4
  14. package/dist/commands/admin.js.map +1 -1
  15. package/dist/commands/cloudflare/api.d.ts +1 -0
  16. package/dist/commands/cloudflare/api.d.ts.map +1 -1
  17. package/dist/commands/cloudflare/api.js +15 -3
  18. package/dist/commands/cloudflare/api.js.map +1 -1
  19. package/dist/commands/cloudflare/deploy.d.ts +10 -0
  20. package/dist/commands/cloudflare/deploy.d.ts.map +1 -1
  21. package/dist/commands/cloudflare/deploy.js +215 -71
  22. package/dist/commands/cloudflare/deploy.js.map +1 -1
  23. package/dist/commands/cloudflare/input-messages.d.ts +3 -0
  24. package/dist/commands/cloudflare/input-messages.d.ts.map +1 -0
  25. package/dist/commands/cloudflare/input-messages.js +2 -0
  26. package/dist/commands/cloudflare/input-messages.js.map +1 -0
  27. package/dist/commands/cloudflare/inventory.d.ts +3 -1
  28. package/dist/commands/cloudflare/inventory.d.ts.map +1 -1
  29. package/dist/commands/cloudflare/inventory.js +17 -2
  30. package/dist/commands/cloudflare/inventory.js.map +1 -1
  31. package/dist/commands/cloudflare/local-auth.d.ts +34 -0
  32. package/dist/commands/cloudflare/local-auth.d.ts.map +1 -0
  33. package/dist/commands/cloudflare/local-auth.js +156 -0
  34. package/dist/commands/cloudflare/local-auth.js.map +1 -0
  35. package/dist/commands/cloudflare/setup.d.ts +3 -0
  36. package/dist/commands/cloudflare/setup.d.ts.map +1 -1
  37. package/dist/commands/cloudflare/setup.js +188 -43
  38. package/dist/commands/cloudflare/setup.js.map +1 -1
  39. package/dist/commands/cloudflare/teardown.d.ts.map +1 -1
  40. package/dist/commands/cloudflare/teardown.js +7 -3
  41. package/dist/commands/cloudflare/teardown.js.map +1 -1
  42. package/dist/commands/openapi-specs.d.ts.map +1 -1
  43. package/dist/commands/openapi-specs.js +20 -0
  44. package/dist/commands/openapi-specs.js.map +1 -1
  45. package/dist/manifest.d.ts +8 -1
  46. package/dist/manifest.d.ts.map +1 -1
  47. package/dist/manifest.js +52 -1
  48. package/dist/manifest.js.map +1 -1
  49. package/dist/prebuilt-expected.json +3 -3
  50. package/package.json +7 -7
@@ -15,11 +15,14 @@ import { DbError } from '@package-broker/core/db/errors';
15
15
  import { createD1HttpExecutor, isLostResponse } from '../../cloudflare/d1-http-executor.js';
16
16
  import { assertPrebuiltConfiguration, materializePrebuilt, prebuiltEnvelope, removePrebuilt, } from '../../prebuilt.js';
17
17
  import { builtRouteProblems, loadBuiltMigrations } from '../built-manifest.js';
18
+ import { snapshotLocalAuthCapability } from '../../manifest.js';
18
19
  import { validateProvidersDocument } from './providers-file.js';
19
20
  import { normalizeQueueConsumer } from './queue-consumer.js';
20
21
  import { assertMigrationsApplied } from './migrations-check.js';
21
22
  import { d1BindingId, workerSettingsDifferences } from './worker-settings.js';
22
23
  import { CloudflareApi, CloudflareError, cloudflareCredentials, commandEnvelope, failCommand, object, wasNotSent, } from './api.js';
24
+ import { inputMessages } from './input-messages.js';
25
+ import { resolveLocalAuth, verifiedDeployment, frozenLocalAuthAttempt, readLocalAuthSources, liveLocalAuthModes, } from './local-auth.js';
23
26
  import { resolveZone } from './zone.js';
24
27
  import { routeConflict } from './routes.js';
25
28
  import { InventoryStore, InventoryTransaction, digest, installIdentity, recordDeploymentHistory, DeploymentHistoryCapacityError, safeBuildInfo, } from './inventory.js';
@@ -44,7 +47,6 @@ const PREVIEW_BINDING_FIELDS = {
44
47
  const uuid = (v) => typeof v === 'string' &&
45
48
  /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(v);
46
49
  const safeId = (v) => typeof v === 'string' && /^[a-zA-Z0-9_-]{1,128}$/.test(v);
47
- const inputMessages = new WeakMap();
48
50
  const invalid = (message) => {
49
51
  const error = new CloudflareError('invalid_input', 2, false, 'deploy');
50
52
  if (message)
@@ -60,6 +62,12 @@ const conflict = (field, ...fields) => {
60
62
  const unknown = () => {
61
63
  throw new CloudflareError('outcome_unknown', 6, false, 'deploy');
62
64
  };
65
+ export function assertLocalAuthAccepted(mode, capability) {
66
+ if (mode === 'enabled')
67
+ return;
68
+ if (mode !== 'disabled' || snapshotLocalAuthCapability(capability) === null)
69
+ invalid('Cannot disable local sign-in on an artifact without local-auth support');
70
+ }
63
71
  export function assertProvidersAccepted(option, identity) {
64
72
  if (option?.kind !== 'set' || identity?.runtimeProviders)
65
73
  return;
@@ -96,12 +104,18 @@ function desiredProviders(option, identity, live) {
96
104
  return invalid('live PB_IDP_PROVIDERS is not valid; pass --providers <file> or --providers none');
97
105
  }
98
106
  }
99
- function planned({ id, kind, anchor }) {
107
+ function planned({ id, kind, anchor, policy, }) {
100
108
  const value = anchor.slice(anchor.indexOf(':') + 1);
101
109
  const slash = value.indexOf('/');
102
110
  return kind === 'oidc'
103
- ? { id, kind, issuer: value }
104
- : { id, kind, teamDomain: value.slice(0, slash), audience: value.slice(slash + 1) };
111
+ ? { id, kind, issuer: value, ...(policy === undefined ? {} : { policy }) }
112
+ : {
113
+ id,
114
+ kind,
115
+ teamDomain: value.slice(0, slash),
116
+ audience: value.slice(slash + 1),
117
+ ...(policy === undefined ? {} : { policy }),
118
+ };
105
119
  }
106
120
  function identityDigest(descriptor, providers) {
107
121
  return descriptor === null && providers === null
@@ -120,6 +134,60 @@ function providerPlan(descriptor, providersText) {
120
134
  const plan = planProviders(descriptor.compiled.map(planned), runtime);
121
135
  return { runtime, plan };
122
136
  }
137
+ export async function resolveProviderText(descriptor, option, sources, frozen, checkManaged) {
138
+ if (frozen) {
139
+ const text = frozenProviders(frozen);
140
+ if (option?.kind === 'set' || option?.kind === 'remove') {
141
+ const requested = desiredProviders(option, descriptor, null);
142
+ if (requested !== text &&
143
+ !(option.kind === 'set' &&
144
+ text !== null &&
145
+ validateProvidersDocument(text).value === requested))
146
+ return conflict('settings_sha256');
147
+ }
148
+ return text;
149
+ }
150
+ if (option?.kind === 'set' || option?.kind === 'remove')
151
+ return desiredProviders(option, descriptor, null);
152
+ if (sources.some((source) => source.bindings.map(object).some((b) => b.name === 'PB_IDP_PROVIDERS')))
153
+ await checkManaged();
154
+ const values = sources.map((source) => desiredProviders(option, descriptor, source));
155
+ if (new Set(values).size > 1)
156
+ return conflict('providers');
157
+ return values[0] ?? null;
158
+ }
159
+ export function setupProviderCandidate(descriptor, text) {
160
+ return descriptor === null ? [] : providerPlan(descriptor, text).plan.effective;
161
+ }
162
+ export async function checkSetupManagedProviders(api, journal, routes, domain) {
163
+ const requested = routeInput(routes, journal.identity.script_name);
164
+ const deployment = journal.deployment;
165
+ const zones = [
166
+ ...new Set([
167
+ deployment?.attempt.prior_settings,
168
+ deployment?.attempt.intended_settings,
169
+ deployment?.last_verified?.intended_settings,
170
+ ].flatMap((settings) => settings?.routes.map((r) => r.zone_id) ?? [])),
171
+ ];
172
+ const base = `/workers/scripts/${journal.identity.script_name}`;
173
+ const live = await readSettings(api, base, journal, requested, domain, true, zones);
174
+ const current = await active(api, base);
175
+ checkManagedSettings(journal.deployment?.attempt, verifiedDeployment(deployment), live, current, requested);
176
+ }
177
+ function checkManagedSettings(priorAttempt, recordedVerified, live, current, requestedRoutes) {
178
+ let managed = priorAttempt?.intended_settings ?? recordedVerified?.intended_settings;
179
+ if (priorAttempt?.phase === 'failed' || priorAttempt?.phase === 'rolled_back')
180
+ managed =
181
+ restoredSnapshot(priorAttempt, priorAttempt.phase === 'failed' && equal(current, beforeCandidate(priorAttempt))
182
+ ? live
183
+ : undefined) ?? conflict('managed_settings');
184
+ if (live.routes.some((r) => !requestedRoutes.some((x) => x.zone_id === r.zone_id && x.pattern === r.pattern)))
185
+ return conflict('routes', 'routes:unmanaged_own');
186
+ // Never take over pre-existing attachments without a previous managed receipt.
187
+ if ((live.consumer || live.schedules.length || live.routes.length || live.domain) &&
188
+ (!managed || !matchesManaged(live, managed)))
189
+ return conflict('managed_settings');
190
+ }
123
191
  function effectiveAnchors(descriptor, providersText) {
124
192
  if (descriptor === null)
125
193
  return [];
@@ -303,6 +371,10 @@ export async function validateDeploymentArtifact(out) {
303
371
  const identity = hasIdentity ? snapshotIdentityDescriptor(manifest.identity) : null;
304
372
  if (hasIdentity && identity === null)
305
373
  return invalid();
374
+ const hasLocalAuth = Object.hasOwn(manifest, 'localAuth');
375
+ const localAuth = hasLocalAuth ? snapshotLocalAuthCapability(manifest.localAuth) : null;
376
+ if (hasLocalAuth && localAuth === null)
377
+ return invalid();
306
378
  if (manifest.schemaVersion !== 1 ||
307
379
  manifest.platform !== 'cloudflare' ||
308
380
  !Array.isArray(manifest.plugins) ||
@@ -380,6 +452,7 @@ export async function validateDeploymentArtifact(out) {
380
452
  artifactHash,
381
453
  migrations,
382
454
  identity,
455
+ localAuth,
383
456
  };
384
457
  }
385
458
  catch {
@@ -521,8 +594,11 @@ function routeInput(raw, script) {
521
594
  }
522
595
  function secretNames(o) {
523
596
  const names = [...(o.secret ?? []), ...(o.secrets === undefined ? [] : o.secrets.split('\n'))];
597
+ if (names.includes('PB_AUTH_LOCAL'))
598
+ return invalid('Remove the secret first: wrangler secret delete PB_AUTH_LOCAL');
524
599
  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)))
600
+ names.some((n) => !/^[A-Z][A-Z0-9_]{0,127}$/.test(n) ||
601
+ ['PB_TOKEN_PEPPER', 'PB_IDP_PROVIDERS', 'PB_AUTH_LOCAL'].includes(n)))
526
602
  return invalid();
527
603
  return names.sort();
528
604
  }
@@ -1242,6 +1318,9 @@ export async function deploy(o, operations = {}) {
1242
1318
  let uploadSent = false;
1243
1319
  let creatingWorker = false;
1244
1320
  try {
1321
+ const cpuMs = o.cpuMs ?? 120000;
1322
+ if (!Number.isSafeInteger(cpuMs) || cpuMs < 1)
1323
+ return invalid();
1245
1324
  if (o.prebuilt)
1246
1325
  await assertPrebuiltConfiguration(resolve(o.project ?? dirname(resolve(o.configPath ?? 'broker.config.ts'))), o.configPath);
1247
1326
  const auth = cloudflareCredentials(o.env);
@@ -1288,6 +1367,7 @@ export async function deploy(o, operations = {}) {
1288
1367
  'DEPLOYMENT_ID',
1289
1368
  'PB_PUBLIC_URL',
1290
1369
  'PB_IDP_PROVIDERS',
1370
+ 'PB_AUTH_LOCAL',
1291
1371
  'ASSETS',
1292
1372
  ...(artifact.ports
1293
1373
  ? [
@@ -1379,6 +1459,24 @@ export async function deploy(o, operations = {}) {
1379
1459
  return conflict('worker:identity');
1380
1460
  const exists = matches.length === 1;
1381
1461
  creatingWorker = !exists;
1462
+ if (!exists) {
1463
+ try {
1464
+ await api.request(`${base}/settings`);
1465
+ if (journal.deployment?.attempt.script_creation !== undefined ||
1466
+ journal.resources.worker?.name === identity.script_name) {
1467
+ const error = new CloudflareError('outcome_unknown', 6, false, 'deploy');
1468
+ inputMessages.set(error, 'The journal owns this Worker creation, but the Worker list is stale; retry deploy after the Worker becomes visible.');
1469
+ throw error;
1470
+ }
1471
+ return conflict('worker:identity');
1472
+ }
1473
+ catch (error) {
1474
+ if (!(error instanceof CloudflareError) ||
1475
+ error.status !== 404 ||
1476
+ error.providerCode !== 10007)
1477
+ throw error;
1478
+ }
1479
+ }
1382
1480
  if (exists) {
1383
1481
  const tags = matches[0].tags;
1384
1482
  if (!Array.isArray(tags) ||
@@ -1402,6 +1500,23 @@ export async function deploy(o, operations = {}) {
1402
1500
  return conflict('preview_cleanup'); // Re-read the complete configuration on a fresh run after interrupted cleanup.
1403
1501
  }
1404
1502
  const workerSettings = exists ? await ownedWorkerSettings(api, base, journal) : null;
1503
+ const interrupted = journal.deployment?.attempt;
1504
+ if (exists &&
1505
+ !o.dryRun &&
1506
+ interrupted &&
1507
+ [
1508
+ 'preparing_upload',
1509
+ 'upload_pending',
1510
+ 'uploaded',
1511
+ 'configuring',
1512
+ 'activation_pending',
1513
+ ].includes(interrupted.phase)) {
1514
+ const liveEndpoints = await endpoints(api, base);
1515
+ await enforceEndpoints(api, store, journal, {
1516
+ enabled: liveEndpoints.enabled && restrictivePolicy(interrupted).enabled,
1517
+ previews_enabled: false,
1518
+ });
1519
+ }
1405
1520
  if (workerSettings) {
1406
1521
  for (const raw of workerSettings.bindings.map(object)) {
1407
1522
  const b = normalizedBinding(raw);
@@ -1426,10 +1541,7 @@ export async function deploy(o, operations = {}) {
1426
1541
  }
1427
1542
  await liveResources(api, journal, artifact.ports);
1428
1543
  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;
1544
+ const recordedVerified = verifiedDeployment(journal.deployment);
1433
1545
  if (current.versions.length > 1 &&
1434
1546
  journal.deployment?.attempt.phase !== 'activation_pending' &&
1435
1547
  !(recordedVerified && equal(current.versions, distribution(recordedVerified))))
@@ -1475,14 +1587,7 @@ export async function deploy(o, operations = {}) {
1475
1587
  if (installedNames.length && !contract.secretInheritance)
1476
1588
  throw new CloudflareError('capability_unknown');
1477
1589
  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);
1590
+ const resuming = priorAttempt && frozenLocalAuthAttempt(priorAttempt, 'deploy') !== undefined;
1486
1591
  if (resuming) {
1487
1592
  if (priorAttempt.artifact_sha256 !== artifact.artifactHash)
1488
1593
  return conflict('artifact_sha256');
@@ -1494,46 +1599,54 @@ export async function deploy(o, operations = {}) {
1494
1599
  const removeSecret = () => invalid('Remove the secret first: wrangler secret delete PB_IDP_PROVIDERS');
1495
1600
  if (installedNames.includes('PB_IDP_PROVIDERS'))
1496
1601
  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
- }
1602
+ const sources = exists
1603
+ ? await readLocalAuthSources({
1604
+ api,
1605
+ base,
1606
+ versions: current.versions,
1607
+ settings: workerSettings,
1608
+ prior,
1609
+ stage: 'deploy',
1610
+ })
1611
+ : [];
1612
+ const localAuth = resolveLocalAuth({
1613
+ flag: o.localAuth,
1614
+ verified: recordedVerified,
1615
+ env: o.env.PB_AUTH_LOCAL,
1616
+ sources: exists ? sources : null,
1617
+ secretNames: [...selectedSecrets, ...installedNames],
1618
+ ...(resuming ? { frozen: priorAttempt } : {}),
1619
+ });
1620
+ const liveModes = liveLocalAuthModes(sources, 'deploy');
1621
+ if (liveModes.some((mode) => mode !== localAuth) ||
1622
+ (recordedVerified && localAuth !== (recordedVerified.local_auth ?? 'enabled')))
1623
+ output.checks.push({
1624
+ name: 'local_auth_change',
1625
+ status: 'notice',
1626
+ source: `Local sign-in configuration changes to ${localAuth}.`,
1627
+ });
1628
+ assertLocalAuthAccepted(localAuth, artifact.localAuth);
1629
+ output.checks.push({ name: 'local_auth', status: localAuth });
1630
+ const providersText = await resolveProviderText(artifact.identity, o.providers, sources, resuming ? priorAttempt : undefined, checkManaged);
1532
1631
  const identityHash = identityDigest(artifact.identity, providersText);
1632
+ if (recordedVerified &&
1633
+ !resuming &&
1634
+ identityHash !== (recordedVerified.identity_sha256 ?? null))
1635
+ output.checks.push({
1636
+ name: 'sso_artifact_upgrade',
1637
+ status: 'notice',
1638
+ source: 'The first deploy of a rebuilt artifact after upgrading counts as an SSO configuration change; one partial rollout will be refused. Deploy with --rollout 100 first.',
1639
+ });
1533
1640
  const anchors = effectiveAnchors(artifact.identity, providersText);
1534
1641
  if (o.dryRun)
1535
1642
  output.identity = dryRunIdentity(artifact.identity, providersText, identityHash);
1643
+ const effectiveProviders = artifact.identity === null
1644
+ ? []
1645
+ : providerPlan(artifact.identity, providersText).plan.effective;
1646
+ if (localAuth === 'disabled' && effectiveProviders.length === 0)
1647
+ return invalid('Cannot disable local sign-in without a usable SSO provider');
1536
1648
  const plainBindings = bindings(journal, artifact.ports, url);
1649
+ plainBindings.push({ type: 'plain_text', name: 'PB_AUTH_LOCAL', text: localAuth });
1537
1650
  if (providersText !== null)
1538
1651
  plainBindings.push({ type: 'plain_text', name: 'PB_IDP_PROVIDERS', text: providersText });
1539
1652
  if (endpointEnv) {
@@ -1550,8 +1663,11 @@ export async function deploy(o, operations = {}) {
1550
1663
  ? pending.secret_names
1551
1664
  : [];
1552
1665
  const missingSecrets = [
1553
- ...new Set(parseRuntimeProviders(providersText ?? undefined)
1554
- .accepted.filter((p) => p.kind === 'oidc')
1666
+ ...new Set([
1667
+ ...parseRuntimeProviders(providersText ?? undefined).accepted,
1668
+ ...(localAuth === 'disabled' ? effectiveProviders : []),
1669
+ ]
1670
+ .filter((p) => p.kind === 'oidc')
1555
1671
  .map((p) => providerSecretName(p.id, 'CLIENT_SECRET'))),
1556
1672
  ].filter((name) => !installedNames.includes(name) &&
1557
1673
  !pendingNames.includes(name) &&
@@ -1627,9 +1743,7 @@ export async function deploy(o, operations = {}) {
1627
1743
  ledgerHash,
1628
1744
  }));
1629
1745
  const priorJournal = journal.deployment;
1630
- const verified = priorAttempt?.phase === 'verified' && priorAttempt.smoke === 'passed'
1631
- ? priorAttempt
1632
- : (priorJournal?.last_verified ?? null);
1746
+ const verified = verifiedDeployment(priorJournal);
1633
1747
  const livePlain = workerSettings &&
1634
1748
  workerSettings.bindings
1635
1749
  .map(object)
@@ -1642,6 +1756,12 @@ export async function deploy(o, operations = {}) {
1642
1756
  if (anchors.length &&
1643
1757
  (await anchorPreflight({ ...auth, ...(o.fetch ? { fetch: o.fetch } : {}) }, journal.resources.d1?.id, anchors, o.dryRun === true)) === 'skipped')
1644
1758
  output.identity?.skipped_checks.push('anchor_preflight');
1759
+ if (rollout < 100 &&
1760
+ !resuming &&
1761
+ (localAuth !== (verified?.local_auth ?? 'enabled') ||
1762
+ sources.some((source) => resolveLocalAuth({ sources: [source] }) !==
1763
+ localAuth)))
1764
+ return invalid('partial rollout is not allowed when local sign-in configuration changes; deploy with --rollout 100 first');
1645
1765
  if (verified &&
1646
1766
  plainMatch &&
1647
1767
  verified.artifact_sha256 === artifact.artifactHash &&
@@ -1651,6 +1771,7 @@ export async function deploy(o, operations = {}) {
1651
1771
  equal(current.versions, distribution(verified)) &&
1652
1772
  equal(withoutIds(live), withoutIds(intended)) &&
1653
1773
  !updates.some((b) => b.name !== 'PB_MASTER_KEY' && b.name !== 'PB_MASTER_KEY_PREVIOUS')) {
1774
+ await version(api, base, verified.version_id, verified, allNames, plainBindings);
1654
1775
  outputAttempt(output, verified);
1655
1776
  output.plan.push({ stage: 'deploy', action: 'unchanged' });
1656
1777
  output.checks.push({ name: 'endpoint_policy', status: 'verified', ...policy });
@@ -1673,30 +1794,37 @@ export async function deploy(o, operations = {}) {
1673
1794
  if (priorAttempt.settings_sha256 !== settingsHash)
1674
1795
  return conflict('settings_sha256');
1675
1796
  }
1797
+ if (resuming &&
1798
+ (priorAttempt.version_id ||
1799
+ priorAttempt.phase === 'upload_pending' ||
1800
+ priorAttempt.script_creation)) {
1801
+ let id = priorAttempt.version_id;
1802
+ const creation = priorAttempt.script_creation !== undefined;
1803
+ if (!id) {
1804
+ const versions = await api.list(`${base}/versions`, 'items');
1805
+ const matches = versions.filter((v) => versionAnnotationsMatch(object(v.annotations), priorAttempt, creation));
1806
+ if (matches.length !== 1 || !uuid(matches[0].id))
1807
+ return unknown();
1808
+ id = matches[0].id;
1809
+ }
1810
+ await version(api, base, id, priorAttempt, allNames, plainBindings, creation);
1811
+ }
1676
1812
  if (!resuming && current.versions.length > 1)
1677
1813
  return conflict('deployment');
1678
1814
  checkManaged();
1679
1815
  function checkManaged() {
1680
1816
  if (resuming)
1681
1817
  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');
1818
+ checkManagedSettings(priorAttempt, recordedVerified, live, current, requestedRoutes);
1694
1819
  }
1695
1820
  if (priorJournal) {
1696
1821
  const pendingHistory = structuredClone(priorJournal);
1697
1822
  recordDeploymentHistory(pendingHistory, {
1698
1823
  plainBinding: { name: 'PB_PUBLIC_URL', text: url },
1699
1824
  });
1825
+ recordDeploymentHistory(pendingHistory, {
1826
+ plainBinding: { name: 'PB_AUTH_LOCAL', text: localAuth },
1827
+ });
1700
1828
  recordDeploymentHistory(pendingHistory, {
1701
1829
  reserveVersion: !resuming || !priorAttempt.version_id,
1702
1830
  ...(endpointEnv ? { plainBinding: { name: endpointEnv, text: telemetryEndpoint } } : {}),
@@ -1719,6 +1847,7 @@ export async function deploy(o, operations = {}) {
1719
1847
  if (resuming) {
1720
1848
  a = priorAttempt;
1721
1849
  a.identity_sha256 = identityHash;
1850
+ a.local_auth = localAuth;
1722
1851
  }
1723
1852
  else {
1724
1853
  a = {
@@ -1728,6 +1857,7 @@ export async function deploy(o, operations = {}) {
1728
1857
  settings_sha256: settingsHash,
1729
1858
  providers_sha256: providersText === null ? null : digest(providersText),
1730
1859
  identity_sha256: identityHash,
1860
+ local_auth: localAuth,
1731
1861
  ...(providersText === null ? {} : { providers_text: providersText }),
1732
1862
  secret_names: allNames,
1733
1863
  build_info: artifact.info,
@@ -1757,6 +1887,9 @@ export async function deploy(o, operations = {}) {
1757
1887
  last_verified: verified,
1758
1888
  ...(priorJournal?.history ? { history: structuredClone(priorJournal.history) } : {}),
1759
1889
  };
1890
+ recordDeploymentHistory(journal.deployment, {
1891
+ plainBinding: { name: 'PB_AUTH_LOCAL', text: localAuth },
1892
+ });
1760
1893
  recordDeploymentHistory(journal.deployment, {
1761
1894
  ...(priorAttempt?.version_id ? { versionId: priorAttempt.version_id } : {}),
1762
1895
  ...(endpointEnv ? { plainBinding: { name: endpointEnv, text: telemetryEndpoint } } : {}),
@@ -1764,10 +1897,16 @@ export async function deploy(o, operations = {}) {
1764
1897
  await save(store, journal, now);
1765
1898
  }
1766
1899
  attemptStarted = true;
1767
- if (endpointEnv && journal.deployment)
1900
+ if (journal.deployment) {
1768
1901
  recordDeploymentHistory(journal.deployment, {
1769
- plainBinding: { name: endpointEnv, text: telemetryEndpoint },
1902
+ plainBinding: { name: 'PB_AUTH_LOCAL', text: localAuth },
1770
1903
  });
1904
+ if (endpointEnv)
1905
+ recordDeploymentHistory(journal.deployment, {
1906
+ plainBinding: { name: endpointEnv, text: telemetryEndpoint },
1907
+ });
1908
+ await save(store, journal, now);
1909
+ }
1771
1910
  outputAttempt(output, a);
1772
1911
  if (a.script_creation?.state === 'pending' ||
1773
1912
  (a.script_creation?.state === 'complete' && a.phase === 'preparing_upload')) {
@@ -1800,6 +1939,7 @@ export async function deploy(o, operations = {}) {
1800
1939
  form.set('metadata', JSON.stringify({
1801
1940
  ...COMPATIBILITY,
1802
1941
  main_module: 'worker.js',
1942
+ limits: { cpu_ms: cpuMs },
1803
1943
  bindings: [...plainBindings, ...updates],
1804
1944
  ...(installedNames.length ? { keep_bindings: ['secret_text'] } : {}),
1805
1945
  assets: { jwt: token, config: ASSET_CONFIG },
@@ -1871,6 +2011,10 @@ export async function deploy(o, operations = {}) {
1871
2011
  if (!a.version_id)
1872
2012
  return unknown();
1873
2013
  await version(api, base, a.version_id, a, allNames, plainBindings);
2014
+ recordDeploymentHistory(journal.deployment, {
2015
+ plainBinding: { name: 'PB_AUTH_LOCAL', text: localAuth },
2016
+ });
2017
+ await save(store, journal, now);
1874
2018
  if (providersText !== null) {
1875
2019
  recordDeploymentHistory(journal.deployment, {
1876
2020
  plainBinding: { name: 'PB_IDP_PROVIDERS', text: providersText },