@switchboard.spot/cli 0.2.4 → 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.
@@ -5,16 +5,8 @@
5
5
  import { configureEnvironment } from "./env.js";
6
6
  import { accountRequest } from "../client.js";
7
7
  import { emit, fail, globalFlags, redactSecrets } from "../output.js";
8
- import { resolveConfig } from "../config.js";
8
+ import { resolveConfig, saveConfig } from "../config.js";
9
9
  import { runDoctorChecks } from "./doctor.js";
10
- import {
11
- ensureProject,
12
- fetchProject,
13
- provisionManagedTurnstile,
14
- requestAccessIfNeeded,
15
- saveProjectConfig,
16
- validateLaunchOptions,
17
- } from "./launch.js";
18
10
 
19
11
  export function registerSetupCommand(program) {
20
12
  const setup = program
@@ -28,24 +20,14 @@ export function registerSetupCommand(program) {
28
20
  .option("--project-name <name>", "Create a project when no project id is provided")
29
21
  .option("--project-slug <slug>", "Slug for a project created by this command")
30
22
  .requiredOption("--origin <origin>", "Exact local, preview, or sandbox browser origin")
31
- .option("--target <target>", "client, server, or both", "client")
23
+ .option("--target <target>", "client", "client")
32
24
  .option("--file <path>", "Local env file to update", ".env.local")
33
- .option("--secret-target <target>", "local or exec", "local")
34
- .option("--secret-command <command>", "Command that stores secrets from stdin JSON")
35
25
  .option("--force", "Replace existing Switchboard-managed env values and project origins")
36
- .option(
37
- "--production-origin <origin>",
38
- "Exact HTTPS production origin, for example https://app.example.com",
39
- )
26
+ .option("--production-origin <origin>", "Exact HTTPS production origin to add as an allowed origin")
40
27
  .option("--end-user-terms-url <url>")
41
28
  .option("--end-user-privacy-url <url>")
42
29
  .option("--support-url <url>")
43
30
  .option("--support-email <email>")
44
- .option("--contact-email <email>")
45
- .option("--use-case <text>")
46
- .option("--expected-monthly-volume <volume>")
47
- .option("--needed-billing-mode <mode>", "Billing mode needed for production")
48
- .option("--notes <text>")
49
31
  .action(async (opts, cmd) => {
50
32
  const flags = globalFlags(cmd);
51
33
  const result = await setupProject(opts, { json: flags.json });
@@ -59,8 +41,6 @@ export async function setupSwitchboard(opts, dependencies = {}) {
59
41
  {
60
42
  mode: target,
61
43
  file: opts.file,
62
- target: opts.secretTarget,
63
- secretCommand: opts.secretCommand,
64
44
  projectId: opts.projectId,
65
45
  force: opts.force,
66
46
  },
@@ -76,11 +56,11 @@ export async function setupSwitchboard(opts, dependencies = {}) {
76
56
  }
77
57
 
78
58
  function normalizeSetupTarget(target, json) {
79
- if (target === "client" || target === "server" || target === "both") {
59
+ if (target === "client" || target === undefined) {
80
60
  return target;
81
61
  }
82
62
 
83
- fail("--target must be client, server, or both", 1, json);
63
+ fail("--target must be client", 1, json);
84
64
  }
85
65
 
86
66
  function smokeCheck(result) {
@@ -94,11 +74,6 @@ function smokeCheck(result) {
94
74
  ok: true,
95
75
  checks: [
96
76
  clientEnvConfigured ? "client_env" : null,
97
- result.changed.includes("SWITCHBOARD_BASE_URL") ||
98
- result.skipped.includes("SWITCHBOARD_BASE_URL")
99
- ? "server_env"
100
- : null,
101
- result.secrets.length > 0 ? "server_secret" : null,
102
77
  ].filter(Boolean),
103
78
  };
104
79
  }
@@ -119,9 +94,6 @@ export async function setupProject(
119
94
  config = resolveConfig(),
120
95
  cwd,
121
96
  json = false,
122
- getSecret,
123
- setSecret,
124
- runSecretCommand,
125
97
  } = {},
126
98
  ) {
127
99
  validateProjectSetupOptions(opts, { json });
@@ -140,12 +112,10 @@ export async function setupProject(
140
112
  {
141
113
  mode: normalizeSetupTarget(opts.target, json),
142
114
  file: opts.file,
143
- target: opts.secretTarget,
144
- secretCommand: opts.secretCommand,
145
115
  projectId: String(configured.id),
146
116
  force: opts.force,
147
117
  },
148
- { request, cwd, json, getSecret, setSecret, runSecretCommand },
118
+ { request, cwd, json },
149
119
  );
150
120
 
151
121
  const idempotencyKey = `setup-project-${configured.id}`;
@@ -153,18 +123,13 @@ export async function setupProject(
153
123
  request,
154
124
  });
155
125
 
156
- let access = null;
157
- if (hasProductionOptions(opts)) {
158
- access = await requestAccessIfNeeded(configured.id, configured, opts, flags, { request });
159
- }
160
-
161
126
  const latest = await fetchProject(configured.id, flags, { request });
162
127
  saveProject(latest);
163
128
 
164
129
  const doctorConfig = {
165
130
  ...config,
166
131
  projectId: String(latest.id),
167
- virtualMicroserviceUrl: latest.virtual_microservice_url || config.virtualMicroserviceUrl,
132
+ clientGatewayUrl: latest.client_gateway_url || config.clientGatewayUrl,
168
133
  };
169
134
  const readiness = await doctor({ config: doctorConfig, request });
170
135
 
@@ -173,7 +138,6 @@ export async function setupProject(
173
138
  env,
174
139
  challenge,
175
140
  readiness,
176
- access,
177
141
  origin: opts.origin,
178
142
  origins,
179
143
  });
@@ -188,19 +152,15 @@ function validateProjectSetupOptions(opts, flags) {
188
152
  fail("--origin must be an exact browser origin such as http://127.0.0.1:4173", 1, flags.json);
189
153
  }
190
154
 
191
- if (!hasProductionOptions(opts)) return;
155
+ if (!opts.productionOrigin) return;
192
156
 
193
- validateLaunchOptions(opts, flags);
157
+ if (!productionHttpsOrigin(opts.productionOrigin)) {
158
+ fail("--production-origin must be an exact HTTPS production origin", 1, flags.json);
159
+ }
194
160
 
195
- for (const name of [
196
- "endUserTermsUrl",
197
- "endUserPrivacyUrl",
198
- "supportEmail",
199
- "contactEmail",
200
- "useCase",
201
- ]) {
161
+ for (const name of ["endUserTermsUrl", "endUserPrivacyUrl", "supportEmail"]) {
202
162
  if (!opts[name]) {
203
- fail(`--${dasherize(name)} is required when production launch fields are provided`, 1, flags.json);
163
+ fail(`--${dasherize(name)} is required when --production-origin is provided`, 1, flags.json);
204
164
  }
205
165
  }
206
166
  }
@@ -212,11 +172,6 @@ function hasProductionOptions(opts) {
212
172
  "endUserPrivacyUrl",
213
173
  "supportUrl",
214
174
  "supportEmail",
215
- "contactEmail",
216
- "useCase",
217
- "expectedMonthlyVolume",
218
- "neededBillingMode",
219
- "notes",
220
175
  ].some((key) => opts[key] != null);
221
176
  }
222
177
 
@@ -233,7 +188,7 @@ function configuredOrigins(project, opts) {
233
188
  async function updateSetupProject(projectId, opts, origins, flags, { request, save }) {
234
189
  const body = {
235
190
  allowed_origins: origins,
236
- virtual_microservice_enabled: true,
191
+ client_gateway_enabled: true,
237
192
  };
238
193
 
239
194
  if (hasProductionOptions(opts)) {
@@ -251,25 +206,20 @@ async function updateSetupProject(projectId, opts, origins, flags, { request, sa
251
206
  return data;
252
207
  }
253
208
 
254
- function projectSetupSummary({ project, env, challenge, readiness, access, origin, origins }) {
209
+ function projectSetupSummary({ project, env, challenge, readiness, origin, origins }) {
255
210
  const hardFailures = readiness.checks.filter((check) => !check.ok).map((check) => check.name);
256
211
  const configured = hardFailures.length === 0;
257
- const productionRequested = Boolean(access);
258
212
 
259
213
  return {
260
214
  object: "setup_project_result",
261
215
  ok: configured,
262
- status: !configured
263
- ? "failed"
264
- : productionRequested
265
- ? "production_requested"
266
- : "ready_for_browser_manual_check",
216
+ status: configured ? "ready_for_browser_manual_check" : "failed",
267
217
  configured,
268
218
  ready_for_browser_manual_check: configured,
269
- production_requested: productionRequested,
219
+ production_requested: false,
270
220
  project_id: project.id,
271
221
  project_slug: project.slug,
272
- client_gateway_url: project.virtual_microservice_url,
222
+ client_gateway_url: project.client_gateway_url,
273
223
  allowed_origins: origins,
274
224
  local_origin: origin,
275
225
  turnstile: challenge,
@@ -277,13 +227,11 @@ function projectSetupSummary({ project, env, challenge, readiness, access, origi
277
227
  env,
278
228
  doctor: readiness,
279
229
  hard_failures: hardFailures,
280
- production_access: access,
230
+ production_access: null,
281
231
  next_steps: [
282
232
  "Use the SDK-managed browser challenge in your app to confirm browser chat manually.",
283
233
  "Run switchboard verify setup after the app integration is wired.",
284
- productionRequested
285
- ? "Run switchboard verify publish after approval and billing readiness are complete."
286
- : "Use switchboard launch prepare when you are ready for production approval.",
234
+ "Run switchboard verify publish after production billing readiness is complete.",
287
235
  ],
288
236
  };
289
237
  }
@@ -298,16 +246,6 @@ export function humanProjectSetupMessage(result) {
298
246
  `Readiness: ${result.configured ? "ready for browser manual check" : "failed"}`,
299
247
  ];
300
248
 
301
- if (result.production_requested) {
302
- lines.push(
303
- `Production access: ${
304
- result.production_access?.project_production_access_status ||
305
- result.production_access?.status ||
306
- "requested"
307
- }`,
308
- );
309
- }
310
-
311
249
  if (result.hard_failures.length > 0) {
312
250
  lines.push(`Hard failures: ${result.hard_failures.join(", ")}`);
313
251
  }
@@ -315,12 +253,79 @@ export function humanProjectSetupMessage(result) {
315
253
  lines.push(
316
254
  "Next: integrate the SDK browser challenge in your app, then run switchboard verify setup.",
317
255
  );
256
+ return lines.join("\n");
257
+ }
318
258
 
319
- if (result.production_requested) {
320
- lines.push("Next: run switchboard verify publish after production approval and billing readiness.");
259
+ export async function ensureProject(opts, flags, { request = accountRequest, save = saveProjectConfig } = {}) {
260
+ if (opts.projectId) {
261
+ const project = await fetchProject(opts.projectId, flags, { request });
262
+ save(project);
263
+ return project;
321
264
  }
322
265
 
323
- return lines.join("\n");
266
+ if (opts.projectName) {
267
+ const { data } = await request("POST", "/projects", {
268
+ body: { name: opts.projectName, slug: opts.projectSlug },
269
+ json: flags.json,
270
+ });
271
+ save(data);
272
+ return data;
273
+ }
274
+
275
+ const { data } = await request("GET", "/me", { json: flags.json });
276
+ if (!data.project?.id) {
277
+ fail(
278
+ "No default project is selected. Use --project-id or --project-name with --project-slug.",
279
+ 1,
280
+ flags.json,
281
+ );
282
+ }
283
+ save(data.project);
284
+ return data.project;
285
+ }
286
+
287
+ export async function fetchProject(projectId, flags, { request = accountRequest } = {}) {
288
+ const { data } = await request("GET", `/projects/${projectId}`, { json: flags.json });
289
+ return data;
290
+ }
291
+
292
+ export async function provisionManagedTurnstile(
293
+ projectId,
294
+ idempotencyKey,
295
+ flags,
296
+ { request = accountRequest } = {},
297
+ ) {
298
+ const { data } = await request("POST", `/projects/${projectId}/turnstile/provision`, {
299
+ body: { idempotency_key: `${idempotencyKey}:turnstile` },
300
+ json: flags.json,
301
+ });
302
+ return data;
303
+ }
304
+
305
+ export function saveProjectConfig(project) {
306
+ saveConfig({
307
+ projectId: String(project.id),
308
+ clientGatewayUrl: project.client_gateway_url || null,
309
+ endUserSession: null,
310
+ });
311
+ }
312
+
313
+ function productionHttpsOrigin(origin) {
314
+ try {
315
+ const url = new URL(origin);
316
+ return (
317
+ url.protocol === "https:" &&
318
+ url.username === "" &&
319
+ url.password === "" &&
320
+ url.pathname === "/" &&
321
+ url.search === "" &&
322
+ url.hash === "" &&
323
+ !["localhost", "127.0.0.1", "::1"].includes(url.hostname) &&
324
+ !url.hostname.includes("*")
325
+ );
326
+ } catch {
327
+ return false;
328
+ }
324
329
  }
325
330
 
326
331
  function turnstileStatus(challenge) {
@@ -26,27 +26,4 @@ export function registerUsageCommands(program) {
26
26
  console.log(`Dev margin (micros): ${data.dev_margin_micros}`);
27
27
  }
28
28
  });
29
-
30
- usage
31
- .command("requests")
32
- .description("List recent gateway requests")
33
- .option("--mode <mode>", "all, sandbox, or live", "all")
34
- .option("--range <range>", "mtd, 7d, 30d, or all", "mtd")
35
- .option("--status <status>")
36
- .option("--q <query>")
37
- .option("--limit <limit>", "Maximum rows", parseInt)
38
- .action(async (opts, cmd) => {
39
- const flags = globalFlags(cmd);
40
- const params = new URLSearchParams();
41
- params.set("mode", opts.mode);
42
- params.set("range", opts.range);
43
- if (opts.status) params.set("status", opts.status);
44
- if (opts.q) params.set("q", opts.q);
45
- if (opts.limit != null) params.set("limit", String(opts.limit));
46
-
47
- const { data } = await accountRequest("GET", `/usage/requests?${params}`, {
48
- json: flags.json,
49
- });
50
- emit(flags.json ? data : JSON.stringify(data, null, 2), flags);
51
- });
52
29
  }
package/lib/config.js CHANGED
@@ -73,14 +73,13 @@ export function resolveConfig() {
73
73
  const file = loadConfig();
74
74
  return {
75
75
  baseUrl: process.env.SWITCHBOARD_BASE_URL || file.baseUrl || DEFAULT_BASE_URL,
76
- virtualMicroserviceUrl:
76
+ clientGatewayUrl:
77
77
  process.env.SWITCHBOARD_CLIENT_URL ||
78
78
  process.env.VITE_SWITCHBOARD_CLIENT_URL ||
79
- file.virtualMicroserviceUrl ||
79
+ file.clientGatewayUrl ||
80
80
  null,
81
81
  accountToken: null,
82
82
  accountTokenSource: null,
83
- apiKey: process.env.SWITCHBOARD_API_KEY || null,
84
83
  endUserSession:
85
84
  process.env.SWITCHBOARD_END_USER_SESSION || file.endUserSession || null,
86
85
  projectId: process.env.SWITCHBOARD_PROJECT_ID || file.projectId || null,
@@ -17,7 +17,6 @@ import { promisify } from "node:util";
17
17
  const execFileAsync = promisify(execFile);
18
18
  const SERVICE = "Switchboard CLI";
19
19
  const ACCOUNT = "account-session";
20
- const PROJECT_SECRET_PREFIX = "project-secret";
21
20
  const ACCOUNT_SESSION_FILE = "account-session.json";
22
21
 
23
22
  /**
@@ -197,139 +196,6 @@ function chmodAccountSessionFile(configDir) {
197
196
  }
198
197
  }
199
198
 
200
- /**
201
- * Reads a stored project secret key from the OS keychain.
202
- */
203
- export async function getProjectSecretKey(projectId, mode = "sandbox") {
204
- return getCredential(projectSecretAccount(projectId, mode));
205
- }
206
-
207
- /**
208
- * Stores a project secret key in the OS keychain.
209
- */
210
- export async function setProjectSecretKey(projectId, mode, token) {
211
- if (!token) {
212
- throw new Error("Cannot store an empty Switchboard project secret key");
213
- }
214
-
215
- return setCredential(projectSecretAccount(projectId, mode), token);
216
- }
217
-
218
- /**
219
- * Deletes a stored project secret key from the OS keychain.
220
- */
221
- export async function deleteProjectSecretKey(projectId, mode = "sandbox") {
222
- return deleteCredential(projectSecretAccount(projectId, mode));
223
- }
224
-
225
- function projectSecretAccount(projectId, mode) {
226
- if (!projectId) {
227
- throw new Error("Project id is required for Switchboard project secret storage");
228
- }
229
-
230
- return `${PROJECT_SECRET_PREFIX}:${projectId}:${mode}`;
231
- }
232
-
233
- async function getCredential(account) {
234
- try {
235
- if (process.platform === "darwin") {
236
- const { stdout } = await execFileAsync("security", [
237
- "find-generic-password",
238
- "-a",
239
- account,
240
- "-s",
241
- SERVICE,
242
- "-w",
243
- ]);
244
- return stdout.trim() || null;
245
- }
246
-
247
- if (process.platform === "linux") {
248
- const { stdout } = await execFileAsync("secret-tool", [
249
- "lookup",
250
- "service",
251
- SERVICE,
252
- "account",
253
- account,
254
- ]);
255
- return stdout.trim() || null;
256
- }
257
-
258
- throw unsupportedPlatformError();
259
- } catch (error) {
260
- if (credentialMissing(error)) {
261
- return null;
262
- }
263
-
264
- throw normalizeKeychainError(error);
265
- }
266
- }
267
-
268
- async function setCredential(account, token) {
269
- try {
270
- if (process.platform === "darwin") {
271
- await execFileAsync("security", [
272
- "add-generic-password",
273
- "-a",
274
- account,
275
- "-s",
276
- SERVICE,
277
- "-w",
278
- token,
279
- "-U",
280
- ]);
281
- return;
282
- }
283
-
284
- if (process.platform === "linux") {
285
- await execFileWithInput(
286
- "secret-tool",
287
- ["store", "--label", SERVICE, "service", SERVICE, "account", account],
288
- token,
289
- );
290
- return;
291
- }
292
-
293
- throw unsupportedPlatformError();
294
- } catch (error) {
295
- throw normalizeKeychainError(error);
296
- }
297
- }
298
-
299
- async function deleteCredential(account) {
300
- try {
301
- if (process.platform === "darwin") {
302
- await execFileAsync("security", [
303
- "delete-generic-password",
304
- "-a",
305
- account,
306
- "-s",
307
- SERVICE,
308
- ]);
309
- return;
310
- }
311
-
312
- if (process.platform === "linux") {
313
- await execFileAsync("secret-tool", [
314
- "clear",
315
- "service",
316
- SERVICE,
317
- "account",
318
- account,
319
- ]);
320
- return;
321
- }
322
-
323
- throw unsupportedPlatformError();
324
- } catch (error) {
325
- if (credentialMissing(error)) {
326
- return;
327
- }
328
-
329
- throw normalizeKeychainError(error);
330
- }
331
- }
332
-
333
199
  function credentialMissing(error) {
334
200
  return error?.code === 44 || error?.code === 1;
335
201
  }
package/lib/docsClient.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Public docs client used by CLI docs commands and the embedded MCP server.
3
3
  */
4
4
 
5
- import { gatewayApiUrl, resolveAccountConfig, resolveConfig, accountApiUrl } from "./config.js";
5
+ import { gatewayApiUrl, resolveConfig } from "./config.js";
6
6
  import { redactSecrets } from "./output.js";
7
7
 
8
8
  const DEFAULT_TIMEOUT_MS = 15_000;
@@ -43,54 +43,6 @@ export async function models({ config } = {}) {
43
43
  return redactSecrets(data);
44
44
  }
45
45
 
46
- export async function integrationKit({ stack, config } = {}) {
47
- let cfg;
48
- try {
49
- cfg = await resolveAccountConfig(config || resolveConfig());
50
- } catch (error) {
51
- return {
52
- ok: false,
53
- error: {
54
- type: "keychain_unavailable",
55
- message: error.message || "Could not read Switchboard account session from the OS keychain.",
56
- },
57
- };
58
- }
59
-
60
- if (!cfg.accountToken) {
61
- return {
62
- ok: false,
63
- error: {
64
- type: "authentication_required",
65
- message: "Run `switchboard auth login`, then select a project with `switchboard projects use <id>`.",
66
- },
67
- };
68
- }
69
-
70
- if (!cfg.projectId) {
71
- return {
72
- ok: false,
73
- error: {
74
- type: "project_required",
75
- message: "Select a project with `switchboard projects use <id>` or set SWITCHBOARD_PROJECT_ID.",
76
- },
77
- };
78
- }
79
-
80
- const params = new URLSearchParams({ project_id: cfg.projectId });
81
- if (stack) params.set("stack", stack);
82
-
83
- const url = `${accountApiUrl(cfg)}/integration_kit?${params}`;
84
- const data = await fetchJson(url, {
85
- headers: {
86
- Authorization: `Bearer ${cfg.accountToken}`,
87
- Accept: "application/json",
88
- },
89
- });
90
-
91
- return redactSecrets(data);
92
- }
93
-
94
46
  export async function publicResource(name, options = {}) {
95
47
  switch (name) {
96
48
  case "docs":
@@ -101,8 +53,6 @@ export async function publicResource(name, options = {}) {
101
53
  return readDoc("knowledge", options);
102
54
  case "openapi":
103
55
  return openApi(options);
104
- case "integration-kit":
105
- return integrationKit(options);
106
56
  case "capabilities":
107
57
  return docsCapabilities(options);
108
58
  default:
package/lib/mcpServer.js CHANGED
@@ -7,7 +7,6 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
  import { z } from "zod";
8
8
  import {
9
9
  docsCapabilities,
10
- integrationKit,
11
10
  listDocs,
12
11
  models,
13
12
  openApi,
@@ -53,13 +52,6 @@ function registerResources(server) {
53
52
  registerJsonResource(server, "switchboard_openapi", "switchboard://openapi", "Switchboard OpenAPI schema", () =>
54
53
  openApi(),
55
54
  );
56
- registerJsonResource(
57
- server,
58
- "switchboard_integration_kit",
59
- "switchboard://integration-kit",
60
- "Project Integration Kit for the selected CLI project",
61
- () => integrationKit(),
62
- );
63
55
  registerJsonResource(
64
56
  server,
65
57
  "switchboard_capabilities",
@@ -132,19 +124,6 @@ function registerTools(server) {
132
124
  async ({ id }) => jsonTool(await readDoc(id)),
133
125
  );
134
126
 
135
- server.registerTool(
136
- "switchboard_integration_kit",
137
- {
138
- title: "Switchboard Integration Kit",
139
- description: "Return Integration Kit data for the logged-in selected CLI project.",
140
- inputSchema: {
141
- stack: z.string().optional(),
142
- },
143
- annotations: { readOnlyHint: true },
144
- },
145
- async ({ stack }) => jsonTool(await integrationKit({ stack })),
146
- );
147
-
148
127
  server.registerTool(
149
128
  "switchboard_models",
150
129
  {