@hs-x/cli 0.4.6 → 0.4.8

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 (81) hide show
  1. package/README.md +6 -5
  2. package/dist/account-store.d.ts +9 -0
  3. package/dist/account-store.d.ts.map +1 -1
  4. package/dist/account-store.js +11 -0
  5. package/dist/account-store.js.map +1 -1
  6. package/dist/cloudflare-auth.js +3 -3
  7. package/dist/cloudflare-auth.js.map +1 -1
  8. package/dist/cloudflare-kv.d.ts +9 -0
  9. package/dist/cloudflare-kv.d.ts.map +1 -1
  10. package/dist/cloudflare-kv.js +16 -7
  11. package/dist/cloudflare-kv.js.map +1 -1
  12. package/dist/cloudflare-oauth.d.ts.map +1 -1
  13. package/dist/cloudflare-oauth.js +5 -3
  14. package/dist/cloudflare-oauth.js.map +1 -1
  15. package/dist/cloudflare-scoped-resource.d.ts +17 -2
  16. package/dist/cloudflare-scoped-resource.d.ts.map +1 -1
  17. package/dist/cloudflare-scoped-resource.js +46 -6
  18. package/dist/cloudflare-scoped-resource.js.map +1 -1
  19. package/dist/cloudflare-workers-logs.d.ts +38 -0
  20. package/dist/cloudflare-workers-logs.d.ts.map +1 -0
  21. package/dist/cloudflare-workers-logs.js +297 -0
  22. package/dist/cloudflare-workers-logs.js.map +1 -0
  23. package/dist/commands/api.d.ts.map +1 -1
  24. package/dist/commands/api.js +1 -1
  25. package/dist/commands/api.js.map +1 -1
  26. package/dist/commands/connect.js +3 -3
  27. package/dist/commands/connect.js.map +1 -1
  28. package/dist/commands/control-plane-read.d.ts +2 -2
  29. package/dist/commands/control-plane-read.d.ts.map +1 -1
  30. package/dist/commands/control-plane-read.js +90 -34
  31. package/dist/commands/control-plane-read.js.map +1 -1
  32. package/dist/commands/deploy-promote.js +3 -3
  33. package/dist/commands/deploy-promote.js.map +1 -1
  34. package/dist/commands/deploy.d.ts +31 -1
  35. package/dist/commands/deploy.d.ts.map +1 -1
  36. package/dist/commands/deploy.js +341 -43
  37. package/dist/commands/deploy.js.map +1 -1
  38. package/dist/commands/dev.js +7 -9
  39. package/dist/commands/dev.js.map +1 -1
  40. package/dist/commands/link.d.ts.map +1 -1
  41. package/dist/commands/link.js +41 -7
  42. package/dist/commands/link.js.map +1 -1
  43. package/dist/commands/rollback.d.ts +23 -0
  44. package/dist/commands/rollback.d.ts.map +1 -1
  45. package/dist/commands/rollback.js +60 -13
  46. package/dist/commands/rollback.js.map +1 -1
  47. package/dist/commands/status.d.ts.map +1 -1
  48. package/dist/commands/status.js +4 -4
  49. package/dist/commands/status.js.map +1 -1
  50. package/dist/commands/sync.d.ts.map +1 -1
  51. package/dist/commands/sync.js +24 -9
  52. package/dist/commands/sync.js.map +1 -1
  53. package/dist/constants.d.ts +1 -1
  54. package/dist/constants.js +1 -1
  55. package/dist/control-plane-fetch.d.ts +3 -1
  56. package/dist/control-plane-fetch.d.ts.map +1 -1
  57. package/dist/control-plane-fetch.js +9 -7
  58. package/dist/control-plane-fetch.js.map +1 -1
  59. package/dist/deploy-targets.d.ts +0 -1
  60. package/dist/deploy-targets.d.ts.map +1 -1
  61. package/dist/deploy-targets.js +2 -5
  62. package/dist/deploy-targets.js.map +1 -1
  63. package/dist/dev/invoke.d.ts +7 -0
  64. package/dist/dev/invoke.d.ts.map +1 -1
  65. package/dist/dev/invoke.js +18 -5
  66. package/dist/dev/invoke.js.map +1 -1
  67. package/dist/errors-registry.js +8 -8
  68. package/dist/errors-registry.js.map +1 -1
  69. package/dist/hubspot-developer-client.js +2 -2
  70. package/dist/hubspot-developer-client.js.map +1 -1
  71. package/dist/services/control-plane.d.ts +3 -1
  72. package/dist/services/control-plane.d.ts.map +1 -1
  73. package/dist/services/control-plane.js +8 -6
  74. package/dist/services/control-plane.js.map +1 -1
  75. package/dist/services/hubspot-dev-client.js +1 -1
  76. package/dist/services/hubspot-dev-client.js.map +1 -1
  77. package/dist/tenant-state.d.ts +12 -2
  78. package/dist/tenant-state.d.ts.map +1 -1
  79. package/dist/tenant-state.js +42 -18
  80. package/dist/tenant-state.js.map +1 -1
  81. package/package.json +7 -7
@@ -16,9 +16,9 @@ import { isLinked } from '../account-store.js';
16
16
  import { exitWith } from '../cli-error.js';
17
17
  import { accountIdOption, controlPlaneUrlOption, cwdOption, deployIdOption, forceOption, jsonOption, projectIdOption, runHandler, runQuarantined, userIdOption, yesOption, } from '../cli/kit.js';
18
18
  import { classifyCloudflareCommandFailure, preflightCloudflareAnalyticsEngine, validateCloudflareResourceForAccount, } from '../cloudflare-account-preflight.js';
19
- import { resolveCloudflareCredentials } from '../cloudflare-kv.js';
19
+ import { applyLeasedCloudflareCredentials, resolveCloudflareCredentials, } from '../cloudflare-kv.js';
20
20
  import { loadCloudflarePointer } from '../cloudflare-pointer.js';
21
- import { resolveOrProvisionCloudflareScopedResource } from '../cloudflare-scoped-resource.js';
21
+ import { parseWranglerJsonArray, resolveOrProvisionCloudflareScopedResource, } from '../cloudflare-scoped-resource.js';
22
22
  import { DEFAULT_CONTROL_PLANE_URL, formatConfigUrl, loadCliConfig, resolveControlPlaneUrl, } from '../config.js';
23
23
  import { CLI_VERSION } from '../constants.js';
24
24
  import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
@@ -27,10 +27,12 @@ import { createControlPlaneTenantProvisioningAuthority, } from '../control-plane
27
27
  import { captureDeployRevision } from '../deploy-revision-capture.js';
28
28
  import { resolveDeployTargets } from '../deploy-targets.js';
29
29
  import { executeCliHttp } from '../effect-http.js';
