@switchboard.spot/cli 0.2.0 → 0.2.1
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 +9 -2
- package/lib/commands/auth.js +75 -11
- package/lib/commands/env.js +3 -1
- package/lib/commands/init.js +4 -3
- package/lib/commands/setup.js +7 -4
- package/lib/config.js +32 -4
- package/lib/credentialStore.js +68 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ switchboard chat test --json
|
|
|
60
60
|
|
|
61
61
|
Use `--json` for automation, CI, and coding agents.
|
|
62
62
|
|
|
63
|
-
CLI account login does not create Client Gateway end-user sessions. End-user sign-up, sign-in, and refresh require a real browser/mobile challenge flow; use `@switchboard/sdk` in the app, or use trusted-server/account APIs for automation.
|
|
63
|
+
CLI account login does not create Client Gateway end-user sessions. End-user sign-up, sign-in, and refresh require a real browser/mobile challenge flow; use `@switchboard.spot/sdk` in the app, or use trusted-server/account APIs for automation.
|
|
64
64
|
|
|
65
65
|
Project-owned browser challenge keys are managed through the CLI:
|
|
66
66
|
|
|
@@ -82,14 +82,21 @@ Environment variables:
|
|
|
82
82
|
| `SWITCHBOARD_PROJECT_ID` | Project context for project-scoped commands. |
|
|
83
83
|
| `SWITCHBOARD_API_KEY` | Secret project key for trusted-server gateway smoke tests. |
|
|
84
84
|
| `SWITCHBOARD_CLIENT_URL` | Public Client Gateway URL for browser/mobile end-user auth and chat. |
|
|
85
|
+
| `VITE_SWITCHBOARD_CLIENT_URL` | Vite-safe public Client Gateway URL for browser apps. |
|
|
85
86
|
| `SWITCHBOARD_END_USER_SESSION` | Existing end-user session for Client Gateway checks; the CLI cannot mint one without browser challenge execution. |
|
|
86
87
|
| `SWITCHBOARD_CONFIG_DIR` | Alternate CLI config directory. |
|
|
87
88
|
|
|
88
89
|
Account sessions are stored in the OS keychain and are not read from
|
|
89
90
|
environment variables.
|
|
90
91
|
|
|
92
|
+
For isolated agent automation, `switchboard auth login --token-store config-dir`
|
|
93
|
+
stores the account session in `SWITCHBOARD_CONFIG_DIR/account-session.json` with
|
|
94
|
+
0600 permissions. The default remains keychain-only. `switchboard auth logout`
|
|
95
|
+
clears both keychain and config-dir account sessions.
|
|
96
|
+
|
|
91
97
|
## Credential safety
|
|
92
98
|
|
|
93
99
|
Do not paste `sb_sess_`, `sb_test_`, `sb_live_`, provider keys, private keys, or
|
|
94
100
|
webhook secrets into frontend, mobile, or public code. Browser and mobile code
|
|
95
|
-
should use `
|
|
101
|
+
should use `VITE_SWITCHBOARD_CLIENT_URL` in Vite apps or `SWITCHBOARD_CLIENT_URL`
|
|
102
|
+
in non-Vite tooling, plus end-user sessions.
|
package/lib/commands/auth.js
CHANGED
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { accountPublicRequest, accountRequest } from "../client.js";
|
|
6
6
|
import { accountApiUrl, resolveAccountConfig, resolveConfig, saveConfig } from "../config.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
deleteAccountToken,
|
|
9
|
+
deleteConfigDirAccountToken,
|
|
10
|
+
setAccountToken,
|
|
11
|
+
setConfigDirAccountToken,
|
|
12
|
+
} from "../credentialStore.js";
|
|
8
13
|
import { emit, fail, globalFlags } from "../output.js";
|
|
9
14
|
import crypto from "node:crypto";
|
|
10
15
|
import http from "node:http";
|
|
@@ -33,10 +38,15 @@ export function registerCommand(program) {
|
|
|
33
38
|
|
|
34
39
|
auth
|
|
35
40
|
.command("login")
|
|
36
|
-
.description("Sign in through the browser and save
|
|
41
|
+
.description("Sign in through the browser and save an account session")
|
|
37
42
|
.option("--email <email>")
|
|
38
43
|
.option("--password <password>")
|
|
39
44
|
.option("--timeout-seconds <seconds>", "Seconds to wait for browser login", "120")
|
|
45
|
+
.option(
|
|
46
|
+
"--token-store <store>",
|
|
47
|
+
"Where to save the session: keychain, config-dir, or both",
|
|
48
|
+
"keychain",
|
|
49
|
+
)
|
|
40
50
|
.action(async (opts, cmd) => {
|
|
41
51
|
const flags = globalFlags(cmd);
|
|
42
52
|
|
|
@@ -49,9 +59,18 @@ export function registerCommand(program) {
|
|
|
49
59
|
fail("timeout-seconds must be a positive integer", 1, flags.json);
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
const
|
|
62
|
+
const tokenStore = normalizeTokenStore(opts.tokenStore, flags.json);
|
|
63
|
+
const data = await browserLogin(flags, timeoutSeconds, tokenStore);
|
|
64
|
+
const sessionLocation =
|
|
65
|
+
tokenStore === "keychain"
|
|
66
|
+
? "the OS keychain"
|
|
67
|
+
: tokenStore === "config-dir"
|
|
68
|
+
? "the Switchboard config directory"
|
|
69
|
+
: "the OS keychain and Switchboard config directory";
|
|
53
70
|
emit(
|
|
54
|
-
flags.json
|
|
71
|
+
flags.json
|
|
72
|
+
? data
|
|
73
|
+
: `Logged in as ${data.user.email}. Session saved in ${sessionLocation}.`,
|
|
55
74
|
flags,
|
|
56
75
|
);
|
|
57
76
|
});
|
|
@@ -61,8 +80,9 @@ export function registerCommand(program) {
|
|
|
61
80
|
.description("Revoke current session and clear saved token")
|
|
62
81
|
.action(async (_opts, cmd) => {
|
|
63
82
|
const flags = globalFlags(cmd);
|
|
64
|
-
await
|
|
65
|
-
await
|
|
83
|
+
await revokeSavedSession();
|
|
84
|
+
await deleteKeychainTokenIfAvailable();
|
|
85
|
+
deleteConfigDirAccountToken();
|
|
66
86
|
saveConfig({
|
|
67
87
|
projectId: null,
|
|
68
88
|
apiKey: null,
|
|
@@ -91,8 +111,30 @@ export function registerCommand(program) {
|
|
|
91
111
|
});
|
|
92
112
|
}
|
|
93
113
|
|
|
94
|
-
async function
|
|
95
|
-
|
|
114
|
+
async function deleteKeychainTokenIfAvailable() {
|
|
115
|
+
try {
|
|
116
|
+
await deleteAccountToken();
|
|
117
|
+
} catch {
|
|
118
|
+
/* Local logout should still clear config-dir credentials without a keychain backend. */
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizeTokenStore(tokenStore, json) {
|
|
123
|
+
if (tokenStore === "keychain" || tokenStore === "config-dir" || tokenStore === "both") {
|
|
124
|
+
return tokenStore;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fail("--token-store must be keychain, config-dir, or both", 1, json);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function revokeSavedSession() {
|
|
131
|
+
let config;
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
config = await resolveAccountConfig();
|
|
135
|
+
} catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
96
138
|
|
|
97
139
|
if (!config.accountToken) {
|
|
98
140
|
return;
|
|
@@ -114,7 +156,7 @@ async function revokeKeychainSession() {
|
|
|
114
156
|
/**
|
|
115
157
|
* Starts the loopback callback server, opens the browser handoff URL, and saves the exchanged session.
|
|
116
158
|
*/
|
|
117
|
-
async function browserLogin(flags, timeoutSeconds) {
|
|
159
|
+
async function browserLogin(flags, timeoutSeconds, tokenStore) {
|
|
118
160
|
const state = randomState();
|
|
119
161
|
const server = await startCallbackServer();
|
|
120
162
|
const callbackUrl = `http://127.0.0.1:${server.port}/callback`;
|
|
@@ -133,6 +175,7 @@ async function browserLogin(flags, timeoutSeconds) {
|
|
|
133
175
|
code: callback.code,
|
|
134
176
|
state: callback.state,
|
|
135
177
|
expectedState: state,
|
|
178
|
+
tokenStore,
|
|
136
179
|
json: flags.json,
|
|
137
180
|
});
|
|
138
181
|
|
|
@@ -149,10 +192,11 @@ export async function exchangeCliLogin({
|
|
|
149
192
|
code,
|
|
150
193
|
state,
|
|
151
194
|
expectedState,
|
|
195
|
+
tokenStore = "keychain",
|
|
152
196
|
json,
|
|
153
197
|
request = accountPublicRequest,
|
|
154
198
|
save = saveConfig,
|
|
155
|
-
credentials = { setAccountToken },
|
|
199
|
+
credentials = { setAccountToken, setConfigDirAccountToken },
|
|
156
200
|
}) {
|
|
157
201
|
if (!code) {
|
|
158
202
|
fail("Browser login did not return a code. Run `switchboard auth login` again.", 2, json);
|
|
@@ -167,7 +211,7 @@ export async function exchangeCliLogin({
|
|
|
167
211
|
json,
|
|
168
212
|
});
|
|
169
213
|
|
|
170
|
-
await
|
|
214
|
+
await storeAccountToken(data.token, tokenStore, credentials);
|
|
171
215
|
|
|
172
216
|
save({
|
|
173
217
|
projectId: null,
|
|
@@ -178,6 +222,26 @@ export async function exchangeCliLogin({
|
|
|
178
222
|
return sanitizeSession(data);
|
|
179
223
|
}
|
|
180
224
|
|
|
225
|
+
async function storeAccountToken(token, tokenStore, credentials) {
|
|
226
|
+
if (tokenStore === "keychain") {
|
|
227
|
+
await credentials.setAccountToken(token);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (tokenStore === "config-dir") {
|
|
232
|
+
await credentials.setConfigDirAccountToken(token);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (tokenStore === "both") {
|
|
237
|
+
await credentials.setAccountToken(token);
|
|
238
|
+
await credentials.setConfigDirAccountToken(token);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw new Error("Unsupported Switchboard account token store");
|
|
243
|
+
}
|
|
244
|
+
|
|
181
245
|
function sanitizeSession(data) {
|
|
182
246
|
const rest = { ...data };
|
|
183
247
|
delete rest.token;
|
package/lib/commands/env.js
CHANGED
|
@@ -88,7 +88,9 @@ export async function configureEnvironment(
|
|
|
88
88
|
const envUpdates = {};
|
|
89
89
|
|
|
90
90
|
if (mode === "client" || mode === "both") {
|
|
91
|
-
|
|
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") {
|
package/lib/commands/init.js
CHANGED
|
@@ -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,15 @@ export function agentManifest(config) {
|
|
|
32
33
|
env: envBlock(clientUrl),
|
|
33
34
|
checklist: [
|
|
34
35
|
"switchboard setup --target client --json",
|
|
35
|
-
"export
|
|
36
|
+
"export VITE_SWITCHBOARD_CLIENT_URL=<client_url from integrations show>",
|
|
36
37
|
"Use mountSwitchboardWidget({ clientUrl, target }) in browser apps",
|
|
37
38
|
"Use createSwitchboardClient({ clientUrl, storage }) only for custom UI",
|
|
38
39
|
"switchboard billing top-up --amount-micros <micros>",
|
|
39
40
|
],
|
|
40
|
-
browser_smoke_test: `import { mountSwitchboardWidget } from "@switchboard/sdk";
|
|
41
|
+
browser_smoke_test: `import { mountSwitchboardWidget } from "@switchboard.spot/sdk";
|
|
41
42
|
|
|
42
43
|
mountSwitchboardWidget({
|
|
43
|
-
clientUrl:
|
|
44
|
+
clientUrl: import.meta.env.VITE_SWITCHBOARD_CLIENT_URL ?? "${clientUrl}",
|
|
44
45
|
target: "#switchboard",
|
|
45
46
|
});`,
|
|
46
47
|
automation_note:
|
package/lib/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
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");
|
|
@@ -106,15 +106,43 @@ export async function resolveAccountConfig(config) {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
const base = config || resolveConfig();
|
|
109
|
-
const
|
|
109
|
+
const keychain = await readKeychainAccountToken();
|
|
110
|
+
if (keychain.token) {
|
|
111
|
+
return {
|
|
112
|
+
...base,
|
|
113
|
+
accountToken: keychain.token,
|
|
114
|
+
accountTokenSource: "keychain",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const configDirToken = getConfigDirAccountToken();
|
|
119
|
+
if (configDirToken) {
|
|
120
|
+
return {
|
|
121
|
+
...base,
|
|
122
|
+
accountToken: configDirToken,
|
|
123
|
+
accountTokenSource: "config-dir",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (keychain.error) {
|
|
128
|
+
throw keychain.error;
|
|
129
|
+
}
|
|
110
130
|
|
|
111
131
|
return {
|
|
112
132
|
...base,
|
|
113
|
-
accountToken:
|
|
114
|
-
accountTokenSource:
|
|
133
|
+
accountToken: null,
|
|
134
|
+
accountTokenSource: null,
|
|
115
135
|
};
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
async function readKeychainAccountToken() {
|
|
139
|
+
try {
|
|
140
|
+
return { token: await getAccountToken(), error: null };
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return { token: null, error };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
118
146
|
/**
|
|
119
147
|
* Account API base URL (host + /v1/account).
|
|
120
148
|
*/
|
package/lib/credentialStore.js
CHANGED
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
12
15
|
import { promisify } from "node:util";
|
|
13
16
|
|
|
14
17
|
const execFileAsync = promisify(execFile);
|
|
15
18
|
const SERVICE = "Switchboard CLI";
|
|
16
19
|
const ACCOUNT = "account-session";
|
|
17
20
|
const PROJECT_SECRET_PREFIX = "project-secret";
|
|
21
|
+
const ACCOUNT_SESSION_FILE = "account-session.json";
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Reads the current account session token from the OS keychain.
|
|
@@ -54,6 +58,30 @@ export async function getAccountToken() {
|
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Reads the current account session token from the isolated CLI config dir.
|
|
63
|
+
*/
|
|
64
|
+
export function getConfigDirAccountToken(configDir = switchboardConfigDir()) {
|
|
65
|
+
const file = accountSessionFile(configDir);
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(file)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let data;
|
|
72
|
+
try {
|
|
73
|
+
data = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(`Could not read Switchboard config-dir account session: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!data || typeof data.token !== "string" || data.token.trim() === "") {
|
|
79
|
+
throw new Error("Switchboard config-dir account session is invalid");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return data.token;
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
/**
|
|
58
86
|
* Writes the current account session token to the OS keychain.
|
|
59
87
|
*/
|
|
@@ -92,6 +120,23 @@ export async function setAccountToken(token) {
|
|
|
92
120
|
}
|
|
93
121
|
}
|
|
94
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Writes the current account session token to the isolated CLI config dir.
|
|
125
|
+
*/
|
|
126
|
+
export function setConfigDirAccountToken(token, configDir = switchboardConfigDir()) {
|
|
127
|
+
if (!token) {
|
|
128
|
+
throw new Error("Cannot store an empty Switchboard account token");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
132
|
+
fs.writeFileSync(
|
|
133
|
+
accountSessionFile(configDir),
|
|
134
|
+
`${JSON.stringify({ token }, null, 2)}\n`,
|
|
135
|
+
{ mode: 0o600 },
|
|
136
|
+
);
|
|
137
|
+
chmodAccountSessionFile(configDir);
|
|
138
|
+
}
|
|
139
|
+
|
|
95
140
|
/**
|
|
96
141
|
* Deletes the current account session token from the OS keychain.
|
|
97
142
|
*/
|
|
@@ -129,6 +174,29 @@ export async function deleteAccountToken() {
|
|
|
129
174
|
}
|
|
130
175
|
}
|
|
131
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Deletes the current account session token from the isolated CLI config dir.
|
|
179
|
+
*/
|
|
180
|
+
export function deleteConfigDirAccountToken(configDir = switchboardConfigDir()) {
|
|
181
|
+
fs.rmSync(accountSessionFile(configDir), { force: true });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function accountSessionFile(configDir = switchboardConfigDir()) {
|
|
185
|
+
return path.join(configDir, ACCOUNT_SESSION_FILE);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function switchboardConfigDir() {
|
|
189
|
+
return process.env.SWITCHBOARD_CONFIG_DIR || path.join(os.homedir(), ".switchboard");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function chmodAccountSessionFile(configDir) {
|
|
193
|
+
try {
|
|
194
|
+
fs.chmodSync(accountSessionFile(configDir), 0o600);
|
|
195
|
+
} catch {
|
|
196
|
+
/* best effort on platforms that do not support chmod */
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
132
200
|
/**
|
|
133
201
|
* Reads a stored project secret key from the OS keychain.
|
|
134
202
|
*/
|