@switchboard.spot/cli 0.2.4 → 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 +18 -7
- package/bin/switchboard.js +1 -17
- package/lib/client.js +0 -3
- package/lib/commands/account.js +0 -9
- package/lib/commands/auth.js +0 -2
- package/lib/commands/billing.js +7 -69
- package/lib/commands/doctor.js +6 -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/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 +47 -7
- 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
|
@@ -193,7 +193,13 @@ export function createReport(mode) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
export function evaluateEvidence(report, evidence) {
|
|
196
|
-
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
|
+
|
|
197
203
|
addCheck(report, {
|
|
198
204
|
id: CHECK_IDS.NO_CONSOLE_ERRORS,
|
|
199
205
|
status: consoleErrors.length === 0 ? "passed" : "failed",
|
|
@@ -205,11 +211,11 @@ export function evaluateEvidence(report, evidence) {
|
|
|
205
211
|
|
|
206
212
|
addCheck(report, {
|
|
207
213
|
id: CHECK_IDS.NO_FAILED_REQUESTS,
|
|
208
|
-
status:
|
|
214
|
+
status: failedRequests.length === 0 ? "passed" : "failed",
|
|
209
215
|
message:
|
|
210
|
-
|
|
216
|
+
failedRequests.length === 0
|
|
211
217
|
? "No failed browser requests were observed."
|
|
212
|
-
: `${
|
|
218
|
+
: `${failedRequests.length} failed browser request(s) were observed.`,
|
|
213
219
|
});
|
|
214
220
|
|
|
215
221
|
const authLeak = browserAuthorizationFinding(evidence.requests);
|
|
@@ -455,10 +461,16 @@ async function executeScenarioCheck({
|
|
|
455
461
|
messages: [{ role: "user", content: prompt ?? check.prompt }],
|
|
456
462
|
},
|
|
457
463
|
});
|
|
464
|
+
const sandboxGuarded = isSandboxCompletionGuard(result);
|
|
465
|
+
if (sandboxGuarded) evidence.allowedSandboxGuardUrls.add(result.url);
|
|
458
466
|
addCheck(report, {
|
|
459
467
|
id: CHECK_IDS.CHAT_COMPLETION_SUCCEEDS,
|
|
460
|
-
status: result.ok ? "passed" : "failed",
|
|
461
|
-
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,
|
|
462
474
|
});
|
|
463
475
|
return;
|
|
464
476
|
}
|
|
@@ -504,11 +516,12 @@ async function browserFetchJson(page, clientUrl, endpointPath, init) {
|
|
|
504
516
|
return {
|
|
505
517
|
ok: response.ok,
|
|
506
518
|
status: response.status,
|
|
519
|
+
url: endpointUrl,
|
|
507
520
|
data,
|
|
508
521
|
error: response.ok ? null : `HTTP ${response.status}`,
|
|
509
522
|
};
|
|
510
523
|
} catch (error) {
|
|
511
|
-
return { ok: false, status: 0, data: null, error: error.message };
|
|
524
|
+
return { ok: false, status: 0, url: endpointUrl, data: null, error: error.message };
|
|
512
525
|
}
|
|
513
526
|
},
|
|
514
527
|
{ endpointUrl, init },
|
|
@@ -523,6 +536,32 @@ function clientEndpointUrl(clientUrl, endpointPath) {
|
|
|
523
536
|
return url.href;
|
|
524
537
|
}
|
|
525
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
|
+
|
|
526
565
|
function wireEvidence(page, evidence) {
|
|
527
566
|
page.on("console", (message) => {
|
|
528
567
|
evidence.consoleMessages.push({
|
|
@@ -592,6 +631,7 @@ function emptyEvidence() {
|
|
|
592
631
|
return {
|
|
593
632
|
consoleMessages: [],
|
|
594
633
|
failedRequests: [],
|
|
634
|
+
allowedSandboxGuardUrls: new Set(),
|
|
595
635
|
requests: [],
|
|
596
636
|
responses: [],
|
|
597
637
|
texts: [],
|
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
|
-
}
|
package/lib/commands/org.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Organization invitation commands.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { accountRequest } from "../client.js";
|
|
6
|
-
import { emit, globalFlags, printList } from "../output.js";
|
|
7
|
-
|
|
8
|
-
export function registerOrgCommands(program) {
|
|
9
|
-
const org = program.command("org").description("Organization and team");
|
|
10
|
-
|
|
11
|
-
org
|
|
12
|
-
.command("invitations")
|
|
13
|
-
.description("List pending invitations")
|
|
14
|
-
.action(async (_opts, cmd) => {
|
|
15
|
-
const flags = globalFlags(cmd);
|
|
16
|
-
const { data } = await accountRequest("GET", "/invitations", { json: flags.json });
|
|
17
|
-
if (flags.json) {
|
|
18
|
-
emit(data, flags);
|
|
19
|
-
} else {
|
|
20
|
-
printList("Pending invitations:", data.data || [], (i) =>
|
|
21
|
-
` ${i.email} (${i.role}) token=${i.token}`,
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
org
|
|
27
|
-
.command("invite")
|
|
28
|
-
.description("Invite a member by email")
|
|
29
|
-
.requiredOption("--email <email>")
|
|
30
|
-
.action(async (opts, cmd) => {
|
|
31
|
-
const flags = globalFlags(cmd);
|
|
32
|
-
const { data } = await accountRequest("POST", "/invitations", {
|
|
33
|
-
body: { email: opts.email },
|
|
34
|
-
json: flags.json,
|
|
35
|
-
});
|
|
36
|
-
emit(
|
|
37
|
-
flags.json ? data : `Invited ${data.email}. Token: ${data.token}`,
|
|
38
|
-
flags,
|
|
39
|
-
);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
org
|
|
43
|
-
.command("accept <token>")
|
|
44
|
-
.description("Accept an invitation")
|
|
45
|
-
.action(async (token, _opts, cmd) => {
|
|
46
|
-
const flags = globalFlags(cmd);
|
|
47
|
-
const { data } = await accountRequest("POST", `/invitations/${token}/accept`, {
|
|
48
|
-
json: flags.json,
|
|
49
|
-
});
|
|
50
|
-
emit(
|
|
51
|
-
flags.json ? data : `Joined organization ${data.name}`,
|
|
52
|
-
flags,
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
}
|