30
+ import { discoverHubSpotAccounts } from '../hubspot-auth.js';
30
31
  import { DirectHubSpotAccountResolutionError, createDeployHubSpotDeveloperClient, resolveDirectHubSpotContext, } from '../hubspot-developer-client.js';
31
32
  import { HubSpotProjectArchiveSourceError, fingerprintHubSpotArchiveSource, writeLocalHubSpotProjectArchive, } from '../hubspot-project-archive.js';
32
33
  import { deployHubSpotProjectBuild, readHubSpotSubbuildStatuses, waitForHubSpotAutoDeploy, waitForHubSpotBuildStatus, } from '../hubspot-project-lifecycle.js';
33
34
  import { detectLocalProjectMode, readHubSpotProjectDescriptor, validateHubSpotProject, } from '../hubspot-project.js';
35
+ import { linkMigrationJournalId, loadLinkMigrationJournal } from '../link-migration-journal.js';
34
36
  import { hydrateAncestorEnv } from '../load-env.js';
35
37
  import { LocalProjectPickerCancelled, resolveLocalProject } from '../local-project-picker.js';
36
38
  import { getMachineId, getMachineSecret, previewMachineId } from '../machine-id.js';
@@ -42,7 +44,7 @@ import { createReporter } from '../reporter/index.js';
42
44
  import { Cwd } from '../services/cwd.js';
43
45
  import { createTenantDataPlaneProvisioningCoordinator, } from '../tenant-data-plane-provisioning.js';
44
46
  import { ensureTenantStateStore, preflightTenantStateCredentials, } from '../tenant-state.js';
45
- import { defaultTokenKeyCustodyAdapter, escrowTokenKey, resolveEscrowedTokenKey, tokenKeyCustodyIdFor, } from '../token-key-custody.js';
47
+ import { defaultTokenKeyCustodyAdapter, escrowTokenKey, resolveEscrowedTokenKey, sealTokenKeyCanary, tokenKeyCustodyAdapterFor, tokenKeyCustodyIdFor, } from '../token-key-custody.js';
46
48
  import { deployPromoteCommand, requestControlPlaneDeployPromotion } from './deploy-promote.js';
47
49
  import { withTransientDeployRetry } from './deploy-retry.js';
