@algosuite/vo-mcp 0.2.0-beta.0 → 0.2.0-beta.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.
package/bin/vo-mcp CHANGED
@@ -5,7 +5,8 @@
5
5
  * Usage:
6
6
  * vo-mcp # MCP stdio server (default)
7
7
  * vo-mcp install # one-command installer
8
- * vo-mcp login # credential login
8
+ * vo-mcp login # credential login (browser loopback)
9
+ * vo-mcp pair # device-code pairing (enter a code in the web)
9
10
  * vo-mcp runner # agent runner daemon
10
11
  * vo-mcp runner --install-autostart # register runner to start at login
11
12
  * vo-mcp runner --uninstall-autostart # remove auto-start registration
@@ -18,6 +19,10 @@ if (command === 'install') {
18
19
  import('../dist/install-cli.js');
19
20
  } else if (command === 'login') {
20
21
  import('../dist/login-cli.js');
22
+ } else if (command === 'pair') {
23
+ import('../dist/pair-cli.js');
24
+ } else if (command === 'set-key') {
25
+ import('../dist/set-key-cli.js');
21
26
  } else if (command === 'runner') {
22
27
  if (subcommand === '--install-autostart') {
23
28
  import('../dist/autostart-cli.js').then((m) => m.installAutostartCli());
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
3
+
4
+ // src/cloud/pairing.ts
5
+ import { hostname, platform } from "node:os";
6
+
7
+ // src/cloud/credential-store.ts
8
+ import { homedir } from "node:os";
9
+ import { join, dirname } from "node:path";
10
+ import {
11
+ existsSync,
12
+ mkdirSync,
13
+ readFileSync,
14
+ writeFileSync,
15
+ chmodSync,
16
+ rmSync
17
+ } from "node:fs";
18
+
19
+ // src/cloud/keychain.ts
20
+ import { createRequire } from "node:module";
21
+ var SERVICE = "vo-mcp";
22
+ var ACCOUNT = "refresh-credential";
23
+ var cached;
24
+ function loadKeyring() {
25
+ if (cached !== void 0) return cached;
26
+ try {
27
+ const req = createRequire(import.meta.url);
28
+ const mod = req("@napi-rs/keyring");
29
+ cached = mod && typeof mod.Entry === "function" ? mod : null;
30
+ } catch {
31
+ cached = null;
32
+ }
33
+ return cached;
34
+ }
35
+ function keychainAvailable() {
36
+ return loadKeyring() !== null;
37
+ }
38
+ function keychainGet() {
39
+ const k = loadKeyring();
40
+ if (!k) return null;
41
+ try {
42
+ return new k.Entry(SERVICE, ACCOUNT).getPassword();
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ function keychainSet(secret) {
48
+ const k = loadKeyring();
49
+ if (!k) return false;
50
+ try {
51
+ new k.Entry(SERVICE, ACCOUNT).setPassword(secret);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+ function keychainDelete() {
58
+ const k = loadKeyring();
59
+ if (!k) return false;
60
+ try {
61
+ return new k.Entry(SERVICE, ACCOUNT).deletePassword();
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ // src/cloud/credential-store.ts
68
+ var realKeychain = {
69
+ available: keychainAvailable,
70
+ get: keychainGet,
71
+ set: keychainSet,
72
+ delete: keychainDelete
73
+ };
74
+ var KEYCHAIN_LOCATION = 'OS keychain (service "vo-mcp")';
75
+ function credentialPath(env = process.env) {
76
+ const override = env["VO_MCP_CREDENTIALS_PATH"]?.trim();
77
+ if (override) return override;
78
+ return join(homedir(), ".config", "vo-mcp", "credentials.json");
79
+ }
80
+ function keychainEnabled(env, keychain) {
81
+ const disabled = (env["VO_MCP_DISABLE_KEYCHAIN"] ?? "").trim().toLowerCase();
82
+ if (disabled === "1" || disabled === "true" || disabled === "yes") return false;
83
+ return keychain.available();
84
+ }
85
+ function deleteFile(env) {
86
+ try {
87
+ rmSync(credentialPath(env), { force: true });
88
+ } catch {
89
+ }
90
+ }
91
+ function writeToFile(payload, env) {
92
+ const p = credentialPath(env);
93
+ mkdirSync(dirname(p), { recursive: true });
94
+ writeFileSync(p, `${JSON.stringify(payload, null, 2)}
95
+ `, { mode: 384 });
96
+ try {
97
+ chmodSync(p, 384);
98
+ } catch {
99
+ }
100
+ return p;
101
+ }
102
+ function writeStoredCredential(cred, storedAt, env = process.env, keychain = realKeychain) {
103
+ const payload = {
104
+ ...cred.refresh_token ? { refresh_token: cred.refresh_token } : {},
105
+ ...cred.api_key ? { api_key: cred.api_key } : {},
106
+ ...cred.vo_credential ? { vo_credential: cred.vo_credential } : {},
107
+ ...cred.vo_credential_expires_at ? { vo_credential_expires_at: cred.vo_credential_expires_at } : {},
108
+ ...cred.email ? { email: cred.email } : {},
109
+ stored_at: cred.stored_at ?? storedAt
110
+ };
111
+ if (keychainEnabled(env, keychain) && keychain.set(JSON.stringify(payload))) {
112
+ deleteFile(env);
113
+ return KEYCHAIN_LOCATION;
114
+ }
115
+ const p = writeToFile(payload, env);
116
+ if (keychainEnabled(env, keychain)) keychain.delete();
117
+ return p;
118
+ }
119
+
120
+ // src/cloud/pairing.ts
121
+ var DEFAULT_CONTROL_PLANE_URL = "https://vo-control-plane-bzjphrajaq-uc.a.run.app";
122
+ var DEFAULT_DASHBOARD_URL = "https://algosuite.ai";
123
+ function formatPairingCode(code) {
124
+ return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
125
+ }
126
+ async function readJson(res) {
127
+ const body = await res.json().catch(() => ({}));
128
+ return body && typeof body === "object" ? body : {};
129
+ }
130
+ async function runPairing(deps = {}) {
131
+ const env = deps.env ?? process.env;
132
+ const log = deps.log ?? ((m) => console.error(m));
133
+ const fetchImpl = deps.fetchImpl ?? fetch;
134
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
135
+ const now = deps.now ?? (() => /* @__PURE__ */ new Date());
136
+ const store = deps.store ?? ((cred, iso) => writeStoredCredential(cred, iso, env));
137
+ const controlPlaneUrl = env["VO_CONTROL_PLANE_URL"]?.trim() || DEFAULT_CONTROL_PLANE_URL;
138
+ const dashboardUrl = env["VO_DASHBOARD_URL"]?.trim() || DEFAULT_DASHBOARD_URL;
139
+ const deviceLabel = `${platform()} on ${hostname()}`.slice(0, 120);
140
+ const initRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/initiate`, {
141
+ method: "POST",
142
+ headers: { "content-type": "application/json" },
143
+ body: JSON.stringify({ device_label: deviceLabel })
144
+ });
145
+ if (!initRes.ok) throw new Error(`Could not start pairing (server ${initRes.status}).`);
146
+ const init = await readJson(initRes);
147
+ const code = String(init["code"] ?? "");
148
+ const pollToken = String(init["poll_token"] ?? "");
149
+ if (!code || !pollToken) throw new Error("Pairing service returned an incomplete response.");
150
+ const intervalMs = (Number(init["poll_interval_seconds"]) || 5) * 1e3;
151
+ const expiresAtMs = new Date(String(init["expires_at"] ?? "")).getTime();
152
+ log("");
153
+ log(" To connect this runner, open this page in your browser:");
154
+ log(` ${dashboardUrl}/pair`);
155
+ log(" and enter this code:");
156
+ log("");
157
+ log(` ${formatPairingCode(code)}`);
158
+ log("");
159
+ log(" Your Anthropic key never leaves this computer. Waiting for you to authorize\u2026");
160
+ for (; ; ) {
161
+ if (Number.isFinite(expiresAtMs) && now().getTime() >= expiresAtMs) {
162
+ throw new Error("The pairing code expired before it was authorized. Run `vo-mcp pair` again.");
163
+ }
164
+ await sleep(intervalMs);
165
+ const pollRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/poll`, {
166
+ method: "GET",
167
+ headers: { "x-vo-poll-token": pollToken }
168
+ });
169
+ if (pollRes.status === 404) {
170
+ throw new Error("The pairing expired. Run `vo-mcp pair` again.");
171
+ }
172
+ if (pollRes.status === 410) {
173
+ throw new Error("This code was already used. Run `vo-mcp pair` again.");
174
+ }
175
+ if (!pollRes.ok) {
176
+ continue;
177
+ }
178
+ const body = await readJson(pollRes);
179
+ if (body["status"] === "pending") continue;
180
+ if (body["status"] === "authorized" && typeof body["vo_credential"] === "string") {
181
+ const credentialPath2 = store(
182
+ {
183
+ vo_credential: body["vo_credential"],
184
+ ...typeof body["expires_at"] === "string" ? { vo_credential_expires_at: body["expires_at"] } : {}
185
+ },
186
+ now().toISOString()
187
+ );
188
+ return { credentialPath: credentialPath2, expires_at: String(body["expires_at"] ?? "") };
189
+ }
190
+ throw new Error("Unexpected response from the pairing service.");
191
+ }
192
+ }
193
+
194
+ // src/pair-cli.ts
195
+ async function main() {
196
+ const log = (m) => console.error(m);
197
+ try {
198
+ const result = await runPairing({ env: process.env, log });
199
+ log("");
200
+ log(`\u2713 Paired! Credential stored at: ${result.credentialPath}`);
201
+ log("");
202
+ log("Next steps:");
203
+ log(" 1. Start your runner: vo-mcp runner");
204
+ log(" 2. Dispatch agents from: https://algosuite.ai/virtualoffice");
205
+ } catch (err) {
206
+ log(`\u2717 Pairing failed: ${err instanceof Error ? err.message : String(err)}`);
207
+ process.exit(1);
208
+ }
209
+ }
210
+ main().catch((err) => {
211
+ console.error("[vo-mcp pair] fatal:", err);
212
+ process.exit(1);
213
+ });
214
+ //# sourceMappingURL=pair-cli.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cloud/pairing.ts", "../src/cloud/credential-store.ts", "../src/cloud/keychain.ts", "../src/pair-cli.ts"],
4
+ "sourcesContent": ["/**\n * Device-code pairing for the BYO runner (M1) \u2014 the no-terminal-token onboarding.\n *\n * The runner calls the control-plane `pair/initiate`, shows the friend a short\n * `code` to type at `<dashboard>/pair`, then polls `pair/poll` with its SECRET\n * `poll_token` until the friend authorizes \u2014 at which point it receives a freshly\n * minted `vocred_` and stores it in the OS keychain (via writeStoredCredential).\n *\n * The friend's AI key NEVER flows through here \u2014 this only obtains the\n * control-plane credential the runner needs to claim THEIR tasks.\n */\nimport { hostname, platform } from 'node:os';\n\nimport { writeStoredCredential, type StoredCredential } from './credential-store.js';\n\nexport const DEFAULT_CONTROL_PLANE_URL = 'https://vo-control-plane-bzjphrajaq-uc.a.run.app';\nexport const DEFAULT_DASHBOARD_URL = 'https://algosuite.ai';\n\nexport interface RunPairingDeps {\n readonly env?: Record<string, string | undefined>;\n readonly log?: (message: string) => void;\n readonly fetchImpl?: typeof fetch;\n readonly sleep?: (ms: number) => Promise<void>;\n readonly now?: () => Date;\n /** Test seam: capture the stored credential instead of touching the real keychain. */\n readonly store?: (cred: StoredCredential, storedAtIso: string) => string;\n}\n\nexport interface PairingResult {\n readonly credentialPath: string;\n readonly expires_at: string;\n}\n\n/** ABCD-EFGH grouping for an 8-char code (easier to read aloud / type). */\nexport function formatPairingCode(code: string): string {\n return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;\n}\n\nasync function readJson(res: { json: () => Promise<unknown> }): Promise<Record<string, unknown>> {\n const body = await res.json().catch(() => ({}));\n return body && typeof body === 'object' ? (body as Record<string, unknown>) : {};\n}\n\n/**\n * Run the full pairing handshake. Resolves once a credential is stored; rejects\n * with a friendly message on expiry / consumption / fatal transport error.\n */\nexport async function runPairing(deps: RunPairingDeps = {}): Promise<PairingResult> {\n const env = deps.env ?? process.env;\n const log = deps.log ?? ((m: string) => console.error(m));\n const fetchImpl = deps.fetchImpl ?? fetch;\n const sleep = deps.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));\n const now = deps.now ?? (() => new Date());\n const store = deps.store ?? ((cred: StoredCredential, iso: string) => writeStoredCredential(cred, iso, env));\n\n const controlPlaneUrl = env['VO_CONTROL_PLANE_URL']?.trim() || DEFAULT_CONTROL_PLANE_URL;\n const dashboardUrl = env['VO_DASHBOARD_URL']?.trim() || DEFAULT_DASHBOARD_URL;\n const deviceLabel = `${platform()} on ${hostname()}`.slice(0, 120);\n\n // 1. initiate \u2014 get a display code + a secret poll token.\n const initRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/initiate`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ device_label: deviceLabel }),\n });\n if (!initRes.ok) throw new Error(`Could not start pairing (server ${initRes.status}).`);\n const init = await readJson(initRes);\n const code = String(init['code'] ?? '');\n const pollToken = String(init['poll_token'] ?? '');\n if (!code || !pollToken) throw new Error('Pairing service returned an incomplete response.');\n const intervalMs = (Number(init['poll_interval_seconds']) || 5) * 1000;\n const expiresAtMs = new Date(String(init['expires_at'] ?? '')).getTime();\n\n // 2. show the friend what to do.\n log('');\n log(' To connect this runner, open this page in your browser:');\n log(` ${dashboardUrl}/pair`);\n log(' and enter this code:');\n log('');\n log(` ${formatPairingCode(code)}`);\n log('');\n log(' Your Anthropic key never leaves this computer. Waiting for you to authorize\u2026');\n\n // 3. poll with the SECRET token until authorized / expired.\n for (;;) {\n if (Number.isFinite(expiresAtMs) && now().getTime() >= expiresAtMs) {\n throw new Error('The pairing code expired before it was authorized. Run `vo-mcp pair` again.');\n }\n await sleep(intervalMs);\n const pollRes = await fetchImpl(`${controlPlaneUrl}/api/v1/pair/poll`, {\n method: 'GET',\n headers: { 'x-vo-poll-token': pollToken },\n });\n if (pollRes.status === 404) {\n throw new Error('The pairing expired. Run `vo-mcp pair` again.');\n }\n if (pollRes.status === 410) {\n throw new Error('This code was already used. Run `vo-mcp pair` again.');\n }\n if (!pollRes.ok) {\n // Transient (rate limit / blip) \u2014 keep waiting.\n continue;\n }\n const body = await readJson(pollRes);\n if (body['status'] === 'pending') continue;\n if (body['status'] === 'authorized' && typeof body['vo_credential'] === 'string') {\n const credentialPath = store(\n {\n vo_credential: body['vo_credential'] as string,\n ...(typeof body['expires_at'] === 'string'\n ? { vo_credential_expires_at: body['expires_at'] as string }\n : {}),\n },\n now().toISOString(),\n );\n return { credentialPath, expires_at: String(body['expires_at'] ?? '') };\n }\n throw new Error('Unexpected response from the pairing service.');\n }\n}\n", "/**\r\n * Local credential store for the thin-client `vo-mcp login` flow (Increment 3b,\r\n * Option A \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md`).\r\n *\r\n * Persists the per-user Firebase refresh token (the user's OWN credential, never\r\n * the god-token, never model keys) captured by `login`, so the auto-refreshing\r\n * token source (Inc 3a) can mint fresh ID tokens across MCP restarts.\r\n *\r\n * Storage precedence (Inc 3b.3):\r\n * 1. **OS keychain** (Windows Credential Manager / macOS Keychain / libsecret)\r\n * via the optional `@napi-rs/keyring` backend (`keychain.ts`). The DEFAULT\r\n * when available \u2014 the secret never lands in plaintext on disk.\r\n * 2. **0600 file** at `$VO_MCP_CREDENTIALS_PATH` or `~/.config/vo-mcp/credentials.json`.\r\n * The fallback when the keychain is unavailable or disabled\r\n * (`VO_MCP_DISABLE_KEYCHAIN`). `VO_MCP_CREDENTIALS_PATH` only sets the file\r\n * LOCATION; force file storage with `VO_MCP_DISABLE_KEYCHAIN`.\r\n *\r\n * `env`-supplied tokens (`VO_USER_REFRESH_TOKEN`, etc.) still win over BOTH\r\n * stores \u2014 that precedence lives upstream in `auth-token-source.ts`.\r\n *\r\n * **Single source of truth.** The credential lives in EITHER the keychain OR the\r\n * file, never both: a write to one store CLEARS the other, so a stale entry can\r\n * never shadow the current credential on read, and the secret never lingers in\r\n * plaintext after a migration to the keychain.\r\n *\r\n * **Keychain durability.** A keychain-stored credential is only readable while\r\n * the `@napi-rs/keyring` native module loads. If the module later becomes\r\n * unavailable (an ABI break across a Node upgrade, a corrupted install), the\r\n * credential can't be read and the user re-runs `vo-mcp login` \u2014 the same\r\n * behaviour as `gh` / `gcloud` / `firebase` keychain storage. We deliberately do\r\n * NOT mirror the secret to a plaintext file as a fallback: that would defeat the\r\n * entire point of keychain storage (keeping the secret off plaintext disk).\r\n */\r\nimport { homedir } from 'node:os';\r\nimport { join, dirname } from 'node:path';\r\nimport {\r\n existsSync,\r\n mkdirSync,\r\n readFileSync,\r\n writeFileSync,\r\n chmodSync,\r\n rmSync,\r\n} from 'node:fs';\r\n\r\nimport { keychainAvailable, keychainGet, keychainSet, keychainDelete } from './keychain.js';\r\n\r\nexport interface StoredCredential {\r\n /**\r\n * Firebase refresh token (long-lived; exchanged for short-lived ID tokens).\r\n * OPTIONAL since Inc 3b.4b: once a scoped `vo_credential` is minted, the raw\r\n * refresh token is dropped, so a stored credential may carry ONLY the vocred_.\r\n */\r\n readonly refresh_token?: string;\r\n /** Firebase Web API key (PUBLIC) needed for the securetoken refresh exchange. */\r\n readonly api_key?: string;\r\n /**\r\n * Scoped, revocable VO credential (`vocred_`) minted by the control-plane\r\n * (Inc 3b.4b). Preferred over the raw refresh token; lets the client present a\r\n * revocable, server-side credential instead of the Firebase refresh token.\r\n */\r\n readonly vo_credential?: string;\r\n /** ISO-8601 expiry of `vo_credential` (the client re-logs-in past this). */\r\n readonly vo_credential_expires_at?: string;\r\n /** The signed-in operator email (diagnostics only). */\r\n readonly email?: string;\r\n /** ISO timestamp the credential was stored. */\r\n readonly stored_at?: string;\r\n}\r\n\r\n/**\r\n * Pluggable OS-keychain backend. Defaults to the real `@napi-rs/keyring` wrapper;\r\n * tests inject a deterministic fake so they never touch the host keychain.\r\n */\r\nexport interface KeychainBackend {\r\n available(): boolean;\r\n get(): string | null;\r\n set(secret: string): boolean;\r\n delete(): boolean;\r\n}\r\n\r\nconst realKeychain: KeychainBackend = {\r\n available: keychainAvailable,\r\n get: keychainGet,\r\n set: keychainSet,\r\n delete: keychainDelete,\r\n};\r\n\r\n/** Human-readable \"location\" returned when the credential was stored in the OS keychain. */\r\nexport const KEYCHAIN_LOCATION = 'OS keychain (service \"vo-mcp\")';\r\n\r\n/** Resolve the credentials file path (env override \u2192 XDG-ish default under home). */\r\nexport function credentialPath(env: Readonly<Record<string, string | undefined>> = process.env): string {\r\n const override = env['VO_MCP_CREDENTIALS_PATH']?.trim();\r\n if (override) return override;\r\n return join(homedir(), '.config', 'vo-mcp', 'credentials.json');\r\n}\r\n\r\n/**\r\n * Whether the keychain should be consulted at all (read OR write). False when the\r\n * native backend is unavailable or `VO_MCP_DISABLE_KEYCHAIN` is set (CI/headless).\r\n */\r\nfunction keychainEnabled(\r\n env: Readonly<Record<string, string | undefined>>,\r\n keychain: KeychainBackend,\r\n): boolean {\r\n const disabled = (env['VO_MCP_DISABLE_KEYCHAIN'] ?? '').trim().toLowerCase();\r\n if (disabled === '1' || disabled === 'true' || disabled === 'yes') return false;\r\n return keychain.available();\r\n}\r\n\r\n/** Parse + validate a stored credential blob. Returns null on any problem (never throws). */\r\nfunction deserialize(raw: string): StoredCredential | null {\r\n try {\r\n const parsed = JSON.parse(raw) as Partial<StoredCredential>;\r\n const refresh = typeof parsed.refresh_token === 'string' ? parsed.refresh_token.trim() : '';\r\n const apiKey = typeof parsed.api_key === 'string' ? parsed.api_key.trim() : '';\r\n const voCred = typeof parsed.vo_credential === 'string' ? parsed.vo_credential.trim() : '';\r\n // Valid if it carries a scoped vocred_ OR a full Firebase refresh pair.\r\n if (!voCred && (!refresh || !apiKey)) return null;\r\n return {\r\n ...(refresh ? { refresh_token: refresh } : {}),\r\n ...(apiKey ? { api_key: apiKey } : {}),\r\n ...(voCred ? { vo_credential: voCred } : {}),\r\n ...(typeof parsed.vo_credential_expires_at === 'string' ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {}),\r\n ...(typeof parsed.email === 'string' ? { email: parsed.email } : {}),\r\n ...(typeof parsed.stored_at === 'string' ? { stored_at: parsed.stored_at } : {}),\r\n };\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\nfunction readFromFile(env: Readonly<Record<string, string | undefined>>): StoredCredential | null {\r\n try {\r\n const p = credentialPath(env);\r\n if (!existsSync(p)) return null;\r\n return deserialize(readFileSync(p, 'utf8'));\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Read the stored credential, or `null` if absent/unreadable/invalid (never\r\n * throws). Consults an ENABLED keychain first (regardless of the write-target\r\n * flags, so a credential written to the keychain is found even if\r\n * `VO_MCP_CREDENTIALS_PATH` is later set), then the 0600 file.\r\n */\r\nexport function readStoredCredential(\r\n env: Readonly<Record<string, string | undefined>> = process.env,\r\n keychain: KeychainBackend = realKeychain,\r\n): StoredCredential | null {\r\n if (keychainEnabled(env, keychain)) {\r\n const raw = keychain.get();\r\n const fromKeychain = raw ? deserialize(raw) : null;\r\n if (fromKeychain) return fromKeychain;\r\n }\r\n return readFromFile(env);\r\n}\r\n\r\nfunction deleteFile(env: Readonly<Record<string, string | undefined>>): void {\r\n try {\r\n rmSync(credentialPath(env), { force: true });\r\n } catch {\r\n /* best-effort */\r\n }\r\n}\r\n\r\nfunction writeToFile(\r\n payload: StoredCredential,\r\n env: Readonly<Record<string, string | undefined>>,\r\n): string {\r\n const p = credentialPath(env);\r\n mkdirSync(dirname(p), { recursive: true });\r\n writeFileSync(p, `${JSON.stringify(payload, null, 2)}\\n`, { mode: 0o600 });\r\n // Best-effort tighten (no-op / throws on some Windows filesystems \u2014 ignore).\r\n try {\r\n chmodSync(p, 0o600);\r\n } catch {\r\n /* best-effort */\r\n }\r\n return p;\r\n}\r\n\r\n/**\r\n * Persist the credential. Prefers the OS keychain (secret never hits plaintext\r\n * disk); otherwise writes the 0600 file. Writing to one store CLEARS the other\r\n * (single source of truth \u2014 no stale shadow, no lingering plaintext). Returns the\r\n * location it was stored (`KEYCHAIN_LOCATION` or the file path).\r\n */\r\nexport function writeStoredCredential(\r\n cred: StoredCredential,\r\n storedAt: string,\r\n env: Readonly<Record<string, string | undefined>> = process.env,\r\n keychain: KeychainBackend = realKeychain,\r\n): string {\r\n const payload: StoredCredential = {\r\n ...(cred.refresh_token ? { refresh_token: cred.refresh_token } : {}),\r\n ...(cred.api_key ? { api_key: cred.api_key } : {}),\r\n ...(cred.vo_credential ? { vo_credential: cred.vo_credential } : {}),\r\n ...(cred.vo_credential_expires_at ? { vo_credential_expires_at: cred.vo_credential_expires_at } : {}),\r\n ...(cred.email ? { email: cred.email } : {}),\r\n stored_at: cred.stored_at ?? storedAt,\r\n };\r\n if (keychainEnabled(env, keychain) && keychain.set(JSON.stringify(payload))) {\r\n // Stored in the keychain \u2192 clear any stale plaintext file so the secret\r\n // doesn't linger on disk and can't shadow the keychain on read.\r\n deleteFile(env);\r\n return KEYCHAIN_LOCATION;\r\n }\r\n const p = writeToFile(payload, env);\r\n // Stored in the file \u2192 clear any stale keychain entry so it can't shadow the\r\n // newer file credential on read.\r\n if (keychainEnabled(env, keychain)) keychain.delete();\r\n return p;\r\n}\r\n", "/**\r\n * Optional OS-keychain backend for the thin-client credential store (Increment\r\n * 3b.3 \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md` \u00A75/\u00A76).\r\n *\r\n * Loads `@napi-rs/keyring` at runtime via `createRequire`, so it is a TRUE\r\n * optional dependency: if the native module is absent or fails to load\r\n * (unsupported platform, prebuilt binary missing, headless CI), every function\r\n * degrades to a no-op and the caller (`credential-store.ts`) falls back to the\r\n * 0600 file store. `@napi-rs/keyring`'s `Entry` API is SYNCHRONOUS, so the\r\n * credential store stays synchronous \u2014 no async ripple into the Inc-3a token\r\n * source that reads it.\r\n *\r\n * Why `createRequire` and not a static/dynamic `import`: a static import would\r\n * make the native module a HARD dependency (a missing prebuilt would crash the\r\n * MCP at startup); a dynamic `import()` is async (would force the whole read\r\n * path async). `createRequire(...)` inside a try/catch loads it lazily and\r\n * synchronously, and a load failure is just \"keychain unavailable\".\r\n */\r\nimport { createRequire } from 'node:module';\r\n\r\n/** Keychain service + account the single refresh credential is stored under. */\r\nconst SERVICE = 'vo-mcp';\r\nconst ACCOUNT = 'refresh-credential';\r\n\r\ninterface KeyringEntry {\r\n getPassword(): string | null;\r\n setPassword(password: string): void;\r\n deletePassword(): boolean;\r\n}\r\ninterface KeyringModule {\r\n Entry: new (service: string, account: string) => KeyringEntry;\r\n}\r\n\r\n// undefined = not yet attempted; null = attempted and unavailable.\r\nlet cached: KeyringModule | null | undefined;\r\n\r\nfunction loadKeyring(): KeyringModule | null {\r\n if (cached !== undefined) return cached;\r\n try {\r\n const req = createRequire(import.meta.url);\r\n const mod = req('@napi-rs/keyring') as Partial<KeyringModule>;\r\n cached = mod && typeof mod.Entry === 'function' ? (mod as KeyringModule) : null;\r\n } catch {\r\n cached = null;\r\n }\r\n return cached;\r\n}\r\n\r\n/** True when the OS keychain backend is usable in this runtime. */\r\nexport function keychainAvailable(): boolean {\r\n return loadKeyring() !== null;\r\n}\r\n\r\n/** Read the raw stored secret string from the OS keychain, or null. Never throws. */\r\nexport function keychainGet(): string | null {\r\n const k = loadKeyring();\r\n if (!k) return null;\r\n try {\r\n return new k.Entry(SERVICE, ACCOUNT).getPassword();\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/** Store the raw secret string in the OS keychain. Returns true on success. Never throws. */\r\nexport function keychainSet(secret: string): boolean {\r\n const k = loadKeyring();\r\n if (!k) return false;\r\n try {\r\n new k.Entry(SERVICE, ACCOUNT).setPassword(secret);\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Delete the stored secret from the OS keychain. Returns true if an entry was\r\n * removed. Never throws \u2014 a no-op (and `false`) when the backend is unavailable\r\n * or the entry is absent. Used to keep ONE source of truth: when the credential\r\n * is (re)written to the file, any stale keychain entry is cleared so it can't\r\n * shadow the newer file credential on read (and vice-versa).\r\n */\r\nexport function keychainDelete(): boolean {\r\n const k = loadKeyring();\r\n if (!k) return false;\r\n try {\r\n return new k.Entry(SERVICE, ACCOUNT).deletePassword();\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/** Test-only seam to reset the memoised module load. */\r\nexport function __resetKeychainCache(): void {\r\n cached = undefined;\r\n}\r\n", "#!/usr/bin/env node\n/**\n * `vo-mcp pair` CLI entry point.\n *\n * Device-code pairing \u2014 shows a short code to enter at <dashboard>/pair, waits\n * for the friend to authorize in their browser, then stores the minted\n * credential. No browser loopback, no token paste.\n */\nimport { runPairing } from './cloud/pairing.js';\n\nasync function main(): Promise<void> {\n const log = (m: string): void => console.error(m);\n try {\n const result = await runPairing({ env: process.env, log });\n log('');\n log(`\u2713 Paired! Credential stored at: ${result.credentialPath}`);\n log('');\n log('Next steps:');\n log(' 1. Start your runner: vo-mcp runner');\n log(' 2. Dispatch agents from: https://algosuite.ai/virtualoffice');\n } catch (err: unknown) {\n log(`\u2717 Pairing failed: ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n}\n\nmain().catch((err: unknown) => {\n console.error('[vo-mcp pair] fatal:', err);\n process.exit(1);\n});\n"],
5
+ "mappings": "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",
6
+ "names": ["credentialPath"]
7
+ }