@switchboard.spot/cli 0.2.3 → 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.
@@ -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;
@@ -194,7 +193,13 @@ export function createReport(mode) {
194
193
  }
195
194
 
196
195
  export function evaluateEvidence(report, evidence) {
197
- const consoleErrors = evidence.consoleMessages.filter((message) => message.type === "error");
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
+
198
203
  addCheck(report, {
199
204
  id: CHECK_IDS.NO_CONSOLE_ERRORS,
200
205
  status: consoleErrors.length === 0 ? "passed" : "failed",
@@ -206,11 +211,11 @@ export function evaluateEvidence(report, evidence) {
206
211
 
207
212
  addCheck(report, {
208
213
  id: CHECK_IDS.NO_FAILED_REQUESTS,
209
- status: evidence.failedRequests.length === 0 ? "passed" : "failed",
214
+ status: failedRequests.length === 0 ? "passed" : "failed",
210
215
  message:
211
- evidence.failedRequests.length === 0
216
+ failedRequests.length === 0
212
217
  ? "No failed browser requests were observed."
213
- : `${evidence.failedRequests.length} failed browser request(s) were observed.`,
218
+ : `${failedRequests.length} failed browser request(s) were observed.`,
214
219
  });
215
220
 
216
221
  const authLeak = browserAuthorizationFinding(evidence.requests);
@@ -456,10 +461,16 @@ async function executeScenarioCheck({
456
461
  messages: [{ role: "user", content: prompt ?? check.prompt }],
457
462
  },
458
463
  });
464
+ const sandboxGuarded = isSandboxCompletionGuard(result);
465
+ if (sandboxGuarded) evidence.allowedSandboxGuardUrls.add(result.url);
459
466
  addCheck(report, {
460
467
  id: CHECK_IDS.CHAT_COMPLETION_SUCCEEDS,
461
- status: result.ok ? "passed" : "failed",
462
- message: result.ok ? "Chat completion succeeded through the client gateway." : result.error,
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,
463
474
  });
464
475
  return;
465
476
  }
@@ -505,11 +516,12 @@ async function browserFetchJson(page, clientUrl, endpointPath, init) {
505
516
  return {
506
517
  ok: response.ok,
507
518
  status: response.status,
519
+ url: endpointUrl,
508
520
  data,
509
521
  error: response.ok ? null : `HTTP ${response.status}`,
510
522
  };
511
523
  } catch (error) {
512
- return { ok: false, status: 0, data: null, error: error.message };
524
+ return { ok: false, status: 0, url: endpointUrl, data: null, error: error.message };
513
525
  }
514
526
  },
515
527
  { endpointUrl, init },
@@ -524,6 +536,32 @@ function clientEndpointUrl(clientUrl, endpointPath) {
524
536
  return url.href;
525
537
  }
526
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
+
527
565
  function wireEvidence(page, evidence) {
528
566
  page.on("console", (message) => {
529
567
  evidence.consoleMessages.push({
@@ -593,6 +631,7 @@ function emptyEvidence() {
593
631
  return {
594
632
  consoleMessages: [],
595
633
  failedRequests: [],
634
+ allowedSandboxGuardUrls: new Set(),
596
635
  requests: [],
597
636
  responses: [],
598
637
  texts: [],
@@ -723,6 +762,7 @@ function normalizedHostname(url) {
723
762
 
724
763
  function browserChallengeTokenFor(check, clientUrl) {
725
764
  if (Object.prototype.hasOwnProperty.call(check, "browserChallengeToken")) {
765
+ validateBrowserChallengeToken(check.browserChallengeToken, clientUrl);
726
766
  return check.browserChallengeToken;
727
767
  }
728
768
 
@@ -730,13 +770,25 @@ function browserChallengeTokenFor(check, clientUrl) {
730
770
  return DEV_BROWSER_CHALLENGE_TOKEN;
731
771
  }
732
772
 
733
- return DEFAULT_BROWSER_CHALLENGE_TOKEN;
773
+ throw new VerificationInputError(
774
+ "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.",
775
+ );
734
776
  }
735
777
 
736
778
  function isLocalSwitchboardUrl(url) {
737
779
  return url instanceof URL && LOCAL_HOSTS.has(normalizedHostname(url));
738
780
  }
739
781
 
782
+ export function validateBrowserChallengeToken(token, clientUrl) {
783
+ if (isLocalSwitchboardUrl(clientUrl)) return;
784
+
785
+ if (token === DEV_BROWSER_CHALLENGE_TOKEN || token === DEFAULT_BROWSER_CHALLENGE_TOKEN) {
786
+ throw new VerificationInputError(
787
+ "Synthetic browser challenge tokens are allowed only against localhost Switchboard URLs.",
788
+ );
789
+ }
790
+ }
791
+
740
792
  function addFailure(report, checkId, message, finding) {
741
793
  addCheck(report, { id: checkId, status: "failed", message });
742
794
  report.findings.push({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@switchboard.spot/cli",
3
- "version": "0.2.3",
4
- "description": "Switchboard CLI full dashboard parity for agents and testing",
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"
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }