@switchboard.spot/cli 0.2.0 → 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.
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Hosted docs and embedded MCP commands.
3
+ */
4
+
5
+ import {
6
+ docsCapabilities,
7
+ listDocs,
8
+ readDoc,
9
+ searchDocs,
10
+ DocsClientError,
11
+ } from "../docsClient.js";
12
+ import { runMcpServer } from "../mcpServer.js";
13
+ import { emit, fail, globalFlags, redactSecrets } from "../output.js";
14
+
15
+ export function registerDocsCommands(program) {
16
+ const docs = program.command("docs").description("Public docs and MCP server");
17
+
18
+ docs
19
+ .command("list")
20
+ .description("List public Switchboard docs")
21
+ .option("--json", "Output JSON to stdout")
22
+ .action(async (_opts, cmd) => {
23
+ await runDocsAction(cmd, async (flags) => {
24
+ const result = await listDocs();
25
+ if (jsonMode(cmd)) return emit(result, { json: true });
26
+
27
+ for (const doc of result.data || []) {
28
+ emit(`${doc.id}\t${doc.title}\t${doc.url}`, flags);
29
+ }
30
+ });
31
+ });
32
+
33
+ docs
34
+ .command("read")
35
+ .description("Read one public Switchboard doc")
36
+ .argument("<id>", "Stable public doc id")
37
+ .option("--json", "Output JSON to stdout")
38
+ .action(async (id, _opts, cmd) => {
39
+ await runDocsAction(cmd, async (flags) => {
40
+ const result = await readDoc(id);
41
+ if (jsonMode(cmd)) return emit(result, { json: true });
42
+ emit(result.data?.content || "", flags);
43
+ });
44
+ });
45
+
46
+ docs
47
+ .command("search")
48
+ .description("Search public Switchboard docs")
49
+ .argument("<query>", "Search query")
50
+ .option("--limit <count>", "Maximum number of snippets", parseInteger)
51
+ .option("--json", "Output JSON to stdout")
52
+ .action(async (query, opts, cmd) => {
53
+ await runDocsAction(cmd, async (flags) => {
54
+ const result = await searchDocs(query, { limit: opts.limit });
55
+ if (jsonMode(cmd)) return emit(result, { json: true });
56
+
57
+ for (const match of result.data || []) {
58
+ emit(`${match.id}\t${match.title}\n${match.snippet}\n`, flags);
59
+ }
60
+ });
61
+ });
62
+
63
+ docs
64
+ .command("mcp")
65
+ .description("Start the embedded Switchboard MCP stdio server")
66
+ .action(async () => {
67
+ await runMcpServer();
68
+ });
69
+
70
+ docs
71
+ .command("mcp-config")
72
+ .description("Print MCP client configuration")
73
+ .requiredOption("--client <client>", "MCP client: codex, claude, or cursor")
74
+ .option("--json", "Output JSON to stdout")
75
+ .action(async (opts, cmd) => {
76
+ const client = opts.client.toLowerCase();
77
+ const config = mcpConfig(client);
78
+
79
+ if (!config) {
80
+ fail("Unsupported MCP client. Expected one of: codex, claude, cursor.", 1, jsonMode(cmd));
81
+ }
82
+
83
+ if (jsonMode(cmd)) {
84
+ emit(config, { json: true });
85
+ } else {
86
+ emit(formatConfig(client, config), globalFlags(cmd));
87
+ }
88
+ });
89
+
90
+ docs
91
+ .command("capabilities")
92
+ .description("Print hosted MCP capability metadata")
93
+ .option("--json", "Output JSON to stdout")
94
+ .action(async (_opts, cmd) => {
95
+ await runDocsAction(cmd, async (flags) => {
96
+ const result = await docsCapabilities();
97
+ emit(result, { json: jsonMode(cmd), quiet: flags.quiet });
98
+ });
99
+ });
100
+ }
101
+
102
+ async function runDocsAction(cmd, callback) {
103
+ const flags = globalFlags(cmd);
104
+
105
+ try {
106
+ await callback(flags);
107
+ } catch (error) {
108
+ if (error instanceof DocsClientError) {
109
+ fail(error.message, exitCode(error.status), jsonMode(cmd), error.type);
110
+ }
111
+
112
+ fail(error.message || "Switchboard docs command failed.", 1, jsonMode(cmd));
113
+ }
114
+ }
115
+
116
+ function jsonMode(cmd) {
117
+ return Boolean(cmd.opts?.().json || globalFlags(cmd).json);
118
+ }
119
+
120
+ function exitCode(status) {
121
+ if (status === 401 || status === 403) return 2;
122
+ if (status >= 500 || status === 3) return 3;
123
+ return 1;
124
+ }
125
+
126
+ function parseInteger(value) {
127
+ const parsed = Number.parseInt(value, 10);
128
+ if (!Number.isInteger(parsed)) {
129
+ throw new Error("Limit must be an integer.");
130
+ }
131
+ return parsed;
132
+ }
133
+
134
+ function mcpConfig(client) {
135
+ const command = "switchboard";
136
+ const args = ["docs", "mcp"];
137
+
138
+ switch (client) {
139
+ case "codex":
140
+ return {
141
+ mcp_servers: {
142
+ switchboard: {
143
+ command,
144
+ args,
145
+ env: {
146
+ SWITCHBOARD_BASE_URL: "https://switchboard.spot",
147
+ },
148
+ },
149
+ },
150
+ };
151
+ case "claude":
152
+ case "cursor":
153
+ return {
154
+ mcpServers: {
155
+ switchboard: {
156
+ command,
157
+ args,
158
+ env: {
159
+ SWITCHBOARD_BASE_URL: "https://switchboard.spot",
160
+ },
161
+ },
162
+ },
163
+ };
164
+ default:
165
+ return null;
166
+ }
167
+ }
168
+
169
+ function formatConfig(client, config) {
170
+ return [
171
+ `# ${client} MCP config`,
172
+ "```json",
173
+ JSON.stringify(redactSecrets(config), null, 2),
174
+ "```",
175
+ ].join("\n");
176
+ }
@@ -67,8 +67,57 @@ export function registerDoctorCommand(program) {
67
67
  checks.push({ name: "project", ok: false, error: "No project selected" });
68
68
  }
