@hs-x/cli 0.1.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 +1001 -0
- package/dist/account-store.d.ts +51 -0
- package/dist/account-store.d.ts.map +1 -0
- package/dist/account-store.js +138 -0
- package/dist/account-store.js.map +1 -0
- package/dist/bin/hs-x.d.ts +3 -0
- package/dist/bin/hs-x.d.ts.map +1 -0
- package/dist/bin/hs-x.js +47 -0
- package/dist/bin/hs-x.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +595 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli-error.d.ts +36 -0
- package/dist/cli-error.d.ts.map +1 -0
- package/dist/cli-error.js +40 -0
- package/dist/cli-error.js.map +1 -0
- package/dist/cloudflare-auth.d.ts +25 -0
- package/dist/cloudflare-auth.d.ts.map +1 -0
- package/dist/cloudflare-auth.js +251 -0
- package/dist/cloudflare-auth.js.map +1 -0
- package/dist/cloudflare-kv.d.ts +23 -0
- package/dist/cloudflare-kv.d.ts.map +1 -0
- package/dist/cloudflare-kv.js +101 -0
- package/dist/cloudflare-kv.js.map +1 -0
- package/dist/cloudflare-oauth-store.d.ts +16 -0
- package/dist/cloudflare-oauth-store.d.ts.map +1 -0
- package/dist/cloudflare-oauth-store.js +80 -0
- package/dist/cloudflare-oauth-store.js.map +1 -0
- package/dist/cloudflare-oauth.d.ts +82 -0
- package/dist/cloudflare-oauth.d.ts.map +1 -0
- package/dist/cloudflare-oauth.js +336 -0
- package/dist/cloudflare-oauth.js.map +1 -0
- package/dist/cloudflare-pointer.d.ts +13 -0
- package/dist/cloudflare-pointer.d.ts.map +1 -0
- package/dist/cloudflare-pointer.js +46 -0
- package/dist/cloudflare-pointer.js.map +1 -0
- package/dist/command-history.d.ts +7 -0
- package/dist/command-history.d.ts.map +1 -0
- package/dist/command-history.js +34 -0
- package/dist/command-history.js.map +1 -0
- package/dist/commands/account.d.ts +7 -0
- package/dist/commands/account.d.ts.map +1 -0
- package/dist/commands/account.js +315 -0
- package/dist/commands/account.js.map +1 -0
- package/dist/commands/api.d.ts +36 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +521 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/completion.d.ts +7 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +121 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/connect.d.ts +7 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +1123 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/control-plane-read.d.ts +22 -0
- package/dist/commands/control-plane-read.d.ts.map +1 -0
- package/dist/commands/control-plane-read.js +350 -0
- package/dist/commands/control-plane-read.js.map +1 -0
- package/dist/commands/deploy-promote.d.ts +14 -0
- package/dist/commands/deploy-promote.d.ts.map +1 -0
- package/dist/commands/deploy-promote.js +105 -0
- package/dist/commands/deploy-promote.js.map +1 -0
- package/dist/commands/deploy.d.ts +18 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +2764 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +913 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +258 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/flags.d.ts +22 -0
- package/dist/commands/flags.d.ts.map +1 -0
- package/dist/commands/flags.js +185 -0
- package/dist/commands/flags.js.map +1 -0
- package/dist/commands/help-command.d.ts +13 -0
- package/dist/commands/help-command.d.ts.map +1 -0
- package/dist/commands/help-command.js +482 -0
- package/dist/commands/help-command.js.map +1 -0
- package/dist/commands/history.d.ts +6 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +42 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +233 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/link.d.ts +26 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +441 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +381 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/migrate.d.ts +8 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +258 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/rollback.d.ts +21 -0
- package/dist/commands/rollback.d.ts.map +1 -0
- package/dist/commands/rollback.js +301 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/secrets.d.ts +7 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/secrets.js +230 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +241 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unlink.d.ts +21 -0
- package/dist/commands/unlink.d.ts.map +1 -0
- package/dist/commands/unlink.js +83 -0
- package/dist/commands/unlink.js.map +1 -0
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +154 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/validate.d.ts +9 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +39 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -0
- package/dist/control-plane-fetch.d.ts +34 -0
- package/dist/control-plane-fetch.d.ts.map +1 -0
- package/dist/control-plane-fetch.js +73 -0
- package/dist/control-plane-fetch.js.map +1 -0
- package/dist/control-plane-loader.d.ts +16 -0
- package/dist/control-plane-loader.d.ts.map +1 -0
- package/dist/control-plane-loader.js +24 -0
- package/dist/control-plane-loader.js.map +1 -0
- package/dist/dev/compat-shim.d.ts +40 -0
- package/dist/dev/compat-shim.d.ts.map +1 -0
- package/dist/dev/compat-shim.js +65 -0
- package/dist/dev/compat-shim.js.map +1 -0
- package/dist/dev/event-bus.d.ts +27 -0
- package/dist/dev/event-bus.d.ts.map +1 -0
- package/dist/dev/event-bus.js +32 -0
- package/dist/dev/event-bus.js.map +1 -0
- package/dist/dev/log-server.d.ts +52 -0
- package/dist/dev/log-server.d.ts.map +1 -0
- package/dist/dev/log-server.js +216 -0
- package/dist/dev/log-server.js.map +1 -0
- package/dist/dev/session-manager.d.ts +33 -0
- package/dist/dev/session-manager.d.ts.map +1 -0
- package/dist/dev/session-manager.js +132 -0
- package/dist/dev/session-manager.js.map +1 -0
- package/dist/dev/stream-renderer.d.ts +22 -0
- package/dist/dev/stream-renderer.d.ts.map +1 -0
- package/dist/dev/stream-renderer.js +65 -0
- package/dist/dev/stream-renderer.js.map +1 -0
- package/dist/dev/tunnel.d.ts +40 -0
- package/dist/dev/tunnel.d.ts.map +1 -0
- package/dist/dev/tunnel.js +139 -0
- package/dist/dev/tunnel.js.map +1 -0
- package/dist/effect-http.d.ts +10 -0
- package/dist/effect-http.d.ts.map +1 -0
- package/dist/effect-http.js +38 -0
- package/dist/effect-http.js.map +1 -0
- package/dist/errors-registry.d.ts +11 -0
- package/dist/errors-registry.d.ts.map +1 -0
- package/dist/errors-registry.js +554 -0
- package/dist/errors-registry.js.map +1 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -0
- package/dist/help.d.ts +6 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +100 -0
- package/dist/help.js.map +1 -0
- package/dist/history.d.ts +15 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +69 -0
- package/dist/history.js.map +1 -0
- package/dist/hubspot-auth.d.ts +53 -0
- package/dist/hubspot-auth.d.ts.map +1 -0
- package/dist/hubspot-auth.js +301 -0
- package/dist/hubspot-auth.js.map +1 -0
- package/dist/hubspot-developer-client.d.ts +10 -0
- package/dist/hubspot-developer-client.d.ts.map +1 -0
- package/dist/hubspot-developer-client.js +212 -0
- package/dist/hubspot-developer-client.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/init/templates.d.ts +18 -0
- package/dist/init/templates.d.ts.map +1 -0
- package/dist/init/templates.js +239 -0
- package/dist/init/templates.js.map +1 -0
- package/dist/load-env.d.ts +16 -0
- package/dist/load-env.d.ts.map +1 -0
- package/dist/load-env.js +69 -0
- package/dist/load-env.js.map +1 -0
- package/dist/machine-id.d.ts +3 -0
- package/dist/machine-id.d.ts.map +1 -0
- package/dist/machine-id.js +41 -0
- package/dist/machine-id.js.map +1 -0
- package/dist/paths.d.ts +4 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +19 -0
- package/dist/paths.js.map +1 -0
- package/dist/prompt.d.ts +43 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +379 -0
- package/dist/prompt.js.map +1 -0
- package/dist/reporter/human.d.ts +28 -0
- package/dist/reporter/human.d.ts.map +1 -0
- package/dist/reporter/human.js +126 -0
- package/dist/reporter/human.js.map +1 -0
- package/dist/reporter/index.d.ts +14 -0
- package/dist/reporter/index.d.ts.map +1 -0
- package/dist/reporter/index.js +37 -0
- package/dist/reporter/index.js.map +1 -0
- package/dist/reporter/json.d.ts +43 -0
- package/dist/reporter/json.d.ts.map +1 -0
- package/dist/reporter/json.js +146 -0
- package/dist/reporter/json.js.map +1 -0
- package/dist/reporter/style.d.ts +34 -0
- package/dist/reporter/style.d.ts.map +1 -0
- package/dist/reporter/style.js +97 -0
- package/dist/reporter/style.js.map +1 -0
- package/dist/reporter/types.d.ts +41 -0
- package/dist/reporter/types.d.ts.map +1 -0
- package/dist/reporter/types.js +2 -0
- package/dist/reporter/types.js.map +1 -0
- package/dist/result.d.ts +4 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +2 -0
- package/dist/result.js.map +1 -0
- package/dist/services/account-store.d.ts +31 -0
- package/dist/services/account-store.d.ts.map +1 -0
- package/dist/services/account-store.js +135 -0
- package/dist/services/account-store.js.map +1 -0
- package/dist/services/app-paths.d.ts +25 -0
- package/dist/services/app-paths.d.ts.map +1 -0
- package/dist/services/app-paths.js +34 -0
- package/dist/services/app-paths.js.map +1 -0
- package/dist/services/cloudflare-auth.d.ts +83 -0
- package/dist/services/cloudflare-auth.d.ts.map +1 -0
- package/dist/services/cloudflare-auth.js +30 -0
- package/dist/services/cloudflare-auth.js.map +1 -0
- package/dist/services/cloudflare-kv.d.ts +45 -0
- package/dist/services/cloudflare-kv.d.ts.map +1 -0
- package/dist/services/cloudflare-kv.js +151 -0
- package/dist/services/cloudflare-kv.js.map +1 -0
- package/dist/services/command-history.d.ts +29 -0
- package/dist/services/command-history.d.ts.map +1 -0
- package/dist/services/command-history.js +62 -0
- package/dist/services/command-history.js.map +1 -0
- package/dist/services/control-plane.d.ts +32 -0
- package/dist/services/control-plane.d.ts.map +1 -0
- package/dist/services/control-plane.js +57 -0
- package/dist/services/control-plane.js.map +1 -0
- package/dist/services/env-loader.d.ts +18 -0
- package/dist/services/env-loader.d.ts.map +1 -0
- package/dist/services/env-loader.js +34 -0
- package/dist/services/env-loader.js.map +1 -0
- package/dist/services/http.d.ts +19 -0
- package/dist/services/http.d.ts.map +1 -0
- package/dist/services/http.js +9 -0
- package/dist/services/http.js.map +1 -0
- package/dist/services/live.d.ts +16 -0
- package/dist/services/live.d.ts.map +1 -0
- package/dist/services/live.js +26 -0
- package/dist/services/live.js.map +1 -0
- package/dist/services/machine-id.d.ts +18 -0
- package/dist/services/machine-id.d.ts.map +1 -0
- package/dist/services/machine-id.js +39 -0
- package/dist/services/machine-id.js.map +1 -0
- package/dist/services/reporter.d.ts +55 -0
- package/dist/services/reporter.d.ts.map +1 -0
- package/dist/services/reporter.js +49 -0
- package/dist/services/reporter.js.map +1 -0
- package/dist/state-store.d.ts +39 -0
- package/dist/state-store.d.ts.map +1 -0
- package/dist/state-store.js +89 -0
- package/dist/state-store.js.map +1 -0
- package/dist/telemetry.d.ts +13 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +129 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tenant-state.d.ts +69 -0
- package/dist/tenant-state.d.ts.map +1 -0
- package/dist/tenant-state.js +161 -0
- package/dist/tenant-state.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,1123 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { HttpClientRequest, Schema, schemas } from '@hs-x/types';
|
|
4
|
+
import { loadCreateControlPlane } from '../control-plane-loader.js';
|
|
5
|
+
import { Effect } from 'effect';
|
|
6
|
+
import { emitHelp } from '../help.js';
|
|
7
|
+
import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
|
|
8
|
+
import { executeCliHttp } from '../effect-http.js';
|
|
9
|
+
import { resolveControlPlaneUrl } from '../config.js';
|
|
10
|
+
import { createReporter } from '../reporter/index.js';
|
|
11
|
+
import { isInteractive, promptConfirm, promptSelect, promptText } from '../prompt.js';
|
|
12
|
+
import { connectHelpText } from './help-command.js';
|
|
13
|
+
async function hostedHttp(input) {
|
|
14
|
+
const headers = { ...(input.headers ?? {}) };
|
|
15
|
+
let request = HttpClientRequest.make(input.method ?? 'GET')(input.url).pipe(HttpClientRequest.setHeaders(headers));
|
|
16
|
+
if (input.body !== undefined) {
|
|
17
|
+
headers['content-type'] = headers['content-type'] ?? 'application/json';
|
|
18
|
+
request = request.pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(typeof input.body === 'string' ? input.body : JSON.stringify(input.body), headers['content-type']));
|
|
19
|
+
}
|
|
20
|
+
return executeCliHttp(request);
|
|
21
|
+
}
|
|
22
|
+
function isRecord(value) {
|
|
23
|
+
return typeof value === 'object' && value !== null;
|
|
24
|
+
}
|
|
25
|
+
export async function connectCommand({ argv, cwd, json, }) {
|
|
26
|
+
const subcommand = argv[1];
|
|
27
|
+
if (subcommand === 'cloudflare') {
|
|
28
|
+
return await connectCloudflareCommand({ argv, json });
|
|
29
|
+
}
|
|
30
|
+
if (subcommand === 'hubspot') {
|
|
31
|
+
return await connectHubspotCommand({ argv, cwd, json });
|
|
32
|
+
}
|
|
33
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
|
|
34
|
+
emitHelp(connectHelpText(), json);
|
|
35
|
+
return { exitCode: 0 };
|
|
36
|
+
}
|
|
37
|
+
if (subcommand && subcommand.length > 0) {
|
|
38
|
+
emitHelp(connectHelpText(), json);
|
|
39
|
+
return { exitCode: 1 };
|
|
40
|
+
}
|
|
41
|
+
return await connectWizard({ argv, cwd, json });
|
|
42
|
+
}
|
|
43
|
+
async function connectWizard({ argv, cwd, json, }) {
|
|
44
|
+
if (json || !isInteractive()) {
|
|
45
|
+
emitHelp(connectHelpText(), json);
|
|
46
|
+
return { exitCode: 0 };
|
|
47
|
+
}
|
|
48
|
+
const reporter = createReporter({ command: 'connect', argv, entry: true });
|
|
49
|
+
reporter.banner();
|
|
50
|
+
reporter.header('platform');
|
|
51
|
+
reporter.info("Let's connect your accounts so HS-X can deploy and run.");
|
|
52
|
+
reporter.info('');
|
|
53
|
+
const hubspotResult = await connectHubspotCommand({
|
|
54
|
+
argv: ['hubspot', ...argv.slice(1)],
|
|
55
|
+
cwd,
|
|
56
|
+
json: false,
|
|
57
|
+
});
|
|
58
|
+
if (hubspotResult.exitCode !== 0) {
|
|
59
|
+
return hubspotResult;
|
|
60
|
+
}
|
|
61
|
+
reporter.info('');
|
|
62
|
+
const wantCloudflare = await promptConfirm({
|
|
63
|
+
message: 'Connect Cloudflare now? (You can do this later with `hs-x connect cloudflare`.)',
|
|
64
|
+
default: false,
|
|
65
|
+
});
|
|
66
|
+
if (wantCloudflare === undefined)
|
|
67
|
+
return cancelledResult(argv, 'connect');
|
|
68
|
+
if (wantCloudflare) {
|
|
69
|
+
const cfResult = await connectCloudflareCommand({
|
|
70
|
+
argv: ['cloudflare', ...argv.slice(1)],
|
|
71
|
+
json: false,
|
|
72
|
+
});
|
|
73
|
+
if (cfResult.exitCode !== 0)
|
|
74
|
+
return cfResult;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
reporter.info("Skipped Cloudflare. Run `hs-x connect cloudflare` when you're ready.");
|
|
78
|
+
}
|
|
79
|
+
reporter.info('');
|
|
80
|
+
reporter.step('Connected to the HS-X platform').ok();
|
|
81
|
+
reporter.info('Next: `hs-x dev` to start the local dev server, or `hs-x deploy --plan` to preview.');
|
|
82
|
+
reporter.done('Connected');
|
|
83
|
+
return { exitCode: 0 };
|
|
84
|
+
}
|
|
85
|
+
async function connectHubspotCommand({ argv, cwd, json, }) {
|
|
86
|
+
const interactive = !json && isInteractive();
|
|
87
|
+
const authMethod = resolveFlag(argv, '--auth-method') ?? 'pak';
|
|
88
|
+
let accountId = resolveFlag(argv, '--account-id');
|
|
89
|
+
let developerAccountId = resolveFlag(argv, '--developer-account-id');
|
|
90
|
+
let displayName = resolveFlag(argv, '--display-name');
|
|
91
|
+
let personalAccessKey = resolveFlag(argv, '--pak') ?? process.env.HSX_HUBSPOT_PAK;
|
|
92
|
+
const developerApiKey = resolveFlag(argv, '--developer-api-key') ?? process.env.HSX_HUBSPOT_DEVELOPER_API_KEY;
|
|
93
|
+
let discoveredDefault;
|
|
94
|
+
if (interactive && (!developerAccountId || !displayName || !personalAccessKey)) {
|
|
95
|
+
const { discoverHubSpotAccounts } = await import('../hubspot-auth.js');
|
|
96
|
+
const discovered = await discoverHubSpotAccounts(cwd);
|
|
97
|
+
const hasAccounts = discovered && discovered.accounts.length > 0;
|
|
98
|
+
if (!hasAccounts) {
|
|
99
|
+
const browserPicked = await promptHubspotBrowserFlow(argv);
|
|
100
|
+
if (!browserPicked)
|
|
101
|
+
return cancelledResult(argv, 'connect hubspot');
|
|
102
|
+
discoveredDefault = {
|
|
103
|
+
name: browserPicked.name,
|
|
104
|
+
portalId: browserPicked.portalId,
|
|
105
|
+
pak: browserPicked.pak,
|
|
106
|
+
persistInfo: {
|
|
107
|
+
accessToken: browserPicked.accessToken,
|
|
108
|
+
expiresAt: browserPicked.expiresAt,
|
|
109
|
+
...(browserPicked.accountType ? { accountType: browserPicked.accountType } : {}),
|
|
110
|
+
...(browserPicked.hubName ? { hubName: browserPicked.hubName } : {}),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
else if (discovered) {
|
|
115
|
+
const reporter = createReporter({ command: 'connect hubspot', argv });
|
|
116
|
+
reporter.info(`Found ${discovered.accounts.length} HubSpot account(s) in ${discovered.source}`);
|
|
117
|
+
const sorted = [...discovered.accounts].sort((a, b) => a.isDefault === b.isDefault ? a.name.localeCompare(b.name) : a.isDefault ? -1 : 1);
|
|
118
|
+
const NEW_VALUE = '__new__';
|
|
119
|
+
const defaultId = sorted.find((a) => a.isDefault)?.name ?? sorted[0]?.name;
|
|
120
|
+
const pickedName = await promptSelect({
|
|
121
|
+
message: 'Which HubSpot account do you want to bind?',
|
|
122
|
+
...(defaultId ? { default: defaultId } : {}),
|
|
123
|
+
options: [
|
|
124
|
+
{
|
|
125
|
+
value: NEW_VALUE,
|
|
126
|
+
label: '+ Connect a new account',
|
|
127
|
+
description: 'opens HubSpot in your browser',
|
|
128
|
+
},
|
|
129
|
+
...sorted.map((a) => ({
|
|
130
|
+
value: a.name,
|
|
131
|
+
label: a.name,
|
|
132
|
+
description: `portal ${a.portalId}${a.isDefault ? ' (default)' : ''}`,
|
|
133
|
+
})),
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
if (!pickedName)
|
|
137
|
+
return cancelledResult(argv, 'connect hubspot');
|
|
138
|
+
if (pickedName === NEW_VALUE) {
|
|
139
|
+
const browserPicked = await promptHubspotBrowserFlow(argv);
|
|
140
|
+
if (!browserPicked)
|
|
141
|
+
return cancelledResult(argv, 'connect hubspot');
|
|
142
|
+
discoveredDefault = browserPicked;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const picked = sorted.find((a) => a.name === pickedName);
|
|
146
|
+
if (picked) {
|
|
147
|
+
discoveredDefault = {
|
|
148
|
+
name: picked.name,
|
|
149
|
+
portalId: picked.portalId,
|
|
150
|
+
...(picked.personalAccessKey ? { pak: picked.personalAccessKey } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (interactive) {
|
|
157
|
+
if (!developerAccountId) {
|
|
158
|
+
if (discoveredDefault) {
|
|
159
|
+
developerAccountId = String(discoveredDefault.portalId);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const answer = await promptText({
|
|
163
|
+
message: 'HubSpot developer account id (portalId)',
|
|
164
|
+
validate: (v) => /^\d+$/.test(v) ? undefined : 'Must be a numeric HubSpot portal id (e.g. 7222370).',
|
|
165
|
+
});
|
|
166
|
+
if (answer === undefined)
|
|
167
|
+
return cancelledResult(argv, 'connect hubspot');
|
|
168
|
+
developerAccountId = answer;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (!displayName) {
|
|
172
|
+
if (discoveredDefault) {
|
|
173
|
+
displayName = discoveredDefault.name;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const answer = await promptText({
|
|
177
|
+
message: 'Display name',
|
|
178
|
+
default: `HubSpot ${developerAccountId ?? ''}`.trim(),
|
|
179
|
+
});
|
|
180
|
+
if (answer === undefined)
|
|
181
|
+
return cancelledResult(argv, 'connect hubspot');
|
|
182
|
+
displayName = answer;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (authMethod === 'pak' && !personalAccessKey) {
|
|
186
|
+
if (discoveredDefault?.pak) {
|
|
187
|
+
personalAccessKey = discoveredDefault.pak;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const answer = await promptText({
|
|
191
|
+
message: 'HubSpot personal access key',
|
|
192
|
+
validate: (v) => (v.length >= 10 ? undefined : 'PAK looks too short.'),
|
|
193
|
+
});
|
|
194
|
+
if (answer === undefined)
|
|
195
|
+
return cancelledResult(argv, 'connect hubspot');
|
|
196
|
+
personalAccessKey = answer;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!accountId) {
|
|
201
|
+
const source = discoveredDefault?.name ?? displayName ?? `portal-${developerAccountId ?? 'dev'}`;
|
|
202
|
+
accountId = `acct_${source
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
205
|
+
.replace(/^_|_$/g, '')}`;
|
|
206
|
+
}
|
|
207
|
+
if (!accountId) {
|
|
208
|
+
const reporter = createReporter({ command: 'connect hubspot', argv });
|
|
209
|
+
const code = 'HSX_E_INPUT_MISSING_ACCOUNT_ID';
|
|
210
|
+
reporter.error(code, 'Missing --account-id.', {
|
|
211
|
+
hint: 'Pass --account-id <id>, or run interactively to be prompted.',
|
|
212
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
213
|
+
});
|
|
214
|
+
reporter.done(undefined, 10);
|
|
215
|
+
return { exitCode: 10 };
|
|
216
|
+
}
|
|
217
|
+
if (authMethod === 'pak' && !personalAccessKey) {
|
|
218
|
+
const reporter = createReporter({ command: 'connect hubspot', argv });
|
|
219
|
+
const code = 'HSX_E_INPUT_MISSING_PAK';
|
|
220
|
+
reporter.error(code, 'HubSpot PAK is required.', {
|
|
221
|
+
hint: 'Pass --pak, set HSX_HUBSPOT_PAK, or run `hs accounts auth` first so we can auto-discover.',
|
|
222
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
223
|
+
});
|
|
224
|
+
reporter.done(undefined, 10);
|
|
225
|
+
return { exitCode: 10 };
|
|
226
|
+
}
|
|
227
|
+
const request = Schema.decodeUnknownSync(schemas.HubSpotDeveloperConnectRequest)({
|
|
228
|
+
developerAccountId,
|
|
229
|
+
displayName,
|
|
230
|
+
authMethod,
|
|
231
|
+
...(personalAccessKey ? { personalAccessKey } : {}),
|
|
232
|
+
...(developerApiKey ? { developerApiKey } : {}),
|
|
233
|
+
});
|
|
234
|
+
const safeRequest = {
|
|
235
|
+
developerAccountId: request.developerAccountId,
|
|
236
|
+
displayName: request.displayName,
|
|
237
|
+
authMethod: request.authMethod,
|
|
238
|
+
};
|
|
239
|
+
const noControlPlane = argv.includes('--no-control-plane');
|
|
240
|
+
const controlPlaneUrl = !noControlPlane && !argv.includes('--local-control-plane')
|
|
241
|
+
? resolveControlPlaneUrl(argv)
|
|
242
|
+
: undefined;
|
|
243
|
+
if (controlPlaneUrl) {
|
|
244
|
+
const connection = await requestHostedControlPlaneHubSpotConnect({
|
|
245
|
+
accountId,
|
|
246
|
+
request,
|
|
247
|
+
controlPlaneUrl,
|
|
248
|
+
userId: resolveFlag(argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user',
|
|
249
|
+
});
|
|
250
|
+
const result = {
|
|
251
|
+
ok: true,
|
|
252
|
+
command: 'connect hubspot',
|
|
253
|
+
mode: 'control-plane-connect',
|
|
254
|
+
accountId,
|
|
255
|
+
request: safeRequest,
|
|
256
|
+
connection,
|
|
257
|
+
};
|
|
258
|
+
await persistConnectedAccount({
|
|
259
|
+
id: accountId,
|
|
260
|
+
displayName: connection.displayName,
|
|
261
|
+
hubspotPortalId: Number(developerAccountId),
|
|
262
|
+
});
|
|
263
|
+
const hscliResult = await maybePersistToHubSpotCli(discoveredDefault, personalAccessKey);
|
|
264
|
+
if (json) {
|
|
265
|
+
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const reporter = createReporter({ command: 'connect hubspot', argv });
|
|
269
|
+
reporter.header(connection.accountId);
|
|
270
|
+
reporter.step('Connected HubSpot developer account').ok(connection.displayName);
|
|
271
|
+
renderCredentialDestinations(reporter, {
|
|
272
|
+
hsxStore: defaultStorePathForLog(),
|
|
273
|
+
hscli: hscliResult?.written ? hscliResult.path : null,
|
|
274
|
+
controlPlane: controlPlaneUrl,
|
|
275
|
+
});
|
|
276
|
+
reporter.done();
|
|
277
|
+
}
|
|
278
|
+
return { exitCode: 0 };
|
|
279
|
+
}
|
|
280
|
+
if (!argv.includes('--local-control-plane')) {
|
|
281
|
+
const result = {
|
|
282
|
+
ok: true,
|
|
283
|
+
command: 'connect hubspot',
|
|
284
|
+
mode: 'request',
|
|
285
|
+
accountId,
|
|
286
|
+
request: safeRequest,
|
|
287
|
+
nextStep: 'Pass --control-plane-url to connect through the control plane or --local-control-plane to exercise the local contract handler.',
|
|
288
|
+
};
|
|
289
|
+
if (json) {
|
|
290
|
+
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
const reporter = createReporter({ command: 'connect hubspot', argv });
|
|
294
|
+
reporter.header(accountId);
|
|
295
|
+
reporter.info(`Prepared HubSpot connect request for ${request.displayName}.`);
|
|
296
|
+
reporter.warn('HSX_W_CONNECT_NO_TRANSPORT', 'Hosted transport is not wired yet.');
|
|
297
|
+
reporter.info('Pass --control-plane-url or --local-control-plane to complete the connect.');
|
|
298
|
+
reporter.done('Prepared');
|
|
299
|
+
}
|
|
300
|
+
return { exitCode: 0 };
|
|
301
|
+
}
|
|
302
|
+
const connection = await requestLocalControlPlaneHubSpotConnect({ accountId, request });
|
|
303
|
+
const result = {
|
|
304
|
+
ok: true,
|
|
305
|
+
command: 'connect hubspot',
|
|
306
|
+
mode: 'local-control-plane-connect',
|
|
307
|
+
accountId,
|
|
308
|
+
request: safeRequest,
|
|
309
|
+
connection,
|
|
310
|
+
};
|
|
311
|
+
await persistConnectedAccount({
|
|
312
|
+
id: accountId,
|
|
313
|
+
displayName: connection.displayName,
|
|
314
|
+
hubspotPortalId: Number(developerAccountId),
|
|
315
|
+
});
|
|
316
|
+
const hscliResult = await maybePersistToHubSpotCli(discoveredDefault, personalAccessKey);
|
|
317
|
+
if (json) {
|
|
318
|
+
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
const reporter = createReporter({ command: 'connect hubspot', argv });
|
|
322
|
+
reporter.header(connection.accountId);
|
|
323
|
+
reporter
|
|
324
|
+
.step('Connected HubSpot developer account (local control plane)')
|
|
325
|
+
.ok(connection.displayName);
|
|
326
|
+
renderCredentialDestinations(reporter, {
|
|
327
|
+
hsxStore: defaultStorePathForLog(),
|
|
328
|
+
hscli: hscliResult?.written ? hscliResult.path : null,
|
|
329
|
+
controlPlane: 'local (in-memory)',
|
|
330
|
+
});
|
|
331
|
+
reporter.done();
|
|
332
|
+
}
|
|
333
|
+
return { exitCode: 0 };
|
|
334
|
+
}
|
|
335
|
+
function renderCredentialDestinations(reporter, destinations) {
|
|
336
|
+
reporter.info('');
|
|
337
|
+
reporter.info('Credentials persisted to:');
|
|
338
|
+
if (destinations.hsxStore)
|
|
339
|
+
reporter.info(` ✓ HS-X store ${destinations.hsxStore}`);
|
|
340
|
+
if (destinations.hscli)
|
|
341
|
+
reporter.info(` ✓ HubSpot CLI ${destinations.hscli}`);
|
|
342
|
+
else if (destinations.hscli === null)
|
|
343
|
+
reporter.info(' · HubSpot CLI skipped (PAK already there or write failed)');
|
|
344
|
+
if (destinations.controlPlane)
|
|
345
|
+
reporter.info(` ✓ Control plane ${destinations.controlPlane}`);
|
|
346
|
+
else
|
|
347
|
+
reporter.info(' ⚠ Control plane not uploaded (re-run with --control-plane-url <url> to push)');
|
|
348
|
+
}
|
|
349
|
+
async function maybePersistToHubSpotCli(discoveredDefault, pak) {
|
|
350
|
+
if (!discoveredDefault?.persistInfo || !pak)
|
|
351
|
+
return undefined;
|
|
352
|
+
try {
|
|
353
|
+
const { persistToHubSpotCliConfig } = await import('../hubspot-auth.js');
|
|
354
|
+
const result = await persistToHubSpotCliConfig({
|
|
355
|
+
portalId: discoveredDefault.portalId,
|
|
356
|
+
pak,
|
|
357
|
+
accessToken: discoveredDefault.persistInfo.accessToken,
|
|
358
|
+
expiresAt: discoveredDefault.persistInfo.expiresAt,
|
|
359
|
+
...(discoveredDefault.persistInfo.hubName
|
|
360
|
+
? { hubName: discoveredDefault.persistInfo.hubName }
|
|
361
|
+
: {}),
|
|
362
|
+
...(discoveredDefault.persistInfo.accountType
|
|
363
|
+
? { accountType: discoveredDefault.persistInfo.accountType }
|
|
364
|
+
: {}),
|
|
365
|
+
});
|
|
366
|
+
return result.written ? result : undefined;
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
async function resolveAccountIdOrPrompt(input) {
|
|
373
|
+
const fromFlag = resolveFlag(input.argv, '--account-id') ?? process.env.HSX_ACCOUNT_ID;
|
|
374
|
+
if (fromFlag && fromFlag.length > 0)
|
|
375
|
+
return { accountId: fromFlag, fromPrompt: false };
|
|
376
|
+
const { loadStore } = await import('../account-store.js');
|
|
377
|
+
const store = await loadStore();
|
|
378
|
+
const ids = Object.keys(store.accounts);
|
|
379
|
+
if (ids.length === 0)
|
|
380
|
+
return { fromPrompt: false };
|
|
381
|
+
const defaultId = store.defaultAccountId;
|
|
382
|
+
// Non-interactive, --json, or --yes: accept the configured default rather than
|
|
383
|
+
// silently degrading to "no account picked" — that path led deploy to exit 0
|
|
384
|
+
// after building artifacts only, looking like success.
|
|
385
|
+
const yes = input.argv.includes('--yes') || input.argv.includes('-y');
|
|
386
|
+
if (input.json || !isInteractive() || yes) {
|
|
387
|
+
if (defaultId && ids.includes(defaultId)) {
|
|
388
|
+
return { accountId: defaultId, fromPrompt: false };
|
|
389
|
+
}
|
|
390
|
+
return { fromPrompt: false };
|
|
391
|
+
}
|
|
392
|
+
const picked = await promptSelect({
|
|
393
|
+
message: `Which HS-X account for ${input.purpose}?`,
|
|
394
|
+
...(defaultId && ids.includes(defaultId) ? { default: defaultId } : {}),
|
|
395
|
+
options: ids.map((id) => {
|
|
396
|
+
const a = store.accounts[id];
|
|
397
|
+
return {
|
|
398
|
+
value: id,
|
|
399
|
+
label: id,
|
|
400
|
+
description: a
|
|
401
|
+
? `portal ${a.hubspotPortalId} — ${a.displayName}${id === defaultId ? ' (default)' : ''}`
|
|
402
|
+
: '',
|
|
403
|
+
};
|
|
404
|
+
}),
|
|
405
|
+
});
|
|
406
|
+
return picked ? { accountId: picked, fromPrompt: true } : { fromPrompt: false };
|
|
407
|
+
}
|
|
408
|
+
function missingAccountIdError(input) {
|
|
409
|
+
const code = 'HSX_E_INPUT_MISSING_ACCOUNT_ID';
|
|
410
|
+
const message = 'Missing --account-id.';
|
|
411
|
+
const hint = isInteractive()
|
|
412
|
+
? 'Run `hs-x accounts list` to see options, then pass --account-id or set HSX_ACCOUNT_ID.'
|
|
413
|
+
: 'Pass --account-id or set HSX_ACCOUNT_ID. (In a TTY you would be prompted.)';
|
|
414
|
+
if (input.json) {
|
|
415
|
+
write(`${JSON.stringify({
|
|
416
|
+
schema_version: 1,
|
|
417
|
+
command: input.command,
|
|
418
|
+
ok: false,
|
|
419
|
+
error: { code, message, hint },
|
|
420
|
+
}, null, 2)}\n`);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const reporter = createReporter({ command: input.command, argv: input.argv });
|
|
424
|
+
reporter.error(code, message, { hint, docs_url: `https://hs-x.dev/errors/${code}` });
|
|
425
|
+
reporter.done(undefined, 10);
|
|
426
|
+
}
|
|
427
|
+
return { exitCode: 10 };
|
|
428
|
+
}
|
|
429
|
+
async function promptMissingText(message, current, validate) {
|
|
430
|
+
if (current && current.length > 0)
|
|
431
|
+
return current;
|
|
432
|
+
const answer = await promptText({
|
|
433
|
+
message,
|
|
434
|
+
...(validate ? { validate } : {}),
|
|
435
|
+
});
|
|
436
|
+
return answer === undefined ? undefined : answer;
|
|
437
|
+
}
|
|
438
|
+
function missingProjectIdError(input) {
|
|
439
|
+
const code = 'HSX_E_INPUT_MISSING_PROJECT_ID';
|
|
440
|
+
const message = 'Missing --project-id.';
|
|
441
|
+
const hint = 'Pass --project-id, set HSX_PROJECT_ID, or add `name: "..."` to hsx.config.ts so we can derive it.';
|
|
442
|
+
if (input.json) {
|
|
443
|
+
write(`${JSON.stringify({
|
|
444
|
+
schema_version: 1,
|
|
445
|
+
command: input.command,
|
|
446
|
+
ok: false,
|
|
447
|
+
error: { code, message, hint },
|
|
448
|
+
}, null, 2)}\n`);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
const reporter = createReporter({ command: input.command, argv: input.argv });
|
|
452
|
+
reporter.error(code, message, { hint, docs_url: `https://hs-x.dev/errors/${code}` });
|
|
453
|
+
reporter.done(undefined, 10);
|
|
454
|
+
}
|
|
455
|
+
return { exitCode: 10 };
|
|
456
|
+
}
|
|
457
|
+
function inputValidationError(input) {
|
|
458
|
+
if (input.json) {
|
|
459
|
+
write(`${JSON.stringify({
|
|
460
|
+
schema_version: 1,
|
|
461
|
+
command: input.command,
|
|
462
|
+
ok: false,
|
|
463
|
+
error: {
|
|
464
|
+
code: input.code,
|
|
465
|
+
message: input.message,
|
|
466
|
+
hint: input.hint,
|
|
467
|
+
detail: input.detail,
|
|
468
|
+
},
|
|
469
|
+
}, null, 2)}\n`);
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
const reporter = createReporter({ command: input.command, argv: input.argv });
|
|
473
|
+
reporter.error(input.code, input.message, {
|
|
474
|
+
hint: input.hint,
|
|
475
|
+
docs_url: `https://hs-x.dev/errors/${input.code}`,
|
|
476
|
+
});
|
|
477
|
+
reporter.done(undefined, 10);
|
|
478
|
+
}
|
|
479
|
+
return { exitCode: 10 };
|
|
480
|
+
}
|
|
481
|
+
async function readHsxConfigProjectId(root) {
|
|
482
|
+
try {
|
|
483
|
+
const contents = await readFile(join(root, 'hsx.config.ts'), 'utf8');
|
|
484
|
+
const match = contents.match(/\bname\s*:\s*["'`]([^"'`]+)["'`]/);
|
|
485
|
+
return match?.[1] ? toProjectId(match[1]) : undefined;
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function toProjectId(value) {
|
|
492
|
+
return value
|
|
493
|
+
.trim()
|
|
494
|
+
.toLowerCase()
|
|
495
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
496
|
+
.replace(/^-+|-+$/g, '');
|
|
497
|
+
}
|
|
498
|
+
function cancelledResult(argv, command) {
|
|
499
|
+
const reporter = createReporter({ command, argv });
|
|
500
|
+
reporter.info('Cancelled.');
|
|
501
|
+
return { exitCode: 130 };
|
|
502
|
+
}
|
|
503
|
+
async function promptCloudflareBrowserFlow(argv) {
|
|
504
|
+
const reporter = createReporter({ command: 'connect cloudflare', argv });
|
|
505
|
+
const wantBrowser = await promptConfirm({
|
|
506
|
+
message: 'Open Cloudflare in your browser to create an API token?',
|
|
507
|
+
default: true,
|
|
508
|
+
});
|
|
509
|
+
if (wantBrowser === undefined)
|
|
510
|
+
return 'cancel';
|
|
511
|
+
if (!wantBrowser)
|
|
512
|
+
return undefined;
|
|
513
|
+
const { CLOUDFLARE_TOKEN_TEMPLATE_URL, CLOUDFLARE_REQUIRED_PERMISSIONS, verifyCloudflareToken, CloudflareAuthError, } = await import('../cloudflare-auth.js');
|
|
514
|
+
const { openInBrowser } = await import('../hubspot-auth.js');
|
|
515
|
+
reporter.info('HS-X needs these Cloudflare permissions:');
|
|
516
|
+
for (const p of CLOUDFLARE_REQUIRED_PERMISSIONS) {
|
|
517
|
+
reporter.info(` • ${p.scope} → ${p.permission} (${p.reason})`);
|
|
518
|
+
}
|
|
519
|
+
reporter.info('');
|
|
520
|
+
reporter.info('Tip: pick the "Edit Cloudflare Workers" template and add D1 + Queues to it.');
|
|
521
|
+
reporter.info(`Opening ${CLOUDFLARE_TOKEN_TEMPLATE_URL}`);
|
|
522
|
+
await openInBrowser(CLOUDFLARE_TOKEN_TEMPLATE_URL);
|
|
523
|
+
for (;;) {
|
|
524
|
+
const token = await promptText({
|
|
525
|
+
message: 'Paste your Cloudflare API token',
|
|
526
|
+
validate: (v) => (v.length >= 30 ? undefined : 'Token looks too short.'),
|
|
527
|
+
});
|
|
528
|
+
if (token === undefined)
|
|
529
|
+
return 'cancel';
|
|
530
|
+
try {
|
|
531
|
+
const info = await verifyCloudflareToken(token);
|
|
532
|
+
reporter
|
|
533
|
+
.step('Token verified')
|
|
534
|
+
.ok(`id ${info.id}${info.accountId ? `, account ${info.accountId}` : ''}`);
|
|
535
|
+
renderCloudflareTokenPermissions(reporter, info, CLOUDFLARE_REQUIRED_PERMISSIONS);
|
|
536
|
+
return { token, ...(info.accountId ? { accountId: info.accountId } : {}) };
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
if (error instanceof CloudflareAuthError) {
|
|
540
|
+
reporter.warn(error.code, error.message);
|
|
541
|
+
const retry = await promptConfirm({ message: 'Try again?', default: true });
|
|
542
|
+
if (retry === undefined)
|
|
543
|
+
return 'cancel';
|
|
544
|
+
if (!retry)
|
|
545
|
+
return undefined;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function renderCloudflareTokenPermissions(reporter, info, requiredPermissions) {
|
|
553
|
+
reporter.info('');
|
|
554
|
+
if (info.permissionInspection === 'unavailable') {
|
|
555
|
+
reporter.warn('HSX_E_CLOUDFLARE_TOKEN_VERIFY', info.permissionInspectionDetail
|
|
556
|
+
? `Could not inspect the token permission groups. ${info.permissionInspectionDetail}`
|
|
557
|
+
: 'Could not inspect the token permission groups.');
|
|
558
|
+
reporter.info('Expected Cloudflare token permissions:');
|
|
559
|
+
for (const permission of requiredPermissions) {
|
|
560
|
+
reporter.info(` • ${permission.scope} → ${permission.permission} (${permission.permissionGroupName})`);
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
reporter.info('Cloudflare approved these token permission groups:');
|
|
565
|
+
if (info.grantedPermissionGroups.length === 0) {
|
|
566
|
+
reporter.info(' (none reported)');
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
for (const permission of info.grantedPermissionGroups) {
|
|
570
|
+
reporter.info(` ✓ ${permission}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (info.missingPermissionGroups.length === 0) {
|
|
574
|
+
reporter.step('Required permissions present').ok();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
reporter.warn('HSX_E_CLOUDFLARE_TOKEN_VERIFY', 'Token is missing permissions HS-X expects.');
|
|
578
|
+
for (const permission of requiredPermissions) {
|
|
579
|
+
if (!info.missingPermissionGroups.includes(permission.permissionGroupName))
|
|
580
|
+
continue;
|
|
581
|
+
reporter.info(` • Missing ${permission.permissionGroupName} — ${permission.reason} (${permission.phase})`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async function runCloudflareOAuthFlow(input) {
|
|
585
|
+
return Effect.runPromise(Effect.promise(() => runCloudflareOAuthFlowBody(input)).pipe(Effect.withSpan('cli.cloudflare_oauth.flow', {
|
|
586
|
+
attributes: {
|
|
587
|
+
account_id: input.hsxAccountId,
|
|
588
|
+
brokered: Boolean(resolveControlPlaneUrl(input.argv)),
|
|
589
|
+
},
|
|
590
|
+
})));
|
|
591
|
+
}
|
|
592
|
+
async function runCloudflareOAuthFlowBody(input) {
|
|
593
|
+
const { CLOUDFLARE_OAUTH_DEFAULT_PORT, CloudflareOAuthError, buildAuthorizeUrl, exchangeAuthorizationCode, generatePkcePair, generateState, loadCloudflareOAuthConfig, startCallbackListener, } = await import('../cloudflare-oauth.js');
|
|
594
|
+
const { saveCloudflareOAuthCredential } = await import('../cloudflare-oauth-store.js');
|
|
595
|
+
const reporter = createReporter({ command: 'connect cloudflare', argv: input.argv });
|
|
596
|
+
const controlPlaneUrl = resolveControlPlaneUrl(input.argv);
|
|
597
|
+
let config;
|
|
598
|
+
try {
|
|
599
|
+
config = await loadCloudflareOAuthConfig({
|
|
600
|
+
...(controlPlaneUrl ? { controlPlaneUrl } : {}),
|
|
601
|
+
port: Number(resolveFlag(input.argv, '--oauth-port') ?? CLOUDFLARE_OAUTH_DEFAULT_PORT),
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
catch (error) {
|
|
605
|
+
if (error instanceof CloudflareOAuthError) {
|
|
606
|
+
return {
|
|
607
|
+
kind: 'error',
|
|
608
|
+
result: inputValidationError({
|
|
609
|
+
argv: input.argv,
|
|
610
|
+
command: 'connect cloudflare',
|
|
611
|
+
json: input.json,
|
|
612
|
+
code: error.code,
|
|
613
|
+
message: error.message,
|
|
614
|
+
hint: error.detail,
|
|
615
|
+
detail: error.detail,
|
|
616
|
+
}),
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
const pkce = generatePkcePair();
|
|
622
|
+
const state = generateState();
|
|
623
|
+
const authorizeUrl = buildAuthorizeUrl({ config, pkce, state });
|
|
624
|
+
const port = new URL(config.redirectUri).port
|
|
625
|
+
? Number(new URL(config.redirectUri).port)
|
|
626
|
+
: CLOUDFLARE_OAUTH_DEFAULT_PORT;
|
|
627
|
+
const timeoutMs = Number(resolveFlag(input.argv, '--oauth-timeout-ms') ?? '180000');
|
|
628
|
+
const listener = startCallbackListener({ port, expectedState: state, timeoutMs });
|
|
629
|
+
const { openInBrowser } = await import('../hubspot-auth.js');
|
|
630
|
+
reporter.info('Opening Cloudflare to authorize HS-X…');
|
|
631
|
+
reporter.info(`If your browser doesn't open, visit: ${authorizeUrl}`);
|
|
632
|
+
await openInBrowser(authorizeUrl);
|
|
633
|
+
let code;
|
|
634
|
+
try {
|
|
635
|
+
const callback = await listener.result;
|
|
636
|
+
code = callback.code;
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
await listener.close();
|
|
640
|
+
if (error instanceof CloudflareOAuthError) {
|
|
641
|
+
return {
|
|
642
|
+
kind: 'error',
|
|
643
|
+
result: inputValidationError({
|
|
644
|
+
argv: input.argv,
|
|
645
|
+
command: 'connect cloudflare',
|
|
646
|
+
json: input.json,
|
|
647
|
+
code: error.code,
|
|
648
|
+
message: error.message,
|
|
649
|
+
hint: 'Re-run `hs-x connect cloudflare --auth-method oauth` to retry.',
|
|
650
|
+
detail: error.detail,
|
|
651
|
+
}),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
await listener.close();
|
|
657
|
+
// Brokered path: control plane has a client id AND we didn't take the env
|
|
658
|
+
// override. The control plane will exchange the code, store the refresh
|
|
659
|
+
// token, and return the CloudflareConnection in one round trip.
|
|
660
|
+
const envHasOverride = typeof process.env.HSX_CLOUDFLARE_OAUTH_CLIENT_ID === 'string' &&
|
|
661
|
+
process.env.HSX_CLOUDFLARE_OAUTH_CLIENT_ID.length > 0;
|
|
662
|
+
if (controlPlaneUrl && !envHasOverride) {
|
|
663
|
+
try {
|
|
664
|
+
const connection = await postCloudflareOAuthComplete({
|
|
665
|
+
controlPlaneUrl,
|
|
666
|
+
accountId: input.hsxAccountId,
|
|
667
|
+
userId: resolveFlag(input.argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user',
|
|
668
|
+
body: {
|
|
669
|
+
code,
|
|
670
|
+
codeVerifier: pkce.verifier,
|
|
671
|
+
redirectUri: config.redirectUri,
|
|
672
|
+
...(input.displayName ? { displayName: input.displayName } : {}),
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
reporter
|
|
676
|
+
.step('Cloudflare OAuth complete')
|
|
677
|
+
.ok(`account ${connection.cloudflareAccountId} (via control plane)`);
|
|
678
|
+
return { kind: 'success-brokered', connection };
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
682
|
+
return {
|
|
683
|
+
kind: 'error',
|
|
684
|
+
result: inputValidationError({
|
|
685
|
+
argv: input.argv,
|
|
686
|
+
command: 'connect cloudflare',
|
|
687
|
+
json: input.json,
|
|
688
|
+
code: 'HSX_E_CLOUDFLARE_OAUTH_EXCHANGE',
|
|
689
|
+
message: 'Control plane failed to complete the Cloudflare OAuth exchange.',
|
|
690
|
+
hint: 'Re-run `hs-x connect cloudflare --auth-method oauth` to retry.',
|
|
691
|
+
detail,
|
|
692
|
+
}),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// Local/dev path: exchange the code against Cloudflare directly using the
|
|
697
|
+
// env-supplied client_id. Used for sandbox development where the control
|
|
698
|
+
// plane isn't reachable.
|
|
699
|
+
let tokens;
|
|
700
|
+
try {
|
|
701
|
+
tokens = await exchangeAuthorizationCode({ config, code, verifier: pkce.verifier });
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
if (error instanceof CloudflareOAuthError) {
|
|
705
|
+
return {
|
|
706
|
+
kind: 'error',
|
|
707
|
+
result: inputValidationError({
|
|
708
|
+
argv: input.argv,
|
|
709
|
+
command: 'connect cloudflare',
|
|
710
|
+
json: input.json,
|
|
711
|
+
code: error.code,
|
|
712
|
+
message: error.message,
|
|
713
|
+
hint: 'The OAuth code may have expired. Re-run to retry.',
|
|
714
|
+
detail: error.detail,
|
|
715
|
+
}),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
let discoveredAccountId;
|
|
721
|
+
try {
|
|
722
|
+
const { verifyCloudflareToken } = await import('../cloudflare-auth.js');
|
|
723
|
+
const info = await verifyCloudflareToken(tokens.accessToken);
|
|
724
|
+
discoveredAccountId = info.accountId;
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
// verification is optional here
|
|
728
|
+
}
|
|
729
|
+
if (tokens.refreshToken && discoveredAccountId) {
|
|
730
|
+
try {
|
|
731
|
+
await saveCloudflareOAuthCredential(discoveredAccountId, {
|
|
732
|
+
refreshToken: tokens.refreshToken,
|
|
733
|
+
...(tokens.scope ? { scope: tokens.scope } : {}),
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
catch {
|
|
737
|
+
// best-effort
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
reporter
|
|
741
|
+
.step('Cloudflare OAuth complete')
|
|
742
|
+
.ok(discoveredAccountId ? `account ${discoveredAccountId} (dev)` : 'token acquired (dev)');
|
|
743
|
+
return {
|
|
744
|
+
kind: 'success-local',
|
|
745
|
+
accessToken: tokens.accessToken,
|
|
746
|
+
...(discoveredAccountId ? { cloudflareAccountId: discoveredAccountId } : {}),
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
async function postCloudflareOAuthComplete(input) {
|
|
750
|
+
const url = new URL(`/v1/accounts/${encodeURIComponent(input.accountId)}/cloudflare/oauth/complete`, input.controlPlaneUrl);
|
|
751
|
+
const response = await hostedHttp({
|
|
752
|
+
url,
|
|
753
|
+
method: 'POST',
|
|
754
|
+
headers: {
|
|
755
|
+
accept: 'application/json',
|
|
756
|
+
...(await controlPlaneAuthHeaders(input.userId)),
|
|
757
|
+
},
|
|
758
|
+
body: input.body,
|
|
759
|
+
});
|
|
760
|
+
if (!response.ok) {
|
|
761
|
+
const text = await response.text().catch(() => '');
|
|
762
|
+
throw new Error(`HTTP ${response.status}: ${text || response.statusText}`);
|
|
763
|
+
}
|
|
764
|
+
const raw = (await response.json());
|
|
765
|
+
return Schema.decodeUnknownSync(schemas.CloudflareConnection)(raw);
|
|
766
|
+
}
|
|
767
|
+
async function promptHubspotBrowserFlow(argv) {
|
|
768
|
+
const reporter = createReporter({ command: 'connect hubspot', argv });
|
|
769
|
+
reporter.info('No HubSpot accounts found in the HubSpot CLI config.');
|
|
770
|
+
const wantBrowser = await promptConfirm({
|
|
771
|
+
message: 'Open HubSpot in your browser to generate a personal access key?',
|
|
772
|
+
default: true,
|
|
773
|
+
});
|
|
774
|
+
if (!wantBrowser)
|
|
775
|
+
return undefined;
|
|
776
|
+
const { HUBSPOT_PAK_GENERATION_URL, openInBrowser, exchangePak, PakExchangeError } = await import('../hubspot-auth.js');
|
|
777
|
+
reporter.info(`Opening ${HUBSPOT_PAK_GENERATION_URL}`);
|
|
778
|
+
await openInBrowser(HUBSPOT_PAK_GENERATION_URL);
|
|
779
|
+
reporter.info('On the HubSpot page: generate a PAK and copy it.');
|
|
780
|
+
for (;;) {
|
|
781
|
+
const pak = await promptText({
|
|
782
|
+
message: 'Paste your HubSpot personal access key',
|
|
783
|
+
validate: (v) => (v.length >= 20 ? undefined : 'PAK looks too short.'),
|
|
784
|
+
});
|
|
785
|
+
if (!pak)
|
|
786
|
+
return undefined;
|
|
787
|
+
try {
|
|
788
|
+
const exchanged = await exchangePak(pak);
|
|
789
|
+
reporter
|
|
790
|
+
.step('PAK validated')
|
|
791
|
+
.ok(`portal ${exchanged.portalId}${exchanged.hubName ? ` (${exchanged.hubName})` : ''}`);
|
|
792
|
+
return {
|
|
793
|
+
name: exchanged.hubName ?? `portal-${exchanged.portalId}`,
|
|
794
|
+
portalId: exchanged.portalId,
|
|
795
|
+
pak,
|
|
796
|
+
accessToken: exchanged.accessToken,
|
|
797
|
+
expiresAt: exchanged.expiresAt,
|
|
798
|
+
...(exchanged.accountType ? { accountType: exchanged.accountType } : {}),
|
|
799
|
+
...(exchanged.hubName ? { hubName: exchanged.hubName } : {}),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
catch (error) {
|
|
803
|
+
if (error instanceof PakExchangeError) {
|
|
804
|
+
reporter.warn(error.code, error.message);
|
|
805
|
+
const retry = await promptConfirm({ message: 'Try again?', default: true });
|
|
806
|
+
if (!retry)
|
|
807
|
+
return undefined;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
throw error;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
async function defaultHsxAccountId() {
|
|
815
|
+
try {
|
|
816
|
+
const { loadStore } = await import('../account-store.js');
|
|
817
|
+
const store = await loadStore();
|
|
818
|
+
if (store.defaultAccountId && store.accounts[store.defaultAccountId]) {
|
|
819
|
+
return store.defaultAccountId;
|
|
820
|
+
}
|
|
821
|
+
const ids = Object.keys(store.accounts);
|
|
822
|
+
return ids.length === 1 ? ids[0] : undefined;
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
return undefined;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function persistConnectedAccount(input) {
|
|
829
|
+
if (!Number.isFinite(input.hubspotPortalId))
|
|
830
|
+
return;
|
|
831
|
+
try {
|
|
832
|
+
const { addAccount } = await import('../account-store.js');
|
|
833
|
+
await addAccount({
|
|
834
|
+
id: input.id,
|
|
835
|
+
displayName: input.displayName,
|
|
836
|
+
hubspotPortalId: input.hubspotPortalId,
|
|
837
|
+
source: 'connect',
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// best-effort persistence; failures don't fail the connect
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
function defaultStorePathForLog() {
|
|
845
|
+
return join(process.env.HOME ?? '~', '.hsx', 'config.json');
|
|
846
|
+
}
|
|
847
|
+
async function connectCloudflareCommand({ argv, json, }) {
|
|
848
|
+
const accountId = resolveFlag(argv, '--account-id') ??
|
|
849
|
+
process.env.HSX_ACCOUNT_ID ??
|
|
850
|
+
(await defaultHsxAccountId());
|
|
851
|
+
if (!accountId) {
|
|
852
|
+
const reporter = createReporter({ command: 'connect cloudflare', argv });
|
|
853
|
+
const code = 'HSX_E_INPUT_NO_HSX_ACCOUNT';
|
|
854
|
+
reporter.error(code, 'No HS-X account to bind Cloudflare to.', {
|
|
855
|
+
hint: 'Run `hs-x connect` first to create an HS-X account.',
|
|
856
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
857
|
+
});
|
|
858
|
+
reporter.done(undefined, 10);
|
|
859
|
+
return { exitCode: 10 };
|
|
860
|
+
}
|
|
861
|
+
const interactive = !json && isInteractive();
|
|
862
|
+
let cloudflareAccountId = resolveFlag(argv, '--cloudflare-account-id');
|
|
863
|
+
let displayName = resolveFlag(argv, '--display-name');
|
|
864
|
+
const authMethod = resolveFlag(argv, '--auth-method') ?? 'api-token';
|
|
865
|
+
let apiToken = resolveFlag(argv, '--api-token') ?? process.env.HSX_CLOUDFLARE_API_TOKEN;
|
|
866
|
+
if (authMethod === 'oauth') {
|
|
867
|
+
const oauthOutcome = await runCloudflareOAuthFlow({
|
|
868
|
+
argv,
|
|
869
|
+
json,
|
|
870
|
+
hsxAccountId: accountId,
|
|
871
|
+
...(displayName ? { displayName } : {}),
|
|
872
|
+
});
|
|
873
|
+
if (oauthOutcome.kind === 'cancelled') {
|
|
874
|
+
return cancelledResult(argv, 'connect cloudflare');
|
|
875
|
+
}
|
|
876
|
+
if (oauthOutcome.kind === 'error') {
|
|
877
|
+
return oauthOutcome.result;
|
|
878
|
+
}
|
|
879
|
+
if (oauthOutcome.kind === 'success-brokered') {
|
|
880
|
+
// Control plane already exchanged the code and stored the refresh
|
|
881
|
+
// token. We just print the connection and we're done — no separate
|
|
882
|
+
// /cloudflare/connect call needed.
|
|
883
|
+
const result = {
|
|
884
|
+
ok: true,
|
|
885
|
+
command: 'connect cloudflare',
|
|
886
|
+
mode: 'control-plane-oauth-complete',
|
|
887
|
+
accountId,
|
|
888
|
+
connection: oauthOutcome.connection,
|
|
889
|
+
};
|
|
890
|
+
if (json) {
|
|
891
|
+
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
const reporter = createReporter({ command: 'connect cloudflare', argv });
|
|
895
|
+
reporter.header(accountId);
|
|
896
|
+
reporter.block(renderCloudflareConnect(oauthOutcome.connection));
|
|
897
|
+
reporter.done('Connected');
|
|
898
|
+
}
|
|
899
|
+
return { exitCode: 0 };
|
|
900
|
+
}
|
|
901
|
+
// success-local: dev path — use access token as if it were an API token,
|
|
902
|
+
// then go through the existing connect call below.
|
|
903
|
+
apiToken = oauthOutcome.accessToken;
|
|
904
|
+
if (!cloudflareAccountId && oauthOutcome.cloudflareAccountId) {
|
|
905
|
+
cloudflareAccountId = oauthOutcome.cloudflareAccountId;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
else if (interactive && authMethod === 'api-token' && !apiToken) {
|
|
909
|
+
const browserResult = await promptCloudflareBrowserFlow(argv);
|
|
910
|
+
if (browserResult === 'cancel')
|
|
911
|
+
return cancelledResult(argv, 'connect cloudflare');
|
|
912
|
+
if (browserResult) {
|
|
913
|
+
apiToken = browserResult.token;
|
|
914
|
+
if (!cloudflareAccountId && browserResult.accountId) {
|
|
915
|
+
cloudflareAccountId = browserResult.accountId;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (interactive) {
|
|
920
|
+
if (!cloudflareAccountId) {
|
|
921
|
+
const answer = await promptText({
|
|
922
|
+
message: 'Cloudflare account id',
|
|
923
|
+
validate: (v) => /^[a-f0-9]{16,}$/i.test(v)
|
|
924
|
+
? undefined
|
|
925
|
+
: 'Looks like a Cloudflare account id is 32 hex chars.',
|
|
926
|
+
});
|
|
927
|
+
if (answer === undefined)
|
|
928
|
+
return cancelledResult(argv, 'connect cloudflare');
|
|
929
|
+
cloudflareAccountId = answer;
|
|
930
|
+
}
|
|
931
|
+
if (!displayName) {
|
|
932
|
+
const answer = await promptText({
|
|
933
|
+
message: 'Display name for this Cloudflare account',
|
|
934
|
+
default: `Cloudflare (${accountId})`,
|
|
935
|
+
});
|
|
936
|
+
if (answer === undefined)
|
|
937
|
+
return cancelledResult(argv, 'connect cloudflare');
|
|
938
|
+
displayName = answer;
|
|
939
|
+
}
|
|
940
|
+
if (authMethod === 'api-token' && !apiToken) {
|
|
941
|
+
const answer = await promptText({
|
|
942
|
+
message: 'Cloudflare API token',
|
|
943
|
+
validate: (v) => (v.length >= 20 ? undefined : 'Token looks too short.'),
|
|
944
|
+
});
|
|
945
|
+
if (answer === undefined)
|
|
946
|
+
return cancelledResult(argv, 'connect cloudflare');
|
|
947
|
+
apiToken = answer;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (authMethod === 'api-token' && !apiToken) {
|
|
951
|
+
const reporter = createReporter({ command: 'connect cloudflare', argv });
|
|
952
|
+
const code = 'HSX_E_INPUT_MISSING_API_TOKEN';
|
|
953
|
+
reporter.error(code, 'Cloudflare API token is required.', {
|
|
954
|
+
hint: 'Pass --api-token, set HSX_CLOUDFLARE_API_TOKEN, or run interactively.',
|
|
955
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
956
|
+
});
|
|
957
|
+
reporter.done(undefined, 10);
|
|
958
|
+
return { exitCode: 10 };
|
|
959
|
+
}
|
|
960
|
+
const request = Schema.decodeUnknownSync(schemas.CloudflareConnectRequest)({
|
|
961
|
+
cloudflareAccountId,
|
|
962
|
+
displayName,
|
|
963
|
+
authMethod,
|
|
964
|
+
...(apiToken ? { apiToken } : {}),
|
|
965
|
+
});
|
|
966
|
+
const safeRequest = {
|
|
967
|
+
cloudflareAccountId: request.cloudflareAccountId,
|
|
968
|
+
displayName: request.displayName,
|
|
969
|
+
authMethod: request.authMethod,
|
|
970
|
+
};
|
|
971
|
+
const controlPlaneUrl = resolveFlag(argv, '--control-plane-url') ?? process.env.HSX_CONTROL_PLANE_URL;
|
|
972
|
+
const userId = resolveFlag(argv, '--user-id') ?? process.env.HSX_USER_ID ?? 'local-cli-user';
|
|
973
|
+
const connection = controlPlaneUrl
|
|
974
|
+
? await requestHostedControlPlaneCloudflareConnect({
|
|
975
|
+
accountId,
|
|
976
|
+
request,
|
|
977
|
+
controlPlaneUrl,
|
|
978
|
+
userId,
|
|
979
|
+
})
|
|
980
|
+
: argv.includes('--local-control-plane')
|
|
981
|
+
? await requestLocalControlPlaneCloudflareConnect({ accountId, request, userId })
|
|
982
|
+
: undefined;
|
|
983
|
+
if (!connection) {
|
|
984
|
+
const result = {
|
|
985
|
+
ok: true,
|
|
986
|
+
command: 'connect cloudflare',
|
|
987
|
+
mode: 'request',
|
|
988
|
+
accountId,
|
|
989
|
+
request: safeRequest,
|
|
990
|
+
nextStep: 'Pass --control-plane-url to connect through the control plane or --local-control-plane to exercise the local contract handler.',
|
|
991
|
+
};
|
|
992
|
+
write(json
|
|
993
|
+
? `${JSON.stringify(result, null, 2)}\n`
|
|
994
|
+
: `Prepared Cloudflare connect request for ${request.displayName}. Hosted transport is not wired yet.\n`);
|
|
995
|
+
return { exitCode: 0 };
|
|
996
|
+
}
|
|
997
|
+
const result = {
|
|
998
|
+
ok: true,
|
|
999
|
+
command: 'connect cloudflare',
|
|
1000
|
+
mode: controlPlaneUrl ? 'control-plane-connect' : 'local-control-plane-connect',
|
|
1001
|
+
accountId,
|
|
1002
|
+
request: safeRequest,
|
|
1003
|
+
connection,
|
|
1004
|
+
};
|
|
1005
|
+
if (json) {
|
|
1006
|
+
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1007
|
+
}
|
|
1008
|
+
else {
|
|
1009
|
+
const reporter = createReporter({ command: 'connect cloudflare', argv });
|
|
1010
|
+
reporter.header(accountId);
|
|
1011
|
+
reporter.block(renderCloudflareConnect(connection));
|
|
1012
|
+
reporter.done('Connected');
|
|
1013
|
+
}
|
|
1014
|
+
return { exitCode: 0 };
|
|
1015
|
+
}
|
|
1016
|
+
async function requestLocalControlPlaneHubSpotConnect({ accountId, request, }) {
|
|
1017
|
+
const controlPlane = (await loadCreateControlPlane())({
|
|
1018
|
+
now: () => new Date('2026-05-18T14:00:00.000Z'),
|
|
1019
|
+
});
|
|
1020
|
+
const response = await controlPlane.fetch(new Request(`https://api.hs-x.dev/v1/accounts/${accountId}/hubspot/connect`, {
|
|
1021
|
+
method: 'POST',
|
|
1022
|
+
headers: {
|
|
1023
|
+
'content-type': 'application/json',
|
|
1024
|
+
...(await controlPlaneAuthHeaders('local-cli-user')),
|
|
1025
|
+
},
|
|
1026
|
+
body: JSON.stringify(request),
|
|
1027
|
+
}));
|
|
1028
|
+
const body = await response.json();
|
|
1029
|
+
if (!response.ok) {
|
|
1030
|
+
throw new Error(isRecord(body) && typeof body.message === 'string'
|
|
1031
|
+
? body.message
|
|
1032
|
+
: `Local control-plane HubSpot connect failed with status ${response.status}`);
|
|
1033
|
+
}
|
|
1034
|
+
return Schema.decodeUnknownSync(schemas.HubSpotDeveloperConnection)(body);
|
|
1035
|
+
}
|
|
1036
|
+
async function requestHostedControlPlaneHubSpotConnect({ accountId, request, controlPlaneUrl, userId, }) {
|
|
1037
|
+
const response = await hostedHttp({
|
|
1038
|
+
url: new URL(`/v1/accounts/${encodeURIComponent(accountId)}/hubspot/connect`, controlPlaneUrl),
|
|
1039
|
+
method: 'POST',
|
|
1040
|
+
headers: await controlPlaneAuthHeaders(userId),
|
|
1041
|
+
body: request,
|
|
1042
|
+
});
|
|
1043
|
+
const body = await response.json();
|
|
1044
|
+
if (!response.ok) {
|
|
1045
|
+
throw new Error(isRecord(body) && typeof body.message === 'string'
|
|
1046
|
+
? body.message
|
|
1047
|
+
: `Control-plane HubSpot connect failed with status ${response.status}`);
|
|
1048
|
+
}
|
|
1049
|
+
return Schema.decodeUnknownSync(schemas.HubSpotDeveloperConnection)(body);
|
|
1050
|
+
}
|
|
1051
|
+
async function requestLocalControlPlaneCloudflareConnect({ accountId, request, userId, }) {
|
|
1052
|
+
const controlPlane = (await loadCreateControlPlane())({
|
|
1053
|
+
now: () => new Date('2026-05-18T14:00:00.000Z'),
|
|
1054
|
+
});
|
|
1055
|
+
const response = await controlPlane.fetch(new Request(`https://api.hs-x.dev/v1/accounts/${accountId}/cloudflare/connect`, {
|
|
1056
|
+
method: 'POST',
|
|
1057
|
+
headers: {
|
|
1058
|
+
'content-type': 'application/json',
|
|
1059
|
+
...(await controlPlaneAuthHeaders(userId)),
|
|
1060
|
+
},
|
|
1061
|
+
body: JSON.stringify(request),
|
|
1062
|
+
}));
|
|
1063
|
+
const body = await response.json();
|
|
1064
|
+
if (!response.ok) {
|
|
1065
|
+
throw new Error(isRecord(body) && typeof body.message === 'string'
|
|
1066
|
+
? body.message
|
|
1067
|
+
: `Local control-plane Cloudflare connect failed with status ${response.status}`);
|
|
1068
|
+
}
|
|
1069
|
+
return Schema.decodeUnknownSync(schemas.CloudflareConnection)(body);
|
|
1070
|
+
}
|
|
1071
|
+
async function requestHostedControlPlaneCloudflareConnect({ accountId, request, controlPlaneUrl, userId, }) {
|
|
1072
|
+
const response = await hostedHttp({
|
|
1073
|
+
url: new URL(`/v1/accounts/${encodeURIComponent(accountId)}/cloudflare/connect`, controlPlaneUrl),
|
|
1074
|
+
method: 'POST',
|
|
1075
|
+
headers: await controlPlaneAuthHeaders(userId),
|
|
1076
|
+
body: request,
|
|
1077
|
+
});
|
|
1078
|
+
const body = await response.json();
|
|
1079
|
+
if (!response.ok) {
|
|
1080
|
+
throw new Error(isRecord(body) && typeof body.message === 'string'
|
|
1081
|
+
? body.message
|
|
1082
|
+
: `Control-plane Cloudflare connect failed with status ${response.status}`);
|
|
1083
|
+
}
|
|
1084
|
+
return Schema.decodeUnknownSync(schemas.CloudflareConnection)(body);
|
|
1085
|
+
}
|
|
1086
|
+
function renderCloudflareConnect(connection) {
|
|
1087
|
+
const siblingLine = connection.visibleSiblingAccountIds && connection.visibleSiblingAccountIds.length > 0
|
|
1088
|
+
? `\nThis Cloudflare account is also connected to HS-X account(s) ${connection.visibleSiblingAccountIds.join(', ')}. Resources will be namespaced by HS-X account id; Cloudflare quota is shared.\n`
|
|
1089
|
+
: '\n';
|
|
1090
|
+
return `Connected Cloudflare account ${connection.displayName} for ${connection.accountId}${siblingLine}`;
|
|
1091
|
+
}
|
|
1092
|
+
function writeDiagnostics(diagnostics) {
|
|
1093
|
+
for (const diagnostic of diagnostics) {
|
|
1094
|
+
const location = diagnostic.file
|
|
1095
|
+
? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}` : ''}`
|
|
1096
|
+
: 'project';
|
|
1097
|
+
write(`${diagnostic.severity.toUpperCase()} ${diagnostic.code} ${location} ${diagnostic.message}\n`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
// A flag value is valid if it exists and doesn't look like another flag.
|
|
1101
|
+
// `--account-id --json` must NOT yield `--json` as the account id; the user
|
|
1102
|
+
// forgot the value. `--flag=` (explicit empty) is also rejected — treat it
|
|
1103
|
+
function isFlagValue(value) {
|
|
1104
|
+
return value !== undefined && value.length > 0 && !value.startsWith('-');
|
|
1105
|
+
}
|
|
1106
|
+
function resolveFlag(argv, flag) {
|
|
1107
|
+
const index = argv.indexOf(flag);
|
|
1108
|
+
if (index !== -1) {
|
|
1109
|
+
const next = argv[index + 1];
|
|
1110
|
+
if (isFlagValue(next))
|
|
1111
|
+
return next;
|
|
1112
|
+
}
|
|
1113
|
+
const prefix = `${flag}=`;
|
|
1114
|
+
const found = argv.find((arg) => arg.startsWith(prefix));
|
|
1115
|
+
if (!found)
|
|
1116
|
+
return undefined;
|
|
1117
|
+
const value = found.slice(prefix.length);
|
|
1118
|
+
return value.length > 0 ? value : undefined;
|
|
1119
|
+
}
|
|
1120
|
+
function write(message) {
|
|
1121
|
+
process.stdout.write(message);
|
|
1122
|
+
}
|
|
1123
|
+
//# sourceMappingURL=connect.js.map
|