@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.
- package/README.md +6 -5
- package/dist/account-store.d.ts +9 -0
- package/dist/account-store.d.ts.map +1 -1
- package/dist/account-store.js +11 -0
- package/dist/account-store.js.map +1 -1
- package/dist/cloudflare-auth.js +3 -3
- package/dist/cloudflare-auth.js.map +1 -1
- package/dist/cloudflare-kv.d.ts +9 -0
- package/dist/cloudflare-kv.d.ts.map +1 -1
- package/dist/cloudflare-kv.js +16 -7
- package/dist/cloudflare-kv.js.map +1 -1
- package/dist/cloudflare-oauth.d.ts.map +1 -1
- package/dist/cloudflare-oauth.js +5 -3
- package/dist/cloudflare-oauth.js.map +1 -1
- package/dist/cloudflare-scoped-resource.d.ts +17 -2
- package/dist/cloudflare-scoped-resource.d.ts.map +1 -1
- package/dist/cloudflare-scoped-resource.js +46 -6
- package/dist/cloudflare-scoped-resource.js.map +1 -1
- package/dist/cloudflare-workers-logs.d.ts +38 -0
- package/dist/cloudflare-workers-logs.d.ts.map +1 -0
- package/dist/cloudflare-workers-logs.js +297 -0
- package/dist/cloudflare-workers-logs.js.map +1 -0
- package/dist/commands/api.d.ts.map +1 -1
- package/dist/commands/api.js +1 -1
- package/dist/commands/api.js.map +1 -1
- package/dist/commands/connect.js +3 -3
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +2 -2
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +90 -34
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-promote.js +3 -3
- package/dist/commands/deploy-promote.js.map +1 -1
- package/dist/commands/deploy.d.ts +31 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +341 -43
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.js +7 -9
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +41 -7
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/rollback.d.ts +23 -0
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +60 -13
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +4 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +24 -9
- package/dist/commands/sync.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/control-plane-fetch.d.ts +3 -1
- package/dist/control-plane-fetch.d.ts.map +1 -1
- package/dist/control-plane-fetch.js +9 -7
- package/dist/control-plane-fetch.js.map +1 -1
- package/dist/deploy-targets.d.ts +0 -1
- package/dist/deploy-targets.d.ts.map +1 -1
- package/dist/deploy-targets.js +2 -5
- package/dist/deploy-targets.js.map +1 -1
- package/dist/dev/invoke.d.ts +7 -0
- package/dist/dev/invoke.d.ts.map +1 -1
- package/dist/dev/invoke.js +18 -5
- package/dist/dev/invoke.js.map +1 -1
- package/dist/errors-registry.js +8 -8
- package/dist/errors-registry.js.map +1 -1
- package/dist/hubspot-developer-client.js +2 -2
- package/dist/hubspot-developer-client.js.map +1 -1
- package/dist/services/control-plane.d.ts +3 -1
- package/dist/services/control-plane.d.ts.map +1 -1
- package/dist/services/control-plane.js +8 -6
- package/dist/services/control-plane.js.map +1 -1
- package/dist/services/hubspot-dev-client.js +1 -1
- package/dist/services/hubspot-dev-client.js.map +1 -1
- package/dist/tenant-state.d.ts +12 -2
- package/dist/tenant-state.d.ts.map +1 -1
- package/dist/tenant-state.js +42 -18
- package/dist/tenant-state.js.map +1 -1
- package/package.json +7 -7
package/dist/commands/deploy.js
CHANGED
|
@@ -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
|
-
|
|
498
|
-
|
|
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 =
|
|
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 =
|
|
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 ||
|
|
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(
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
4711
|
-
|
|
4712
|
-
}
|
|
4713
|
-
|
|
4714
|
-
|
|
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
|
-
|
|
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);
|