@switchboard.spot/cli 0.2.1 → 0.2.2
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 +19 -6
- package/bin/switchboard.js +5 -1
- package/lib/commands/auth.js +147 -38
- package/lib/commands/billing.js +43 -0
- package/lib/commands/docs.js +176 -0
- package/lib/commands/doctor.js +49 -0
- package/lib/commands/init.js +4 -2
- package/lib/commands/launch.js +192 -0
- package/lib/commands/projects.js +50 -4
- package/lib/config.js +12 -9
- package/lib/docsClient.js +157 -0
- package/lib/mcpServer.js +193 -0
- package/lib/output.js +65 -9
- package/lib/verify/index.js +32 -4
- package/package.json +4 -2
package/lib/output.js
CHANGED
|
@@ -2,16 +2,30 @@
|
|
|
2
2
|
* Human-readable and JSON output helpers for the Switchboard CLI.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
const REDACTED = "[REDACTED]";
|
|
6
|
+
|
|
7
|
+
const SECRET_PATTERNS = [
|
|
8
|
+
/\bsb_(?:test|live|sess|eusr)_[A-Za-z0-9._-]+\b/g,
|
|
9
|
+
/\bsk-(?:proj-|ant-)?[A-Za-z0-9._-]{20,}\b/g,
|
|
10
|
+
/\bsk_(?:live|test)_[A-Za-z0-9._-]+\b/g,
|
|
11
|
+
/\brk_(?:live|test)_[A-Za-z0-9._-]+\b/g,
|
|
12
|
+
/\bpk_(?:live|test)_[A-Za-z0-9._-]+\b/g,
|
|
13
|
+
/\bwhsec_[A-Za-z0-9._-]+\b/g,
|
|
14
|
+
/\b0x[0-9A-Za-z_-]{20,}\b/g,
|
|
15
|
+
/\b[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{24,}\b/g,
|
|
16
|
+
/\b(?:OPENAI|ANTHROPIC|GROQ|MISTRAL|GOOGLE|STRIPE|CLOUDFLARE)_[A-Z0-9_]*(?:KEY|TOKEN|SECRET)\s*=\s*[^\s]+/gi,
|
|
17
|
+
];
|
|
18
|
+
|
|
5
19
|
/**
|
|
6
20
|
* Prints data as JSON or a human message depending on global flags.
|
|
7
21
|
*/
|
|
8
22
|
export function emit(data, { json, quiet } = {}) {
|
|
9
23
|
if (json) {
|
|
10
|
-
console.log(JSON.stringify(data, null, 2));
|
|
24
|
+
console.log(JSON.stringify(redactSecrets(data), null, 2));
|
|
11
25
|
return;
|
|
12
26
|
}
|
|
13
27
|
if (!quiet && typeof data === "string") {
|
|
14
|
-
console.log(data);
|
|
28
|
+
console.log(redactSecrets(data));
|
|
15
29
|
}
|
|
16
30
|
}
|
|
17
31
|
|
|
@@ -35,13 +49,13 @@ export function fail(message, code = 1, json = false, type = "invalid_request")
|
|
|
35
49
|
if (json) {
|
|
36
50
|
console.log(
|
|
37
51
|
JSON.stringify(
|
|
38
|
-
{
|
|
52
|
+
redactSecrets({
|
|
39
53
|
ok: false,
|
|
40
54
|
error: {
|
|
41
55
|
type,
|
|
42
56
|
message,
|
|
43
57
|
},
|
|
44
|
-
},
|
|
58
|
+
}),
|
|
45
59
|
null,
|
|
46
60
|
2,
|
|
47
61
|
),
|
|
@@ -49,7 +63,7 @@ export function fail(message, code = 1, json = false, type = "invalid_request")
|
|
|
49
63
|
process.exit(code);
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
console.error(message);
|
|
66
|
+
console.error(redactSecrets(message));
|
|
53
67
|
process.exit(code);
|
|
54
68
|
}
|
|
55
69
|
|
|
@@ -93,9 +107,9 @@ export function normalizeError(data, text, status) {
|
|
|
93
107
|
|
|
94
108
|
export function emitHttpError(error, status, flags = {}) {
|
|
95
109
|
if (flags.json) {
|
|
96
|
-
console.log(JSON.stringify({ ok: false, status, error }, null, 2));
|
|
110
|
+
console.log(JSON.stringify(redactSecrets({ ok: false, status, error }), null, 2));
|
|
97
111
|
} else {
|
|
98
|
-
console.error(error.message);
|
|
112
|
+
console.error(redactSecrets(error.message));
|
|
99
113
|
}
|
|
100
114
|
|
|
101
115
|
process.exit(exitCodeForStatus(status));
|
|
@@ -105,8 +119,50 @@ export function emitHttpError(error, status, flags = {}) {
|
|
|
105
119
|
* Formats a simple key-value listing for human output.
|
|
106
120
|
*/
|
|
107
121
|
export function printList(title, items, formatter) {
|
|
108
|
-
console.log(title);
|
|
122
|
+
console.log(redactSecrets(title));
|
|
109
123
|
for (const item of items) {
|
|
110
|
-
console.log(formatter(item));
|
|
124
|
+
console.log(redactSecrets(formatter(item)));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function redactSecrets(value) {
|
|
129
|
+
if (typeof value === "string") return redactString(value);
|
|
130
|
+
if (Array.isArray(value)) return value.map((entry) => redactSecrets(entry));
|
|
131
|
+
if (!value || typeof value !== "object") return value;
|
|
132
|
+
|
|
133
|
+
const redacted = {};
|
|
134
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
135
|
+
redacted[key] = isSecretKeyName(key) ? redactKnownPublicKey(key, entry) : redactSecrets(entry);
|
|
111
136
|
}
|
|
137
|
+
|
|
138
|
+
return redacted;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function redactKnownPublicKey(key, value) {
|
|
142
|
+
if (key === "site_key" || key === "turnstile_site_key") return redactSecrets(value);
|
|
143
|
+
return typeof value === "boolean" || value == null ? value : REDACTED;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function redactString(value) {
|
|
147
|
+
return SECRET_PATTERNS.reduce(
|
|
148
|
+
(text, pattern) => text.replace(pattern, (match) => redactAssignment(match)),
|
|
149
|
+
value,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function redactAssignment(match) {
|
|
154
|
+
const index = match.indexOf("=");
|
|
155
|
+
if (index === -1) return REDACTED;
|
|
156
|
+
return `${match.slice(0, index + 1)}${REDACTED}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isSecretKeyName(key) {
|
|
160
|
+
const normalized = key.replace(/[A-Z]/g, "_$&").toLowerCase();
|
|
161
|
+
if (normalized.endsWith("_count") || normalized.endsWith("_configured")) return false;
|
|
162
|
+
if (normalized === "site_key" || normalized === "turnstile_site_key") return false;
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
/(^|_)(secret|token|password|plaintext|session)($|_)/.test(normalized) ||
|
|
166
|
+
/(^|_)api_key($|_)/.test(normalized)
|
|
167
|
+
);
|
|
112
168
|
}
|
package/lib/verify/index.js
CHANGED
|
@@ -70,6 +70,8 @@ const PROTECTED_AUTH_HOST_PATTERNS = [
|
|
|
70
70
|
];
|
|
71
71
|
|
|
72
72
|
const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
|
|
73
|
+
const DEV_BROWSER_CHALLENGE_TOKEN = "dev_browser_challenge";
|
|
74
|
+
const DEFAULT_BROWSER_CHALLENGE_TOKEN = "switchboard-verification";
|
|
73
75
|
|
|
74
76
|
export function loadScenario(filePath) {
|
|
75
77
|
if (!filePath) return DEFAULT_SCENARIO;
|
|
@@ -427,7 +429,7 @@ async function executeScenarioCheck({
|
|
|
427
429
|
const result = await browserFetchJson(page, clientUrl, "/auth/anonymous/session", {
|
|
428
430
|
method: "POST",
|
|
429
431
|
body: {
|
|
430
|
-
browser_challenge_token: check
|
|
432
|
+
browser_challenge_token: browserChallengeTokenFor(check, clientUrl),
|
|
431
433
|
},
|
|
432
434
|
});
|
|
433
435
|
if (result.data?.token) {
|
|
@@ -478,14 +480,16 @@ async function executeScenarioCheck({
|
|
|
478
480
|
}
|
|
479
481
|
|
|
480
482
|
async function browserFetchJson(page, clientUrl, endpointPath, init) {
|
|
483
|
+
const endpointUrl = clientEndpointUrl(clientUrl, endpointPath);
|
|
484
|
+
|
|
481
485
|
return page.evaluate(
|
|
482
|
-
async ({
|
|
486
|
+
async ({ endpointUrl, init }) => {
|
|
483
487
|
try {
|
|
484
488
|
const headers = new Headers(init.headers ?? {});
|
|
485
489
|
headers.set("Accept", "application/json");
|
|
486
490
|
if (init.body) headers.set("Content-Type", "application/json");
|
|
487
491
|
|
|
488
|
-
const response = await fetch(
|
|
492
|
+
const response = await fetch(endpointUrl, {
|
|
489
493
|
method: init.method,
|
|
490
494
|
headers,
|
|
491
495
|
body: init.body ? JSON.stringify(init.body) : undefined,
|
|
@@ -508,10 +512,18 @@ async function browserFetchJson(page, clientUrl, endpointPath, init) {
|
|
|
508
512
|
return { ok: false, status: 0, data: null, error: error.message };
|
|
509
513
|
}
|
|
510
514
|
},
|
|
511
|
-
{
|
|
515
|
+
{ endpointUrl, init },
|
|
512
516
|
);
|
|
513
517
|
}
|
|
514
518
|
|
|
519
|
+
function clientEndpointUrl(clientUrl, endpointPath) {
|
|
520
|
+
const endpoint = endpointPath.startsWith("/") ? endpointPath.slice(1) : endpointPath;
|
|
521
|
+
const basePath = clientUrl.pathname.endsWith("/") ? clientUrl.pathname : `${clientUrl.pathname}/`;
|
|
522
|
+
const url = new URL(clientUrl.href);
|
|
523
|
+
url.pathname = `${basePath}${endpoint}`.replace(/\/{2,}/g, "/");
|
|
524
|
+
return url.href;
|
|
525
|
+
}
|
|
526
|
+
|
|
515
527
|
function wireEvidence(page, evidence) {
|
|
516
528
|
page.on("console", (message) => {
|
|
517
529
|
evidence.consoleMessages.push({
|
|
@@ -709,6 +721,22 @@ function normalizedHostname(url) {
|
|
|
709
721
|
return url.hostname.replace(/^\[|\]$/g, "");
|
|
710
722
|
}
|
|
711
723
|
|
|
724
|
+
function browserChallengeTokenFor(check, clientUrl) {
|
|
725
|
+
if (Object.prototype.hasOwnProperty.call(check, "browserChallengeToken")) {
|
|
726
|
+
return check.browserChallengeToken;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (isLocalSwitchboardUrl(clientUrl)) {
|
|
730
|
+
return DEV_BROWSER_CHALLENGE_TOKEN;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return DEFAULT_BROWSER_CHALLENGE_TOKEN;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function isLocalSwitchboardUrl(url) {
|
|
737
|
+
return url instanceof URL && LOCAL_HOSTS.has(normalizedHostname(url));
|
|
738
|
+
}
|
|
739
|
+
|
|
712
740
|
function addFailure(report, checkId, message, finding) {
|
|
713
741
|
addCheck(report, { id: checkId, status: "failed", message });
|
|
714
742
|
report.findings.push({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchboard.spot/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Switchboard CLI — full dashboard parity for agents and testing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
"coverage": "node --test --experimental-test-coverage --test-coverage-include='bin/**/*.js' --test-coverage-include='lib/**/*.js' test/*.test.js"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
21
|
"commander": "^13.1.0",
|
|
21
|
-
"playwright": "^1.61.0"
|
|
22
|
+
"playwright": "^1.61.0",
|
|
23
|
+
"zod": "^4.4.3"
|
|
22
24
|
},
|
|
23
25
|
"files": [
|
|
24
26
|
"bin/",
|