@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/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
  }
@@ -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.browserChallengeToken ?? "switchboard-verification",
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 ({ clientUrl, endpointPath, init }) => {
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(new URL(endpointPath, clientUrl).href, {
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
- { clientUrl: clientUrl.href, endpointPath, init },
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.1",
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/",