@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 +3 -3
- package/lib/commands/auth.js +16 -122
- package/lib/commands/doctor.js +4 -0
- package/lib/commands/verify.js +4 -0
- package/lib/verify/index.js +15 -3
- package/package.json +1 -1
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
|
|
package/lib/commands/auth.js
CHANGED
|
@@ -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
|
|
324
|
-
const
|
|
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
|
-
.
|
|
349
|
-
|
|
350
|
-
|
|
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:
|
|
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:
|
|
412
|
-
height:
|
|
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:
|
|
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
|
-
.
|
|
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
|
-
<
|
|
495
|
-
<
|
|
496
|
-
|
|
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
|
}
|
package/lib/commands/doctor.js
CHANGED
|
@@ -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
|
);
|
package/lib/commands/verify.js
CHANGED
|
@@ -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,
|
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;
|
|
@@ -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
|
-
|
|
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({
|