@hs-x/cli 0.3.6 → 0.4.0
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 +126 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +58 -59
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/kit.d.ts.map +1 -1
- package/dist/cli/kit.js +1 -1
- package/dist/cli/kit.js.map +1 -1
- package/dist/cloudflare-auth.d.ts +19 -0
- package/dist/cloudflare-auth.d.ts.map +1 -1
- package/dist/cloudflare-auth.js +83 -2
- package/dist/cloudflare-auth.js.map +1 -1
- package/dist/cloudflare-oauth.d.ts.map +1 -1
- package/dist/cloudflare-oauth.js +18 -19
- package/dist/cloudflare-oauth.js.map +1 -1
- package/dist/cloudflare-scoped-resource.d.ts +41 -0
- package/dist/cloudflare-scoped-resource.d.ts.map +1 -0
- package/dist/cloudflare-scoped-resource.js +74 -0
- package/dist/cloudflare-scoped-resource.js.map +1 -0
- package/dist/command-catalog.d.ts +13 -0
- package/dist/command-catalog.d.ts.map +1 -0
- package/dist/command-catalog.js +50 -0
- package/dist/command-catalog.js.map +1 -0
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +287 -47
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +274 -38
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +31 -3
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +136 -37
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-retry.d.ts +20 -0
- package/dist/commands/deploy-retry.d.ts.map +1 -0
- package/dist/commands/deploy-retry.js +106 -0
- package/dist/commands/deploy-retry.js.map +1 -0
- package/dist/commands/deploy.d.ts +16 -8
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +1027 -373
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +19 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +1378 -88
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +27 -10
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flags.d.ts +9 -4
- package/dist/commands/flags.d.ts.map +1 -1
- package/dist/commands/flags.js +208 -80
- package/dist/commands/flags.js.map +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +177 -86
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +5 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +74 -15
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/project.d.ts +107 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +586 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/secrets.d.ts +2 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +15 -5
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/validate.d.ts +19 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +26 -7
- package/dist/commands/validate.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 +24 -0
- package/dist/control-plane-tenant-provisioning.d.ts.map +1 -0
- package/dist/control-plane-tenant-provisioning.js +85 -0
- package/dist/control-plane-tenant-provisioning.js.map +1 -0
- package/dist/deploy-targets.d.ts +24 -0
- package/dist/deploy-targets.d.ts.map +1 -0
- package/dist/deploy-targets.js +38 -0
- package/dist/deploy-targets.js.map +1 -0
- package/dist/dev/account-selection.d.ts +16 -0
- package/dist/dev/account-selection.d.ts.map +1 -0
- package/dist/dev/account-selection.js +22 -0
- package/dist/dev/account-selection.js.map +1 -0
- package/dist/dev/browser-log-bridge.d.ts +32 -0
- package/dist/dev/browser-log-bridge.d.ts.map +1 -0
- package/dist/dev/browser-log-bridge.js +156 -0
- package/dist/dev/browser-log-bridge.js.map +1 -0
- package/dist/dev/compat-shim.d.ts +3 -3
- package/dist/dev/compat-shim.d.ts.map +1 -1
- package/dist/dev/compat-shim.js +29 -4
- package/dist/dev/compat-shim.js.map +1 -1
- package/dist/dev/component-selection.d.ts +25 -0
- package/dist/dev/component-selection.d.ts.map +1 -0
- package/dist/dev/component-selection.js +79 -0
- package/dist/dev/component-selection.js.map +1 -0
- package/dist/dev/console-capture.d.ts +18 -0
- package/dist/dev/console-capture.d.ts.map +1 -0
- package/dist/dev/console-capture.js +57 -0
- package/dist/dev/console-capture.js.map +1 -0
- package/dist/dev/event-bus.d.ts +1 -1
- package/dist/dev/event-bus.js +1 -1
- package/dist/dev/frontend-log-relay.d.ts +27 -0
- package/dist/dev/frontend-log-relay.d.ts.map +1 -0
- package/dist/dev/frontend-log-relay.js +102 -0
- package/dist/dev/frontend-log-relay.js.map +1 -0
- package/dist/dev/invoke.d.ts +3 -4
- package/dist/dev/invoke.d.ts.map +1 -1
- package/dist/dev/invoke.js +9 -4
- package/dist/dev/invoke.js.map +1 -1
- package/dist/dev/keyboard.d.ts +1 -0
- package/dist/dev/keyboard.d.ts.map +1 -1
- package/dist/dev/keyboard.js +9 -3
- package/dist/dev/keyboard.js.map +1 -1
- package/dist/dev/log-server.js +3 -0
- package/dist/dev/log-server.js.map +1 -1
- package/dist/dev/loopback-http-listener.d.ts +18 -0
- package/dist/dev/loopback-http-listener.d.ts.map +1 -0
- package/dist/dev/loopback-http-listener.js +91 -0
- package/dist/dev/loopback-http-listener.js.map +1 -0
- package/dist/dev/managed-session.d.ts +124 -0
- package/dist/dev/managed-session.d.ts.map +1 -0
- package/dist/dev/managed-session.js +597 -0
- package/dist/dev/managed-session.js.map +1 -0
- package/dist/dev/port-process.d.ts +21 -0
- package/dist/dev/port-process.d.ts.map +1 -0
- package/dist/dev/port-process.js +145 -0
- package/dist/dev/port-process.js.map +1 -0
- package/dist/dev/session-manager.d.ts +11 -1
- package/dist/dev/session-manager.d.ts.map +1 -1
- package/dist/dev/session-manager.js +65 -14
- package/dist/dev/session-manager.js.map +1 -1
- package/dist/dev/stream-event-format.d.ts +14 -0
- package/dist/dev/stream-event-format.d.ts.map +1 -0
- package/dist/dev/stream-event-format.js +262 -0
- package/dist/dev/stream-event-format.js.map +1 -0
- package/dist/dev/stream-filter.d.ts.map +1 -1
- package/dist/dev/stream-filter.js +16 -1
- package/dist/dev/stream-filter.js.map +1 -1
- package/dist/dev/stream-renderer.d.ts +4 -2
- package/dist/dev/stream-renderer.d.ts.map +1 -1
- package/dist/dev/stream-renderer.js +224 -84
- package/dist/dev/stream-renderer.js.map +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +246 -0
- package/dist/errors-registry.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +20 -3
- package/dist/help.js.map +1 -1
- package/dist/hubspot-app-logs.d.ts +38 -5
- package/dist/hubspot-app-logs.d.ts.map +1 -1
- package/dist/hubspot-app-logs.js +88 -17
- package/dist/hubspot-app-logs.js.map +1 -1
- package/dist/hubspot-auth.d.ts +19 -1
- package/dist/hubspot-auth.d.ts.map +1 -1
- package/dist/hubspot-auth.js +228 -83
- package/dist/hubspot-auth.js.map +1 -1
- package/dist/hubspot-developer-client.d.ts +87 -0
- package/dist/hubspot-developer-client.d.ts.map +1 -1
- package/dist/hubspot-developer-client.js +232 -1
- package/dist/hubspot-developer-client.js.map +1 -1
- package/dist/hubspot-project-archive.d.ts +21 -0
- package/dist/hubspot-project-archive.d.ts.map +1 -0
- package/dist/hubspot-project-archive.js +281 -0
- package/dist/hubspot-project-archive.js.map +1 -0
- package/dist/hubspot-project.d.ts +32 -0
- package/dist/hubspot-project.d.ts.map +1 -0
- package/dist/hubspot-project.js +269 -0
- package/dist/hubspot-project.js.map +1 -0
- package/dist/local-project-picker.js +1 -1
- package/dist/local-project-picker.js.map +1 -1
- package/dist/machine-artifact.d.ts +16 -0
- package/dist/machine-artifact.d.ts.map +1 -0
- package/dist/machine-artifact.js +18 -0
- package/dist/machine-artifact.js.map +1 -0
- package/dist/machine-id.d.ts +15 -0
- package/dist/machine-id.d.ts.map +1 -1
- package/dist/machine-id.js +53 -0
- package/dist/machine-id.js.map +1 -1
- package/dist/mutation-consent.d.ts +20 -0
- package/dist/mutation-consent.d.ts.map +1 -0
- package/dist/mutation-consent.js +9 -0
- package/dist/mutation-consent.js.map +1 -0
- package/dist/output-context.d.ts +3 -0
- package/dist/output-context.d.ts.map +1 -1
- package/dist/output-context.js +7 -1
- package/dist/output-context.js.map +1 -1
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +123 -48
- package/dist/prompt.js.map +1 -1
- package/dist/reporter/index.js +1 -1
- package/dist/reporter/index.js.map +1 -1
- package/dist/reporter/style.d.ts.map +1 -1
- package/dist/reporter/style.js +5 -0
- package/dist/reporter/style.js.map +1 -1
- package/dist/services/hubspot-dev-client.js +3 -0
- package/dist/services/hubspot-dev-client.js.map +1 -1
- package/dist/services/telemetry.d.ts +1 -2
- package/dist/services/telemetry.d.ts.map +1 -1
- package/dist/services/telemetry.js +1 -10
- package/dist/services/telemetry.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +1 -7
- package/dist/telemetry.js.map +1 -1
- package/dist/tenant-data-plane-provisioning.d.ts +35 -0
- package/dist/tenant-data-plane-provisioning.d.ts.map +1 -0
- package/dist/tenant-data-plane-provisioning.js +38 -0
- package/dist/tenant-data-plane-provisioning.js.map +1 -0
- package/package.json +14 -12
package/dist/commands/connect.js
CHANGED
|
@@ -7,7 +7,7 @@ import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
|
|
|
7
7
|
import { loadCreateControlPlane } from '../control-plane-loader.js';
|
|
8
8
|
import { executeCliHttp } from '../effect-http.js';
|
|
9
9
|
import { emitHelp } from '../help.js';
|
|
10
|
-
import { isInteractive, promptConfirm, promptSelect, promptText } from '../prompt.js';
|
|
10
|
+
import { isInteractive, promptConfirm, promptPassword, promptSelect, promptText, } from '../prompt.js';
|
|
11
11
|
import { createReporter } from '../reporter/index.js';
|
|
12
12
|
import { connectHelpText } from './help-command.js';
|
|
13
13
|
async function hostedHttp(input) {
|
|
@@ -22,6 +22,87 @@ async function hostedHttp(input) {
|
|
|
22
22
|
function isRecord(value) {
|
|
23
23
|
return typeof value === 'object' && value !== null;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* A non-2xx from a hosted control-plane credential route, with the status and
|
|
27
|
+
* the CP's `{ error, message }` body preserved so callers can branch on
|
|
28
|
+
* authorization failures instead of collapsing everything into one retry
|
|
29
|
+
* message.
|
|
30
|
+
*/
|
|
31
|
+
class ControlPlaneRequestError extends Error {
|
|
32
|
+
name = 'ControlPlaneRequestError';
|
|
33
|
+
status;
|
|
34
|
+
errorCode;
|
|
35
|
+
constructor(status, errorCode, message) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.status = status;
|
|
38
|
+
this.errorCode = errorCode;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function toControlPlaneRequestError(response, fallback) {
|
|
42
|
+
const body = await response.json().catch(() => undefined);
|
|
43
|
+
const errorCode = isRecord(body) && typeof body.error === 'string' ? body.error : undefined;
|
|
44
|
+
const message = isRecord(body) && typeof body.message === 'string' ? body.message : fallback;
|
|
45
|
+
return new ControlPlaneRequestError(response.status, errorCode, message);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Credential routes (`/v1/accounts/:id/{hubspot,cloudflare}/…`) are admin-class
|
|
49
|
+
* under the control plane's PAT policy, while ordinary reads only need `read`.
|
|
50
|
+
* A deploy/read-scope token (the dashboard's create form defaults to deploy)
|
|
51
|
+
* therefore sails through every preliminary check and 403s only here — an
|
|
52
|
+
* unfixable-by-retry failure that must be reported as a scope problem, not a
|
|
53
|
+
* generic exchange error.
|
|
54
|
+
*/
|
|
55
|
+
function credentialAuthorizationDenied(input) {
|
|
56
|
+
if (input.error.status === 401) {
|
|
57
|
+
return inputValidationError({
|
|
58
|
+
argv: input.argv,
|
|
59
|
+
command: input.command,
|
|
60
|
+
json: input.json,
|
|
61
|
+
code: 'HSX_E_API_UNAUTHORIZED',
|
|
62
|
+
message: 'The control plane rejected the CLI credential.',
|
|
63
|
+
hint: 'Run `hs-x login` to refresh your CLI session, then retry.',
|
|
64
|
+
detail: input.error.message,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return inputValidationError({
|
|
68
|
+
argv: input.argv,
|
|
69
|
+
command: input.command,
|
|
70
|
+
json: input.json,
|
|
71
|
+
code: 'HSX_E_CREDENTIAL_SCOPE',
|
|
72
|
+
message: 'This CLI credential is not allowed to manage account credentials (admin scope required).',
|
|
73
|
+
hint: 'Run `hs-x login` — owners and admins get a full-access CLI session. If you logged in with a pasted API token, it needs admin scope (dashboard → API tokens); deploy and read tokens cannot connect credentials. Account members need an owner/admin to connect.',
|
|
74
|
+
detail: input.error.message,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function isCredentialAuthFailure(error) {
|
|
78
|
+
return (error instanceof ControlPlaneRequestError && (error.status === 401 || error.status === 403));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Probe whether the current CLI credential may manage credentials on
|
|
82
|
+
* `accountId` BEFORE any browser consent or credential prompting. Uses the
|
|
83
|
+
* credential-connection read (same admin PAT class as the connect/complete
|
|
84
|
+
* mutations); 404 just means nothing is connected yet, so only 401/403 count
|
|
85
|
+
* as a refusal. Network errors are swallowed — the real call will surface
|
|
86
|
+
* them with full context.
|
|
87
|
+
*/
|
|
88
|
+
async function probeCredentialAuthorization(input) {
|
|
89
|
+
let response;
|
|
90
|
+
try {
|
|
91
|
+
response = await hostedHttp({
|
|
92
|
+
url: new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/${input.surface}`, input.controlPlaneUrl),
|
|
93
|
+
headers: {
|
|
94
|
+
accept: 'application/json',
|
|
95
|
+
...(await controlPlaneAuthHeaders(input.userId)),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
if (response.status !== 401 && response.status !== 403)
|
|
103
|
+
return undefined;
|
|
104
|
+
return await toControlPlaneRequestError(response, `Credential authorization probe returned HTTP ${response.status}.`);
|
|
105
|
+
}
|
|
25
106
|
export async function connectCommand({ argv, cwd, json, }) {
|
|
26
107
|
// Only a non-flag token is a subcommand — bare `connect --account-id x`
|
|
27
108
|
// routes to the wizard with the flags passed through, not the help fallback.
|
|
@@ -133,6 +214,33 @@ async function connectHubspotCommand({ argv, cwd, json, }) {
|
|
|
133
214
|
reporter.info(`Binding into HS-X account: ${active.displayName} (${active.id})`);
|
|
134
215
|
}
|
|
135
216
|
}
|
|
217
|
+
// Linked connects mutate an admin-class credential route. Fail here — before
|
|
218
|
+
// the PAK browser flow and prompts — when the CLI credential can't manage
|
|
219
|
+
// credentials at all (e.g. a deploy-scope API token), instead of letting the
|
|
220
|
+
// user generate and paste a PAK only to be refused at the connect call.
|
|
221
|
+
if (linked && !localControlPlane && !noControlPlane) {
|
|
222
|
+
const { activeSession } = await import('../account-store.js');
|
|
223
|
+
const store = await loadStore();
|
|
224
|
+
const probeAccountId = resolveFlag(argv, '--account-id') ??
|
|
225
|
+
activeSession(store)?.accountId?.trim() ??
|
|
226
|
+
store.defaultAccountId?.trim();
|
|
227
|
+
if (probeAccountId) {
|
|
228
|
+
const denied = await probeCredentialAuthorization({
|
|
229
|
+
controlPlaneUrl: resolveControlPlaneUrl(argv),
|
|
230
|
+
accountId: probeAccountId,
|
|
231
|
+
surface: 'hubspot',
|
|
232
|
+
userId: resolveFlag(argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user',
|
|
233
|
+
});
|
|
234
|
+
if (denied) {
|
|
235
|
+
return credentialAuthorizationDenied({
|
|
236
|
+
argv,
|
|
237
|
+
command: 'connect hubspot',
|
|
238
|
+
json,
|
|
239
|
+
error: denied,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
136
244
|
let accountId = resolveFlag(argv, '--account-id');
|
|
137
245
|
let developerAccountId = resolveFlag(argv, '--developer-account-id');
|
|
138
246
|
let displayName = resolveFlag(argv, '--display-name');
|
|
@@ -235,7 +343,7 @@ async function connectHubspotCommand({ argv, cwd, json, }) {
|
|
|
235
343
|
personalAccessKey = discoveredDefault.pak;
|
|
236
344
|
}
|
|
237
345
|
else {
|
|
238
|
-
const answer = await
|
|
346
|
+
const answer = await promptPassword({
|
|
239
347
|
message: 'HubSpot personal access key',
|
|
240
348
|
validate: (v) => (v.length >= 10 ? undefined : 'PAK looks too short.'),
|
|
241
349
|
});
|
|
@@ -434,12 +542,21 @@ async function connectHubspotCommand({ argv, cwd, json, }) {
|
|
|
434
542
|
throw error;
|
|
435
543
|
}
|
|
436
544
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
545
|
+
let connection;
|
|
546
|
+
try {
|
|
547
|
+
connection = await requestHostedControlPlaneHubSpotConnect({
|
|
548
|
+
accountId,
|
|
549
|
+
request,
|
|
550
|
+
controlPlaneUrl,
|
|
551
|
+
userId: resolveFlag(argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user',
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (isCredentialAuthFailure(error)) {
|
|
556
|
+
return credentialAuthorizationDenied({ argv, command: 'connect hubspot', json, error });
|
|
557
|
+
}
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
443
560
|
const result = {
|
|
444
561
|
ok: true,
|
|
445
562
|
command: 'connect hubspot',
|
|
@@ -758,12 +875,13 @@ async function runCloudflareOAuthFlowBody(input) {
|
|
|
758
875
|
controlPlaneUrl,
|
|
759
876
|
accountId: input.hsxAccountId,
|
|
760
877
|
userId: resolveFlag(input.argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user',
|
|
761
|
-
body: {
|
|
878
|
+
body: Schema.decodeUnknownSync(schemas.CloudflareOAuthCompleteRequest)({
|
|
762
879
|
code,
|
|
763
880
|
codeVerifier: pkce.verifier,
|
|
764
881
|
redirectUri: config.redirectUri,
|
|
882
|
+
...(input.cloudflareAccountId ? { cloudflareAccountId: input.cloudflareAccountId } : {}),
|
|
765
883
|
...(input.displayName ? { displayName: input.displayName } : {}),
|
|
766
|
-
},
|
|
884
|
+
}),
|
|
767
885
|
});
|
|
768
886
|
reporter
|
|
769
887
|
.step('Cloudflare OAuth complete')
|
|
@@ -771,6 +889,20 @@ async function runCloudflareOAuthFlowBody(input) {
|
|
|
771
889
|
return { kind: 'success-brokered', connection };
|
|
772
890
|
}
|
|
773
891
|
catch (error) {
|
|
892
|
+
// An authorization refusal (deploy/read-scope PAT, expired session) can
|
|
893
|
+
// never be fixed by re-running the OAuth flow — telling the user to
|
|
894
|
+
// retry sends them through browser consent in a loop.
|
|
895
|
+
if (isCredentialAuthFailure(error)) {
|
|
896
|
+
return {
|
|
897
|
+
kind: 'error',
|
|
898
|
+
result: credentialAuthorizationDenied({
|
|
899
|
+
argv: input.argv,
|
|
900
|
+
command: 'connect cloudflare',
|
|
901
|
+
json: input.json,
|
|
902
|
+
error,
|
|
903
|
+
}),
|
|
904
|
+
};
|
|
905
|
+
}
|
|
774
906
|
const detail = error instanceof Error ? error.message : String(error);
|
|
775
907
|
return {
|
|
776
908
|
kind: 'error',
|
|
@@ -810,25 +942,48 @@ async function runCloudflareOAuthFlowBody(input) {
|
|
|
810
942
|
}
|
|
811
943
|
throw error;
|
|
812
944
|
}
|
|
813
|
-
let
|
|
945
|
+
let selectedAccountId;
|
|
814
946
|
try {
|
|
815
|
-
//
|
|
816
|
-
//
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
const
|
|
820
|
-
|
|
947
|
+
// OAuth access tokens 400 on the API-token verify endpoint. Discover the
|
|
948
|
+
// first visible account when no selector was supplied; when the user did
|
|
949
|
+
// supply one, require Cloudflare to confirm access to that exact account.
|
|
950
|
+
const { selectCloudflareAccountId } = await import('../cloudflare-auth.js');
|
|
951
|
+
const cloudflareApiBaseUrl = process.env.HSX_CLOUDFLARE_API_BASE_URL;
|
|
952
|
+
selectedAccountId = await selectCloudflareAccountId({
|
|
953
|
+
token: tokens.accessToken,
|
|
954
|
+
...(input.cloudflareAccountId ? { requestedAccountId: input.cloudflareAccountId } : {}),
|
|
955
|
+
...(cloudflareApiBaseUrl ? { baseUrl: cloudflareApiBaseUrl } : {}),
|
|
956
|
+
});
|
|
821
957
|
}
|
|
822
|
-
catch {
|
|
823
|
-
|
|
958
|
+
catch (error) {
|
|
959
|
+
const { CloudflareAccountSelectionError } = await import('../cloudflare-auth.js');
|
|
960
|
+
if (error instanceof CloudflareAccountSelectionError) {
|
|
961
|
+
return {
|
|
962
|
+
kind: 'error',
|
|
963
|
+
result: inputValidationError({
|
|
964
|
+
argv: input.argv,
|
|
965
|
+
command: 'connect cloudflare',
|
|
966
|
+
json: input.json,
|
|
967
|
+
code: error.code,
|
|
968
|
+
message: error.message,
|
|
969
|
+
hint: error.reason === 'verification-failed'
|
|
970
|
+
? 'Check the Cloudflare API connection and retry. The requested account was not selected or stored.'
|
|
971
|
+
: 'Choose a Cloudflare account this OAuth token can access, or omit --cloudflare-account-id to use the first visible account.',
|
|
972
|
+
detail: error.detail,
|
|
973
|
+
}),
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
if (input.cloudflareAccountId)
|
|
977
|
+
throw error;
|
|
978
|
+
// Discovery without an explicit selector remains best-effort.
|
|
824
979
|
}
|
|
825
980
|
// Persist the credential so a later `hs-x deploy` can reuse this connection
|
|
826
981
|
// without re-authorizing. Cloudflare OAuth returns no refresh token, so the
|
|
827
982
|
// access token IS what we store (with its expiry); deploy uses it directly
|
|
828
983
|
// while valid and prompts a reconnect once it lapses.
|
|
829
|
-
if (
|
|
984
|
+
if (selectedAccountId && (tokens.refreshToken || tokens.accessToken)) {
|
|
830
985
|
try {
|
|
831
|
-
await saveCloudflareOAuthCredential(
|
|
986
|
+
await saveCloudflareOAuthCredential(selectedAccountId, {
|
|
832
987
|
...(tokens.refreshToken ? { refreshToken: tokens.refreshToken } : {}),
|
|
833
988
|
...(tokens.accessToken
|
|
834
989
|
? { accessToken: tokens.accessToken, accessTokenExpiresAt: tokens.expiresAt }
|
|
@@ -842,11 +997,11 @@ async function runCloudflareOAuthFlowBody(input) {
|
|
|
842
997
|
}
|
|
843
998
|
reporter
|
|
844
999
|
.step('Cloudflare OAuth complete')
|
|
845
|
-
.ok(
|
|
1000
|
+
.ok(selectedAccountId ? `account ${selectedAccountId} (dev)` : 'token acquired (dev)');
|
|
846
1001
|
return {
|
|
847
1002
|
kind: 'success-local',
|
|
848
1003
|
accessToken: tokens.accessToken,
|
|
849
|
-
...(
|
|
1004
|
+
...(selectedAccountId ? { cloudflareAccountId: selectedAccountId } : {}),
|
|
850
1005
|
};
|
|
851
1006
|
}
|
|
852
1007
|
async function postCloudflareOAuthComplete(input) {
|
|
@@ -861,8 +1016,7 @@ async function postCloudflareOAuthComplete(input) {
|
|
|
861
1016
|
body: input.body,
|
|
862
1017
|
});
|
|
863
1018
|
if (!response.ok) {
|
|
864
|
-
|
|
865
|
-
throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
|
|
1019
|
+
throw await toControlPlaneRequestError(response, `Cloudflare OAuth completion failed with HTTP ${response.status}.`);
|
|
866
1020
|
}
|
|
867
1021
|
const raw = (await response.json());
|
|
868
1022
|
return Schema.decodeUnknownSync(schemas.CloudflareConnection)(raw);
|
|
@@ -881,7 +1035,7 @@ async function promptHubspotBrowserFlow(argv) {
|
|
|
881
1035
|
await openInBrowser(HUBSPOT_PAK_GENERATION_URL);
|
|
882
1036
|
reporter.info('On the HubSpot page: generate a PAK and copy it.');
|
|
883
1037
|
for (;;) {
|
|
884
|
-
const pak = await
|
|
1038
|
+
const pak = await promptPassword({
|
|
885
1039
|
message: 'Paste your HubSpot personal access key',
|
|
886
1040
|
validate: (v) => (v.length >= 20 ? undefined : 'PAK looks too short.'),
|
|
887
1041
|
});
|
|
@@ -966,6 +1120,78 @@ async function connectCloudflareCommand({ argv, json, }) {
|
|
|
966
1120
|
const interactive = !json && isInteractive();
|
|
967
1121
|
let cloudflareAccountId = resolveFlag(argv, '--cloudflare-account-id');
|
|
968
1122
|
let displayName = resolveFlag(argv, '--display-name');
|
|
1123
|
+
// Linked OAuth mutates the named HS-X account after the user grants consent.
|
|
1124
|
+
// Fail before opening the browser when that account is only a stale local
|
|
1125
|
+
// record or the current user is not a member. Otherwise the user completes
|
|
1126
|
+
// Cloudflare consent only to receive a late 404 from the broker callback.
|
|
1127
|
+
if (linked && !localControlPlane) {
|
|
1128
|
+
const controlPlaneUrl = resolveControlPlaneUrl(argv);
|
|
1129
|
+
const userId = resolveFlag(argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user';
|
|
1130
|
+
let response;
|
|
1131
|
+
try {
|
|
1132
|
+
response = await hostedHttp({
|
|
1133
|
+
url: new URL('/v1/accounts/me', controlPlaneUrl),
|
|
1134
|
+
headers: {
|
|
1135
|
+
accept: 'application/json',
|
|
1136
|
+
'x-hsx-account-id': accountId,
|
|
1137
|
+
...(await controlPlaneAuthHeaders(userId)),
|
|
1138
|
+
},
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
catch (error) {
|
|
1142
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1143
|
+
return inputValidationError({
|
|
1144
|
+
argv,
|
|
1145
|
+
command: 'connect cloudflare',
|
|
1146
|
+
json,
|
|
1147
|
+
code: 'HSX_E_API_NETWORK',
|
|
1148
|
+
message: 'Could not verify the target HS-X account before Cloudflare authorization.',
|
|
1149
|
+
hint: 'Check the control-plane URL and network connection, then retry.',
|
|
1150
|
+
detail,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
if (!response.ok) {
|
|
1154
|
+
const status = response.status;
|
|
1155
|
+
const code = status === 404
|
|
1156
|
+
? 'HSX_E_API_NOT_FOUND'
|
|
1157
|
+
: status === 401 || status === 403
|
|
1158
|
+
? 'HSX_E_API_UNAUTHORIZED'
|
|
1159
|
+
: status >= 500
|
|
1160
|
+
? 'HSX_E_API_SERVER'
|
|
1161
|
+
: 'HSX_E_API_BAD_REQUEST';
|
|
1162
|
+
return inputValidationError({
|
|
1163
|
+
argv,
|
|
1164
|
+
command: 'connect cloudflare',
|
|
1165
|
+
json,
|
|
1166
|
+
code,
|
|
1167
|
+
message: status === 404
|
|
1168
|
+
? `HS-X account ${accountId} does not exist on this control plane or is not available to the current user.`
|
|
1169
|
+
: `Could not verify HS-X account ${accountId} before Cloudflare authorization.`,
|
|
1170
|
+
hint: status === 404
|
|
1171
|
+
? 'Run `hs-x api /v1/users/me/accounts` and choose an account returned by the control plane.'
|
|
1172
|
+
: 'Refresh your HS-X login and retry.',
|
|
1173
|
+
detail: `Account preflight returned HTTP ${status}.`,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
// Membership verified — now verify this credential may MANAGE credentials.
|
|
1177
|
+
// The account read above is read-class under PAT policy; the OAuth
|
|
1178
|
+
// completion is admin-class. Without this probe a deploy-scope token
|
|
1179
|
+
// passes preflight, burns a full browser consent, and only then 403s.
|
|
1180
|
+
const denied = await probeCredentialAuthorization({
|
|
1181
|
+
controlPlaneUrl,
|
|
1182
|
+
accountId,
|
|
1183
|
+
surface: 'cloudflare',
|
|
1184
|
+
userId,
|
|
1185
|
+
});
|
|
1186
|
+
if (denied) {
|
|
1187
|
+
return credentialAuthorizationDenied({
|
|
1188
|
+
argv,
|
|
1189
|
+
command: 'connect cloudflare',
|
|
1190
|
+
json,
|
|
1191
|
+
error: denied,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
969
1195
|
// OAuth (authorization code + PKCE) is the ONLY connection method — owner
|
|
970
1196
|
// decision 2026-06-11. No API-token paste path in the CLI or the dashboard:
|
|
971
1197
|
// the consent screen grants exactly the scopes HS-X needs, and the
|
|
@@ -996,6 +1222,7 @@ async function connectCloudflareCommand({ argv, json, }) {
|
|
|
996
1222
|
json,
|
|
997
1223
|
hsxAccountId: accountId,
|
|
998
1224
|
brokered: linked,
|
|
1225
|
+
...(cloudflareAccountId ? { cloudflareAccountId } : {}),
|
|
999
1226
|
...(displayName ? { displayName } : {}),
|
|
1000
1227
|
});
|
|
1001
1228
|
if (oauthOutcome.kind === 'cancelled') {
|
|
@@ -1102,16 +1329,25 @@ async function connectCloudflareCommand({ argv, json, }) {
|
|
|
1102
1329
|
};
|
|
1103
1330
|
const controlPlaneUrl = resolveFlag(argv, '--control-plane-url') ?? process.env.HSX_CONTROL_PLANE_URL;
|
|
1104
1331
|
const userId = resolveFlag(argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user';
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
:
|
|
1332
|
+
let connection;
|
|
1333
|
+
try {
|
|
1334
|
+
connection = controlPlaneUrl
|
|
1335
|
+
? await requestHostedControlPlaneCloudflareConnect({
|
|
1336
|
+
accountId,
|
|
1337
|
+
request,
|
|
1338
|
+
controlPlaneUrl,
|
|
1339
|
+
userId,
|
|
1340
|
+
})
|
|
1341
|
+
: argv.includes('--local-control-plane')
|
|
1342
|
+
? await requestLocalControlPlaneCloudflareConnect({ accountId, request, userId })
|
|
1343
|
+
: undefined;
|
|
1344
|
+
}
|
|
1345
|
+
catch (error) {
|
|
1346
|
+
if (isCredentialAuthFailure(error)) {
|
|
1347
|
+
return credentialAuthorizationDenied({ argv, command: 'connect cloudflare', json, error });
|
|
1348
|
+
}
|
|
1349
|
+
throw error;
|
|
1350
|
+
}
|
|
1115
1351
|
if (!connection) {
|
|
1116
1352
|
const result = {
|
|
1117
1353
|
ok: true,
|
|
@@ -1198,7 +1434,7 @@ async function requestHostedControlPlaneHubSpotConnect({ accountId, request, con
|
|
|
1198
1434
|
});
|
|
1199
1435
|
const body = await response.json();
|
|
1200
1436
|
if (!response.ok) {
|
|
1201
|
-
throw new
|
|
1437
|
+
throw new ControlPlaneRequestError(response.status, isRecord(body) && typeof body.error === 'string' ? body.error : undefined, isRecord(body) && typeof body.message === 'string'
|
|
1202
1438
|
? body.message
|
|
1203
1439
|
: `Control-plane HubSpot connect failed with status ${response.status}`);
|
|
1204
1440
|
}
|
|
@@ -1234,7 +1470,7 @@ async function requestHostedControlPlaneCloudflareConnect({ accountId, request,
|
|
|
1234
1470
|
});
|
|
1235
1471
|
const body = await response.json();
|
|
1236
1472
|
if (!response.ok) {
|
|
1237
|
-
throw new
|
|
1473
|
+
throw new ControlPlaneRequestError(response.status, isRecord(body) && typeof body.error === 'string' ? body.error : undefined, isRecord(body) && typeof body.message === 'string'
|
|
1238
1474
|
? body.message
|
|
1239
1475
|
: `Control-plane Cloudflare connect failed with status ${response.status}`);
|
|
1240
1476
|
}
|