@switchboard.spot/cli 0.2.3 → 0.2.5
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 +20 -9
- package/bin/switchboard.js +1 -17
- package/lib/client.js +0 -3
- package/lib/commands/account.js +0 -9
- package/lib/commands/auth.js +16 -124
- package/lib/commands/billing.js +7 -69
- package/lib/commands/doctor.js +10 -6
- package/lib/commands/env.js +31 -245
- package/lib/commands/projects.js +83 -63
- package/lib/commands/setup.js +90 -85
- package/lib/commands/usage.js +0 -23
- package/lib/commands/verify.js +4 -0
- package/lib/config.js +2 -3
- package/lib/credentialStore.js +0 -134
- package/lib/docsClient.js +1 -51
- package/lib/mcpServer.js +0 -21
- package/lib/verify/index.js +62 -10
- package/package.json +2 -2
- package/lib/commands/endUsers.js +0 -51
- package/lib/commands/init.js +0 -78
- package/lib/commands/integration.js +0 -38
- package/lib/commands/keys.js +0 -106
- package/lib/commands/launch.js +0 -213
- package/lib/commands/org.js +0 -55
- package/lib/commands/workspaces.js +0 -92
package/lib/verify/index.js
CHANGED
|
@@ -48,7 +48,6 @@ const CHECK_TYPE_TO_ID = new Map([
|
|
|
48
48
|
]);
|
|
49
49
|
|
|
50
50
|
const SERVER_SECRET_PATTERNS = [
|
|
51
|
-
{ name: "SWITCHBOARD_API_KEY", pattern: /\bSWITCHBOARD_API_KEY\b/ },
|
|
52
51
|
{ name: "switchboard live key", pattern: /\bsb_live_[A-Za-z0-9_-]{6,}\b/ },
|
|
53
52
|
{ name: "switchboard test key", pattern: /\bsb_test_[A-Za-z0-9_-]{6,}\b/ },
|
|
54
53
|
{ name: "account session token", pattern: /\bsb_sess_[A-Za-z0-9_-]{6,}\b/ },
|
|
@@ -125,7 +124,7 @@ export async function runVerification(options = {}) {
|
|
|
125
124
|
const scenario = validateScenario(options.scenario ?? loadScenario(options.scenarioPath));
|
|
126
125
|
const report = createReport(mode);
|
|
127
126
|
const artifactsDir = options.artifactsDir ?? DEFAULT_ARTIFACTS_DIR;
|
|
128
|
-
const appUrl = parseRequiredUrl(options.url, "--url");
|
|
127
|
+
const appUrl = parseRequiredUrl(options.appUrl ?? options.url, options.appUrl ? "--app-url" : "--url");
|
|
129
128
|
const clientUrl = options.clientUrl ? parseRequiredUrl(options.clientUrl, "--client-url") : null;
|
|
130
129
|
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
131
130
|
let productionOrigin = options.productionOrigin;
|
|
@@ -194,7 +193,13 @@ export function createReport(mode) {
|
|
|
194
193
|
}
|
|
195
194
|
|
|
196
195
|
export function evaluateEvidence(report, evidence) {
|
|
197
|
-
const consoleErrors = evidence.consoleMessages.filter(
|
|
196
|
+
const consoleErrors = evidence.consoleMessages.filter(
|
|
197
|
+
(message) => message.type === "error" && !allowedSandboxGuardConsoleError(message, evidence),
|
|
198
|
+
);
|
|
199
|
+
const failedRequests = evidence.failedRequests.filter(
|
|
200
|
+
(request) => !allowedSandboxGuardUrls(evidence).has(request.url),
|
|
201
|
+
);
|
|
202
|
+
|
|
198
203
|
addCheck(report, {
|
|
199
204
|
id: CHECK_IDS.NO_CONSOLE_ERRORS,
|
|
200
205
|
status: consoleErrors.length === 0 ? "passed" : "failed",
|
|
@@ -206,11 +211,11 @@ export function evaluateEvidence(report, evidence) {
|
|
|
206
211
|
|
|
207
212
|
addCheck(report, {
|
|
208
213
|
id: CHECK_IDS.NO_FAILED_REQUESTS,
|
|
209
|
-
status:
|
|
214
|
+
status: failedRequests.length === 0 ? "passed" : "failed",
|
|
210
215
|
message:
|
|
211
|
-
|
|
216
|
+
failedRequests.length === 0
|
|
212
217
|
? "No failed browser requests were observed."
|
|
213
|
-
: `${
|
|
218
|
+
: `${failedRequests.length} failed browser request(s) were observed.`,
|
|
214
219
|
});
|
|
215
220
|
|
|
216
221
|
const authLeak = browserAuthorizationFinding(evidence.requests);
|
|
@@ -456,10 +461,16 @@ async function executeScenarioCheck({
|
|
|
456
461
|
messages: [{ role: "user", content: prompt ?? check.prompt }],
|
|
457
462
|
},
|
|
458
463
|
});
|
|
464
|
+
const sandboxGuarded = isSandboxCompletionGuard(result);
|
|
465
|
+
if (sandboxGuarded) evidence.allowedSandboxGuardUrls.add(result.url);
|
|
459
466
|
addCheck(report, {
|
|
460
467
|
id: CHECK_IDS.CHAT_COMPLETION_SUCCEEDS,
|
|
461
|
-
status: result.ok ? "passed" : "failed",
|
|
462
|
-
message: result.ok
|
|
468
|
+
status: result.ok || sandboxGuarded ? "passed" : "failed",
|
|
469
|
+
message: result.ok
|
|
470
|
+
? "Chat completion succeeded through the client gateway."
|
|
471
|
+
: sandboxGuarded
|
|
472
|
+
? "Sandbox chat reached the client gateway and was blocked before any real AI completion."
|
|
473
|
+
: result.error,
|
|
463
474
|
});
|
|
464
475
|
return;
|
|
465
476
|
}
|
|
@@ -505,11 +516,12 @@ async function browserFetchJson(page, clientUrl, endpointPath, init) {
|
|
|
505
516
|
return {
|
|
506
517
|
ok: response.ok,
|
|
507
518
|
status: response.status,
|
|
519
|
+
url: endpointUrl,
|
|
508
520
|
data,
|
|
509
521
|
error: response.ok ? null : `HTTP ${response.status}`,
|
|
510
522
|
};
|
|
511
523
|
} catch (error) {
|
|
512
|
-
return { ok: false, status: 0, data: null, error: error.message };
|
|
524
|
+
return { ok: false, status: 0, url: endpointUrl, data: null, error: error.message };
|
|
513
525
|
}
|
|
514
526
|
},
|
|
515
527
|
{ endpointUrl, init },
|
|
@@ -524,6 +536,32 @@ function clientEndpointUrl(clientUrl, endpointPath) {
|
|
|
524
536
|
return url.href;
|
|
525
537
|
}
|
|
526
538
|
|
|
539
|
+
function isSandboxCompletionGuard(result) {
|
|
540
|
+
const error = result?.data?.error;
|
|
541
|
+
|
|
542
|
+
return (
|
|
543
|
+
result?.status === 402 &&
|
|
544
|
+
error?.type === "sandbox_completion_requires_prepaid_balance" &&
|
|
545
|
+
typeof error.message === "string" &&
|
|
546
|
+
error.message.includes("Sandbox mode does not run real AI completions") &&
|
|
547
|
+
error.message.includes("No provider was called") &&
|
|
548
|
+
error.message.includes("funded prepaid project balance") &&
|
|
549
|
+
error.message.includes("live mode")
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function allowedSandboxGuardConsoleError(message, evidence) {
|
|
554
|
+
return (
|
|
555
|
+
allowedSandboxGuardUrls(evidence).size > 0 &&
|
|
556
|
+
/Failed to load resource/i.test(message.text ?? "") &&
|
|
557
|
+
/\b402\b/.test(message.text ?? "")
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function allowedSandboxGuardUrls(evidence) {
|
|
562
|
+
return evidence.allowedSandboxGuardUrls ?? new Set();
|
|
563
|
+
}
|
|
564
|
+
|
|
527
565
|
function wireEvidence(page, evidence) {
|
|
528
566
|
page.on("console", (message) => {
|
|
529
567
|
evidence.consoleMessages.push({
|
|
@@ -593,6 +631,7 @@ function emptyEvidence() {
|
|
|
593
631
|
return {
|
|
594
632
|
consoleMessages: [],
|
|
595
633
|
failedRequests: [],
|
|
634
|
+
allowedSandboxGuardUrls: new Set(),
|
|
596
635
|
requests: [],
|
|
597
636
|
responses: [],
|
|
598
637
|
texts: [],
|
|
@@ -723,6 +762,7 @@ function normalizedHostname(url) {
|
|
|
723
762
|
|
|
724
763
|
function browserChallengeTokenFor(check, clientUrl) {
|
|
725
764
|
if (Object.prototype.hasOwnProperty.call(check, "browserChallengeToken")) {
|
|
765
|
+
validateBrowserChallengeToken(check.browserChallengeToken, clientUrl);
|
|
726
766
|
return check.browserChallengeToken;
|
|
727
767
|
}
|
|
728
768
|
|
|
@@ -730,13 +770,25 @@ function browserChallengeTokenFor(check, clientUrl) {
|
|
|
730
770
|
return DEV_BROWSER_CHALLENGE_TOKEN;
|
|
731
771
|
}
|
|
732
772
|
|
|
733
|
-
|
|
773
|
+
throw new VerificationInputError(
|
|
774
|
+
"Hosted Switchboard verification cannot submit synthetic browser challenge tokens. Run the SDK-managed browser challenge in the real app, or target a localhost Switchboard URL for dev verification.",
|
|
775
|
+
);
|
|
734
776
|
}
|
|
735
777
|
|
|
736
778
|
function isLocalSwitchboardUrl(url) {
|
|
737
779
|
return url instanceof URL && LOCAL_HOSTS.has(normalizedHostname(url));
|
|
738
780
|
}
|
|
739
781
|
|
|
782
|
+
export function validateBrowserChallengeToken(token, clientUrl) {
|
|
783
|
+
if (isLocalSwitchboardUrl(clientUrl)) return;
|
|
784
|
+
|
|
785
|
+
if (token === DEV_BROWSER_CHALLENGE_TOKEN || token === DEFAULT_BROWSER_CHALLENGE_TOKEN) {
|
|
786
|
+
throw new VerificationInputError(
|
|
787
|
+
"Synthetic browser challenge tokens are allowed only against localhost Switchboard URLs.",
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
740
792
|
function addFailure(report, checkId, message, finding) {
|
|
741
793
|
addCheck(report, { id: checkId, status: "failed", message });
|
|
742
794
|
report.findings.push({
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchboard.spot/cli",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Switchboard CLI
|
|
3
|
+
"version": "0.2.5",
|
|
4
|
+
"description": "Switchboard CLI for account, project, gateway, and docs automation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"switchboard": "bin/switchboard.js"
|
package/lib/commands/endUsers.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* End-user management commands.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { accountRequest } from "../client.js";
|
|
6
|
-
import { emit, globalFlags, printList } from "../output.js";
|
|
7
|
-
|
|
8
|
-
export function registerEndUsersCommands(program) {
|
|
9
|
-
const endUsers = program
|
|
10
|
-
.command("end-users")
|
|
11
|
-
.description("Anonymous Client Gateway end users");
|
|
12
|
-
|
|
13
|
-
endUsers
|
|
14
|
-
.command("list")
|
|
15
|
-
.description("List end users")
|
|
16
|
-
.action(async (_opts, cmd) => {
|
|
17
|
-
const flags = globalFlags(cmd);
|
|
18
|
-
const { data } = await accountRequest("GET", "/end_users", { json: flags.json });
|
|
19
|
-
if (flags.json) {
|
|
20
|
-
emit(data, flags);
|
|
21
|
-
} else {
|
|
22
|
-
printList("End users:", data.data || [], (u) =>
|
|
23
|
-
` ${u.id} ${u.reference} — ${u.billing_status}`,
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
endUsers
|
|
29
|
-
.command("block <id>")
|
|
30
|
-
.description("Block an end user")
|
|
31
|
-
.action(async (id, _opts, cmd) => {
|
|
32
|
-
const flags = globalFlags(cmd);
|
|
33
|
-
const { data } = await accountRequest("PATCH", `/end_users/${id}`, {
|
|
34
|
-
body: { billing_status: "blocked" },
|
|
35
|
-
json: flags.json,
|
|
36
|
-
});
|
|
37
|
-
emit(flags.json ? data : `Blocked ${data.reference}`, flags);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
endUsers
|
|
41
|
-
.command("unblock <id>")
|
|
42
|
-
.description("Unblock an end user")
|
|
43
|
-
.action(async (id, _opts, cmd) => {
|
|
44
|
-
const flags = globalFlags(cmd);
|
|
45
|
-
const { data } = await accountRequest("PATCH", `/end_users/${id}`, {
|
|
46
|
-
body: { billing_status: "active" },
|
|
47
|
-
json: flags.json,
|
|
48
|
-
});
|
|
49
|
-
emit(flags.json ? data : `Unblocked ${data.reference}`, flags);
|
|
50
|
-
});
|
|
51
|
-
}
|
package/lib/commands/init.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project init and agent manifest commands.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import fs from "fs";
|
|
6
|
-
import path from "path";
|
|
7
|
-
import { resolveConfig, gatewayApiUrl } from "../config.js";
|
|
8
|
-
import { emit } from "../output.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Builds default frontend environment placeholders for Client Gateway apps.
|
|
12
|
-
*
|
|
13
|
-
* The generated Client Gateway URL is the only value a browser app needs by default.
|
|
14
|
-
*/
|
|
15
|
-
function envBlock(clientUrl) {
|
|
16
|
-
return `SWITCHBOARD_CLIENT_URL=${clientUrl}
|
|
17
|
-
VITE_SWITCHBOARD_CLIENT_URL=${clientUrl}
|
|
18
|
-
`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Builds the agent manifest for automated setup.
|
|
23
|
-
*/
|
|
24
|
-
export function agentManifest(config) {
|
|
25
|
-
const base = gatewayApiUrl(config);
|
|
26
|
-
const clientUrl = `${config.baseUrl.replace(/\/$/, "")}/m/<client-gateway-slug>/v1`;
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
base_url: base,
|
|
30
|
-
client_url: clientUrl,
|
|
31
|
-
virtual_microservice_url: clientUrl,
|
|
32
|
-
account_api: `${config.baseUrl.replace(/\/$/, "")}/v1/account`,
|
|
33
|
-
env: envBlock(clientUrl),
|
|
34
|
-
checklist: [
|
|
35
|
-
"switchboard setup project --origin <origin> --json",
|
|
36
|
-
"export VITE_SWITCHBOARD_CLIENT_URL=<client_url from integration kit>",
|
|
37
|
-
"Use mountSwitchboardWidget({ clientUrl, target }) in browser apps",
|
|
38
|
-
"Use createSwitchboardClient({ clientUrl, storage }) only for custom UI",
|
|
39
|
-
"switchboard verify setup",
|
|
40
|
-
"switchboard launch prepare --production-origin https://app.example.com --end-user-terms-url https://app.example.com/terms --end-user-privacy-url https://app.example.com/privacy --support-email support@example.com --contact-email owner@example.com --use-case \"Browser chat for signed-in customers\"",
|
|
41
|
-
"switchboard verify publish",
|
|
42
|
-
],
|
|
43
|
-
browser_smoke_test: `import { mountSwitchboardWidget } from "@switchboard.spot/sdk";
|
|
44
|
-
|
|
45
|
-
mountSwitchboardWidget({
|
|
46
|
-
clientUrl: import.meta.env.VITE_SWITCHBOARD_CLIENT_URL ?? "${clientUrl}",
|
|
47
|
-
target: "#switchboard",
|
|
48
|
-
});`,
|
|
49
|
-
automation_note:
|
|
50
|
-
"Client Gateway auth is browser/mobile only and requires a real browser challenge. Use account/CLI APIs only for project configuration automation.",
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function registerInitCommand(program) {
|
|
55
|
-
program
|
|
56
|
-
.command("init")
|
|
57
|
-
.description("Write .env.local with Switchboard placeholders")
|
|
58
|
-
.option("--agent", "Print agent manifest JSON")
|
|
59
|
-
.action((opts, cmd) => {
|
|
60
|
-
const flags = cmd.opts();
|
|
61
|
-
const config = resolveConfig();
|
|
62
|
-
const clientUrl = `${config.baseUrl.replace(/\/$/, "")}/m/<client-gateway-slug>/v1`;
|
|
63
|
-
const target = path.join(process.cwd(), ".env.local");
|
|
64
|
-
|
|
65
|
-
if (!fs.existsSync(target)) {
|
|
66
|
-
fs.writeFileSync(target, envBlock(clientUrl));
|
|
67
|
-
if (!flags.quiet && !flags.json) {
|
|
68
|
-
console.log(`Wrote ${target}`);
|
|
69
|
-
}
|
|
70
|
-
} else if (!flags.quiet && !flags.json) {
|
|
71
|
-
console.log(`${target} already exists`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (opts.agent || flags.json) {
|
|
75
|
-
emit(agentManifest(config), { json: true });
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration kit command.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { accountRequest } from "../client.js";
|
|
6
|
-
import { emit, globalFlags } from "../output.js";
|
|
7
|
-
|
|
8
|
-
export function registerIntegrationCommands(program) {
|
|
9
|
-
const integrations = program.command("integrations").description("Integration helpers");
|
|
10
|
-
|
|
11
|
-
integrations
|
|
12
|
-
.command("show")
|
|
13
|
-
.description("Fetch integration setup JSON")
|
|
14
|
-
.option("--stack <stack>", "node or python", "node")
|
|
15
|
-
.action(async (opts, cmd) => {
|
|
16
|
-
await showIntegration(opts, cmd);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const integration = program.command("integration").description("Legacy integration helpers");
|
|
20
|
-
|
|
21
|
-
integration
|
|
22
|
-
.command("kit")
|
|
23
|
-
.description("Legacy alias for integrations show")
|
|
24
|
-
.option("--stack <stack>", "node or python", "node")
|
|
25
|
-
.action(async (opts, cmd) => {
|
|
26
|
-
await showIntegration(opts, cmd);
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function showIntegration(opts, cmd) {
|
|
31
|
-
const flags = globalFlags(cmd);
|
|
32
|
-
const { data } = await accountRequest(
|
|
33
|
-
"GET",
|
|
34
|
-
`/integration_kit?stack=${opts.stack}`,
|
|
35
|
-
{ json: flags.json },
|
|
36
|
-
);
|
|
37
|
-
emit(data, flags);
|
|
38
|
-
}
|
package/lib/commands/keys.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API key management commands.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { accountRequest } from "../client.js";
|
|
6
|
-
import { saveConfig } from "../config.js";
|
|
7
|
-
import { emit, globalFlags, printList } from "../output.js";
|
|
8
|
-
|
|
9
|
-
function saveProjectContext(data) {
|
|
10
|
-
const updates = {};
|
|
11
|
-
|
|
12
|
-
if (data.project_id != null) {
|
|
13
|
-
updates.projectId = String(data.project_id);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
updates.apiKey = null;
|
|
17
|
-
|
|
18
|
-
if (Object.keys(updates).length > 0) {
|
|
19
|
-
saveConfig(updates);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function registerKeysCommands(program) {
|
|
24
|
-
const keys = program.command("keys").description("Project API keys");
|
|
25
|
-
|
|
26
|
-
keys
|
|
27
|
-
.command("list")
|
|
28
|
-
.description("List API keys for the selected project")
|
|
29
|
-
.action(async (_opts, cmd) => {
|
|
30
|
-
const flags = globalFlags(cmd);
|
|
31
|
-
const { data } = await accountRequest("GET", "/keys", { json: flags.json });
|
|
32
|
-
if (flags.json) {
|
|
33
|
-
emit(data, flags);
|
|
34
|
-
} else {
|
|
35
|
-
printList("API keys:", data.data || [], (k) =>
|
|
36
|
-
` ${k.id} ${k.mode} ${k.key_type || "secret"} ${k.name} …${k.last_four}${k.revoked_at ? " (revoked)" : ""}`,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
keys
|
|
42
|
-
.command("create")
|
|
43
|
-
.description("Create a new API key")
|
|
44
|
-
.option("--mode <mode>", "sandbox or live", "sandbox")
|
|
45
|
-
.option("--name <name>", "Key label")
|
|
46
|
-
.action(async (opts, cmd) => {
|
|
47
|
-
const flags = globalFlags(cmd);
|
|
48
|
-
const body = { mode: opts.mode, name: opts.name };
|
|
49
|
-
|
|
50
|
-
const { data } = await accountRequest("POST", "/keys", {
|
|
51
|
-
body,
|
|
52
|
-
json: flags.json,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
saveProjectContext(data);
|
|
56
|
-
emit(
|
|
57
|
-
flags.json
|
|
58
|
-
? data
|
|
59
|
-
: `Created ${data.mode} key. Plaintext (shown once): ${data.plaintext}`,
|
|
60
|
-
flags,
|
|
61
|
-
);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
keys
|
|
65
|
-
.command("regenerate-sandbox")
|
|
66
|
-
.description("Revoke sandbox secret keys and create a new one")
|
|
67
|
-
.action(async (_opts, cmd) => {
|
|
68
|
-
const flags = globalFlags(cmd);
|
|
69
|
-
const { data } = await accountRequest("POST", "/keys/sandbox/regenerate", {
|
|
70
|
-
json: flags.json,
|
|
71
|
-
});
|
|
72
|
-
saveProjectContext(data);
|
|
73
|
-
emit(
|
|
74
|
-
flags.json
|
|
75
|
-
? data
|
|
76
|
-
: `New sandbox key: ${data.plaintext}`,
|
|
77
|
-
flags,
|
|
78
|
-
);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
keys
|
|
82
|
-
.command("revoke <id>")
|
|
83
|
-
.description("Revoke an API key")
|
|
84
|
-
.action(async (id, _opts, cmd) => {
|
|
85
|
-
const flags = globalFlags(cmd);
|
|
86
|
-
const { data } = await accountRequest("POST", `/keys/${id}/revoke`, {
|
|
87
|
-
json: flags.json,
|
|
88
|
-
});
|
|
89
|
-
emit(flags.json ? data : `Revoked key ${id}`, flags);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
keys
|
|
93
|
-
.command("rotate <id>")
|
|
94
|
-
.description("Rotate an API key")
|
|
95
|
-
.action(async (id, _opts, cmd) => {
|
|
96
|
-
const flags = globalFlags(cmd);
|
|
97
|
-
const { data } = await accountRequest("POST", `/keys/${id}/rotate`, {
|
|
98
|
-
json: flags.json,
|
|
99
|
-
});
|
|
100
|
-
saveProjectContext(data);
|
|
101
|
-
emit(
|
|
102
|
-
flags.json ? data : `Rotated key ${id}. New plaintext: ${data.plaintext}`,
|
|
103
|
-
flags,
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
}
|
package/lib/commands/launch.js
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Production launch orchestration commands.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { accountRequest } from "../client.js";
|
|
6
|
-
import { saveConfig } from "../config.js";
|
|
7
|
-
import { emit, fail, globalFlags } from "../output.js";
|
|
8
|
-
import { buildProductionAccessRequestBody } from "./projects.js";
|
|
9
|
-
|
|
10
|
-
export function registerLaunchCommands(program) {
|
|
11
|
-
const launch = program.command("launch").description("Production launch workflows");
|
|
12
|
-
|
|
13
|
-
launch
|
|
14
|
-
.command("prepare")
|
|
15
|
-
.description("Prepare a project for production Client Gateway launch")
|
|
16
|
-
.option("--project-id <id>", "Existing project id")
|
|
17
|
-
.option("--project-name <name>", "Create a project when no project id is provided")
|
|
18
|
-
.option("--project-slug <slug>", "Slug for a project created by this command")
|
|
19
|
-
.requiredOption(
|
|
20
|
-
"--production-origin <origin>",
|
|
21
|
-
"Exact HTTPS production origin, for example https://app.example.com",
|
|
22
|
-
)
|
|
23
|
-
.requiredOption("--end-user-terms-url <url>")
|
|
24
|
-
.requiredOption("--end-user-privacy-url <url>")
|
|
25
|
-
.option("--support-url <url>")
|
|
26
|
-
.requiredOption("--support-email <email>")
|
|
27
|
-
.requiredOption("--contact-email <email>")
|
|
28
|
-
.requiredOption("--use-case <text>")
|
|
29
|
-
.option("--expected-monthly-volume <volume>")
|
|
30
|
-
.option("--needed-billing-mode <mode>", "Billing mode needed for production", "developer_paid")
|
|
31
|
-
.option("--notes <text>")
|
|
32
|
-
.option("--idempotency-key <key>")
|
|
33
|
-
.action(async (opts, cmd) => {
|
|
34
|
-
const flags = globalFlags(cmd);
|
|
35
|
-
validateLaunchOptions(opts, flags);
|
|
36
|
-
|
|
37
|
-
const project = await ensureProject(opts, flags);
|
|
38
|
-
const idempotencyKey = opts.idempotencyKey || `launch-prepare-project-${project.id}`;
|
|
39
|
-
|
|
40
|
-
const configured = await updateLaunchProject(project.id, opts, flags);
|
|
41
|
-
const challenge = await provisionManagedTurnstile(project.id, idempotencyKey, flags);
|
|
42
|
-
const access = await requestAccessIfNeeded(project.id, configured, opts, flags);
|
|
43
|
-
const readiness = await fetchProject(project.id, flags);
|
|
44
|
-
|
|
45
|
-
emit(
|
|
46
|
-
flags.json
|
|
47
|
-
? launchSummary(readiness, challenge, access)
|
|
48
|
-
: humanLaunchSummary(readiness, access),
|
|
49
|
-
flags,
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function validateLaunchOptions(opts, flags) {
|
|
55
|
-
if (!productionHttpsOrigin(opts.productionOrigin)) {
|
|
56
|
-
fail("--production-origin must be an exact HTTPS production origin", 1, flags.json);
|
|
57
|
-
}
|
|
58
|
-
if ((opts.projectName && !opts.projectSlug) || (!opts.projectName && opts.projectSlug)) {
|
|
59
|
-
fail("--project-name and --project-slug must be provided together", 1, flags.json);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function ensureProject(opts, flags, { request = accountRequest, save = saveProjectConfig } = {}) {
|
|
64
|
-
if (opts.projectId) {
|
|
65
|
-
const project = await fetchProject(opts.projectId, flags, { request });
|
|
66
|
-
save(project);
|
|
67
|
-
return project;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (opts.projectName) {
|
|
71
|
-
const { data } = await request("POST", "/projects", {
|
|
72
|
-
body: { name: opts.projectName, slug: opts.projectSlug },
|
|
73
|
-
json: flags.json,
|
|
74
|
-
});
|
|
75
|
-
save(data);
|
|
76
|
-
return data;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const { data } = await request("GET", "/me", { json: flags.json });
|
|
80
|
-
if (!data.project?.id) {
|
|
81
|
-
fail(
|
|
82
|
-
"No default project is selected. Use --project-id or --project-name with --project-slug.",
|
|
83
|
-
1,
|
|
84
|
-
flags.json,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
save(data.project);
|
|
88
|
-
return data.project;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function updateLaunchProject(
|
|
92
|
-
projectId,
|
|
93
|
-
opts,
|
|
94
|
-
flags,
|
|
95
|
-
{ request = accountRequest, save = saveProjectConfig } = {},
|
|
96
|
-
) {
|
|
97
|
-
const body = {
|
|
98
|
-
allowed_origins: [opts.productionOrigin],
|
|
99
|
-
end_user_terms_url: opts.endUserTermsUrl,
|
|
100
|
-
end_user_privacy_url: opts.endUserPrivacyUrl,
|
|
101
|
-
support_email: opts.supportEmail,
|
|
102
|
-
virtual_microservice_enabled: true,
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
if (opts.supportUrl) body.support_url = opts.supportUrl;
|
|
106
|
-
|
|
107
|
-
const { data } = await request("PATCH", `/projects/${projectId}`, {
|
|
108
|
-
body,
|
|
109
|
-
json: flags.json,
|
|
110
|
-
});
|
|
111
|
-
save(data);
|
|
112
|
-
return data;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function provisionManagedTurnstile(
|
|
116
|
-
projectId,
|
|
117
|
-
idempotencyKey,
|
|
118
|
-
flags,
|
|
119
|
-
{ request = accountRequest } = {},
|
|
120
|
-
) {
|
|
121
|
-
const { data } = await request("POST", `/projects/${projectId}/turnstile/provision`, {
|
|
122
|
-
body: { idempotency_key: `${idempotencyKey}:turnstile` },
|
|
123
|
-
json: flags.json,
|
|
124
|
-
});
|
|
125
|
-
return data;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export async function requestAccessIfNeeded(
|
|
129
|
-
projectId,
|
|
130
|
-
project,
|
|
131
|
-
opts,
|
|
132
|
-
flags,
|
|
133
|
-
{ request = accountRequest } = {},
|
|
134
|
-
) {
|
|
135
|
-
if (project.production_access_status === "approved") {
|
|
136
|
-
return {
|
|
137
|
-
object: "production_access_request",
|
|
138
|
-
project_id: project.id,
|
|
139
|
-
project_production_access_status: "approved",
|
|
140
|
-
status: "approved",
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const { data } = await request("POST", `/projects/${projectId}/production_access_request`, {
|
|
145
|
-
body: buildProductionAccessRequestBody(opts),
|
|
146
|
-
json: flags.json,
|
|
147
|
-
});
|
|
148
|
-
return data;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function fetchProject(projectId, flags, { request = accountRequest } = {}) {
|
|
152
|
-
const { data } = await request("GET", `/projects/${projectId}`, { json: flags.json });
|
|
153
|
-
return data;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function saveProjectConfig(project) {
|
|
157
|
-
saveConfig({
|
|
158
|
-
projectId: String(project.id),
|
|
159
|
-
apiKey: null,
|
|
160
|
-
virtualMicroserviceUrl: project.virtual_microservice_url || null,
|
|
161
|
-
endUserSession: null,
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function launchSummary(project, challenge, access) {
|
|
166
|
-
return {
|
|
167
|
-
object: "launch_prepare_result",
|
|
168
|
-
project_id: project.id,
|
|
169
|
-
project_slug: project.slug,
|
|
170
|
-
virtual_microservice_url: project.virtual_microservice_url,
|
|
171
|
-
browser_challenge: challenge,
|
|
172
|
-
production_access: access,
|
|
173
|
-
production_safety: project.production_safety,
|
|
174
|
-
next_steps: [
|
|
175
|
-
"Run switchboard verify setup.",
|
|
176
|
-
"Use the SDK-managed browser challenge to mint browser sessions.",
|
|
177
|
-
"Run switchboard verify publish after approval and billing readiness are complete.",
|
|
178
|
-
],
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function humanLaunchSummary(project, access) {
|
|
183
|
-
const blocked = (project.production_safety?.checks || [])
|
|
184
|
-
.filter((check) => check.status !== "ready")
|
|
185
|
-
.map((check) => check.id);
|
|
186
|
-
|
|
187
|
-
return [
|
|
188
|
-
`Prepared project ${project.name} (${project.id})`,
|
|
189
|
-
`Client Gateway: ${project.virtual_microservice_url}`,
|
|
190
|
-
`Production access: ${access.project_production_access_status || access.status}`,
|
|
191
|
-
`Readiness: ${project.production_safety?.status || "unknown"}`,
|
|
192
|
-
blocked.length ? `Blocked checks: ${blocked.join(", ")}` : "Blocked checks: none",
|
|
193
|
-
"Next: run switchboard verify setup, then switchboard verify publish after approval and billing readiness.",
|
|
194
|
-
].join("\n");
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export function productionHttpsOrigin(origin) {
|
|
198
|
-
try {
|
|
199
|
-
const url = new URL(origin);
|
|
200
|
-
return (
|
|
201
|
-
url.protocol === "https:" &&
|
|
202
|
-
url.username === "" &&
|
|
203
|
-
url.password === "" &&
|
|
204
|
-
url.pathname === "/" &&
|
|
205
|
-
url.search === "" &&
|
|
206
|
-
url.hash === "" &&
|
|
207
|
-
!["localhost", "127.0.0.1", "::1"].includes(url.hostname) &&
|
|
208
|
-
!url.hostname.includes("*")
|
|
209
|
-
);
|
|
210
|
-
} catch {
|
|
211
|
-
return false;
|
|
212
|
-
}
|
|
213
|
-
}
|