@switchboard.spot/cli 0.2.3 → 0.2.4

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 CHANGED
@@ -56,9 +56,9 @@ If a first publish fails, `npm view @switchboard.spot/cli` will continue to retu
56
56
  switchboard auth login
57
57
  switchboard projects create --name "My App" --slug my-app
58
58
  switchboard setup project --origin http://localhost:5173 --json
59
- switchboard verify setup
59
+ switchboard verify setup --app-url <app-url>
60
60
  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"
61
- switchboard verify publish
61
+ switchboard verify publish --app-url <preview-url>
62
62
  ```
63
63
 
64
64
  Use `--json` for automation, CI, and coding agents.
@@ -67,7 +67,7 @@ Use `--json` for automation, CI, and coding agents.
67
67
 
68
68
  CLI account login does not create Client Gateway end-user sessions. End-user sign-up, sign-in, and refresh require the SDK-managed browser challenge; curl, Node scripts, CI, and CLI account login cannot mint browser sessions without running that real browser/mobile flow.
69
69
 
70
- For local Switchboard development, `switchboard verify setup --client-url http://localhost:4000/m/<slug>/v1` uses the built-in `dev_browser_challenge` token unless a scenario supplies an explicit `browserChallengeToken`. Hosted Switchboard should use managed Turnstile and the SDK-managed real browser challenge. A local sandbox smoke path is: configure allowed origin plus legal/support fields, create an anonymous session through the SDK or browser verification flow, then call Client Gateway chat.
70
+ For local Switchboard development, `switchboard verify setup --app-url <app-url> --client-url http://localhost:4000/m/<slug>/v1` uses the built-in `dev_browser_challenge` token unless a scenario supplies an explicit `browserChallengeToken`. Hosted Switchboard should use managed Turnstile and the SDK-managed real browser challenge. A local sandbox smoke path is: configure allowed origin plus legal/support fields, create an anonymous session through the SDK or browser verification flow, then call Client Gateway chat.
71
71
 
72
72
  Model discovery is global. Use `GET /v1/models` for OpenAI-compatible discovery or `GET /v1/catalog/models` for catalog/pricing metadata; Client Gateway chat is project-scoped at `/m/<slug>/v1/chat/completions`.
73
73
 