69
69
 
70
+ if (config.virtualMicroserviceUrl) {
71
+ checks.push(
72
+ await check("client_gateway_config", async () => {
73
+ const res = await fetch(new URL("client/config", ensureTrailingSlash(config.virtualMicroserviceUrl)).href, {
74
+ headers: { Accept: "application/json" },
75
+ });
76
+ const data = await readJson(res);
77
+ if (!res.ok) {
78
+ throw new Error(data?.error?.message || data?.message || `HTTP ${res.status}`);
79
+ }
80
+
81
+ const productionSafety = data?.production_safety ?? null;
82
+ const productionBlocked = productionSafety?.status === "blocked";
83
+
84
+ return {
85
+ status: res.status,
86
+ clientUrl: config.virtualMicroserviceUrl,
87
+ browserChallengeProvider: data?.browser_challenge?.provider ?? null,
88
+ production_safety: productionSafety,
89
+ warning: productionBlocked,
90
+ warningCode: productionBlocked ? "production_safety_blocked" : null,
91
+ warningMessage: productionBlocked
92
+ ? "Sandbox Client Gateway config is reachable, but production launch blockers remain."
93
+ : null,
94
+ };
95
+ }),
96
+ );
97
+ } else {
98
+ checks.push({
99
+ name: "client_gateway_config",
100
+ ok: false,
101
+ error: "No SWITCHBOARD_CLIENT_URL or VITE_SWITCHBOARD_CLIENT_URL configured",
102
+ });
103
+ }
104
+
70
105
  const ok = checks.every((item) => item.ok);
71
106
  emit({ object: "doctor_report", ok, baseUrl: config.baseUrl, checks }, flags);
72
107
  if (!ok) process.exit(1);
108
+ process.exit(0);
73
109
  });
74
110
  }
