@hs-x/cli 0.4.5 → 0.4.6
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/dist/cloudflare-account-preflight.d.ts +26 -0
- package/dist/cloudflare-account-preflight.d.ts.map +1 -0
- package/dist/cloudflare-account-preflight.js +64 -0
- package/dist/cloudflare-account-preflight.js.map +1 -0
- package/dist/cloudflare-kv.d.ts.map +1 -1
- package/dist/cloudflare-kv.js +31 -6
- package/dist/cloudflare-kv.js.map +1 -1
- package/dist/cloudflare-pointer.d.ts.map +1 -1
- package/dist/cloudflare-pointer.js +9 -3
- package/dist/cloudflare-pointer.js.map +1 -1
- package/dist/cloudflare-scoped-resource.d.ts +6 -0
- package/dist/cloudflare-scoped-resource.d.ts.map +1 -1
- package/dist/cloudflare-scoped-resource.js +13 -6
- package/dist/cloudflare-scoped-resource.js.map +1 -1
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +226 -23
- package/dist/commands/deploy.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/control-plane-tenant-provisioning.d.ts +2 -0
- package/dist/control-plane-tenant-provisioning.d.ts.map +1 -1
- package/dist/control-plane-tenant-provisioning.js +15 -2
- package/dist/control-plane-tenant-provisioning.js.map +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +36 -0
- package/dist/errors-registry.js.map +1 -1
- package/dist/services/cloudflare-kv.d.ts.map +1 -1
- package/dist/services/cloudflare-kv.js +22 -4
- package/dist/services/cloudflare-kv.js.map +1 -1
- package/dist/tenant-state.d.ts +18 -0
- package/dist/tenant-state.d.ts.map +1 -1
- package/dist/tenant-state.js +72 -29
- package/dist/tenant-state.js.map +1 -1
- package/package.json +8 -8
package/dist/commands/deploy.js
CHANGED
|
@@ -15,6 +15,7 @@ import { Effect, Option } from 'effect';
|
|
|
15
15
|
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
|
+
import { classifyCloudflareCommandFailure, preflightCloudflareAnalyticsEngine, validateCloudflareResourceForAccount, } from '../cloudflare-account-preflight.js';
|
|
18
19
|
import { resolveCloudflareCredentials } from '../cloudflare-kv.js';
|
|
19
20
|
import { loadCloudflarePointer } from '../cloudflare-pointer.js';
|
|
20
21
|
import { resolveOrProvisionCloudflareScopedResource } from '../cloudflare-scoped-resource.js';
|
|
@@ -475,6 +476,28 @@ async function hydrateCloudflareTokenFromStoredOAuth(input) {
|
|
|
475
476
|
return false;
|
|
476
477
|
}
|
|
477
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Prefer the account-scoped Cloudflare connection held by the control plane.
|
|
481
|
+
* This keeps an explicit `--account-id` deploy from being redirected by a
|
|
482
|
+
* stale local `.hs-x/cloudflare.json` pointer or an unrelated stored OAuth
|
|
483
|
+
* connection on a machine that works across several HS-X accounts.
|
|
484
|
+
*/
|
|
485
|
+
async function hydrateCloudflareTokenFromControlPlane(input) {
|
|
486
|
+
// A command-line credential is an intentional one-run override. Ambient
|
|
487
|
+
// env credentials are not: on a multi-account machine they commonly point
|
|
488
|
+
// at a different Cloudflare account, so a linked deploy must replace them
|
|
489
|
+
// with the selected HS-X account's leased credential pair.
|
|
490
|
+
if (resolveFlag(input.argv, '--cloudflare-api-token') || resolveFlag(input.argv, '--api-token')) {
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
const { leaseCloudflareViaSession } = await import('../tenant-state.js');
|
|
494
|
+
const lease = await leaseCloudflareViaSession(input.argv, input.root);
|
|
495
|
+
if (!lease)
|
|
496
|
+
return false;
|
|
497
|
+
process.env.CLOUDFLARE_API_TOKEN = lease.apiToken;
|
|
498
|
+
process.env.CLOUDFLARE_ACCOUNT_ID = lease.cloudflareAccountId;
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
478
501
|
async function waitForHealthyControlPlaneDrift({ controlPlaneUrl, projectId, deployId, userId, timeoutMs, workerUrl, }) {
|
|
479
502
|
const startedAt = Date.now();
|
|
480
503
|
let lastState = 'unknown';
|
|
@@ -1417,10 +1440,8 @@ export async function deployCommand({ argv, root, json, }) {
|
|
|
1417
1440
|
// (`--yes`) preflight succeed instead of demanding a token connect says
|
|
1418
1441
|
// doesn't exist (cold-stranger run 2026-07-22-001, finding #1).
|
|
1419
1442
|
if (cloudflareDeployRequested && !planOnly && !cloudflareDryRun) {
|
|
1420
|
-
const hydrated = await
|
|
1421
|
-
argv,
|
|
1422
|
-
root,
|
|
1423
|
-
});
|
|
1443
|
+
const hydrated = (controlPlaneUrl ? await hydrateCloudflareTokenFromControlPlane({ argv, root }) : false) ||
|
|
1444
|
+
(await hydrateCloudflareTokenFromStoredOAuth({ argv, root }));
|
|
1424
1445
|
if (hydrated && echo) {
|
|
1425
1446
|
echo.info('Using the Cloudflare connection from `hs-x connect cloudflare`.');
|
|
1426
1447
|
}
|
|
@@ -2687,6 +2708,26 @@ async function executeCloudflareDeploy({ argv, root, workers, controlPlanePlan,
|
|
|
2687
2708
|
if (!controlPlaneUrl && !dryRun) {
|
|
2688
2709
|
await preflightTenantStateCredentials({ argv, root });
|
|
2689
2710
|
}
|
|
2711
|
+
if (controlPlaneUrl && !dryRun) {
|
|
2712
|
+
const credentials = resolveCloudflareCredentials(argv);
|
|
2713
|
+
if (!credentials.accountId || !credentials.apiToken) {
|
|
2714
|
+
throw new Error('Linked Cloudflare deploy requires an account id and API token from the connected credential lease.');
|
|
2715
|
+
}
|
|
2716
|
+
// Read-only and intentionally first: a fresh Cloudflare account otherwise
|
|
2717
|
+
// fails only at Worker upload, after KV/D1 provisioning has already run.
|
|
2718
|
+
await preflightCloudflareAnalyticsEngine({
|
|
2719
|
+
accountId: credentials.accountId,
|
|
2720
|
+
apiToken: credentials.apiToken,
|
|
2721
|
+
});
|
|
2722
|
+
// Tenant provisioning authorizes only registered projects. Register the
|
|
2723
|
+
// plan scope before any Worker enters that authority, not after recordDeploy.
|
|
2724
|
+
await ensureHostedProjectRegistration({
|
|
2725
|
+
controlPlaneUrl,
|
|
2726
|
+
userId,
|
|
2727
|
+
accountId: controlPlanePlan.accountId,
|
|
2728
|
+
projectId: controlPlanePlan.projectId,
|
|
2729
|
+
});
|
|
2730
|
+
}
|
|
2690
2731
|
const hasCardBackends = workers.some((worker) => worker.capabilities.some((capability) => capability.kind === 'card-backend'));
|
|
2691
2732
|
if (hasCardBackends && !dryRun) {
|
|
2692
2733
|
const appConfig = await readHsxAppConfig(root);
|
|
@@ -2731,6 +2772,7 @@ async function executeCloudflareDeploy({ argv, root, workers, controlPlanePlan,
|
|
|
2731
2772
|
controlPlaneUrl,
|
|
2732
2773
|
authorizationHeaders: () => controlPlaneAuthHeaders(userId),
|
|
2733
2774
|
http: hostedHttp,
|
|
2775
|
+
validateCompleteResources: (resources) => validateTenantDataPlaneResourcesForActiveAccount(argv, root, resources),
|
|
2734
2776
|
}),
|
|
2735
2777
|
}
|
|
2736
2778
|
: {});
|
|
@@ -2952,7 +2994,13 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, allowC
|
|
|
2952
2994
|
`${resolvedHubSpotClientSecret ? '' : ' — no client secret; card verification will fail closed'}\n`);
|
|
2953
2995
|
}
|
|
2954
2996
|
const hubSpotScopesForRuntime = await readHsxAppScopesForDeploy(root);
|
|
2955
|
-
|
|
2997
|
+
// The same scoped runtime bearer authenticates both hosted billing and the
|
|
2998
|
+
// redacted install-lifecycle telemetry emitted by linked OAuth Workers.
|
|
2999
|
+
// Provision it whenever either consumer is present. Gating this solely on
|
|
3000
|
+
// `appConfig.billing` leaves ordinary OAuth apps with generated telemetry
|
|
3001
|
+
// code but no HSX_RUNTIME_TOKEN binding, making every emit a silent no-op.
|
|
3002
|
+
const needsRuntimeControlPlaneToken = appConfig.billing !== undefined || appConfig.auth === 'oauth';
|
|
3003
|
+
const billingRuntimeToken = controlPlaneUrl && needsRuntimeControlPlaneToken && !dryRun
|
|
2956
3004
|
? await requestDeployBillingRuntimeToken({
|
|
2957
3005
|
controlPlaneUrl,
|
|
2958
3006
|
userId,
|
|
@@ -3273,8 +3321,24 @@ async function ensureTenantDeployInstallRuntimeBinding(input) {
|
|
|
3273
3321
|
], {
|
|
3274
3322
|
cwd: input.root,
|
|
3275
3323
|
env: cloudflareDeployEnv(input.argv),
|
|
3324
|
+
}).catch((error) => {
|
|
3325
|
+
if (!/already exists/i.test(String(error)))
|
|
3326
|
+
throw error;
|
|
3327
|
+
return undefined;
|
|
3276
3328
|
});
|
|
3277
|
-
|
|
3329
|
+
let resourceId = namespaceOutput
|
|
3330
|
+
? extractKvNamespaceId(`${namespaceOutput.stdout}\n${namespaceOutput.stderr}`)
|
|
3331
|
+
: undefined;
|
|
3332
|
+
if (!namespaceOutput) {
|
|
3333
|
+
const listed = await runCloudflareCommand(['bun', 'x', 'wrangler', 'kv', 'namespace', 'list'], { cwd: input.root, env: cloudflareDeployEnv(input.argv) });
|
|
3334
|
+
try {
|
|
3335
|
+
const namespaces = JSON.parse(listed.stdout);
|
|
3336
|
+
resourceId = namespaces.find((namespace) => namespace.title === installKvNamespaceName)?.id;
|
|
3337
|
+
}
|
|
3338
|
+
catch {
|
|
3339
|
+
resourceId = undefined;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3278
3342
|
if (!resourceId) {
|
|
3279
3343
|
throw new Error('Could not read Cloudflare KV namespace id from wrangler output.');
|
|
3280
3344
|
}
|
|
@@ -3310,21 +3374,33 @@ async function ensureTenantDeployInstallRuntimeBinding(input) {
|
|
|
3310
3374
|
async function ensureDeployInstallRuntimeBinding(input) {
|
|
3311
3375
|
const existing = await readDeployInstallRuntimeBinding(input);
|
|
3312
3376
|
if (existing) {
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3377
|
+
const credentials = resolveCloudflareCredentials(input.argv);
|
|
3378
|
+
if (!credentials.accountId || !credentials.apiToken) {
|
|
3379
|
+
throw new Error('Could not validate the stored install KV without Cloudflare credentials.');
|
|
3380
|
+
}
|
|
3381
|
+
const valid = await validateCloudflareResourceForDeploy({
|
|
3382
|
+
argv: input.argv,
|
|
3383
|
+
root: input.root,
|
|
3384
|
+
kind: 'install-kv',
|
|
3385
|
+
resourceId: existing.installKvNamespaceId,
|
|
3386
|
+
});
|
|
3387
|
+
if (valid) {
|
|
3388
|
+
// Control-plane metadata carries no key custody (and never key bytes) —
|
|
3389
|
+
// on a machine that never generated the key this is worker-secret-only.
|
|
3390
|
+
return {
|
|
3391
|
+
installKvNamespaceId: existing.installKvNamespaceId,
|
|
3392
|
+
installKvNamespaceName: existing.installKvNamespaceName,
|
|
3393
|
+
tokenKeySecretName: existing.tokenKeySecretName,
|
|
3394
|
+
...(existing.tenantD1DatabaseId ? { tenantD1DatabaseId: existing.tenantD1DatabaseId } : {}),
|
|
3395
|
+
...(existing.tenantD1DatabaseName
|
|
3396
|
+
? { tenantD1DatabaseName: existing.tenantD1DatabaseName }
|
|
3397
|
+
: {}),
|
|
3398
|
+
...(existing.flagsKvNamespaceId ? { flagsKvNamespaceId: existing.flagsKvNamespaceId } : {}),
|
|
3399
|
+
...(existing.flagsKvNamespaceName
|
|
3400
|
+
? { flagsKvNamespaceName: existing.flagsKvNamespaceName }
|
|
3401
|
+
: {}),
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3328
3404
|
}
|
|
3329
3405
|
if (input.dryRun) {
|
|
3330
3406
|
return undefined;
|
|
@@ -3364,8 +3440,24 @@ async function ensureDeployInstallRuntimeBinding(input) {
|
|
|
3364
3440
|
], {
|
|
3365
3441
|
cwd: input.root,
|
|
3366
3442
|
env: cloudflareDeployEnv(input.argv),
|
|
3443
|
+
}).catch((error) => {
|
|
3444
|
+
if (!/already exists/i.test(String(error)))
|
|
3445
|
+
throw error;
|
|
3446
|
+
return undefined;
|
|
3367
3447
|
});
|
|
3368
|
-
|
|
3448
|
+
let resourceId = namespaceOutput
|
|
3449
|
+
? extractKvNamespaceId(`${namespaceOutput.stdout}\n${namespaceOutput.stderr}`)
|
|
3450
|
+
: undefined;
|
|
3451
|
+
if (!namespaceOutput) {
|
|
3452
|
+
const listed = await runCloudflareCommand(['bun', 'x', 'wrangler', 'kv', 'namespace', 'list'], { cwd: input.root, env: cloudflareDeployEnv(input.argv) });
|
|
3453
|
+
try {
|
|
3454
|
+
const namespaces = JSON.parse(listed.stdout);
|
|
3455
|
+
resourceId = namespaces.find((namespace) => namespace.title === installKvNamespaceName)?.id;
|
|
3456
|
+
}
|
|
3457
|
+
catch {
|
|
3458
|
+
resourceId = undefined;
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3369
3461
|
if (!resourceId) {
|
|
3370
3462
|
throw new Error('Could not read Cloudflare KV namespace id from wrangler output.');
|
|
3371
3463
|
}
|
|
@@ -3436,6 +3528,25 @@ async function putDeployInstallRuntimeBinding(input) {
|
|
|
3436
3528
|
throw new Error(`Could not store scoped install runtime binding: ${message}`);
|
|
3437
3529
|
}
|
|
3438
3530
|
}
|
|
3531
|
+
async function ensureHostedProjectRegistration(input) {
|
|
3532
|
+
const response = await hostedHttp({
|
|
3533
|
+
url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects`, input.controlPlaneUrl),
|
|
3534
|
+
method: 'POST',
|
|
3535
|
+
headers: await controlPlaneAuthHeaders(input.userId),
|
|
3536
|
+
body: { projectId: input.projectId, displayName: input.projectId },
|
|
3537
|
+
});
|
|
3538
|
+
if (response.ok || response.status === 409)
|
|
3539
|
+
return;
|
|
3540
|
+
// The in-process legacy control-plane test adapter predates the extracted
|
|
3541
|
+
// project router. Production exposes this route; tolerate only absence.
|
|
3542
|
+
if (response.status === 404 || response.status === 405)
|
|
3543
|
+
return;
|
|
3544
|
+
const body = await response.json().catch(() => undefined);
|
|
3545
|
+
const message = isRecord(body) && typeof body.message === 'string'
|
|
3546
|
+
? body.message
|
|
3547
|
+
: `Control plane returned ${response.status}.`;
|
|
3548
|
+
throw new Error(`Could not register project before tenant provisioning: ${message}`);
|
|
3549
|
+
}
|
|
3439
3550
|
async function requestDeployBillingRuntimeToken(input) {
|
|
3440
3551
|
const response = await hostedHttp({
|
|
3441
3552
|
url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/projects/${encodeURIComponent(input.projectId)}/billing/runtime-token`, input.controlPlaneUrl),
|
|
@@ -3499,6 +3610,10 @@ async function ensureTenantDataPlane(input) {
|
|
|
3499
3610
|
hubSpotAppId: input.hubSpotAppId,
|
|
3500
3611
|
});
|
|
3501
3612
|
const env = cloudflareDeployEnv(input.argv);
|
|
3613
|
+
const cloudflareCredentials = resolveCloudflareCredentials(input.argv);
|
|
3614
|
+
if (!cloudflareCredentials.accountId || !cloudflareCredentials.apiToken) {
|
|
3615
|
+
throw new Error('Tenant data-plane provisioning requires Cloudflare credentials.');
|
|
3616
|
+
}
|
|
3502
3617
|
// Exact local state wins; exact control-plane refs recover a linked deploy
|
|
3503
3618
|
// when its tenant-state binding was lost. Name-only create/adopt remains
|
|
3504
3619
|
// available for short legacy-compatible names only.
|
|
@@ -3509,6 +3624,18 @@ async function ensureTenantDataPlane(input) {
|
|
|
3509
3624
|
kind: 'tenant-d1',
|
|
3510
3625
|
...(exactTenantD1DatabaseId ? { exactResourceId: exactTenantD1DatabaseId } : {}),
|
|
3511
3626
|
...(exactTenantD1DatabaseName ? { exactResourceName: exactTenantD1DatabaseName } : {}),
|
|
3627
|
+
validateExactResource: (resourceId) => {
|
|
3628
|
+
const credentials = resolveCloudflareCredentials(input.argv);
|
|
3629
|
+
if (!credentials.accountId || !credentials.apiToken) {
|
|
3630
|
+
throw new Error('Could not validate the stored tenant D1 without Cloudflare credentials.');
|
|
3631
|
+
}
|
|
3632
|
+
return validateCloudflareResourceForDeploy({
|
|
3633
|
+
argv: input.argv,
|
|
3634
|
+
root: input.root,
|
|
3635
|
+
kind: 'tenant-d1',
|
|
3636
|
+
resourceId,
|
|
3637
|
+
});
|
|
3638
|
+
},
|
|
3512
3639
|
async provisionByName(tenantD1DatabaseName) {
|
|
3513
3640
|
const created = await runCloudflareCommand(['bun', 'x', 'wrangler', 'd1', 'create', tenantD1DatabaseName], { cwd: input.root, env }).catch((error) => {
|
|
3514
3641
|
if (!/already exists/i.test(String(error)))
|
|
@@ -3538,6 +3665,18 @@ async function ensureTenantDataPlane(input) {
|
|
|
3538
3665
|
kind: 'flags-kv',
|
|
3539
3666
|
...(exactFlagsKvNamespaceId ? { exactResourceId: exactFlagsKvNamespaceId } : {}),
|
|
3540
3667
|
...(exactFlagsKvNamespaceName ? { exactResourceName: exactFlagsKvNamespaceName } : {}),
|
|
3668
|
+
validateExactResource: (resourceId) => {
|
|
3669
|
+
const credentials = resolveCloudflareCredentials(input.argv);
|
|
3670
|
+
if (!credentials.accountId || !credentials.apiToken) {
|
|
3671
|
+
throw new Error('Could not validate the stored flags KV without Cloudflare credentials.');
|
|
3672
|
+
}
|
|
3673
|
+
return validateCloudflareResourceForDeploy({
|
|
3674
|
+
argv: input.argv,
|
|
3675
|
+
root: input.root,
|
|
3676
|
+
kind: 'flags-kv',
|
|
3677
|
+
resourceId,
|
|
3678
|
+
});
|
|
3679
|
+
},
|
|
3541
3680
|
async provisionByName(flagsKvNamespaceName) {
|
|
3542
3681
|
const created = await runCloudflareCommand([
|
|
3543
3682
|
'bun',
|
|
@@ -3607,6 +3746,7 @@ async function ensureTenantDataPlane(input) {
|
|
|
3607
3746
|
updatedAt: new Date().toISOString(),
|
|
3608
3747
|
}));
|
|
3609
3748
|
return {
|
|
3749
|
+
cloudflareAccountId: cloudflareCredentials.accountId,
|
|
3610
3750
|
tenantD1DatabaseId,
|
|
3611
3751
|
tenantD1DatabaseName,
|
|
3612
3752
|
flagsKvNamespaceId,
|
|
@@ -3615,6 +3755,62 @@ async function ensureTenantDataPlane(input) {
|
|
|
3615
3755
|
syncGrantSecretValue,
|
|
3616
3756
|
};
|
|
3617
3757
|
}
|
|
3758
|
+
async function validateTenantDataPlaneResourcesForActiveAccount(argv, root, resources) {
|
|
3759
|
+
const credentials = resolveCloudflareCredentials(argv);
|
|
3760
|
+
if (!credentials.accountId || !credentials.apiToken) {
|
|
3761
|
+
throw new Error('Could not validate tenant resources without Cloudflare credentials.');
|
|
3762
|
+
}
|
|
3763
|
+
if (resources.cloudflareAccountId && resources.cloudflareAccountId !== credentials.accountId) {
|
|
3764
|
+
return false;
|
|
3765
|
+
}
|
|
3766
|
+
const [tenantD1Valid, flagsKvValid] = await Promise.all([
|
|
3767
|
+
validateCloudflareResourceForDeploy({
|
|
3768
|
+
argv,
|
|
3769
|
+
root,
|
|
3770
|
+
kind: 'tenant-d1',
|
|
3771
|
+
resourceId: resources.tenantD1DatabaseId,
|
|
3772
|
+
}),
|
|
3773
|
+
validateCloudflareResourceForDeploy({
|
|
3774
|
+
argv,
|
|
3775
|
+
root,
|
|
3776
|
+
kind: 'flags-kv',
|
|
3777
|
+
resourceId: resources.flagsKvNamespaceId,
|
|
3778
|
+
}),
|
|
3779
|
+
]);
|
|
3780
|
+
return tenantD1Valid && flagsKvValid;
|
|
3781
|
+
}
|
|
3782
|
+
async function validateCloudflareResourceForDeploy(input) {
|
|
3783
|
+
const credentials = resolveCloudflareCredentials(input.argv);
|
|
3784
|
+
if (!credentials.accountId || !credentials.apiToken) {
|
|
3785
|
+
throw new Error('Could not validate Cloudflare resources without connected credentials.');
|
|
3786
|
+
}
|
|
3787
|
+
try {
|
|
3788
|
+
return await validateCloudflareResourceForAccount({
|
|
3789
|
+
accountId: credentials.accountId,
|
|
3790
|
+
apiToken: credentials.apiToken,
|
|
3791
|
+
kind: input.kind,
|
|
3792
|
+
resourceId: input.resourceId,
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
catch (error) {
|
|
3796
|
+
if (!/\((?:401|403)\)/.test(String(error)))
|
|
3797
|
+
throw error;
|
|
3798
|
+
}
|
|
3799
|
+
const command = input.kind === 'tenant-d1'
|
|
3800
|
+
? ['bun', 'x', 'wrangler', 'd1', 'list', '--json']
|
|
3801
|
+
: ['bun', 'x', 'wrangler', 'kv', 'namespace', 'list'];
|
|
3802
|
+
const listed = await runCloudflareCommand(command, {
|
|
3803
|
+
cwd: input.root,
|
|
3804
|
+
env: cloudflareDeployEnv(input.argv),
|
|
3805
|
+
});
|
|
3806
|
+
try {
|
|
3807
|
+
const rows = JSON.parse(listed.stdout);
|
|
3808
|
+
return rows.some((row) => row.id === input.resourceId || row.uuid === input.resourceId);
|
|
3809
|
+
}
|
|
3810
|
+
catch {
|
|
3811
|
+
throw new Error(`HSX_E_CLOUDFLARE_RESOURCE_VALIDATION_FAILED: Wrangler returned malformed ${input.kind} list output.`);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3618
3814
|
export function renderWranglerConfig(input) {
|
|
3619
3815
|
const lines = [
|
|
3620
3816
|
'# Generated by hs-x. Do not edit by hand.',
|
|
@@ -4279,7 +4475,14 @@ function runCloudflareCommand(command, options) {
|
|
|
4279
4475
|
resolvePromise({ stdout, stderr });
|
|
4280
4476
|
}
|
|
4281
4477
|
else {
|
|
4282
|
-
rejectPromise(
|
|
4478
|
+
rejectPromise(classifyCloudflareCommandFailure({
|
|
4479
|
+
...(options.env.CLOUDFLARE_ACCOUNT_ID
|
|
4480
|
+
? { accountId: options.env.CLOUDFLARE_ACCOUNT_ID }
|
|
4481
|
+
: {}),
|
|
4482
|
+
stdout,
|
|
4483
|
+
stderr,
|
|
4484
|
+
exitCode: code,
|
|
4485
|
+
}));
|
|
4283
4486
|
}
|
|
4284
4487
|
});
|
|
4285
4488
|
})).pipe(Effect.withSpan('cli.deploy.cloudflare_command', {
|