48
50
  async function hostedHttp(input) {
@@ -494,11 +496,13 @@ async function hydrateCloudflareTokenFromControlPlane(input) {
494
496
  const lease = await leaseCloudflareViaSession(input.argv, input.root);
495
497
  if (!lease)
496
498
  return false;
497
- process.env.CLOUDFLARE_API_TOKEN = lease.apiToken;
498
- process.env.CLOUDFLARE_ACCOUNT_ID = lease.cloudflareAccountId;
499
+ applyLeasedCloudflareCredentials({
500
+ apiToken: lease.apiToken,
501
+ accountId: lease.cloudflareAccountId,
502
+ });
499
503
  return true;
500
504
  }
501
- async function waitForHealthyControlPlaneDrift({ controlPlaneUrl, projectId, deployId, userId, timeoutMs, workerUrl, }) {
505
+ async function waitForHealthyControlPlaneDrift({ controlPlaneUrl, accountId, projectId, deployId, userId, timeoutMs, workerUrl, }) {
502
506
  const startedAt = Date.now();
503
507
  let lastState = 'unknown';
504
508
  let lastReason;
@@ -508,7 +512,7 @@ async function waitForHealthyControlPlaneDrift({ controlPlaneUrl, projectId, dep
508
512
  }
509
513
  const response = await hostedHttp({
510
514
  url: new URL(`/v1/projects/${encodeURIComponent(projectId)}/drift`, controlPlaneUrl),
511
- headers: await controlPlaneAuthHeaders(userId),
515
+ headers: await controlPlaneAuthHeaders(userId, undefined, accountId),
512
516
  });
513
517
  const body = await response.json().catch(() => undefined);
514
518
  if (response.ok) {
@@ -976,7 +980,7 @@ async function executeHubSpotDirectDeployCommand({ argv, root, json, }) {
976
980
  ? (readStatus(uploaded.deployStatus) ?? 'UNKNOWN')
977
981
  : undefined;
978
982
  const deployUnverified = uploaded.autoDeployExpected && deployState === undefined;
979
- const ok = buildState === 'SUCCESS' && !deployUnverified && (!deployState || deployState === 'SUCCESS');
983
+ const ok = hubSpotUploadSucceeded(uploaded);
980
984
  const result = {
981
985
  schema_version: 1,
982
986
  ok,
@@ -1101,8 +1105,14 @@ export async function deployCommand({ argv, root, json, }) {
1101
1105
  const noHeartbeat = argv.includes('--no-heartbeat');
1102
1106
  const noRecord = argv.includes('--no-record');
1103
1107
  const explicitControlPlaneUrl = resolveFlag(argv, '--control-plane-url');
1104
- const controlPlaneUrl = explicitControlPlaneUrl ?? (planOnly ? undefined : process.env.HSX_CONTROL_PLANE_URL);
1105
1108
  const linked = await isLinked();
1109
+ const controlPlaneUrl = resolveDeployControlPlaneUrl({
1110
+ explicitControlPlaneUrl,
1111
+ environmentControlPlaneUrl: process.env.HSX_CONTROL_PLANE_URL,
1112
+ platformControlPlaneUrl: resolveControlPlaneUrl(argv),
1113
+ linked,
1114
+ planOnly,
1115
+ });
1106
1116
  const userId = resolveFlag(argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user';
1107
1117
  const resolvedAcct = await resolveAccountIdOrPrompt({
1108
1118
  argv,
@@ -1156,7 +1166,6 @@ export async function deployCommand({ argv, root, json, }) {
1156
1166
  const needsHubSpotBundle = needsRuntime || app.mcpServers.length > 0;
1157
1167
  const { deploymentMode, cloudflareDeployRequested, hubspotUploadRequested } = resolveDeployTargets({
1158
1168
  buildOnly,
1159
- planOnly,
1160
1169
  needsRuntime,
1161
1170
  hasProjectId: Boolean(projectId),
1162
1171
  hubspotOnly,
@@ -1468,7 +1477,17 @@ export async function deployCommand({ argv, root, json, }) {
1468
1477
  // Worker origin is resolvable up front, upload BEFORE the Cloudflare deploy so
1469
1478
  // the minted app id binds in a single pass. Falls back to the original
1470
1479
  // deploy-then-upload order whenever the origin can't be resolved.
1471
- const appIdKnownBeforeDeploy = readOptionalHubSpotAppId(resolveFlag(argv, '--hubspot-app-id') ?? process.env.HSX_HUBSPOT_APP_ID) ?? (await readPersistedHubSpotAppId(root, await resolveHubSpotProjectName(argv, root)));
1480
+ const appIdKnownBeforeDeploy = controlPlanePlan && controlPlaneBackedPlan && controlPlaneUrl
1481
+ ? await resolveHubSpotAppIdForDeploy({
1482
+ argv,
1483
+ root,
1484
+ controlPlaneUrl,
1485
+ userId,
1486
+ accountId: controlPlanePlan.accountId,
1487
+ projectId: controlPlanePlan.projectId,
1488
+ environment: resolveFlag(argv, '--env') ?? resolveFlag(argv, '--environment') ?? 'production',
1489
+ })
1490
+ : (readOptionalHubSpotAppId(resolveFlag(argv, '--hubspot-app-id') ?? process.env.HSX_HUBSPOT_APP_ID) ?? (await readPersistedHubSpotAppIdForDeploy(argv, root)));
1472
1491
  const uploadFirstOrigin = cloudflareDeployRequested &&
1473
1492
  !cloudflareDryRun &&
1474
1493
  !planOnly &&
@@ -1617,6 +1636,10 @@ export async function deployCommand({ argv, root, json, }) {
1617
1636
  plan: controlPlanePlan,
1618
1637
  promoteWhenHealthy,
1619
1638
  ...(capturedRevision ? { revision: capturedRevision } : {}),
1639
+ ...(() => {
1640
+ const deployedWorkerUrl = cloudflareDeployResult?.workers.find((worker) => worker.url)?.url;
1641
+ return deployedWorkerUrl ? { workerUrl: deployedWorkerUrl } : {};
1642
+ })(),
1620
1643
  })
1621
1644
  : controlPlaneUrl
1622
1645
  ? await requestHostedControlPlaneDeployRecordAndMaybePromote({
@@ -1715,7 +1738,7 @@ export async function deployCommand({ argv, root, json, }) {
1715
1738
  // run-008 build #2's failure mode. Only a SUCCESS status counts.
1716
1739
  const hubspotUploadFailed = !planOnly &&
1717
1740
  hubspotUploadRequested &&
1718
- (!hubspotUploadResult || readStatus(hubspotUploadResult.buildStatus) !== 'SUCCESS');
1741
+ (!hubspotUploadResult || !hubSpotUploadSucceeded(hubspotUploadResult));
1719
1742
  const cloudflareDeployed = !planOnly && cloudflareDeployRequested && Boolean(cloudflareDeployResult);
1720
1743
  // A live worker with only the HubSpot upload failing: not a clean success,
1721
1744
  // but distinct from total failure so callers don't tear down a good worker.
@@ -1831,6 +1854,16 @@ export async function deployCommand({ argv, root, json, }) {
1831
1854
  // automation doesn't treat a serving worker as a total failure), 1 = failure.
1832
1855
  return { exitCode: plan.ok ? 0 : plan.partial ? 20 : 1 };
1833
1856
  }
1857
+ /** @internal Exported for the linked-default deploy regression suite. */
1858
+ export function resolveDeployControlPlaneUrl(input) {
1859
+ if (input.explicitControlPlaneUrl)
1860
+ return input.explicitControlPlaneUrl;
1861
+ if (input.planOnly)
1862
+ return undefined;
1863
+ if (input.linked)
1864
+ return input.platformControlPlaneUrl;
1865
+ return input.environmentControlPlaneUrl;
1866
+ }
1834
1867
  function buildDeployPlanSummary(input) {
1835
1868
  const lines = ['', 'Planned changes:'];
1836
1869
  const capCount = input.workers.reduce((c, w) => c + w.capabilities.length, 0);
@@ -2701,6 +2734,14 @@ function defaultHubSpotFieldType(type) {
2701
2734
  return 'text';
2702
2735
  }
2703
2736
  }
2737
+ /** @internal Exported for deterministic deploy-outcome regression tests. */
2738
+ export function hubSpotUploadSucceeded(upload) {
2739
+ if (readStatus(upload.buildStatus) !== 'SUCCESS')
2740
+ return false;
2741
+ if (upload.deployStatus !== undefined)
2742
+ return readStatus(upload.deployStatus) === 'SUCCESS';
2743
+ return !upload.autoDeployExpected;
2744
+ }
2704
2745
  async function executeCloudflareDeploy({ argv, root, workers, controlPlanePlan, controlPlaneUrl, userId, machineId, machineSecret, heartbeatEnabled, dryRun, note, }) {
2705
2746
  // Fail fast on missing Cloudflare credentials BEFORE deploying any Worker, so
2706
2747
  // an unlinked deploy can't leave a Worker live and then error on the
@@ -2733,7 +2774,15 @@ async function executeCloudflareDeploy({ argv, root, workers, controlPlanePlan,
2733
2774
  const appConfig = await readHsxAppConfig(root);
2734
2775
  const environment = resolveFlag(argv, '--env') ?? resolveFlag(argv, '--environment') ?? 'production';
2735
2776
  const authUrl = await resolveHubSpotAppAuthUrl(argv, root);
2736
- const hubspotAppId = requireHubSpotCardAppId(readOptionalHubSpotAppId(resolveFlag(argv, '--hubspot-app-id') ?? process.env.HSX_HUBSPOT_APP_ID) ?? (await readPersistedHubSpotAppId(root, await resolveHubSpotProjectName(argv, root))), authUrl);
2777
+ const hubspotAppId = requireHubSpotCardAppId(await resolveHubSpotAppIdForDeploy({
2778
+ argv,
2779
+ root,
2780
+ ...(controlPlaneUrl ? { controlPlaneUrl } : {}),
2781
+ userId,
2782
+ accountId: controlPlanePlan.accountId,
2783
+ projectId: controlPlanePlan.projectId,
2784
+ environment,
2785
+ }), authUrl);
2737
2786
  const stored = controlPlaneUrl
2738
2787
  ? await readDeployHubSpotOAuthSecret({
2739
2788
  controlPlaneUrl,
@@ -2770,7 +2819,7 @@ async function executeCloudflareDeploy({ argv, root, workers, controlPlanePlan,
2770
2819
  ? {
2771
2820
  authority: createControlPlaneTenantProvisioningAuthority({
2772
2821
  controlPlaneUrl,
2773
- authorizationHeaders: () => controlPlaneAuthHeaders(userId),
2822
+ authorizationHeaders: () => controlPlaneAuthHeaders(userId, undefined, controlPlanePlan.accountId),
2774
2823
  http: hostedHttp,
2775
2824
  validateCompleteResources: (resources) => validateTenantDataPlaneResourcesForActiveAccount(argv, root, resources),
2776
2825
  }),
@@ -2892,7 +2941,7 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, allowC
2892
2941
  // Resolution: flag → env → the binding persisted by the last successful
2893
2942
  // upload (.hs-x/hubspot.json). Users should never have to replay an app id
2894
2943
  // the CLI already learned (owner directive, run-008).
2895
- const hubspotAppId = readOptionalHubSpotAppId(resolveFlag(argv, '--hubspot-app-id') ?? process.env.HSX_HUBSPOT_APP_ID) ?? (await readPersistedHubSpotAppId(root, await resolveHubSpotProjectName(argv, root)));
2944
+ const hubspotAppId = readOptionalHubSpotAppId(resolveFlag(argv, '--hubspot-app-id') ?? process.env.HSX_HUBSPOT_APP_ID) ?? (await readPersistedHubSpotAppIdForDeploy(argv, root));
2896
2945
  const installRuntimeBinding = hubspotAppId
2897
2946
  ? controlPlaneUrl
2898
2947
  ? await ensureDeployInstallRuntimeBinding({
@@ -3021,7 +3070,10 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, allowC
3021
3070
  ...(installRuntimeBinding
3022
3071
  ? { installKvNamespaceId: installRuntimeBinding.installKvNamespaceId }
3023
3072
  : {}),
3024
- ...(controlPlaneUrl ? { checkpointDataset: 'hsx_checkpoint', observability: true } : {}),
3073
+ // Workers Logs is the observability source for both deployment modes.
3074
+ // Checkpoint remains linked-only and is added separately below.
3075
+ observability: true,
3076
+ ...(controlPlaneUrl ? { checkpointDataset: 'hsx_checkpoint' } : {}),
3025
3077
  ...(controlPlaneUrl ? { versionMetadataBinding: 'CF_VERSION_METADATA' } : {}),
3026
3078
  ...(tenantDataPlane
3027
3079
  ? {
@@ -3294,9 +3346,15 @@ async function ensureTenantDeployInstallRuntimeBinding(input) {
3294
3346
  hubSpotAppId: input.hubSpotAppId,
3295
3347
  },
3296
3348
  kind: 'install-kv',
3349
+ legacyResourceNameExists: (resourceName) => cloudflareResourceNameExists({
3350
+ argv: input.argv,
3351
+ root: input.root,
3352
+ kind: 'install-kv',
3353
+ resourceName,
3354
+ }),
3297
3355
  async provisionByName(installKvNamespaceName) {
3298
3356
  // ADR-022: generate and escrow the token key before creating Cloudflare
3299
- // state, but only after long ambiguous names have already failed closed.
3357
+ // state, after long-name recovery ambiguity has already been ruled out.
3300
3358
  const tokenKeySecretValue = randomBytes(32).toString('base64url');
3301
3359
  const tokenKeyCustody = await escrowTokenKey({
3302
3360
  adapter: defaultTokenKeyCustodyAdapter(),
@@ -3332,7 +3390,7 @@ async function ensureTenantDeployInstallRuntimeBinding(input) {
3332
3390
  if (!namespaceOutput) {
3333
3391
  const listed = await runCloudflareCommand(['bun', 'x', 'wrangler', 'kv', 'namespace', 'list'], { cwd: input.root, env: cloudflareDeployEnv(input.argv) });
3334
3392
  try {
3335
- const namespaces = JSON.parse(listed.stdout);
3393
+ const namespaces = parseWranglerJsonArray(listed.stdout);
3336
3394
  resourceId = namespaces.find((namespace) => namespace.title === installKvNamespaceName)?.id;
3337
3395
  }
3338
3396
  catch {
@@ -3372,6 +3430,9 @@ async function ensureTenantDeployInstallRuntimeBinding(input) {
3372
3430
  };
3373
3431
  }
3374
3432
  async function ensureDeployInstallRuntimeBinding(input) {
3433
+ const preserved = await recoverCompletedLinkInstallRuntimeBinding(input);
3434
+ if (preserved)
3435
+ return preserved;
3375
3436
  const existing = await readDeployInstallRuntimeBinding(input);
3376
3437
  if (existing) {
3377
3438
  const credentials = resolveCloudflareCredentials(input.argv);
@@ -3413,9 +3474,15 @@ async function ensureDeployInstallRuntimeBinding(input) {
3413
3474
  hubSpotAppId: input.hubSpotAppId,
3414
3475
  },
3415
3476
  kind: 'install-kv',
3477
+ legacyResourceNameExists: (resourceName) => cloudflareResourceNameExists({
3478
+ argv: input.argv,
3479
+ root: input.root,
3480
+ kind: 'install-kv',
3481
+ resourceName,
3482
+ }),
3416
3483
  async provisionByName(installKvNamespaceName) {
3417
- // ADR-022: escrow precedes Cloudflare mutation, while the ambiguity
3418
- // guard precedes both. The control plane never receives key bytes.
3484
+ // ADR-022: escrow precedes Cloudflare mutation, while the legacy-name
3485
+ // ambiguity guard precedes both. The control plane never receives key bytes.
3419
3486
  const tokenKeySecretValue = randomBytes(32).toString('base64url');
3420
3487
  const tokenKeyCustody = await escrowTokenKey({
3421
3488
  adapter: defaultTokenKeyCustodyAdapter(),
@@ -3451,7 +3518,7 @@ async function ensureDeployInstallRuntimeBinding(input) {
3451
3518
  if (!namespaceOutput) {
3452
3519
  const listed = await runCloudflareCommand(['bun', 'x', 'wrangler', 'kv', 'namespace', 'list'], { cwd: input.root, env: cloudflareDeployEnv(input.argv) });
3453
3520
  try {
3454
- const namespaces = JSON.parse(listed.stdout);
3521
+ const namespaces = parseWranglerJsonArray(listed.stdout);
3455
3522
  resourceId = namespaces.find((namespace) => namespace.title === installKvNamespaceName)?.id;
3456
3523
  }
3457
3524
  catch {
@@ -3485,10 +3552,106 @@ async function ensureDeployInstallRuntimeBinding(input) {
3485
3552
  tokenKeyCustody,
3486
3553
  };
3487
3554
  }
3555
+ /**
3556
+ * A completed ADR-022 link journal is the authoritative preserve-set for the
3557
+ * first linked redeploy. Rehydrate its exact INSTALL_KV and re-keyed token-key
3558
+ * custody before consulting account-scoped control-plane metadata, which may
3559
+ * not have been backfilled yet. This prevents link from silently provisioning
3560
+ * a replacement namespace or key.
3561
+ */
3562
+ async function recoverCompletedLinkInstallRuntimeBinding(input) {
3563
+ const pointer = await loadCloudflarePointer(input.root);
3564
+ if (!pointer || !pointer.ownerId.startsWith('acct_'))
3565
+ return undefined;
3566
+ const journal = await loadLinkMigrationJournal(linkMigrationJournalId({
3567
+ cloudflareAccountId: pointer.cloudflareAccountId,
3568
+ stateKvNamespaceId: pointer.stateKvNamespaceId,
3569
+ newOwnerId: pointer.ownerId,
3570
+ }));
3571
+ if (!journal || journal.status !== 'done')
3572
+ return undefined;
3573
+ const binding = journal.bindings.find((candidate) => candidate.projectId === input.projectId &&
3574
+ candidate.environment === input.environment &&
3575
+ candidate.hubSpotAppId === input.hubSpotAppId);
3576
+ if (!binding)
3577
+ return undefined;
3578
+ if (input.dryRun) {
3579
+ return {
3580
+ installKvNamespaceId: binding.installKvNamespaceId,
3581
+ installKvNamespaceName: binding.installKvNamespaceName,
3582
+ tokenKeySecretName: binding.tokenKeySecretName,
3583
+ };
3584
+ }
3585
+ const valid = await validateCloudflareResourceForDeploy({
3586
+ argv: input.argv,
3587
+ root: input.root,
3588
+ kind: 'install-kv',
3589
+ resourceId: binding.installKvNamespaceId,
3590
+ });
3591
+ if (!valid) {
3592
+ throw new Error(`The completed link journal's preserved install KV ${binding.installKvNamespaceId} is no longer present in the connected Cloudflare account.`);
3593
+ }
3594
+ const custodyMechanism = Schema.decodeUnknownSync(schemas.TokenKeyCustodyMechanism)(binding.custodyMechanism);
3595
+ const adapter = tokenKeyCustodyAdapterFor(custodyMechanism);
3596
+ const tokenKeySecretValue = await adapter.retrieve(binding.newCustodyId);
3597
+ if (!tokenKeySecretValue) {
3598
+ throw new Error(`The completed link journal's re-keyed token custody ${binding.newCustodyId} is unavailable.`);
3599
+ }
3600
+ const tokenKeyCustody = Schema.decodeUnknownSync(schemas.EscrowedTokenKeyCustody)({
3601
+ kind: 'escrowed',
3602
+ mechanism: custodyMechanism,
3603
+ custodyId: binding.newCustodyId,
3604
+ escrowedAt: journal.updatedAt,
3605
+ canary: await sealTokenKeyCanary(tokenKeySecretValue),
3606
+ });
3607
+ await putDeployInstallRuntimeBinding({
3608
+ ...input,
3609
+ installKvNamespaceId: binding.installKvNamespaceId,
3610
+ installKvNamespaceName: binding.installKvNamespaceName,
3611
+ tokenKeySecretName: binding.tokenKeySecretName,
3612
+ });
3613
+ const { pointer: currentPointer, stateStore } = await ensureTenantStateStore({
3614
+ argv: input.argv,
3615
+ root: input.root,
3616
+ });
3617
+ const existing = await stateStore.getBinding({
3618
+ projectId: input.projectId,
3619
+ environment: input.environment,
3620
+ hubSpotAppId: input.hubSpotAppId,
3621
+ });
3622
+ await stateStore.putBinding(Schema.decodeUnknownSync(schemas.LocalAppRuntimeBinding)({
3623
+ ...(existing ?? {}),
3624
+ schemaVersion: 2,
3625
+ ownerId: currentPointer.ownerId,
3626
+ projectId: input.projectId,
3627
+ environment: input.environment,
3628
+ hubSpotAppId: input.hubSpotAppId,
3629
+ installKvNamespaceId: binding.installKvNamespaceId,
3630
+ installKvNamespaceName: binding.installKvNamespaceName,
3631
+ tokenKeySecretName: binding.tokenKeySecretName,
3632
+ tokenKeyCustody,
3633
+ updatedAt: new Date().toISOString(),
3634
+ }));
3635
+ return {
3636
+ installKvNamespaceId: binding.installKvNamespaceId,
3637
+ installKvNamespaceName: binding.installKvNamespaceName,
3638
+ tokenKeySecretName: binding.tokenKeySecretName,
3639
+ tokenKeySecretValue,
3640
+ tokenKeyCustody,
3641
+ ...(existing?.tenantD1DatabaseId ? { tenantD1DatabaseId: existing.tenantD1DatabaseId } : {}),
3642
+ ...(existing?.tenantD1DatabaseName
3643
+ ? { tenantD1DatabaseName: existing.tenantD1DatabaseName }
3644
+ : {}),
3645
+ ...(existing?.flagsKvNamespaceId ? { flagsKvNamespaceId: existing.flagsKvNamespaceId } : {}),
3646
+ ...(existing?.flagsKvNamespaceName
3647
+ ? { flagsKvNamespaceName: existing.flagsKvNamespaceName }
3648
+ : {}),
3649
+ };
3650
+ }
3488
3651
  async function readDeployInstallRuntimeBinding(input) {
3489
3652
  const response = await hostedHttp({
3490
3653
  url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/hubspot-apps/${encodeURIComponent(input.hubSpotAppId)}/install-runtime?environment=${encodeURIComponent(input.environment)}`, input.controlPlaneUrl),
3491
- headers: await controlPlaneAuthHeaders(input.userId),
3654
+ headers: await controlPlaneAuthHeaders(input.userId, undefined, input.accountId),
3492
3655
  });
3493
3656
  if (response.status === 404) {
3494
3657
  return undefined;
@@ -3506,7 +3669,7 @@ async function putDeployInstallRuntimeBinding(input) {
3506
3669
  const response = await hostedHttp({
3507
3670
  url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/hubspot-apps/${encodeURIComponent(input.hubSpotAppId)}/install-runtime`, input.controlPlaneUrl),
3508
3671
  method: 'PUT',
3509
- headers: await controlPlaneAuthHeaders(input.userId),
3672
+ headers: await controlPlaneAuthHeaders(input.userId, undefined, input.accountId),
3510
3673
  body: {
3511
3674
  environment: input.environment,
3512
3675
  installKvNamespaceId: input.installKvNamespaceId,
@@ -3532,7 +3695,7 @@ async function ensureHostedProjectRegistration(input) {
3532
3695
  const response = await hostedHttp({
3533
3696
  url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects`, input.controlPlaneUrl),
3534
3697
  method: 'POST',
3535
- headers: await controlPlaneAuthHeaders(input.userId),
3698
+ headers: await controlPlaneAuthHeaders(input.userId, undefined, input.accountId),
3536
3699
  body: { projectId: input.projectId, displayName: input.projectId },
3537
3700
  });
3538
3701
  if (response.ok || response.status === 409)
@@ -3551,7 +3714,7 @@ async function requestDeployBillingRuntimeToken(input) {
3551
3714
  const response = await hostedHttp({
3552
3715
  url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/billing/runtime-token`, input.controlPlaneUrl),
3553
3716
  method: 'POST',
3554
- headers: await controlPlaneAuthHeaders(input.userId),
3717
+ headers: await controlPlaneAuthHeaders(input.userId, undefined, input.accountId),
3555
3718
  body: {
3556
3719
  environment: input.environment,
3557
3720
  },
@@ -3624,6 +3787,12 @@ async function ensureTenantDataPlane(input) {
3624
3787
  kind: 'tenant-d1',
3625
3788
  ...(exactTenantD1DatabaseId ? { exactResourceId: exactTenantD1DatabaseId } : {}),
3626
3789
  ...(exactTenantD1DatabaseName ? { exactResourceName: exactTenantD1DatabaseName } : {}),
3790
+ legacyResourceNameExists: (resourceName) => cloudflareResourceNameExists({
3791
+ argv: input.argv,
3792
+ root: input.root,
3793
+ kind: 'tenant-d1',
3794
+ resourceName,
3795
+ }),
3627
3796
  validateExactResource: (resourceId) => {
3628
3797
  const credentials = resolveCloudflareCredentials(input.argv);
3629
3798
  if (!credentials.accountId || !credentials.apiToken) {
@@ -3665,6 +3834,12 @@ async function ensureTenantDataPlane(input) {
3665
3834
  kind: 'flags-kv',
3666
3835
  ...(exactFlagsKvNamespaceId ? { exactResourceId: exactFlagsKvNamespaceId } : {}),
3667
3836
  ...(exactFlagsKvNamespaceName ? { exactResourceName: exactFlagsKvNamespaceName } : {}),
3837
+ legacyResourceNameExists: (resourceName) => cloudflareResourceNameExists({
3838
+ argv: input.argv,
3839
+ root: input.root,
3840
+ kind: 'flags-kv',
3841
+ resourceName,
3842
+ }),
3668
3843
  validateExactResource: (resourceId) => {
3669
3844
  const credentials = resolveCloudflareCredentials(input.argv);
3670
3845
  if (!credentials.accountId || !credentials.apiToken) {
@@ -3699,7 +3874,7 @@ async function ensureTenantDataPlane(input) {
3699
3874
  if (!created) {
3700
3875
  const listed = await runCloudflareCommand(['bun', 'x', 'wrangler', 'kv', 'namespace', 'list'], { cwd: input.root, env });
3701
3876
  try {
3702
- const namespaces = JSON.parse(listed.stdout);
3877
+ const namespaces = parseWranglerJsonArray(listed.stdout);
3703
3878
  resourceId = namespaces.find((namespace) => namespace.title === flagsKvNamespaceName)?.id;
3704
3879
  }
3705
3880
  catch {
@@ -3804,13 +3979,29 @@ async function validateCloudflareResourceForDeploy(input) {
3804
3979
  env: cloudflareDeployEnv(input.argv),
3805
3980
  });
3806
3981
  try {
3807
- const rows = JSON.parse(listed.stdout);
3982
+ const rows = parseWranglerJsonArray(listed.stdout);
3808
3983
  return rows.some((row) => row.id === input.resourceId || row.uuid === input.resourceId);
3809
3984
  }
3810
3985
  catch {
3811
3986
  throw new Error(`HSX_E_CLOUDFLARE_RESOURCE_VALIDATION_FAILED: Wrangler returned malformed ${input.kind} list output.`);
3812
3987
  }
3813
3988
  }
3989
+ async function cloudflareResourceNameExists(input) {
3990
+ const command = input.kind === 'tenant-d1'
3991
+ ? ['bun', 'x', 'wrangler', 'd1', 'list', '--json']
3992
+ : ['bun', 'x', 'wrangler', 'kv', 'namespace', 'list'];
3993
+ const listed = await runCloudflareCommand(command, {
3994
+ cwd: input.root,
3995
+ env: cloudflareDeployEnv(input.argv),
3996
+ });
3997
+ try {
3998
+ const rows = parseWranglerJsonArray(listed.stdout);
3999
+ return rows.some((row) => row.name === input.resourceName || row.title === input.resourceName);
4000
+ }
4001
+ catch {
4002
+ throw new Error(`HSX_E_CLOUDFLARE_RESOURCE_VALIDATION_FAILED: Wrangler returned malformed ${input.kind} list output.`);
4003
+ }
4004
+ }
3814
4005
  export function renderWranglerConfig(input) {
3815
4006
  const lines = [
3816
4007
  '# Generated by hs-x. Do not edit by hand.',
@@ -3884,7 +4075,7 @@ function tomlString(value) {
3884
4075
  async function readDeployHubSpotOAuthSecret(input) {
3885
4076
  const response = await hostedHttp({
3886
4077
  url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/hubspot-apps/${encodeURIComponent(input.hubSpotAppId)}/oauth-secret?environment=${encodeURIComponent(input.environment)}`, input.controlPlaneUrl),
3887
- headers: await controlPlaneAuthHeaders(input.userId),
4078
+ headers: await controlPlaneAuthHeaders(input.userId, undefined, input.accountId),
3888
4079
  });
3889
4080
  if (response.status === 404) {
3890
4081
  return undefined;
@@ -4597,6 +4788,14 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4597
4788
  throw error;
4598
4789
  }
4599
4790
  const buildState = readStatus(buildStatus) ?? 'UNKNOWN';
4791
+ // Legacy ~/.hscli/config.yml accounts can authenticate and upload without
4792
+ // exposing their developer-account id through the selected account object.
4793
+ // HubSpot's build response is authoritative for that same id; retain it so
4794
+ // the app-auth handoff and later zero-selector log reads do not disappear.
4795
+ const resolvedDeveloperAccountId = resolveHubSpotDeveloperAccountIdFromBuild({
4796
+ developerAccountId,
4797
+ buildStatus,
4798
+ });
4600
4799
  buildStep?.[buildState === 'SUCCESS' ? 'ok' : 'fail'](`${buildState} · ${elapsedSince(buildStartedAt)}`);
4601
4800
  const autoDeployEnabled = readBooleanProperty(buildStatus, 'isAutoDeployEnabled');
4602
4801
  if (uploadOnly || readStatus(buildStatus) !== 'SUCCESS' || autoDeployEnabled) {
@@ -4607,7 +4806,7 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4607
4806
  await persistHubSpotAppBinding(root, {
4608
4807
  appId,
4609
4808
  projectName,
4610
- developerAccountId,
4809
+ developerAccountId: resolvedDeveloperAccountId,
4611
4810
  });
4612
4811
  // Build SUCCESS does NOT mean the app shipped: HubSpot's auto-deploy can
4613
4812
  // still fail (found live: "Apps are not allowed to have 0 redirect URLs"
@@ -4646,7 +4845,7 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4646
4845
  archivePath,
4647
4846
  appId,
4648
4847
  appUid,
4649
- developerAccountId,
4848
+ developerAccountId: resolvedDeveloperAccountId,
4650
4849
  buildId: upload.buildId,
4651
4850
  deployId: undefined,
4652
4851
  buildStatus,
@@ -4688,7 +4887,7 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4688
4887
  await persistHubSpotAppBinding(root, {
4689
4888
  appId,
4690
4889
  projectName,
4691
- developerAccountId,
4890
+ developerAccountId: resolvedDeveloperAccountId,
4692
4891
  });
4693
4892
  }
4694
4893
  return {
@@ -4696,7 +4895,7 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4696
4895
  archivePath,
4697
4896
  appId,
4698
4897
  appUid,
4699
- developerAccountId,
4898
+ developerAccountId: resolvedDeveloperAccountId,
4700
4899
  buildId: upload.buildId,
4701
4900
  deployId,
4702
4901
  buildStatus,
@@ -4707,12 +4906,27 @@ async function executeHubSpotOnlyUpload({ argv, root, local, persistBinding = tr
4707
4906
  };
4708
4907
  }
4709
4908
  async function optionalHubSpotAppId(client, projectName, appUid) {
4710
- try {
4711
- return await client.projects.resolveAppId(appUid === undefined ? { projectName } : { projectName, appUid });
4712
- }
4713
- catch {
4714
- return 0;
4909
+ return resolveHubSpotAppIdWithRetry({
4910
+ resolve: () => client.projects.resolveAppId(appUid === undefined ? { projectName } : { projectName, appUid }),
4911
+ });
4912
+ }
4913
+ /** @internal Exported for deterministic eventual-consistency regressions. */
4914
+ export async function resolveHubSpotAppIdWithRetry(input) {
4915
+ const attempts = Math.max(1, input.attempts ?? 4);
4916
+ const retryDelayMs = Math.max(0, input.retryDelayMs ?? 500);
4917
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
4918
+ try {
4919
+ const appId = await input.resolve();
4920
+ if (appId > 0)
4921
+ return appId;
4922
+ }
4923
+ catch {
4924
+ // A newly-created app can lag its successful build/deploy metadata.
4925
+ }
4926
+ if (attempt < attempts && retryDelayMs > 0)
4927
+ await sleep(retryDelayMs);
4715
4928
  }
4929
+ return 0;
4716
4930
  }
4717
4931
  async function readHubSpotProjectIntermediateRepresentation(root) {
4718
4932
  const hsproject = await readHubSpotProjectConfig(root);
@@ -4877,8 +5091,71 @@ async function readPersistedHubSpotAppBinding(root, expectedProjectName) {
4877
5091
  return undefined;
4878
5092
  }
4879
5093
  }
4880
- async function readPersistedHubSpotAppId(root, expectedProjectName) {
4881
- return (await readPersistedHubSpotAppBinding(root, expectedProjectName))?.appId;
5094
+ async function readPersistedHubSpotAppId(root, expectedProjectName, expectedDeveloperAccountId) {
5095
+ const binding = await readPersistedHubSpotAppBinding(root, expectedProjectName);
5096
+ if (!binding)
5097
+ return undefined;
5098
+ return hubSpotAppBindingMatchesDeveloperAccount(binding.developerAccountId, expectedDeveloperAccountId)
5099
+ ? binding.appId
5100
+ : undefined;
5101
+ }
5102
+ /** @internal Exported for deterministic account-switch regressions. */
5103
+ export function hubSpotAppBindingMatchesDeveloperAccount(persistedDeveloperAccountId, selectedDeveloperAccountId) {
5104
+ if (!selectedDeveloperAccountId)
5105
+ return true;
5106
+ return persistedDeveloperAccountId === selectedDeveloperAccountId;
5107
+ }
5108
+ async function resolveHubSpotDeployDeveloperAccountId(argv, root) {
5109
+ const explicit = resolveFlag(argv, '--developer-account-id') ?? process.env.HSX_HUBSPOT_DEVELOPER_ACCOUNT_ID;
5110
+ if (explicit?.trim())
5111
+ return explicit.trim();
5112
+ const configPath = resolveFlag(argv, '--config');
5113
+ const discovered = await discoverHubSpotAccounts(root, configPath ? { configPath } : {});
5114
+ const selected = discovered?.accounts.find((account) => account.isDefault) ??
5115
+ (discovered?.accounts.length === 1 ? discovered.accounts[0] : undefined);
5116
+ return selected ? String(selected.portalId) : undefined;
5117
+ }
5118
+ async function readPersistedHubSpotAppIdForDeploy(argv, root) {
5119
+ return readPersistedHubSpotAppId(root, await resolveHubSpotProjectName(argv, root), await resolveHubSpotDeployDeveloperAccountId(argv, root));
5120
+ }
5121
+ /**
5122
+ * Resolve the one HubSpot app a linked project already registered. The common
5123
+ * path remains implicit: a first combined deploy persists the app id, while a
5124
+ * checkout missing that advisory file can recover an unambiguous binding from
5125
+ * the control plane. Explicit ids remain the advanced override.
5126
+ */
5127
+ async function resolveHubSpotAppIdForDeploy(input) {
5128
+ const explicit = readOptionalHubSpotAppId(resolveFlag(input.argv, '--hubspot-app-id') ?? process.env.HSX_HUBSPOT_APP_ID);
5129
+ const projectName = await resolveHubSpotProjectName(input.argv, input.root);
5130
+ const persisted = await readPersistedHubSpotAppIdForDeploy(input.argv, input.root);
5131
+ const selected = explicit ?? persisted;
5132
+ if (selected || !input.controlPlaneUrl)
5133
+ return selected;
5134
+ const response = await hostedHttp({
5135
+ url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/runtime-bindings`, input.controlPlaneUrl),
5136
+ headers: await controlPlaneAuthHeaders(input.userId, undefined, input.accountId),
5137
+ });
5138
+ if (!response.ok)
5139
+ return undefined;
5140
+ const body = await response.json();
5141
+ const bindings = Schema.decodeUnknownSync(schemas.ListProjectRuntimeBindingsResponse)(body).bindings;
5142
+ const appIds = [
5143
+ ...new Set(bindings
5144
+ .filter((binding) => binding.environment === input.environment)
5145
+ .map((binding) => binding.hubSpotAppId)),
5146
+ ];
5147
+ if (appIds.length === 0)
5148
+ return undefined;
5149
+ if (appIds.length > 1) {
5150
+ throw new Error(`Project ${input.projectId} has multiple HubSpot apps in ${input.environment} (${appIds.join(', ')}). Pass --hubspot-app-id once to choose one.`);
5151
+ }
5152
+ const appId = appIds[0];
5153
+ await persistHubSpotAppBinding(input.root, {
5154
+ appId,
5155
+ projectName,
5156
+ developerAccountId: undefined,
5157
+ });
5158
+ return appId;
4882
5159
  }
4883
5160
  async function resolveHubSpotAppAuthUrl(argv, root) {
4884
5161
  const projectName = await resolveHubSpotProjectName(argv, root);
@@ -4894,6 +5171,18 @@ async function resolveHubSpotAppAuthUrl(argv, root) {
4894
5171
  function readStatus(value) {
4895
5172
  return isRecord(value) && typeof value.status === 'string' ? value.status : undefined;
4896
5173
  }
5174
+ /** @internal Exported for deterministic legacy-account deploy regressions. */
5175
+ export function resolveHubSpotDeveloperAccountIdFromBuild(input) {
5176
+ if (input.developerAccountId?.trim())
5177
+ return input.developerAccountId.trim();
5178
+ if (!isRecord(input.buildStatus))
5179
+ return undefined;
5180
+ const portalId = input.buildStatus.portalId;
5181
+ if (typeof portalId === 'number' && Number.isInteger(portalId) && portalId > 0) {
5182
+ return String(portalId);
5183
+ }
5184
+ return typeof portalId === 'string' && portalId.trim().length > 0 ? portalId.trim() : undefined;
5185
+ }
4897
5186
  const readSubbuildStatuses = readHubSpotSubbuildStatuses;
4898
5187
  const HUBSPOT_COMPONENT_TERMINAL_STATES = new Set(['SUCCESS', 'FAILURE', 'ERROR', 'CANCELLED']);
4899
5188
  /** Emit each HubSpot component once, when it reaches a terminal build state. */
@@ -4997,7 +5286,7 @@ async function requestHostedControlPlaneDeployPlan({ request, controlPlaneUrl, u
4997
5286
  const response = await hostedHttp({
4998
5287
  url: new URL('/v1/deploys/plan', controlPlaneUrl),
4999
5288
  method: 'POST',
5000
- headers: await controlPlaneAuthHeaders(userId),
5289
+ headers: await controlPlaneAuthHeaders(userId, undefined, request.accountId),
5001
5290
  body: request,
5002
5291
  });
5003
5292
  const body = await response.json();
@@ -5008,7 +5297,7 @@ async function requestHostedControlPlaneDeployPlan({ request, controlPlaneUrl, u
5008
5297
  }
5009
5298
  return Schema.decodeUnknownSync(schemas.DeployPlanResponse)(body);
5010
5299
  }
5011
- async function requestLocalControlPlaneDeployRecordAndMaybePromote({ request, plan, promoteWhenHealthy, revision, }) {
5300
+ async function requestLocalControlPlaneDeployRecordAndMaybePromote({ request, plan, promoteWhenHealthy, workerUrl, revision, }) {
5012
5301
  const localAttestationAuth = revision
5013
5302
  ? { grantSecret: randomBytes(32).toString('base64url'), hubSpotAppId: 1 }
5014
5303
  : undefined;
@@ -5051,6 +5340,7 @@ async function requestLocalControlPlaneDeployRecordAndMaybePromote({ request, pl
5051
5340
  manifestHash: plan.manifestHash,
5052
5341
  signedManifest: request.manifest,
5053
5342
  signature: `sig_${plan.deployId}`,
5343
+ ...(workerUrl ? { workerUrl } : {}),
5054
5344
  // ADR-021 R03: the real Cloudflare Worker version identity captured
5055
5345
  // back from the confirmed active deployment. Absent only when capture
5056
5346
  // was skipped/failed (best-effort) — then this deploy is history-only.
@@ -5133,13 +5423,14 @@ async function requestHostedControlPlaneDeployRecordAndMaybePromote({ request, p
5133
5423
  const response = await hostedHttp({
5134
5424
  url: new URL(`/v1/deploys/${encodeURIComponent(plan.deployId)}/record`, controlPlaneUrl),
5135
5425
  method: 'POST',
5136
- headers: await controlPlaneAuthHeaders(userId),
5426
+ headers: await controlPlaneAuthHeaders(userId, undefined, request.accountId),
5137
5427
  body: {
5138
5428
  accountId: request.accountId,
5139
5429
  projectId: request.projectId,
5140
5430
  manifestHash: plan.manifestHash,
5141
5431
  signedManifest: request.manifest,
5142
5432
  signature: `sig_${plan.deployId}`,
5433
+ ...(workerUrl ? { workerUrl } : {}),
5143
5434
  // ADR-021 R03: the real Cloudflare Worker version identity captured back
5144
5435
  // from the confirmed active deployment. Absent only when capture was
5145
5436
  // skipped/failed (best-effort) — then this deploy is history-only.
@@ -5159,6 +5450,7 @@ async function requestHostedControlPlaneDeployRecordAndMaybePromote({ request, p
5159
5450
  }
5160
5451
  const drift = await waitForHealthyControlPlaneDrift({
5161
5452
  controlPlaneUrl,
5453
+ accountId: request.accountId,
5162
5454
  projectId: request.projectId,
5163
5455
  deployId: plan.deployId,
5164
5456
  userId,
@@ -5461,7 +5753,7 @@ function discoverManageSchemaMode(definition) {
5461
5753
  }
5462
5754
  function discoverSourceDefinitions(source) {
5463
5755
  const definitions = new Map();
5464
- for (const match of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*defineSource(\.push)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
5756
+ for (const match of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*defineSource(\.push)?\s*(?:<[\s\S]*?>)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
5465
5757
  const variableName = match[1];
5466
5758
  const body = match[3] ?? '';
5467
5759
  if (!variableName) {
@@ -5585,6 +5877,9 @@ const deployOptions = {
5585
5877
  message: Options.text('message').pipe(Options.withAlias('m'), Options.optional), // --message / -m
5586
5878
  platformVersion: Options.text('platform-version').pipe(Options.optional), // --platform-version
5587
5879
  hubspotProjectName: Options.text('hubspot-project-name').pipe(Options.optional), // --hubspot-project-name
5880
+ cloudflareAccountId: Options.text('cloudflare-account-id').pipe(Options.optional), // --cloudflare-account-id
5881
+ cloudflareApiToken: Options.text('cloudflare-api-token').pipe(Options.optional), // --cloudflare-api-token
5882
+ apiToken: Options.text('api-token').pipe(Options.optional), // --api-token
5588
5883
  // ── boolean flags (argv.includes) ──
5589
5884
  plan: Options.boolean('plan').pipe(Options.withDefault(false)), // --plan
5590
5885
  dryRun: Options.boolean('dry-run').pipe(Options.withDefault(false)), // --dry-run
@@ -5651,6 +5946,9 @@ const deployBaseCmd = Command.make('deploy', deployOptions, (opts) => runHandler
5651
5946
  push('--message', opts.message);
5652
5947
  push('--platform-version', opts.platformVersion);
5653
5948
  push('--hubspot-project-name', opts.hubspotProjectName);
5949
+ push('--cloudflare-account-id', opts.cloudflareAccountId);
5950
+ push('--cloudflare-api-token', opts.cloudflareApiToken);
5951
+ push('--api-token', opts.apiToken);
5654
5952
  flag('--plan', opts.plan);
5655
5953
  flag('--dry-run', opts.dryRun);
5656
5954
  flag('--build-only', opts.buildOnly);