@switchboard.spot/cli 0.2.0

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,197 @@
1
+ /**
2
+ * Project 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
+ export function registerProjectsCommands(program) {
10
+ const projects = program.command("projects").description("Projects");
11
+
12
+ projects
13
+ .command("list")
14
+ .description("List projects")
15
+ .action(async (_opts, cmd) => {
16
+ const flags = globalFlags(cmd);
17
+ const { data } = await accountRequest("GET", "/projects", { json: flags.json });
18
+ if (flags.json) {
19
+ emit(data, flags);
20
+ } else {
21
+ printList("Projects:", data.data || [], (p) =>
22
+ ` ${p.id} ${p.name} (${p.slug}) — ${p.api_key_count} keys`,
23
+ );
24
+ }
25
+ });
26
+
27
+ projects
28
+ .command("create")
29
+ .description("Create a project")
30
+ .requiredOption("--name <name>")
31
+ .requiredOption("--slug <slug>")
32
+ .action(async (opts, cmd) => {
33
+ const flags = globalFlags(cmd);
34
+ const { data } = await accountRequest("POST", "/projects", {
35
+ body: { name: opts.name, slug: opts.slug },
36
+ json: flags.json,
37
+ });
38
+ saveConfig({
39
+ projectId: String(data.id),
40
+ apiKey: null,
41
+ virtualMicroserviceUrl: data.virtual_microservice_url || null,
42
+ endUserSession: null,
43
+ });
44
+ emit(flags.json ? data : `Created project ${data.name} (${data.id})`, flags);
45
+ });
46
+
47
+ projects
48
+ .command("show <id>")
49
+ .description("Show a project")
50
+ .action(async (id, _opts, cmd) => {
51
+ const flags = globalFlags(cmd);
52
+ const { data } = await accountRequest("GET", `/projects/${id}`, { json: flags.json });
53
+ emit(flags.json ? data : JSON.stringify(data, null, 2), flags);
54
+ });
55
+
56
+ projects
57
+ .command("update <id>")
58
+ .description("Update project settings")
59
+ .option("--name <name>")
60
+ .option("--slug <slug>")
61
+ .option("--billing-mode <mode>")
62
+ .option("--embed-billing-end-user-ref <ref>")
63
+ .option("--end-user-terms-url <url>")
64
+ .option("--end-user-privacy-url <url>")
65
+ .option("--support-url <url>")
66
+ .option("--support-email <email>")
67
+ .option(
68
+ "--allowed-origins <urls>",
69
+ "Comma-separated browser origins for Pro-bono Embed requests",
70
+ )
71
+ .option(
72
+ "--allowed-ios-bundle-ids <ids>",
73
+ "Comma-separated iOS bundle IDs",
74
+ )
75
+ .option(
76
+ "--allowed-android-packages <packages>",
77
+ "Comma-separated Android package names",
78
+ )
79
+ .option("--virtual-microservice-enabled <boolean>")
80
+ .action(async (id, opts, cmd) => {
81
+ const flags = globalFlags(cmd);
82
+ const body = buildProjectUpdateBody(opts);
83
+
84
+ const { data } = await accountRequest("PATCH", `/projects/${id}`, {
85
+ body,
86
+ json: flags.json,
87
+ });
88
+ emit(flags.json ? data : `Updated project ${data.name}`, flags);
89
+ });
90
+
91
+ projects
92
+ .command("turnstile <id>")
93
+ .description("Configure project-owned Turnstile keys")
94
+ .option("--site-key <key>")
95
+ .option("--secret-key <key>")
96
+ .option("--clear", "Remove project-owned Turnstile keys")
97
+ .action(async (id, opts, cmd) => {
98
+ const flags = globalFlags(cmd);
99
+ const body = buildTurnstileBody(opts);
100
+ const { data } = await accountRequest("PATCH", `/projects/${id}`, {
101
+ body,
102
+ json: flags.json,
103
+ });
104
+ emit(flags.json ? data : `Updated Turnstile settings for ${data.name}`, flags);
105
+ });
106
+
107
+ projects
108
+ .command("use <id>")
109
+ .description("Set default project for subsequent commands")
110
+ .action(async (id, _opts, cmd) => {
111
+ const flags = globalFlags(cmd);
112
+ const { data } = await accountRequest("GET", `/projects/${id}`, { json: flags.json });
113
+ saveConfig({
114
+ projectId: String(data.id),
115
+ apiKey: null,
116
+ virtualMicroserviceUrl: data.virtual_microservice_url || null,
117
+ endUserSession: null,
118
+ });
119
+ emit(flags.json ? data : `Using project ${data.name} (${data.id})`, flags);
120
+ });
121
+
122
+ projects
123
+ .command("delete <id>")
124
+ .description("Delete a project")
125
+ .action(async (id, _opts, cmd) => {
126
+ const flags = globalFlags(cmd);
127
+ const { data } = await accountRequest("DELETE", `/projects/${id}`, {
128
+ json: flags.json,
129
+ });
130
+ emit(flags.json ? data : `Deleted project ${data.name}`, flags);
131
+ });
132
+
133
+ projects
134
+ .command("restore <id>")
135
+ .description("Restore a deleted project")
136
+ .action(async (id, _opts, cmd) => {
137
+ const flags = globalFlags(cmd);
138
+ const { data } = await accountRequest("POST", `/projects/${id}/restore`, {
139
+ json: flags.json,
140
+ });
141
+ emit(flags.json ? data : `Restored project ${data.name}`, flags);
142
+ });
143
+ }
144
+
145
+ /**
146
+ * Splits a comma-separated CLI option into a trimmed string array.
147
+ */
148
+ function splitList(value) {
149
+ return value
150
+ .split(",")
151
+ .map((s) => s.trim())
152
+ .filter(Boolean);
153
+ }
154
+
155
+ export function buildProjectUpdateBody(opts) {
156
+ const body = {};
157
+ if (opts.name) body.name = opts.name;
158
+ if (opts.slug) body.slug = opts.slug;
159
+ if (opts.billingMode) body.billing_mode = opts.billingMode;
160
+ if (opts.embedBillingEndUserRef) {
161
+ body.embed_billing_end_user_ref = opts.embedBillingEndUserRef;
162
+ }
163
+ if (opts.endUserTermsUrl) body.end_user_terms_url = opts.endUserTermsUrl;
164
+ if (opts.endUserPrivacyUrl) body.end_user_privacy_url = opts.endUserPrivacyUrl;
165
+ if (opts.supportUrl) body.support_url = opts.supportUrl;
166
+ if (opts.supportEmail) body.support_email = opts.supportEmail;
167
+ if (opts.allowedOrigins != null) {
168
+ body.allowed_origins = splitList(opts.allowedOrigins);
169
+ }
170
+ if (opts.allowedIosBundleIds != null) {
171
+ body.allowed_ios_bundle_ids = splitList(opts.allowedIosBundleIds);
172
+ }
173
+ if (opts.allowedAndroidPackages != null) {
174
+ body.allowed_android_packages = splitList(opts.allowedAndroidPackages);
175
+ }
176
+ if (opts.virtualMicroserviceEnabled != null) {
177
+ body.virtual_microservice_enabled = parseBoolean(opts.virtualMicroserviceEnabled);
178
+ }
179
+ return body;
180
+ }
181
+
182
+ export function buildTurnstileBody(opts) {
183
+ if (opts.clear) {
184
+ return { turnstile_site_key: "", turnstile_secret_key: "" };
185
+ }
186
+
187
+ const body = {};
188
+ if (opts.siteKey) body.turnstile_site_key = opts.siteKey;
189
+ if (opts.secretKey) body.turnstile_secret_key = opts.secretKey;
190
+ return body;
191
+ }
192
+
193
+ function parseBoolean(value) {
194
+ if (value === true || value === "true") return true;
195
+ if (value === false || value === "false") return false;
196
+ throw new Error(`Expected boolean value true or false, got ${value}`);
197
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Guided Switchboard setup.
3
+ */
4
+
5
+ import { configureEnvironment } from "./env.js";
6
+ import { emit, fail, globalFlags } from "../output.js";
7
+
8
+ export function registerSetupCommand(program) {
9
+ program
10
+ .command("setup")
11
+ .description("Configure Switchboard for client apps, server apps, or both")
12
+ .option("--target <target>", "client, server, or both", "client")
13
+ .option("--file <path>", "Local env file to update", ".env.local")
14
+ .option("--secret-target <target>", "local or exec", "local")
15
+ .option("--secret-command <command>", "Command that stores secrets from stdin JSON")
16
+ .option("--project-id <id>", "Override selected project")
17
+ .option("--force", "Replace existing Switchboard-managed values")
18
+ .action(async (opts, cmd) => {
19
+ const flags = globalFlags(cmd);
20
+ const result = await setupSwitchboard(opts, { json: flags.json });
21
+ emit(flags.json ? result : humanSetupMessage(result), flags);
22
+ });
23
+ }
24
+
25
+ export async function setupSwitchboard(opts, dependencies = {}) {
26
+ const target = normalizeSetupTarget(opts.target, dependencies.json);
27
+ const result = await configureEnvironment(
28
+ {
29
+ mode: target,
30
+ file: opts.file,
31
+ target: opts.secretTarget,
32
+ secretCommand: opts.secretCommand,
33
+ projectId: opts.projectId,
34
+ force: opts.force,
35
+ },
36
+ dependencies,
37
+ );
38
+
39
+ return {
40
+ ...result,
41
+ setup: true,
42
+ target,
43
+ smoke_check: smokeCheck(result),
44
+ };
45
+ }
46
+
47
+ function normalizeSetupTarget(target, json) {
48
+ if (target === "client" || target === "server" || target === "both") {
49
+ return target;
50
+ }
51
+
52
+ fail("--target must be client, server, or both", 1, json);
53
+ }
54
+
55
+ function smokeCheck(result) {
56
+ return {
57
+ ok: true,
58
+ checks: [
59
+ result.changed.includes("SWITCHBOARD_CLIENT_URL") ||
60
+ result.skipped.includes("SWITCHBOARD_CLIENT_URL")
61
+ ? "client_env"
62
+ : null,
63
+ result.changed.includes("SWITCHBOARD_BASE_URL") ||
64
+ result.skipped.includes("SWITCHBOARD_BASE_URL")
65
+ ? "server_env"
66
+ : null,
67
+ result.secrets.length > 0 ? "server_secret" : null,
68
+ ].filter(Boolean),
69
+ };
70
+ }
71
+
72
+ function humanSetupMessage(result) {
73
+ const changed = result.changed.length > 0 ? result.changed.join(", ") : "none";
74
+ const skipped = result.skipped.length > 0 ? ` Skipped existing: ${result.skipped.join(", ")}.` : "";
75
+ return `Configured Switchboard setup (${result.target}) for project ${result.project_id}. Changed: ${changed}.${skipped}`;
76
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Usage analytics commands.
3
+ */
4
+
5
+ import { accountRequest } from "../client.js";
6
+ import { emit, globalFlags } from "../output.js";
7
+
8
+ export function registerUsageCommands(program) {
9
+ const usage = program
10
+ .command("usage")
11
+ .description("Usage and revenue summary")
12
+ .option("--mode <mode>", "all, sandbox, or live", "all")
13
+ .action(async (opts, cmd) => {
14
+ const global = globalFlags(cmd);
15
+ const json = global.json;
16
+ const { data } = await accountRequest("GET", `/usage?mode=${opts.mode}`, {
17
+ json,
18
+ });
19
+ if (json) {
20
+ emit(data, { json });
21
+ } else {
22
+ console.log(`Mode: ${data.mode}`);
23
+ console.log(`Requests: ${data.request_count}`);
24
+ console.log(`Tokens: ${data.total_tokens}`);
25
+ console.log(`End-user charges (micros): ${data.end_user_charge_micros}`);
26
+ console.log(`Dev margin (micros): ${data.dev_margin_micros}`);
27
+ }
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
+ }
@@ -0,0 +1,143 @@
1
+ import { accountApiUrl, resolveAccountConfig, resolveConfig } from "../config.js";
2
+ import { emit, globalFlags } from "../output.js";
3
+ import {
4
+ VerificationInputError,
5
+ verifySwitchboardBrowser,
6
+ verifySwitchboardPublish,
7
+ verifySwitchboardSetup,
8
+ } from "../verify/index.js";
9
+
10
+ export function registerVerifyCommands(program) {
11
+ const verify = program.command("verify").description("Verify a Switchboard browser integration");
12
+
13
+ addCommonOptions(
14
+ verify
15
+ .command("setup")
16
+ .description("Verify a local or preview Switchboard integration setup")
17
+ .option("--url <app-url>", "Application URL to test")
18
+ .option("--client-url <switchboard-client-url>", "Switchboard Pro-bono Embed client URL"),
19
+ ).action(async (opts, cmd) => {
20
+ await runVerifyCommand("setup", opts, cmd);
21
+ });
22
+
23
+ addCommonOptions(
24
+ verify
25
+ .command("publish")
26
+ .description("Verify an HTTPS preview is safe to publish")
27
+ .option("--url <preview-url>", "HTTPS preview URL to test")
28
+ .option("--client-url <switchboard-client-url>", "Switchboard Pro-bono Embed client URL")
29
+ .option("--production-origin <origin>", "Production origin that must be allowed"),
30
+ )
31
+ .option("--project-id <id>", "Switchboard project id for production safety checks")
32
+ .action(async (opts, cmd) => {
33
+ await runVerifyCommand("publish", opts, cmd);
34
+ });
35
+
36
+ addCommonOptions(
37
+ verify
38
+ .command("browser")
39
+ .description("Run a declarative browser verification scenario")
40
+ .option("--url <app-url>", "Application URL to test")
41
+ .option("--client-url <switchboard-client-url>", "Switchboard Pro-bono Embed client URL"),
42
+ ).action(async (opts, cmd) => {
43
+ await runVerifyCommand("browser", opts, cmd);
44
+ });
45
+ }
46
+
47
+ function addCommonOptions(command) {
48
+ return command
49
+ .option("--scenario <file>", "Declarative JSON scenario file")
50
+ .option("--artifacts-dir <dir>", "Directory for screenshots and verification artifacts")
51
+ .option("--headed", "Run Chromium headed")
52
+ .option("--timeout-ms <milliseconds>", "Browser action timeout", parsePositiveInteger)
53
+ .option("--prompt <text>", "Prompt for switchboardChat scenario checks");
54
+ }
55
+
56
+ async function runVerifyCommand(mode, opts, cmd) {
57
+ const flags = globalFlags(cmd);
58
+
59
+ try {
60
+ const project = mode === "publish" ? await loadProject(opts, flags) : null;
61
+ const report = await verifierForMode(mode)({
62
+ url: opts.url,
63
+ clientUrl: opts.clientUrl,
64
+ productionOrigin: opts.productionOrigin,
65
+ scenarioPath: opts.scenario,
66
+ artifactsDir: opts.artifactsDir,
67
+ headed: opts.headed,
68
+ timeoutMs: opts.timeoutMs,
69
+ prompt: opts.prompt,
70
+ project,
71
+ });
72
+
73
+ emitVerifyReport(report, flags);
74
+ if (report.status !== "passed") process.exit(1);
75
+ } catch (error) {
76
+ const report = inputErrorReport(mode, error);
77
+ emitVerifyReport(report, flags);
78
+ process.exit(error instanceof VerificationInputError ? 1 : 3);
79
+ }
80
+ }
81
+
82
+ function emitVerifyReport(report, flags) {
83
+ emit(report, { ...flags, json: true });
84
+ }
85
+
86
+ function verifierForMode(mode) {
87
+ if (mode === "publish") return verifySwitchboardPublish;
88
+ if (mode === "browser") return verifySwitchboardBrowser;
89
+ return verifySwitchboardSetup;
90
+ }
91
+
92
+ async function loadProject(opts, flags) {
93
+ const config = resolveConfig();
94
+ const projectId = opts.projectId ?? config.projectId;
95
+
96
+ if (!projectId) return null;
97
+
98
+ const accountConfig = await resolveAccountConfig(config);
99
+ if (!accountConfig.accountToken) return null;
100
+
101
+ const url = `${accountApiUrl(accountConfig)}/projects/${encodeURIComponent(projectId)}`;
102
+ const response = await fetch(url, {
103
+ headers: {
104
+ Accept: "application/json",
105
+ Authorization: `Bearer ${accountConfig.accountToken}`,
106
+ },
107
+ });
108
+
109
+ if (!response.ok) {
110
+ if (!flags.quiet) {
111
+ console.error(`Could not load Switchboard project ${projectId}: HTTP ${response.status}`);
112
+ }
113
+ return null;
114
+ }
115
+
116
+ const data = await response.json();
117
+ return data?.data ?? data?.project ?? data;
118
+ }
119
+
120
+ function inputErrorReport(mode, error) {
121
+ return {
122
+ object: "verification_report",
123
+ mode,
124
+ status: "failed",
125
+ publishable: false,
126
+ functional: false,
127
+ secure: false,
128
+ production_safety: mode === "publish" ? "blocked" : "not_checked",
129
+ checks: [],
130
+ findings: [
131
+ {
132
+ severity: error instanceof VerificationInputError ? "critical" : "error",
133
+ code: error instanceof VerificationInputError ? "invalid_verification_input" : "verification_error",
134
+ message: error.message || String(error),
135
+ },
136
+ ],
137
+ artifacts: {},
138
+ };
139
+ }
140
+
141
+ function parsePositiveInteger(value) {
142
+ return value;
143
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Workspace management commands.
3
+ */
4
+
5
+ import { accountRequest } from "../client.js";
6
+ import { emit, globalFlags, printList } from "../output.js";
7
+
8
+ export function registerWorkspacesCommands(program) {
9
+ const workspaces = program.command("workspaces").description("Workspaces");
10
+
11
+ workspaces
12
+ .command("list")
13
+ .description("List workspaces")
14
+ .option("--trash <filter>", "active, deleted, or all", "active")
15
+ .action(async (opts, cmd) => {
16
+ const flags = globalFlags(cmd);
17
+ const { data } = await accountRequest("GET", `/workspaces?trash=${opts.trash}`, {
18
+ json: flags.json,
19
+ });
20
+ if (flags.json) {
21
+ emit(data, flags);
22
+ } else {
23
+ printList("Workspaces:", data.data || [], (w) => ` ${w.id} ${w.name} (${w.slug})`);
24
+ }
25
+ });
26
+
27
+ workspaces
28
+ .command("show <id>")
29
+ .description("Show a workspace")
30
+ .option("--trash <filter>", "active, deleted, or all", "active")
31
+ .action(async (id, opts, cmd) => {
32
+ const flags = globalFlags(cmd);
33
+ const { data } = await accountRequest("GET", `/workspaces/${id}?trash=${opts.trash}`, {
34
+ json: flags.json,
35
+ });
36
+ emit(flags.json ? data : JSON.stringify(data, null, 2), flags);
37
+ });
38
+
39
+ workspaces
40
+ .command("create")
41
+ .description("Create a workspace")
42
+ .requiredOption("--name <name>")
43
+ .requiredOption("--slug <slug>")
44
+ .action(async (opts, cmd) => {
45
+ const flags = globalFlags(cmd);
46
+ const { data } = await accountRequest("POST", "/workspaces", {
47
+ body: { name: opts.name, slug: opts.slug },
48
+ json: flags.json,
49
+ });
50
+ emit(flags.json ? data : `Created workspace ${data.name} (${data.id})`, flags);
51
+ });
52
+
53
+ workspaces
54
+ .command("update <id>")
55
+ .description("Update a workspace")
56
+ .option("--name <name>")
57
+ .option("--slug <slug>")
58
+ .action(async (id, opts, cmd) => {
59
+ const flags = globalFlags(cmd);
60
+ const body = {};
61
+ if (opts.name) body.name = opts.name;
62
+ if (opts.slug) body.slug = opts.slug;
63
+
64
+ const { data } = await accountRequest("PATCH", `/workspaces/${id}`, {
65
+ body,
66
+ json: flags.json,
67
+ });
68
+ emit(flags.json ? data : `Updated workspace ${data.name}`, flags);
69
+ });
70
+
71
+ workspaces
72
+ .command("delete <id>")
73
+ .description("Delete a workspace")
74
+ .action(async (id, _opts, cmd) => {
75
+ const flags = globalFlags(cmd);
76
+ const { data } = await accountRequest("DELETE", `/workspaces/${id}`, {
77
+ json: flags.json,
78
+ });
79
+ emit(flags.json ? data : `Deleted workspace ${data.name}`, flags);
80
+ });
81
+
82
+ workspaces
83
+ .command("restore <id>")
84
+ .description("Restore a deleted workspace")
85
+ .action(async (id, _opts, cmd) => {
86
+ const flags = globalFlags(cmd);
87
+ const { data } = await accountRequest("POST", `/workspaces/${id}/restore`, {
88
+ json: flags.json,
89
+ });
90
+ emit(flags.json ? data : `Restored workspace ${data.name}`, flags);
91
+ });
92
+ }
package/lib/config.js ADDED
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Resolves Switchboard CLI configuration from environment, ~/.switchboard/config.json,
3
+ * and the OS keychain for account sessions.
4
+ */
5
+
6
+ import fs from "fs";
7
+ import os from "os";
8
+ import path from "path";
9
+ import { getAccountToken } from "./credentialStore.js";
10
+
11
+ const CONFIG_DIR =
12
+ process.env.SWITCHBOARD_CONFIG_DIR || path.join(os.homedir(), ".switchboard");
13
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
14
+ const DEFAULT_BASE_URL = "https://switchboard.spot";
15
+
16
+ /**
17
+ * Loads persisted CLI configuration from disk.
18
+ */
19
+ export function loadConfig() {
20
+ try {
21
+ if (fs.existsSync(CONFIG_FILE)) {
22
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
23
+ return scrubLegacyAccountToken(config);
24
+ }
25
+ } catch {
26
+ /* ignore corrupt config */
27
+ }
28
+ return {};
29
+ }
30
+
31
+ function scrubLegacyAccountToken(config) {
32
+ if (!Object.prototype.hasOwnProperty.call(config, "accountToken")) {
33
+ return config;
34
+ }
35
+
36
+ const scrubbed = { ...config };
37
+ delete scrubbed.accountToken;
38
+
39
+ try {
40
+ fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(scrubbed, null, 2)}\n`, { mode: 0o600 });
41
+ } catch {
42
+ /* ignore cleanup failures; account token is still ignored in memory */
43
+ }
44
+
45
+ return scrubbed;
46
+ }
47
+
48
+ /**
49
+ * Merges and saves CLI configuration to disk.
50
+ */
51
+ export function saveConfig(updates) {
52
+ const current = loadConfig();
53
+ const next = { ...current, ...updates };
54
+ delete next.accountToken;
55
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
56
+ fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
57
+ try {
58
+ fs.chmodSync(CONFIG_FILE, 0o600);
59
+ } catch {
60
+ /* best effort on platforms that do not support chmod */
61
+ }
62
+ return next;
63
+ }
64
+
65
+ /**
66
+ * Returns effective non-secret settings with environment variable overrides.
67
+ *
68
+ * Account session tokens intentionally do not come from this config file or
69
+ * from environment variables. Authenticated commands call `resolveAccountConfig`
70
+ * so the token is read from the OS keychain only when needed.
71
+ */
72
+ export function resolveConfig() {
73
+ const file = loadConfig();
74
+ return {
75
+ baseUrl: process.env.SWITCHBOARD_BASE_URL || file.baseUrl || DEFAULT_BASE_URL,
76
+ virtualMicroserviceUrl:
77
+ process.env.SWITCHBOARD_CLIENT_URL || file.virtualMicroserviceUrl || null,
78
+ accountToken: null,
79
+ accountTokenSource: null,
80
+ apiKey: process.env.SWITCHBOARD_API_KEY || null,
81
+ endUserSession:
82
+ process.env.SWITCHBOARD_END_USER_SESSION || file.endUserSession || null,
83
+ projectId: process.env.SWITCHBOARD_PROJECT_ID || file.projectId || null,
84
+ projectIdSource: projectIdSource(file),
85
+ };
86
+ }
87
+
88
+ function projectIdSource(file) {
89
+ if (process.env.SWITCHBOARD_PROJECT_ID) {
90
+ return "env";
91
+ }
92
+
93
+ if (file.projectId) {
94
+ return "config";
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Returns effective settings plus the hidden keychain account session.
102
+ */
103
+ export async function resolveAccountConfig(config) {
104
+ if (config?.accountToken || config?.disableKeychain) {
105
+ return config;
106
+ }
107
+
108
+ const base = config || resolveConfig();
109
+ const token = await getAccountToken();
110
+
111
+ return {
112
+ ...base,
113
+ accountToken: token,
114
+ accountTokenSource: token ? "keychain" : null,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Account API base URL (host + /v1/account).
120
+ */
121
+ export function accountApiUrl(config) {
122
+ const base = config.baseUrl.replace(/\/$/, "");
123
+ return `${base}/v1/account`;
124
+ }
125
+
126
+ /**
127
+ * Gateway API base URL (host + /v1).
128
+ */
129
+ export function gatewayApiUrl(config) {
130
+ const base = config.baseUrl.replace(/\/$/, "");
131
+ return `${base}/v1`;
132
+ }