111
+
112
+ function ensureTrailingSlash(value) {
113
+ return String(value).endsWith("/") ? String(value) : `${value}/`;
114
+ }
115
+
116
+ async function readJson(response) {
117
+ const text = await response.text();
118
+ try {
119
+ return text ? JSON.parse(text) : null;
120
+ } catch {
121
+ return { raw: text };
122
+ }
123
+ }
@@ -88,7 +88,9 @@ export async function configureEnvironment(
88
88
  const envUpdates = {};
89
89
 
90
90
  if (mode === "client" || mode === "both") {
91
- envUpdates.SWITCHBOARD_CLIENT_URL = kit.client_url || kit.virtual_microservice_url;
91
+ const clientUrl = kit.client_url || kit.virtual_microservice_url;
92
+ envUpdates.SWITCHBOARD_CLIENT_URL = clientUrl;
93
+ envUpdates.VITE_SWITCHBOARD_CLIENT_URL = clientUrl;
92
94
  }
93
95
 
94
96
  if (mode === "server" || mode === "both") {
@@ -14,6 +14,7 @@ import { emit } from "../output.js";
14
14
  */
15
15
  function envBlock(clientUrl) {
16
16
  return `SWITCHBOARD_CLIENT_URL=${clientUrl}
17
+ VITE_SWITCHBOARD_CLIENT_URL=${clientUrl}
17
18
  `;
18
19
  }
19
20
 
@@ -32,15 +33,17 @@ export function agentManifest(config) {
32
33
  env: envBlock(clientUrl),
33
34
  checklist: [
34
35
  "switchboard setup --target client --json",
35
- "export SWITCHBOARD_CLIENT_URL=<client_url from integrations show>",
36
+ "export VITE_SWITCHBOARD_CLIENT_URL=<client_url from integration kit>",
36
37
  "Use mountSwitchboardWidget({ clientUrl, target }) in browser apps",
37
38
  "Use createSwitchboardClient({ clientUrl, storage }) only for custom UI",
38
- "switchboard billing top-up --amount-micros <micros>",
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",
39
42
  ],
40
- browser_smoke_test: `import { mountSwitchboardWidget } from "@switchboard/sdk";
43
+ browser_smoke_test: `import { mountSwitchboardWidget } from "@switchboard.spot/sdk";
41
44
 
42
45
  mountSwitchboardWidget({
43
- clientUrl: process.env.SWITCHBOARD_CLIENT_URL ?? "${clientUrl}",
46
+ clientUrl: import.meta.env.VITE_SWITCHBOARD_CLIENT_URL ?? "${clientUrl}",
44
47
  target: "#switchboard",
45
48
  });`,
46
49
  automation_note:
@@ -0,0 +1,192 @@
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
+ 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
+ async function ensureProject(opts, flags) {
64
+ if (opts.projectId) return fetchProject(opts.projectId, flags);
65
+
66
+ if (opts.projectName) {
67
+ const { data } = await accountRequest("POST", "/projects", {
68
+ body: { name: opts.projectName, slug: opts.projectSlug },
69
+ json: flags.json,
70
+ });
71
+ saveProjectConfig(data);
72
+ return data;
73
+ }
74
+
75
+ const { data } = await accountRequest("GET", "/me", { json: flags.json });
76
+ if (!data.project?.id) {
77
+ fail(
78
+ "No default project is selected. Use --project-id or --project-name with --project-slug.",
79
+ 1,
80
+ flags.json,
81
+ );
82
+ }
83
+ return data.project;
84
+ }
85
+
86
+ async function updateLaunchProject(projectId, opts, flags) {
87
+ const body = {
88
+ allowed_origins: [opts.productionOrigin],
89
+ end_user_terms_url: opts.endUserTermsUrl,
90
+ end_user_privacy_url: opts.endUserPrivacyUrl,
91
+ support_email: opts.supportEmail,
92
+ virtual_microservice_enabled: true,
93
+ };
94
+
95
+ if (opts.supportUrl) body.support_url = opts.supportUrl;
96
+
97
+ const { data } = await accountRequest("PATCH", `/projects/${projectId}`, {
98
+ body,
99
+ json: flags.json,
100
+ });
101
+ saveProjectConfig(data);
102
+ return data;
103
+ }
104
+
105
+ async function provisionManagedTurnstile(projectId, idempotencyKey, flags) {
106
+ const { data } = await accountRequest("POST", `/projects/${projectId}/turnstile/provision`, {
107
+ body: { idempotency_key: `${idempotencyKey}:turnstile` },
108
+ json: flags.json,
109
+ });
110
+ return data;
111
+ }
112
+
113
+ async function requestAccessIfNeeded(projectId, project, opts, flags) {
114
+ if (project.production_access_status === "approved") {
115
+ return {
116
+ object: "production_access_request",
117
+ project_id: project.id,
118
+ project_production_access_status: "approved",
119
+ status: "approved",
120
+ };
121
+ }
122
+
123
+ const { data } = await accountRequest("POST", `/projects/${projectId}/production_access_request`, {
124
+ body: buildProductionAccessRequestBody(opts),
125
+ json: flags.json,
126
+ });
127
+ return data;
128
+ }
129
+
130
+ async function fetchProject(projectId, flags) {
131
+ const { data } = await accountRequest("GET", `/projects/${projectId}`, { json: flags.json });
132
+ return data;
133
+ }
134
+
135
+ function saveProjectConfig(project) {
136
+ saveConfig({
137
+ projectId: String(project.id),
138
+ apiKey: null,
139
+ virtualMicroserviceUrl: project.virtual_microservice_url || null,
140
+ endUserSession: null,
141
+ });
142
+ }
143
+
144
+ function launchSummary(project, challenge, access) {
145
+ return {
146
+ object: "launch_prepare_result",
147
+ project_id: project.id,
148
+ project_slug: project.slug,
149
+ virtual_microservice_url: project.virtual_microservice_url,
150
+ browser_challenge: challenge,
151
+ production_access: access,
152
+ production_safety: project.production_safety,
153
+ next_steps: [
154
+ "Run switchboard verify setup.",
155
+ "Use the SDK-managed browser challenge to mint browser sessions.",
156
+ "Run switchboard verify publish after approval and billing readiness are complete.",
157
+ ],
158
+ };
159
+ }
160
+
161
+ function humanLaunchSummary(project, access) {
162
+ const blocked = (project.production_safety?.checks || [])
163
+ .filter((check) => check.status !== "ready")
164
+ .map((check) => check.id);
165
+
166
+ return [
167
+ `Prepared project ${project.name} (${project.id})`,
168
+ `Client Gateway: ${project.virtual_microservice_url}`,
169
+ `Production access: ${access.project_production_access_status || access.status}`,
170
+ `Readiness: ${project.production_safety?.status || "unknown"}`,
171
+ blocked.length ? `Blocked checks: ${blocked.join(", ")}` : "Blocked checks: none",
172
+ "Next: run switchboard verify setup, then switchboard verify publish after approval and billing readiness.",
173
+ ].join("\n");
174
+ }
175
+
176
+ function productionHttpsOrigin(origin) {
177
+ try {
178
+ const url = new URL(origin);
179
+ return (
180
+ url.protocol === "https:" &&
181
+ url.username === "" &&
182
+ url.password === "" &&
183
+ url.pathname === "/" &&
184
+ url.search === "" &&
185
+ url.hash === "" &&
186
+ !["localhost", "127.0.0.1", "::1"].includes(url.hostname) &&
187
+ !url.hostname.includes("*")
188
+ );
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
@@ -90,10 +90,9 @@ export function registerProjectsCommands(program) {
90
90
 
91
91
  projects
92
92
  .command("turnstile <id>")
93
- .description("Configure project-owned Turnstile keys")
93
+ .description("Configure project-owned public Turnstile metadata")
94
94
  .option("--site-key <key>")
95
- .option("--secret-key <key>")
96
- .option("--clear", "Remove project-owned Turnstile keys")
95
+ .option("--clear", "Remove project-owned Turnstile metadata")
97
96
  .action(async (id, opts, cmd) => {
98
97
  const flags = globalFlags(cmd);
99
98
  const body = buildTurnstileBody(opts);
@@ -104,6 +103,39 @@ export function registerProjectsCommands(program) {
104
103
  emit(flags.json ? data : `Updated Turnstile settings for ${data.name}`, flags);
105
104
  });
106
105
 
106
+ projects
107
+ .command("provision-turnstile <id>")
108
+ .description("Provision Switchboard-managed Turnstile for a project")
109
+ .option("--idempotency-key <key>")
110
+ .action(async (id, opts, cmd) => {
111
+ const flags = globalFlags(cmd);
112
+ const body = {};
113
+ if (opts.idempotencyKey) body.idempotency_key = opts.idempotencyKey;
114
+
115
+ const { data } = await accountRequest("POST", `/projects/${id}/turnstile/provision`, {
116
+ body,
117
+ json: flags.json,
118
+ });
119
+ emit(flags.json ? data : `Provisioned managed Turnstile for project ${id}`, flags);
120
+ });
121
+
122
+ projects
123
+ .command("request-production-access <id>")
124
+ .description("Submit a production access request")
125
+ .requiredOption("--contact-email <email>")
126
+ .requiredOption("--use-case <text>")
127
+ .option("--expected-monthly-volume <volume>")
128
+ .option("--needed-billing-mode <mode>", "Billing mode needed for production", "developer_paid")
129
+ .option("--notes <text>")
130
+ .action(async (id, opts, cmd) => {
131
+ const flags = globalFlags(cmd);
132
+ const { data } = await accountRequest("POST", `/projects/${id}/production_access_request`, {
133
+ body: buildProductionAccessRequestBody(opts),
134
+ json: flags.json,
135
+ });
136
+ emit(flags.json ? data : `Submitted production access request for project ${id}`, flags);
137
+ });
138
+
107
139
  projects
108
140
  .command("use <id>")
109
141
  .description("Set default project for subsequent commands")
@@ -186,7 +218,21 @@ export function buildTurnstileBody(opts) {
186
218
 
187
219
  const body = {};
188
220
  if (opts.siteKey) body.turnstile_site_key = opts.siteKey;
189
- if (opts.secretKey) body.turnstile_secret_key = opts.secretKey;
221
+ return body;
222
+ }
223
+
224
+ export function buildProductionAccessRequestBody(opts) {
225
+ const body = {
226
+ contact_email: opts.contactEmail,
227
+ use_case: opts.useCase,
228
+ needed_billing_mode: opts.neededBillingMode || "developer_paid",
229
+ };
230
+
231
+ if (opts.expectedMonthlyVolume) {
232
+ body.expected_monthly_volume = opts.expectedMonthlyVolume;
233
+ }
234
+ if (opts.notes) body.notes = opts.notes;
235
+
190
236
  return body;
191
237
  }
192
238
 
@@ -53,13 +53,16 @@ function normalizeSetupTarget(target, json) {
53
53
  }
54
54
 
55
55
  function smokeCheck(result) {
56
+ const clientEnvConfigured =
57
+ result.changed.includes("VITE_SWITCHBOARD_CLIENT_URL") ||
58
+ result.skipped.includes("VITE_SWITCHBOARD_CLIENT_URL") ||
59
+ result.changed.includes("SWITCHBOARD_CLIENT_URL") ||
60
+ result.skipped.includes("SWITCHBOARD_CLIENT_URL");
61
+
56
62
  return {
57
63
  ok: true,
58
64
  checks: [
59
- result.changed.includes("SWITCHBOARD_CLIENT_URL") ||
60
- result.skipped.includes("SWITCHBOARD_CLIENT_URL")
61
- ? "client_env"
62
- : null,
65
+ clientEnvConfigured ? "client_env" : null,
63
66
  result.changed.includes("SWITCHBOARD_BASE_URL") ||
64
67
  result.skipped.includes("SWITCHBOARD_BASE_URL")
65
68
  ? "server_env"
package/lib/config.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import fs from "fs";
7
7
  import os from "os";
8
8
  import path from "path";
9
- import { getAccountToken } from "./credentialStore.js";
9
+ import { getAccountToken, getConfigDirAccountToken } from "./credentialStore.js";
10
10
 
11
11
  const CONFIG_DIR =
12
12
  process.env.SWITCHBOARD_CONFIG_DIR || path.join(os.homedir(), ".switchboard");
@@ -74,7 +74,10 @@ export function resolveConfig() {
74
74
  return {
75
75
  baseUrl: process.env.SWITCHBOARD_BASE_URL || file.baseUrl || DEFAULT_BASE_URL,
76
76
  virtualMicroserviceUrl:
77
- process.env.SWITCHBOARD_CLIENT_URL || file.virtualMicroserviceUrl || null,
77
+ process.env.SWITCHBOARD_CLIENT_URL ||
78
+ process.env.VITE_SWITCHBOARD_CLIENT_URL ||
79
+ file.virtualMicroserviceUrl ||
80
+ null,
78
81
  accountToken: null,
79
82
  accountTokenSource: null,
80
83
  apiKey: process.env.SWITCHBOARD_API_KEY || null,
@@ -106,15 +109,43 @@ export async function resolveAccountConfig(config) {
106
109
  }
107
110
 
108
111
  const base = config || resolveConfig();
109
- const token = await getAccountToken();
112
+ const configDirToken = getConfigDirAccountToken();
113
+ if (configDirToken) {
114
+ return {
115
+ ...base,
116
+ accountToken: configDirToken,
117
+ accountTokenSource: "config-dir",
118
+ };
119
+ }
120
+
121
+ const keychain = await readKeychainAccountToken();
122
+ if (keychain.token) {
123
+ return {
124
+ ...base,
125
+ accountToken: keychain.token,
126
+ accountTokenSource: "keychain",
127
+ };
128
+ }
129
+
130
+ if (keychain.error) {
131
+ throw keychain.error;
132
+ }
110
133
 
111
134
  return {
112
135
  ...base,
113
- accountToken: token,
114
- accountTokenSource: token ? "keychain" : null,
136
+ accountToken: null,
137
+ accountTokenSource: null,
115
138
  };
116
139
  }
117
140
 
141
+ async function readKeychainAccountToken() {
142
+ try {
143
+ return { token: await getAccountToken(), error: null };
144
+ } catch (error) {
145
+ return { token: null, error };
146
+ }
147
+ }
148
+
118
149
  /**
119
150
  * Account API base URL (host + /v1/account).
120
151
  */