@@ -320,8 +320,8 @@ function waitForCallback(callbackServer, timeoutSeconds, json) {
320
320
  export function callbackPage(title, message, tone) {
321
321
  const success = tone === "success";
322
322
  const accent = success ? "#14b8a6" : "#ef4444";
323
- const accentSoft = success ? "#ccfbf1" : "#fee2e2";
324
- const mark = success ? "" : "!";
323
+ const mark = success ? "" : "×";
324
+ const ariaLabel = success ? "Switchboard CLI login complete" : "Switchboard CLI login failed";
325
325
 
326
326
  return `<!doctype html>
327
327
  <html lang="en">
@@ -340,114 +340,41 @@ export function callbackPage(title, message, tone) {
340
340
  body {
341
341
  min-height: 100vh;
342
342
  margin: 0;
343
+ display: grid;
344
+ place-items: center;
343
345
  color: #1f2937;
344
346
  background:
345
347
  repeating-linear-gradient(125deg, transparent, transparent 6px, #e8e8e8 6px, #e8e8e8 7px),
346
348
  #ffffff;
347
349
  }
348
- .page {
349
- min-height: 100vh;
350
- width: min(100%, 80rem);
351
- margin: 0 auto;
352
- display: flex;
353
- flex-direction: column;
354
- border-inline: 1px solid #e5e7eb;
355
- background:
356
- repeating-linear-gradient(125deg, transparent, transparent 6px, #e8e8e8 6px, #e8e8e8 7px),
357
- #ffffff;
358
- }
359
- header {
360
- height: 4.5rem;
361
- display: flex;
362
- align-items: center;
363
- justify-content: space-between;
364
- padding: 0 1.5rem;
365
- border-bottom: 1px solid #e5e7eb;
366
- background: rgba(255, 255, 255, 0.88);
367
- backdrop-filter: blur(12px);
368
- }
369
- .logo {
370
- color: #1f2937;
371
- font-size: 0.95rem;
372
- font-weight: 800;
373
- letter-spacing: 0;
374
- }
375
- .pill {
376
- border: 1px solid #e5e7eb;
377
- border-radius: 999px;
378
- padding: 0.45rem 0.8rem;
379
- background: #ffffff;
380
- color: #4b5563;
381
- font-size: 0.82rem;
382
- font-weight: 600;
383
- }
384
- main {
385
- flex: 1;
350
+ .panel {
351
+ width: min(100%, 12rem);
352
+ aspect-ratio: 1;
386
353
  display: grid;
387
354
  place-items: center;
388
- padding: 5rem 1.5rem;
389
- }
390
- .panel {
391
- width: min(100%, 34rem);
392
355
  border: 1px solid #e5e7eb;
393
356
  border-radius: 0.5rem;
394
- padding: clamp(1.5rem, 5vw, 2.25rem);
357
+ padding: 1.5rem;
395
358
  background: rgba(255, 255, 255, 0.94);
396
359
  box-shadow:
397
360
  0 1.34368px 0.537473px -0.625px rgba(0, 0, 0, 0.09),
398
361
  0 15.5969px 6.23877px -3.125px rgba(0, 0, 0, 0.07),
399
362
  0 43.962px 17.5848px -4.375px rgba(0, 0, 0, 0.04);
400
- text-align: center;
401
- }
402
- .eyebrow {
403
- margin: 0 0 1.25rem;
404
- color: #4b5563;
405
- font-size: 0.78rem;
406
- font-weight: 700;
407
- letter-spacing: 0.12em;
408
- text-transform: uppercase;
409
363
  }
410
364
  .mark {
411
- width: 4rem;
412
- height: 4rem;
413
- margin: 0 auto 1.25rem;
365
+ width: 5rem;
366
+ height: 5rem;
414
367
  display: grid;
415
368
  place-items: center;
416
369
  border: 1px solid ${accent};
417
370
  border-radius: 0.5rem;
418
371
  background: ${accent};
419
372
  color: #111827;
420
- font-size: 1.9rem;
373
+ font-size: 3rem;
374
+ line-height: 1;
421
375
  font-weight: 900;
422
376
  box-shadow: 0 18px 40px -20px ${accent};
423
377
  }
424
- h1 {
425
- margin: 0;
426
- color: #1f2937;
427
- font-size: clamp(2rem, 7vw, 3.5rem);
428
- line-height: 0.98;
429
- letter-spacing: 0;
430
- }
431
- p {
432
- margin: 1rem auto 0;
433
- max-width: 26rem;
434
- color: #4b5563;
435
- font-size: 1rem;
436
- line-height: 1.65;
437
- }
438
- .status {
439
- display: inline-flex;
440
- align-items: center;
441
- gap: 0.5rem;
442
- margin-top: 1.5rem;
443
- border: 1px solid ${accent};
444
- border-radius: 999px;
445
- padding: 0.5rem 0.8rem;
446
- background: ${accentSoft};
447
- color: #1f2937;
448
- font-size: 0.86rem;
449
- font-weight: 700;
450
- }
451
378
  @media (prefers-color-scheme: dark) {
452
379
  :root {
453
380
  background: #020617;
@@ -459,53 +386,20 @@ export function callbackPage(title, message, tone) {
459
386
  repeating-linear-gradient(125deg, transparent, transparent 6px, rgba(55, 65, 81, 0.6) 6px, rgba(55, 65, 81, 0.6) 7px),
460
387
  #020617;
461
388
  }
462
- .page {
463
- border-color: #1f2937;
464
- background:
465
- repeating-linear-gradient(125deg, transparent, transparent 6px, rgba(55, 65, 81, 0.6) 6px, rgba(55, 65, 81, 0.6) 7px),
466
- #020617;
467
- }
468
- header,
469
389
  .panel {
470
390
  border-color: #1f2937;
471
391
  background: rgba(2, 6, 23, 0.94);
472
392
  }
473
- .logo,
474
- h1 {
475
- color: #f3f4f6;
476
- }
477
- .pill,
478
- .eyebrow,
479
- p {
480
- color: #d1d5db;
481
- }
482
- .pill {
483
- border-color: #1f2937;
484
- background: #111827;
485
- }
486
- .mark,
487
- .status {
393
+ .mark {
488
394
  color: #020617;
489
395
  }
490
396
  }
491
397
  </style>
492
398
  </head>
493
399
  <body>
494
- <div class="page">
495
- <header>
496
- <div class="logo">Switchboard</div>
497
- <div class="pill">CLI session</div>
498
- </header>
499
- <main>
500
- <section class="panel" aria-labelledby="callback-title">
501
- <p class="eyebrow">Switchboard CLI</p>
502
- <div class="mark" aria-hidden="true">${mark}</div>
503
- <h1 id="callback-title">${escapeHtml(title)}</h1>
504
- <p>${escapeHtml(message)}</p>
505
- <div class="status">${success ? "Session approved" : "Session not approved"}</div>
506
- </section>
507
- </main>
508
- </div>
400
+ <main class="panel" aria-label="${ariaLabel}">
401
+ <div class="mark" aria-hidden="true">${mark}</div>
402
+ </main>
509
403
  </body>
510
404
  </html>`;
511
405
  }
@@ -42,6 +42,10 @@ export async function runDoctorChecks({
42
42
  checks.push(
43
43
  await check("health", async () => {
44
44
  const result = await health(config);
45
+ if (!result.ok) {
46
+ throw new Error(result.data?.status || result.data?.message || `HTTP ${result.status}`);
47
+ }
48
+
45
49
  return { status: result.status, data: result.data };
46
50
  }),
47
51
  );
@@ -15,6 +15,7 @@ export function registerVerifyCommands(program) {
15
15
  .command("setup")
16
16
  .description("Verify a local or preview Switchboard integration setup")
17
17
  .option("--url <app-url>", "Application URL to test")
18
+ .option("--app-url <app-url>", "Alias for --url")
18
19
  .option("--client-url <switchboard-client-url>", "Switchboard Client Gateway client URL"),
19
20
  ).action(async (opts, cmd) => {
20
21
  await runVerifyCommand("setup", opts, cmd);
@@ -25,6 +26,7 @@ export function registerVerifyCommands(program) {
25
26
  .command("publish")
26
27
  .description("Verify an HTTPS preview is safe to publish")
27
28
  .option("--url <preview-url>", "HTTPS preview URL to test")
29
+ .option("--app-url <preview-url>", "Alias for --url")
28
30
  .option("--client-url <switchboard-client-url>", "Switchboard Client Gateway client URL")
29
31
  .option("--production-origin <origin>", "Production origin that must be allowed"),
30
32
  )
@@ -38,6 +40,7 @@ export function registerVerifyCommands(program) {
38
40
  .command("browser")
39
41
  .description("Run a declarative browser verification scenario")
40
42
  .option("--url <app-url>", "Application URL to test")
43
+ .option("--app-url <app-url>", "Alias for --url")
41
44
  .option("--client-url <switchboard-client-url>", "Switchboard Client Gateway client URL"),
42
45
  ).action(async (opts, cmd) => {
43
46
  await runVerifyCommand("browser", opts, cmd);
@@ -60,6 +63,7 @@ async function runVerifyCommand(mode, opts, cmd) {
60
63
  const project = mode === "publish" ? await loadProject(opts, flags) : null;
61
64
  const report = await verifierForMode(mode)({
62
65
  url: opts.url,
66
+ appUrl: opts.appUrl,
63
67
  clientUrl: opts.clientUrl,
64
68
  productionOrigin: opts.productionOrigin,
65
69
  scenarioPath: opts.scenario,
@@ -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;
@@ -723,6 +722,7 @@ function normalizedHostname(url) {
723
722
 
724
723
  function browserChallengeTokenFor(check, clientUrl) {
725
724
  if (Object.prototype.hasOwnProperty.call(check, "browserChallengeToken")) {
725
+ validateBrowserChallengeToken(check.browserChallengeToken, clientUrl);
726
726
  return check.browserChallengeToken;
727
727
  }
728
728
 
@@ -730,13 +730,25 @@ function browserChallengeTokenFor(check, clientUrl) {
730
730
  return DEV_BROWSER_CHALLENGE_TOKEN;
731
731
  }
732
732
 
733
- return DEFAULT_BROWSER_CHALLENGE_TOKEN;
733
+ throw new VerificationInputError(
734
+ "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.",
735
+ );
734
736
  }
735
737
 
736
738
  function isLocalSwitchboardUrl(url) {
737
739
  return url instanceof URL && LOCAL_HOSTS.has(normalizedHostname(url));
738
740
  }
739
741
 
742
+ export function validateBrowserChallengeToken(token, clientUrl) {
743
+ if (isLocalSwitchboardUrl(clientUrl)) return;
744
+
745
+ if (token === DEV_BROWSER_CHALLENGE_TOKEN || token === DEFAULT_BROWSER_CHALLENGE_TOKEN) {
746
+ throw new VerificationInputError(
747
+ "Synthetic browser challenge tokens are allowed only against localhost Switchboard URLs.",
748
+ );
749
+ }
750
+ }
751
+
740
752
  function addFailure(report, checkId, message, finding) {
741
753
  addCheck(report, { id: checkId, status: "failed", message });
742
754
  report.findings.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchboard.spot/cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Switchboard CLI — full dashboard parity for agents and testing",
5
5
  "type": "module",
6
6
  "bin": {