@algosuite/vo-mcp 0.2.0-beta.2 → 0.2.0-beta.4

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/cloud/login.ts", "../src/cloud/credential-store.ts", "../src/cloud/keychain.ts", "../src/cloud/auth-token-source.ts", "../src/cloud/vo-credential-exchange.ts", "../src/login-cli.ts"],
4
- "sourcesContent": ["/**\r\n * `vo-mcp login` \u2014 the thin-client loopback browser-callback flow (Increment 3b,\r\n * Option A; `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md`).\r\n *\r\n * The standard CLI-auth pattern (`gcloud auth login`, `gh auth login`): start a\r\n * loopback HTTP server on 127.0.0.1, open the browser to the dashboard's\r\n * `/cli-login` page (which runs the live Firebase Google/GitHub SSO), and capture\r\n * the user's refresh token when the page redirects back to the loopback. The\r\n * captured token (the user's OWN credential \u2014 never the god-token, never model\r\n * keys) is stored locally; Inc 3a's source auto-refreshes ID tokens from it.\r\n *\r\n * FROZEN loopback contract v1 (the dashboard `/cli-login` page builds against this):\r\n * CLI \u2192 browser: <dashboard>/cli-login?port=<loopbackPort>&state=<csrf>\r\n * page \u2192 browser: redirect to http://127.0.0.1:<port>/callback#state=..&refresh_token=..&api_key=..&email=..\r\n * (data in the URL FRAGMENT \u2014 never sent to a server log; the loopback /callback\r\n * page reads the fragment and POSTs it same-origin to /capture as JSON.)\r\n * CLI /capture: validate `state`, store {refresh_token, api_key, email}, 200, close.\r\n */\r\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\r\nimport { randomBytes } from 'node:crypto';\r\nimport { spawn } from 'node:child_process';\r\nimport { writeStoredCredential, type StoredCredential } from './credential-store.js';\r\n\r\n// The /cli-login page lives in the MAIN app (algosuite.ai) per the 2026-06-08\r\n// surface pivot (unification-spec \u00A711) \u2014 the standalone vo-dashboard never\r\n// shipped the page and its deploy lane is operator-dispatch-only. Override with\r\n// VO_DASHBOARD_URL (env) or opts.dashboardUrl.\r\nexport const DEFAULT_DASHBOARD_URL = 'https://algosuite.ai';\r\nconst DEFAULT_TIMEOUT_MS = 120_000;\r\nconst MAX_BODY_BYTES = 16_384;\r\n\r\nexport interface LoginResult {\r\n readonly email?: string;\r\n readonly credentialPath: string;\r\n}\r\n\r\nexport interface CaptureOutcome {\r\n readonly ok: boolean;\r\n readonly httpStatus: number;\r\n readonly result?: LoginResult;\r\n readonly error?: string;\r\n /**\r\n * The captured Firebase refresh credential (Inc 3b.4b). The caller may exchange\r\n * it for a scoped `vocred_` and re-store, dropping the raw refresh token.\r\n */\r\n readonly captured?: { readonly refresh_token: string; readonly api_key: string; readonly email?: string };\r\n}\r\n\r\n/**\r\n * Pure core of the /capture handler \u2014 validate the CSRF state + required fields,\r\n * then persist via the injected `store`. Fully unit-testable; the http server is\r\n * a thin shell around this.\r\n */\r\nexport function processCapture(\r\n rawBody: string,\r\n expectedState: string,\r\n store: (cred: StoredCredential) => string,\r\n): CaptureOutcome {\r\n let data: { state?: unknown; refresh_token?: unknown; api_key?: unknown; email?: unknown };\r\n try {\r\n data = JSON.parse(rawBody);\r\n } catch {\r\n return { ok: false, httpStatus: 400, error: 'invalid JSON body' };\r\n }\r\n if (!data || typeof data !== 'object') return { ok: false, httpStatus: 400, error: 'invalid body' };\r\n // CSRF: the state echoed by the browser MUST match the one we generated.\r\n if (typeof data.state !== 'string' || data.state !== expectedState) {\r\n return { ok: false, httpStatus: 403, error: 'state mismatch (possible CSRF) \u2014 login aborted' };\r\n }\r\n const refresh = typeof data.refresh_token === 'string' ? data.refresh_token.trim() : '';\r\n const apiKey = typeof data.api_key === 'string' ? data.api_key.trim() : '';\r\n if (!refresh || !apiKey) {\r\n return { ok: false, httpStatus: 400, error: 'login response missing refresh_token / api_key' };\r\n }\r\n const email = typeof data.email === 'string' && data.email.trim() ? data.email.trim() : undefined;\r\n const path = store({ refresh_token: refresh, api_key: apiKey, ...(email ? { email } : {}) });\r\n return {\r\n ok: true,\r\n httpStatus: 200,\r\n result: { ...(email ? { email } : {}), credentialPath: path },\r\n captured: { refresh_token: refresh, api_key: apiKey, ...(email ? { email } : {}) },\r\n };\r\n}\r\n\r\n/** The tiny page served at /callback: read the fragment, POST it same-origin to /capture. */\r\nfunction captureHtml(): string {\r\n return `<!doctype html><html><head><meta charset=\"utf-8\"><title>VO login</title></head>\r\n<body style=\"font-family:system-ui;max-width:32rem;margin:4rem auto;text-align:center\">\r\n<h2 id=\"m\">Completing sign-in\u2026</h2>\r\n<script>\r\n(function(){\r\n var h=location.hash.replace(/^#/,''), p=new URLSearchParams(h), b={};\r\n ['state','refresh_token','api_key','email'].forEach(function(k){ if(p.get(k)) b[k]=p.get(k); });\r\n fetch('/capture',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(b)})\r\n .then(function(r){ document.getElementById('m').textContent = r.ok ? 'Sign-in complete \u2014 you can close this tab.' : 'Sign-in failed \u2014 check the terminal.'; })\r\n .catch(function(){ document.getElementById('m').textContent = 'Sign-in failed \u2014 check the terminal.'; });\r\n})();\r\n</script></body></html>`;\r\n}\r\n\r\n/** Cross-platform browser open (best-effort; the URL is also printed for manual open). */\r\nfunction defaultOpenBrowser(url: string): void {\r\n const platform = process.platform;\r\n if (platform === 'win32') {\r\n spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();\r\n } else if (platform === 'darwin') {\r\n spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();\r\n } else {\r\n spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();\r\n }\r\n}\r\n\r\nexport interface RunLoginOptions {\r\n readonly dashboardUrl?: string;\r\n readonly timeoutMs?: number;\r\n readonly env?: Readonly<Record<string, string | undefined>>;\r\n readonly openBrowser?: (url: string) => void;\r\n readonly log?: (msg: string) => void;\r\n readonly nowIso?: () => string;\r\n /**\r\n * Inc 3b.4b: exchange the captured Firebase refresh credential for a scoped\r\n * `vocred_`. When provided AND it succeeds, the vocred_ is stored INSTEAD of the\r\n * raw refresh token (spec \u00A76). Fail-open: a null/throwing exchange keeps the\r\n * refresh credential. Production wires `exchangeForVoCredential` when a\r\n * `VO_CONTROL_PLANE_URL` is configured.\r\n */\r\n readonly exchange?: (\r\n refreshToken: string,\r\n apiKey: string,\r\n label?: string,\r\n ) => Promise<{ vo_credential: string; expires_at: string } | null>;\r\n}\r\n\r\n/**\r\n * Run the interactive loopback login. Resolves with the stored credential path on\r\n * success; rejects on timeout / CSRF mismatch / malformed response. FAIL-CLOSED:\r\n * nothing is stored unless the state matches and the fields are present.\r\n */\r\nexport async function runLogin(opts: RunLoginOptions = {}): Promise<LoginResult> {\r\n const env = opts.env ?? process.env;\r\n const dashboardUrl = (opts.dashboardUrl ?? env['VO_DASHBOARD_URL']?.trim() ?? DEFAULT_DASHBOARD_URL)\r\n .replace(/\\/+$/, '') || DEFAULT_DASHBOARD_URL;\r\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\r\n const log = opts.log ?? ((m: string) => console.error(m));\r\n const nowIso = opts.nowIso ?? (() => new Date().toISOString());\r\n const openBrowser = opts.openBrowser ?? defaultOpenBrowser;\r\n const state = randomBytes(32).toString('base64url');\r\n\r\n return new Promise<LoginResult>((resolve, reject) => {\r\n let settled = false;\r\n const finish = (err: Error | null, result?: LoginResult): void => {\r\n if (settled) return;\r\n settled = true;\r\n clearTimeout(timer);\r\n server.close();\r\n if (err) reject(err);\r\n else resolve(result as LoginResult);\r\n };\r\n\r\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\r\n const url = new URL(req.url ?? '/', 'http://127.0.0.1');\r\n if (req.method === 'GET' && url.pathname === '/callback') {\r\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\r\n res.end(captureHtml());\r\n return;\r\n }\r\n if (req.method === 'POST' && url.pathname === '/capture') {\r\n let body = '';\r\n req.on('data', (chunk: Buffer) => {\r\n body += chunk.toString('utf8');\r\n if (body.length > MAX_BODY_BYTES) req.destroy();\r\n });\r\n req.on('end', () => {\r\n void (async () => {\r\n // Guard against a dribble that crossed the cap before destroy() took effect.\r\n if (body.length > MAX_BODY_BYTES) {\r\n res.writeHead(413, { 'content-type': 'text/plain' });\r\n res.end('payload too large');\r\n finish(new Error('login request body exceeded the size cap'));\r\n return;\r\n }\r\n // Inc 3b.4b: when an exchange is configured, VALIDATE without writing the\r\n // raw refresh token first \u2014 then write exactly ONE credential (the\r\n // vocred_ on success, else the refresh on fail-open). This avoids a\r\n // window where the raw Firebase refresh token persists on disk during\r\n // the exchange round-trip (\u00A76: the raw token must not persist at all).\r\n const writeNow = (cred: StoredCredential): string => writeStoredCredential(cred, nowIso(), env);\r\n const outcome = processCapture(body, state, opts.exchange ? () => 'pending' : writeNow);\r\n let result = outcome.result;\r\n if (outcome.ok && outcome.captured && opts.exchange) {\r\n const capt = outcome.captured;\r\n let cred: StoredCredential = {\r\n refresh_token: capt.refresh_token,\r\n api_key: capt.api_key,\r\n ...(capt.email ? { email: capt.email } : {}),\r\n };\r\n try {\r\n const voc = await opts.exchange(capt.refresh_token, capt.api_key);\r\n if (voc && voc.vo_credential) {\r\n // Store ONLY the vocred_ \u2014 the raw refresh token never lands on disk.\r\n cred = {\r\n vo_credential: voc.vo_credential,\r\n vo_credential_expires_at: voc.expires_at,\r\n ...(capt.email ? { email: capt.email } : {}),\r\n };\r\n }\r\n } catch {\r\n /* keep the refresh credential \u2014 fail-open */\r\n }\r\n const path = writeNow(cred);\r\n result = { ...(capt.email ? { email: capt.email } : {}), credentialPath: path };\r\n }\r\n res.writeHead(outcome.httpStatus, { 'content-type': 'text/html; charset=utf-8' });\r\n res.end(outcome.ok ? '<h2>VO login complete \u2014 you can close this tab.</h2>' : `<h2>Login failed: ${outcome.error}</h2>`);\r\n finish(outcome.ok ? null : new Error(outcome.error ?? 'login failed'), result);\r\n })();\r\n });\r\n return;\r\n }\r\n res.writeHead(404);\r\n res.end('not found');\r\n });\r\n\r\n const timer = setTimeout(\r\n () => finish(new Error(`login timed out after ${timeoutMs}ms \u2014 no sign-in captured`)),\r\n timeoutMs,\r\n );\r\n\r\n server.on('error', (e: Error) => finish(e));\r\n server.listen(0, '127.0.0.1', () => {\r\n const addr = server.address();\r\n const port = addr && typeof addr === 'object' ? addr.port : 0;\r\n if (!port) {\r\n finish(new Error('failed to bind a loopback port'));\r\n return;\r\n }\r\n const loginUrl = `${dashboardUrl}/cli-login?port=${port}&state=${encodeURIComponent(state)}`;\r\n log(`[vo-mcp] Opening your browser to sign in:\\n ${loginUrl}`);\r\n log('[vo-mcp] If it did not open, paste that URL into your browser. Waiting for sign-in\u2026');\r\n try {\r\n openBrowser(loginUrl);\r\n } catch {\r\n /* user opens manually */\r\n }\r\n });\r\n });\r\n}\r\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", "/**\r\n * Auth token sources for the cloud admin client \u2014 Increment 3 of the VO Command\r\n * Center unification (`docs/vo/vo-command-center-unification-spec-2026-06-05.md`\r\n * \u00A76): retire the shared god admin-token from the LOCAL install. Instead of a\r\n * single static `VO_CONTROL_PLANE_ADMIN_TOKEN`, the client can authenticate as\r\n * the USER with a short-lived Firebase ID token (the same credential the\r\n * dashboard sends; the control-plane already accepts it for an allow-listed\r\n * operator \u2014 `cloud-run/vo-control-plane/src/firebase-auth.ts`).\r\n *\r\n * Three sources, selected by env (per-user preferred):\r\n * 1. `VO_USER_REFRESH_TOKEN` (+ `VO_FIREBASE_API_KEY`) \u2192 auto-refreshing Firebase\r\n * user token (durable; exchanges the refresh token for fresh ID tokens via\r\n * the PUBLIC securetoken endpoint \u2014 no backend, no god-token, no model keys).\r\n * 2. `VO_USER_ID_TOKEN` \u2192 a raw Firebase ID token (simplest interim; expires ~1h).\r\n * 3. `VO_CONTROL_PLANE_ADMIN_TOKEN` \u2192 the legacy static god-token (back-compat).\r\n *\r\n * FAIL-CLOSED: a source that cannot produce a token returns `null`; the caller\r\n * (admin client) then refuses the request rather than sending an empty Bearer.\r\n *\r\n * NOTE: this slice removes the god-token from the *client config* (the user's\r\n * own credential is sent instead). Acquiring the initial refresh/ID token via a\r\n * browser/device-flow `login` command, and OS-keychain storage, are follow-up\r\n * slices (spec \u00A77); for now the token is supplied via env.\r\n */\r\n\r\n/** A source of a fresh bearer token for the control-plane. */\r\nexport interface AuthTokenSource {\r\n /** Stable label for diagnostics/logs \u2014 NEVER the token value. */\r\n readonly kind: 'admin-token' | 'firebase-id-token' | 'firebase-refresh' | 'vo-credential';\r\n /** Returns a fresh bearer token, or `null` when unavailable (fail-closed). */\r\n getToken(): Promise<string | null>;\r\n}\r\n\r\ntype FetchLike = (\r\n url: string,\r\n init?: { method?: string; headers?: Record<string, string>; body?: string },\r\n) => Promise<{ status: number; text: () => Promise<string> }>;\r\n\r\n/** Public Google endpoint that exchanges a Firebase refresh token for a fresh ID token. */\r\nexport const FIREBASE_SECURETOKEN_URL = 'https://securetoken.googleapis.com/v1/token';\r\n\r\n/**\r\n * The Algosuite Firebase Web API key is HTTP-referrer-restricted, and Google's\r\n * Identity Toolkit / securetoken endpoints reject key'd requests that arrive with\r\n * an EMPTY Referer (HTTP 403 \"Requests from referer <empty> are blocked\"). Browser\r\n * callers send one automatically; Node `fetch` sends none \u2014 so every server-side\r\n * exchange must pin an origin the key's restriction allows. Live-diagnosed\r\n * 2026-06-09: without this header the whole Inc 3a refresh flow (and the Inc 3b.4b\r\n * vocred_ exchange that builds on it) fail-opens silently.\r\n */\r\nexport const FIREBASE_TOKEN_REFERER = 'https://algosuite.ai/';\r\n\r\n/** Refresh this many ms BEFORE the ID token actually expires (clock-skew margin). */\r\nconst REFRESH_SKEW_MS = 60_000;\r\n\r\n/** A fixed, already-available token (the god-token or a pasted ID token). */\r\nexport function createStaticTokenSource(\r\n token: string,\r\n kind: AuthTokenSource['kind'] = 'admin-token',\r\n): AuthTokenSource {\r\n const value = token.trim();\r\n return { kind, getToken: async () => (value.length > 0 ? value : null) };\r\n}\r\n\r\nexport interface FirebaseRefreshTokenSourceOptions {\r\n readonly refreshToken: string;\r\n /** The Firebase Web API key (PUBLIC, not a secret) for the Algosuite project. */\r\n readonly apiKey: string;\r\n /** Injectable clock (ms) for tests; defaults to `Date.now`. */\r\n readonly now?: () => number;\r\n /** Injectable fetch for tests; defaults to the global `fetch`. */\r\n readonly fetchFn?: FetchLike;\r\n}\r\n\r\n/**\r\n * Auto-refreshing Firebase user token. Exchanges the long-lived refresh token\r\n * for short-lived ID tokens via the public securetoken endpoint and caches the\r\n * ID token until shortly before it expires. Fail-closed: any error \u21D2 `null`.\r\n */\r\nexport function createFirebaseRefreshTokenSource(\r\n opts: FirebaseRefreshTokenSourceOptions,\r\n): AuthTokenSource {\r\n const refreshToken = opts.refreshToken.trim();\r\n const apiKey = opts.apiKey.trim();\r\n const now = opts.now ?? (() => Date.now());\r\n const fetchFn: FetchLike = opts.fetchFn ?? (globalThis.fetch as unknown as FetchLike);\r\n\r\n let cachedToken: string | null = null;\r\n let expiresAtMs = 0;\r\n // Collapse concurrent refreshes so N parallel tool calls trigger ONE exchange.\r\n let inFlight: Promise<string | null> | null = null;\r\n\r\n async function refresh(): Promise<string | null> {\r\n try {\r\n const res = await fetchFn(`${FIREBASE_SECURETOKEN_URL}?key=${encodeURIComponent(apiKey)}`, {\r\n method: 'POST',\r\n headers: {\r\n 'content-type': 'application/x-www-form-urlencoded',\r\n referer: FIREBASE_TOKEN_REFERER,\r\n },\r\n body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,\r\n });\r\n const text = await res.text();\r\n if (res.status < 200 || res.status >= 300) {\r\n cachedToken = null;\r\n return null;\r\n }\r\n const parsed = JSON.parse(text) as { id_token?: unknown; expires_in?: unknown };\r\n const idToken = typeof parsed.id_token === 'string' ? parsed.id_token : '';\r\n if (!idToken) {\r\n cachedToken = null;\r\n return null;\r\n }\r\n const expiresInSec = Number(parsed.expires_in);\r\n const ttlMs = Number.isFinite(expiresInSec) && expiresInSec > 0 ? expiresInSec * 1000 : 3_600_000;\r\n cachedToken = idToken;\r\n expiresAtMs = now() + ttlMs;\r\n return idToken;\r\n } catch {\r\n cachedToken = null;\r\n return null;\r\n }\r\n }\r\n\r\n return {\r\n kind: 'firebase-refresh',\r\n async getToken(): Promise<string | null> {\r\n if (cachedToken && now() < expiresAtMs - REFRESH_SKEW_MS) return cachedToken;\r\n if (!inFlight) {\r\n inFlight = refresh().finally(() => {\r\n inFlight = null;\r\n });\r\n }\r\n return inFlight;\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Select an auth token source from env. Per-user credentials win over the legacy\r\n * god-token, so a thin install configured with the user's token needs NO\r\n * `VO_CONTROL_PLANE_ADMIN_TOKEN`. Returns `null` when nothing is configured.\r\n * Throws on a half-configured refresh source (refresh token without API key).\r\n */\r\n/** Minimal stored-credential shape this selector consumes (from `vo-mcp login`). */\r\nexport interface StoredRefreshCredential {\r\n /** Firebase refresh token (optional once a scoped vocred_ is minted \u2014 Inc 3b.4b). */\r\n readonly refresh_token?: string;\r\n readonly api_key?: string;\r\n /** Scoped, revocable VO credential (Inc 3b.4b) \u2014 preferred over the raw refresh. */\r\n readonly vo_credential?: string;\r\n}\r\n\r\nexport function createAuthTokenSourceFromEnv(\r\n env: Readonly<Record<string, string | undefined>> = process.env,\r\n fetchFn?: FetchLike,\r\n /**\r\n * Inc 3b: a reader for a stored login credential (`vo-mcp login`). Injected so\r\n * the env-only callers (and tests) stay filesystem-free; production passes\r\n * `readStoredCredential`. A stored login credential is preferred over the legacy\r\n * god-token (so logging in retires it) but explicit env user-tokens still win.\r\n */\r\n readStoredCred: () => StoredRefreshCredential | null = () => null,\r\n): AuthTokenSource | null {\r\n const refreshToken = env['VO_USER_REFRESH_TOKEN']?.trim();\r\n const apiKey = env['VO_FIREBASE_API_KEY']?.trim();\r\n const idToken = env['VO_USER_ID_TOKEN']?.trim();\r\n const adminToken = env['VO_CONTROL_PLANE_ADMIN_TOKEN']?.trim();\r\n\r\n // 1. explicit env per-user refresh (CI / override).\r\n if (refreshToken || apiKey) {\r\n if (!refreshToken || !apiKey) {\r\n throw new Error(\r\n 'Per-user refresh auth requires BOTH VO_USER_REFRESH_TOKEN and VO_FIREBASE_API_KEY',\r\n );\r\n }\r\n return createFirebaseRefreshTokenSource({\r\n refreshToken,\r\n apiKey,\r\n ...(fetchFn ? { fetchFn } : {}),\r\n });\r\n }\r\n // 2. explicit env ID token.\r\n if (idToken) return createStaticTokenSource(idToken, 'firebase-id-token');\r\n // 3. stored login credential (`vo-mcp login`) \u2014 preferred over the god-token.\r\n const stored = readStoredCred();\r\n // 3a. a scoped vocred_ (Inc 3b.4b) wins \u2014 revocable, server-validated, and it\r\n // means no raw Firebase refresh token sits on disk. Sent as a static bearer;\r\n // the control-plane validates expiry/revocation (a stale one \u21D2 401 \u21D2 re-login).\r\n if (stored?.vo_credential && stored.vo_credential.trim()) {\r\n return createStaticTokenSource(stored.vo_credential.trim(), 'vo-credential');\r\n }\r\n // 3b. else the Firebase refresh credential.\r\n if (stored && stored.refresh_token?.trim() && stored.api_key?.trim()) {\r\n return createFirebaseRefreshTokenSource({\r\n refreshToken: stored.refresh_token.trim(),\r\n apiKey: stored.api_key.trim(),\r\n ...(fetchFn ? { fetchFn } : {}),\r\n });\r\n }\r\n // 4. legacy static god-token (back-compat).\r\n if (adminToken) return createStaticTokenSource(adminToken, 'admin-token');\r\n return null;\r\n}\r\n", "/**\r\n * Increment 3b.4b \u2014 exchange a captured Firebase refresh credential for a scoped,\r\n * revocable VO credential. The thin client mints ONCE via the control-plane\r\n * `POST /api/v1/auth/vo-credential` (authenticated with a fresh Firebase ID token\r\n * derived from the refresh token), then presents the `vocred_` instead of the raw\r\n * Firebase token thereafter \u2014 so the raw refresh token need not persist on disk\r\n * (spec \u00A76: kill the local secret).\r\n *\r\n * FAIL-OPEN to the refresh credential: ANY failure (an older control-plane without\r\n * the mint endpoint, a network error, a non-operator principal, a malformed\r\n * response) returns `null`, and the caller keeps using the Firebase refresh flow.\r\n * Never throws.\r\n */\r\nimport { createFirebaseRefreshTokenSource } from './auth-token-source.js';\r\n\r\n/** Minimal fetch shape (matches `auth-token-source`'s injectable fetch). */\r\ntype ExchangeFetch = (\r\n url: string,\r\n init?: { method?: string; headers?: Record<string, string>; body?: string },\r\n) => Promise<{ status: number; text: () => Promise<string> }>;\r\n\r\nexport interface VoCredentialResult {\r\n readonly vo_credential: string;\r\n readonly expires_at: string;\r\n}\r\n\r\nexport interface ExchangeOptions {\r\n readonly refreshToken: string;\r\n readonly apiKey: string;\r\n /** Control-plane base URL (e.g. `https://vo-control-plane-\u2026run.app`). */\r\n readonly controlPlaneUrl: string;\r\n /** Operator-friendly label stored with the credential (e.g. host + client). */\r\n readonly label?: string;\r\n /** Injectable fetch (tests); defaults to the global `fetch`. */\r\n readonly fetchFn?: ExchangeFetch;\r\n}\r\n\r\n/**\r\n * Mint a `vocred_` from a Firebase refresh credential. Returns the credential on\r\n * success, else `null` (fail-open). Never throws.\r\n */\r\nexport async function exchangeForVoCredential(opts: ExchangeOptions): Promise<VoCredentialResult | null> {\r\n const base = opts.controlPlaneUrl.replace(/\\/+$/, '');\r\n if (!base) return null;\r\n const fetchFn: ExchangeFetch = opts.fetchFn ?? (globalThis.fetch as unknown as ExchangeFetch);\r\n try {\r\n // 1. Derive a fresh Firebase ID token from the refresh credential (Inc 3a flow).\r\n const idToken = await createFirebaseRefreshTokenSource({\r\n refreshToken: opts.refreshToken,\r\n apiKey: opts.apiKey,\r\n ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}),\r\n }).getToken();\r\n if (!idToken) return null;\r\n\r\n // 2. Mint the scoped credential with the ID token.\r\n const res = await fetchFn(`${base}/api/v1/auth/vo-credential`, {\r\n method: 'POST',\r\n headers: { 'content-type': 'application/json', authorization: `Bearer ${idToken}` },\r\n body: JSON.stringify(opts.label ? { label: opts.label } : {}),\r\n });\r\n if (res.status !== 200) return null;\r\n const parsed = JSON.parse(await res.text()) as { token?: unknown; expires_at?: unknown };\r\n const token = typeof parsed.token === 'string' ? parsed.token : '';\r\n const expiresAt = typeof parsed.expires_at === 'string' ? parsed.expires_at : '';\r\n if (!token.startsWith('vocred_') || !expiresAt) return null;\r\n return { vo_credential: token, expires_at: expiresAt };\r\n } catch {\r\n return null;\r\n }\r\n}\r\n", "#!/usr/bin/env node\r\n/**\r\n * `vo-mcp login` CLI entry point.\r\n *\r\n * Runs the interactive browser-callback login flow and stores the credential.\r\n */\r\nimport { runLogin } from './cloud/login.js';\r\nimport { exchangeForVoCredential } from './cloud/vo-credential-exchange.js';\r\nimport { platform } from 'node:os';\r\n\r\nasync function main(): Promise<void> {\r\n const env = process.env;\r\n const log = (m: string): void => console.error(m);\r\n\r\n const controlPlaneUrl = env['VO_CONTROL_PLANE_URL']?.trim() || 'https://vo-control-plane-bzjphrajaq-uc.a.run.app';\r\n const exchange = async (refreshToken: string, apiKey: string): Promise<{ vo_credential: string; expires_at: string } | null> => {\r\n return exchangeForVoCredential({\r\n refreshToken,\r\n apiKey,\r\n controlPlaneUrl,\r\n label: `vo-mcp login (${platform()})`,\r\n });\r\n };\r\n\r\n try {\r\n const result = await runLogin({ env, log, exchange });\r\n log(`\u2713 Signed in${result.email ? ` as ${result.email}` : ''}. Credential stored at: ${result.credentialPath}`);\r\n log('\\nNext steps:');\r\n log(' 1. Start the runner: vo-mcp runner');\r\n log(' 2. Dispatch agents from: https://algosuite.ai/virtualoffice');\r\n } catch (err: unknown) {\r\n log(`\u2717 Login failed: ${err instanceof Error ? err.message : String(err)}`);\r\n process.exit(1);\r\n }\r\n}\r\n\r\nmain().catch((err: unknown) => {\r\n console.error('[vo-mcp login] fatal:', err);\r\n process.exit(1);\r\n});\r\n"],
4
+ "sourcesContent": ["/**\n * `vo-mcp login` \u2014 the thin-client loopback browser-callback flow (Increment 3b,\n * Option A; `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md`).\n *\n * The standard CLI-auth pattern (`gcloud auth login`, `gh auth login`): start a\n * loopback HTTP server on 127.0.0.1, open the browser to the dashboard's\n * `/cli-login` page (which runs the live Firebase Google/GitHub SSO), and capture\n * the user's refresh token when the page redirects back to the loopback. The\n * captured token (the user's OWN credential \u2014 never the god-token, never model\n * keys) is stored locally; Inc 3a's source auto-refreshes ID tokens from it.\n *\n * FROZEN loopback contract v1 (the dashboard `/cli-login` page builds against this):\n * CLI \u2192 browser: <dashboard>/cli-login?port=<loopbackPort>&state=<csrf>\n * page \u2192 browser: redirect to http://127.0.0.1:<port>/callback#state=..&refresh_token=..&api_key=..&email=..\n * (data in the URL FRAGMENT \u2014 never sent to a server log; the loopback /callback\n * page reads the fragment and POSTs it same-origin to /capture as JSON.)\n * CLI /capture: validate `state`, store {refresh_token, api_key, email}, 200, close.\n */\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http';\nimport { randomBytes } from 'node:crypto';\nimport { spawn } from 'node:child_process';\nimport { writeStoredCredential, type StoredCredential } from './credential-store.js';\n\n// The /cli-login page lives in the MAIN app (algosuite.ai) per the 2026-06-08\n// surface pivot (unification-spec \u00A711) \u2014 the standalone vo-dashboard never\n// shipped the page and its deploy lane is operator-dispatch-only. Override with\n// VO_DASHBOARD_URL (env) or opts.dashboardUrl.\nexport const DEFAULT_DASHBOARD_URL = 'https://algosuite.ai';\nconst DEFAULT_TIMEOUT_MS = 120_000;\nconst MAX_BODY_BYTES = 16_384;\n\nexport interface LoginResult {\n readonly email?: string;\n readonly credentialPath: string;\n}\n\nexport interface CaptureOutcome {\n readonly ok: boolean;\n readonly httpStatus: number;\n readonly result?: LoginResult;\n readonly error?: string;\n /**\n * The captured Firebase refresh credential (Inc 3b.4b). The caller may exchange\n * it for a scoped `vocred_` and re-store, dropping the raw refresh token.\n */\n readonly captured?: { readonly refresh_token: string; readonly api_key: string; readonly email?: string };\n}\n\n/**\n * Pure core of the /capture handler \u2014 validate the CSRF state + required fields,\n * then persist via the injected `store`. Fully unit-testable; the http server is\n * a thin shell around this.\n */\nexport function processCapture(\n rawBody: string,\n expectedState: string,\n store: (cred: StoredCredential) => string,\n): CaptureOutcome {\n let data: { state?: unknown; refresh_token?: unknown; api_key?: unknown; email?: unknown };\n try {\n data = JSON.parse(rawBody);\n } catch {\n return { ok: false, httpStatus: 400, error: 'invalid JSON body' };\n }\n if (!data || typeof data !== 'object') return { ok: false, httpStatus: 400, error: 'invalid body' };\n // CSRF: the state echoed by the browser MUST match the one we generated.\n if (typeof data.state !== 'string' || data.state !== expectedState) {\n return { ok: false, httpStatus: 403, error: 'state mismatch (possible CSRF) \u2014 login aborted' };\n }\n const refresh = typeof data.refresh_token === 'string' ? data.refresh_token.trim() : '';\n const apiKey = typeof data.api_key === 'string' ? data.api_key.trim() : '';\n if (!refresh || !apiKey) {\n return { ok: false, httpStatus: 400, error: 'login response missing refresh_token / api_key' };\n }\n const email = typeof data.email === 'string' && data.email.trim() ? data.email.trim() : undefined;\n const path = store({ refresh_token: refresh, api_key: apiKey, ...(email ? { email } : {}) });\n return {\n ok: true,\n httpStatus: 200,\n result: { ...(email ? { email } : {}), credentialPath: path },\n captured: { refresh_token: refresh, api_key: apiKey, ...(email ? { email } : {}) },\n };\n}\n\n/** The tiny page served at /callback: read the fragment, POST it same-origin to /capture. */\nfunction captureHtml(): string {\n return `<!doctype html><html><head><meta charset=\"utf-8\"><title>VO login</title></head>\n<body style=\"font-family:system-ui;max-width:32rem;margin:4rem auto;text-align:center\">\n<h2 id=\"m\">Completing sign-in\u2026</h2>\n<script>\n(function(){\n var h=location.hash.replace(/^#/,''), p=new URLSearchParams(h), b={};\n ['state','refresh_token','api_key','email'].forEach(function(k){ if(p.get(k)) b[k]=p.get(k); });\n fetch('/capture',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(b)})\n .then(function(r){ document.getElementById('m').textContent = r.ok ? 'Sign-in complete \u2014 you can close this tab.' : 'Sign-in failed \u2014 check the terminal.'; })\n .catch(function(){ document.getElementById('m').textContent = 'Sign-in failed \u2014 check the terminal.'; });\n})();\n</script></body></html>`;\n}\n\n/** Cross-platform browser open (best-effort; the URL is also printed for manual open). */\nfunction defaultOpenBrowser(url: string): void {\n const platform = process.platform;\n if (platform === 'win32') {\n spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' }).unref();\n } else if (platform === 'darwin') {\n spawn('open', [url], { detached: true, stdio: 'ignore' }).unref();\n } else {\n spawn('xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();\n }\n}\n\nexport interface RunLoginOptions {\n readonly dashboardUrl?: string;\n readonly timeoutMs?: number;\n readonly env?: Readonly<Record<string, string | undefined>>;\n readonly openBrowser?: (url: string) => void;\n readonly log?: (msg: string) => void;\n readonly nowIso?: () => string;\n /**\n * Inc 3b.4b: exchange the captured Firebase refresh credential for a scoped\n * `vocred_`. When provided AND it succeeds, the vocred_ is stored INSTEAD of the\n * raw refresh token (spec \u00A76). Fail-open: a null/throwing exchange keeps the\n * refresh credential. Production wires `exchangeForVoCredential` when a\n * `VO_CONTROL_PLANE_URL` is configured.\n */\n readonly exchange?: (\n refreshToken: string,\n apiKey: string,\n label?: string,\n ) => Promise<{ vo_credential: string; expires_at: string } | null>;\n}\n\n/**\n * Run the interactive loopback login. Resolves with the stored credential path on\n * success; rejects on timeout / CSRF mismatch / malformed response. FAIL-CLOSED:\n * nothing is stored unless the state matches and the fields are present.\n */\nexport async function runLogin(opts: RunLoginOptions = {}): Promise<LoginResult> {\n const env = opts.env ?? process.env;\n const dashboardUrl = (opts.dashboardUrl ?? env['VO_DASHBOARD_URL']?.trim() ?? DEFAULT_DASHBOARD_URL)\n .replace(/\\/+$/, '') || DEFAULT_DASHBOARD_URL;\n const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const log = opts.log ?? ((m: string) => console.error(m));\n const nowIso = opts.nowIso ?? (() => new Date().toISOString());\n const openBrowser = opts.openBrowser ?? defaultOpenBrowser;\n const state = randomBytes(32).toString('base64url');\n\n return new Promise<LoginResult>((resolve, reject) => {\n let settled = false;\n const finish = (err: Error | null, result?: LoginResult): void => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n server.close();\n if (err) reject(err);\n else resolve(result as LoginResult);\n };\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url ?? '/', 'http://127.0.0.1');\n if (req.method === 'GET' && url.pathname === '/callback') {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });\n res.end(captureHtml());\n return;\n }\n if (req.method === 'POST' && url.pathname === '/capture') {\n let body = '';\n req.on('data', (chunk: Buffer) => {\n body += chunk.toString('utf8');\n if (body.length > MAX_BODY_BYTES) req.destroy();\n });\n req.on('end', () => {\n void (async () => {\n // Guard against a dribble that crossed the cap before destroy() took effect.\n if (body.length > MAX_BODY_BYTES) {\n res.writeHead(413, { 'content-type': 'text/plain' });\n res.end('payload too large');\n finish(new Error('login request body exceeded the size cap'));\n return;\n }\n // Inc 3b.4b: when an exchange is configured, VALIDATE without writing the\n // raw refresh token first \u2014 then write exactly ONE credential (the\n // vocred_ on success, else the refresh on fail-open). This avoids a\n // window where the raw Firebase refresh token persists on disk during\n // the exchange round-trip (\u00A76: the raw token must not persist at all).\n const writeNow = (cred: StoredCredential): string => writeStoredCredential(cred, nowIso(), env);\n const outcome = processCapture(body, state, opts.exchange ? () => 'pending' : writeNow);\n let result = outcome.result;\n if (outcome.ok && outcome.captured && opts.exchange) {\n const capt = outcome.captured;\n let cred: StoredCredential = {\n refresh_token: capt.refresh_token,\n api_key: capt.api_key,\n ...(capt.email ? { email: capt.email } : {}),\n };\n try {\n const voc = await opts.exchange(capt.refresh_token, capt.api_key);\n if (voc && voc.vo_credential) {\n // Store ONLY the vocred_ \u2014 the raw refresh token never lands on disk.\n cred = {\n vo_credential: voc.vo_credential,\n vo_credential_expires_at: voc.expires_at,\n ...(capt.email ? { email: capt.email } : {}),\n };\n }\n } catch {\n /* keep the refresh credential \u2014 fail-open */\n }\n const path = writeNow(cred);\n result = { ...(capt.email ? { email: capt.email } : {}), credentialPath: path };\n }\n res.writeHead(outcome.httpStatus, { 'content-type': 'text/html; charset=utf-8' });\n res.end(outcome.ok ? '<h2>VO login complete \u2014 you can close this tab.</h2>' : `<h2>Login failed: ${outcome.error}</h2>`);\n finish(outcome.ok ? null : new Error(outcome.error ?? 'login failed'), result);\n })();\n });\n return;\n }\n res.writeHead(404);\n res.end('not found');\n });\n\n const timer = setTimeout(\n () => finish(new Error(`login timed out after ${timeoutMs}ms \u2014 no sign-in captured`)),\n timeoutMs,\n );\n\n server.on('error', (e: Error) => finish(e));\n server.listen(0, '127.0.0.1', () => {\n const addr = server.address();\n const port = addr && typeof addr === 'object' ? addr.port : 0;\n if (!port) {\n finish(new Error('failed to bind a loopback port'));\n return;\n }\n const loginUrl = `${dashboardUrl}/cli-login?port=${port}&state=${encodeURIComponent(state)}`;\n log(`[vo-mcp] Opening your browser to sign in:\\n ${loginUrl}`);\n log('[vo-mcp] If it did not open, paste that URL into your browser. Waiting for sign-in\u2026');\n try {\n openBrowser(loginUrl);\n } catch {\n /* user opens manually */\n }\n });\n });\n}\n", "/**\n * Local credential store for the thin-client `vo-mcp login` flow (Increment 3b,\n * Option A \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md`).\n *\n * Persists the per-user Firebase refresh token (the user's OWN credential, never\n * the god-token, never model keys) captured by `login`, so the auto-refreshing\n * token source (Inc 3a) can mint fresh ID tokens across MCP restarts.\n *\n * Storage precedence (Inc 3b.3):\n * 1. **OS keychain** (Windows Credential Manager / macOS Keychain / libsecret)\n * via the optional `@napi-rs/keyring` backend (`keychain.ts`). The DEFAULT\n * when available \u2014 the secret never lands in plaintext on disk.\n * 2. **0600 file** at `$VO_MCP_CREDENTIALS_PATH` or `~/.config/vo-mcp/credentials.json`.\n * The fallback when the keychain is unavailable or disabled\n * (`VO_MCP_DISABLE_KEYCHAIN`). `VO_MCP_CREDENTIALS_PATH` only sets the file\n * LOCATION; force file storage with `VO_MCP_DISABLE_KEYCHAIN`.\n *\n * `env`-supplied tokens (`VO_USER_REFRESH_TOKEN`, etc.) still win over BOTH\n * stores \u2014 that precedence lives upstream in `auth-token-source.ts`.\n *\n * **Single source of truth.** The credential lives in EITHER the keychain OR the\n * file, never both: a write to one store CLEARS the other, so a stale entry can\n * never shadow the current credential on read, and the secret never lingers in\n * plaintext after a migration to the keychain.\n *\n * **Keychain durability.** A keychain-stored credential is only readable while\n * the `@napi-rs/keyring` native module loads. If the module later becomes\n * unavailable (an ABI break across a Node upgrade, a corrupted install), the\n * credential can't be read and the user re-runs `vo-mcp login` \u2014 the same\n * behaviour as `gh` / `gcloud` / `firebase` keychain storage. We deliberately do\n * NOT mirror the secret to a plaintext file as a fallback: that would defeat the\n * entire point of keychain storage (keeping the secret off plaintext disk).\n */\nimport { homedir } from 'node:os';\nimport { join, dirname } from 'node:path';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n chmodSync,\n rmSync,\n} from 'node:fs';\n\nimport { keychainAvailable, keychainGet, keychainSet, keychainDelete } from './keychain.js';\n\nexport interface StoredCredential {\n /**\n * Firebase refresh token (long-lived; exchanged for short-lived ID tokens).\n * OPTIONAL since Inc 3b.4b: once a scoped `vo_credential` is minted, the raw\n * refresh token is dropped, so a stored credential may carry ONLY the vocred_.\n */\n readonly refresh_token?: string;\n /** Firebase Web API key (PUBLIC) needed for the securetoken refresh exchange. */\n readonly api_key?: string;\n /**\n * Scoped, revocable VO credential (`vocred_`) minted by the control-plane\n * (Inc 3b.4b). Preferred over the raw refresh token; lets the client present a\n * revocable, server-side credential instead of the Firebase refresh token.\n */\n readonly vo_credential?: string;\n /** ISO-8601 expiry of `vo_credential` (the client re-logs-in past this). */\n readonly vo_credential_expires_at?: string;\n /** The signed-in operator email (diagnostics only). */\n readonly email?: string;\n /** ISO timestamp the credential was stored. */\n readonly stored_at?: string;\n}\n\n/**\n * Pluggable OS-keychain backend. Defaults to the real `@napi-rs/keyring` wrapper;\n * tests inject a deterministic fake so they never touch the host keychain.\n */\nexport interface KeychainBackend {\n available(): boolean;\n get(): string | null;\n set(secret: string): boolean;\n delete(): boolean;\n}\n\nconst realKeychain: KeychainBackend = {\n available: keychainAvailable,\n get: keychainGet,\n set: keychainSet,\n delete: keychainDelete,\n};\n\n/** Human-readable \"location\" returned when the credential was stored in the OS keychain. */\nexport const KEYCHAIN_LOCATION = 'OS keychain (service \"vo-mcp\")';\n\n/** Resolve the credentials file path (env override \u2192 XDG-ish default under home). */\nexport function credentialPath(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const override = env['VO_MCP_CREDENTIALS_PATH']?.trim();\n if (override) return override;\n return join(homedir(), '.config', 'vo-mcp', 'credentials.json');\n}\n\n/**\n * Whether the keychain should be consulted at all (read OR write). False when the\n * native backend is unavailable or `VO_MCP_DISABLE_KEYCHAIN` is set (CI/headless).\n */\nfunction keychainEnabled(\n env: Readonly<Record<string, string | undefined>>,\n keychain: KeychainBackend,\n): boolean {\n const disabled = (env['VO_MCP_DISABLE_KEYCHAIN'] ?? '').trim().toLowerCase();\n if (disabled === '1' || disabled === 'true' || disabled === 'yes') return false;\n return keychain.available();\n}\n\n/** Parse + validate a stored credential blob. Returns null on any problem (never throws). */\nfunction deserialize(raw: string): StoredCredential | null {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredCredential>;\n const refresh = typeof parsed.refresh_token === 'string' ? parsed.refresh_token.trim() : '';\n const apiKey = typeof parsed.api_key === 'string' ? parsed.api_key.trim() : '';\n const voCred = typeof parsed.vo_credential === 'string' ? parsed.vo_credential.trim() : '';\n // Valid if it carries a scoped vocred_ OR a full Firebase refresh pair.\n if (!voCred && (!refresh || !apiKey)) return null;\n return {\n ...(refresh ? { refresh_token: refresh } : {}),\n ...(apiKey ? { api_key: apiKey } : {}),\n ...(voCred ? { vo_credential: voCred } : {}),\n ...(typeof parsed.vo_credential_expires_at === 'string' ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {}),\n ...(typeof parsed.email === 'string' ? { email: parsed.email } : {}),\n ...(typeof parsed.stored_at === 'string' ? { stored_at: parsed.stored_at } : {}),\n };\n } catch {\n return null;\n }\n}\n\nfunction readFromFile(env: Readonly<Record<string, string | undefined>>): StoredCredential | null {\n try {\n const p = credentialPath(env);\n if (!existsSync(p)) return null;\n return deserialize(readFileSync(p, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Read the stored credential, or `null` if absent/unreadable/invalid (never\n * throws). Consults an ENABLED keychain first (regardless of the write-target\n * flags, so a credential written to the keychain is found even if\n * `VO_MCP_CREDENTIALS_PATH` is later set), then the 0600 file.\n */\nexport function readStoredCredential(\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): StoredCredential | null {\n if (keychainEnabled(env, keychain)) {\n const raw = keychain.get();\n const fromKeychain = raw ? deserialize(raw) : null;\n if (fromKeychain) return fromKeychain;\n }\n return readFromFile(env);\n}\n\nfunction deleteFile(env: Readonly<Record<string, string | undefined>>): void {\n try {\n rmSync(credentialPath(env), { force: true });\n } catch {\n /* best-effort */\n }\n}\n\nfunction writeToFile(\n payload: StoredCredential,\n env: Readonly<Record<string, string | undefined>>,\n): string {\n const p = credentialPath(env);\n mkdirSync(dirname(p), { recursive: true });\n writeFileSync(p, `${JSON.stringify(payload, null, 2)}\\n`, { mode: 0o600 });\n // Best-effort tighten (no-op / throws on some Windows filesystems \u2014 ignore).\n try {\n chmodSync(p, 0o600);\n } catch {\n /* best-effort */\n }\n return p;\n}\n\n/**\n * Persist the credential. Prefers the OS keychain (secret never hits plaintext\n * disk); otherwise writes the 0600 file. Writing to one store CLEARS the other\n * (single source of truth \u2014 no stale shadow, no lingering plaintext). Returns the\n * location it was stored (`KEYCHAIN_LOCATION` or the file path).\n */\nexport function writeStoredCredential(\n cred: StoredCredential,\n storedAt: string,\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): string {\n const payload: StoredCredential = {\n ...(cred.refresh_token ? { refresh_token: cred.refresh_token } : {}),\n ...(cred.api_key ? { api_key: cred.api_key } : {}),\n ...(cred.vo_credential ? { vo_credential: cred.vo_credential } : {}),\n ...(cred.vo_credential_expires_at ? { vo_credential_expires_at: cred.vo_credential_expires_at } : {}),\n ...(cred.email ? { email: cred.email } : {}),\n stored_at: cred.stored_at ?? storedAt,\n };\n if (keychainEnabled(env, keychain) && keychain.set(JSON.stringify(payload))) {\n // Stored in the keychain \u2192 clear any stale plaintext file so the secret\n // doesn't linger on disk and can't shadow the keychain on read.\n deleteFile(env);\n return KEYCHAIN_LOCATION;\n }\n const p = writeToFile(payload, env);\n // Stored in the file \u2192 clear any stale keychain entry so it can't shadow the\n // newer file credential on read.\n if (keychainEnabled(env, keychain)) keychain.delete();\n return p;\n}\n", "/**\n * Optional OS-keychain backend for the thin-client credential store (Increment\n * 3b.3 \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md` \u00A75/\u00A76).\n *\n * Loads `@napi-rs/keyring` at runtime via `createRequire`, so it is a TRUE\n * optional dependency: if the native module is absent or fails to load\n * (unsupported platform, prebuilt binary missing, headless CI), every function\n * degrades to a no-op and the caller (`credential-store.ts`) falls back to the\n * 0600 file store. `@napi-rs/keyring`'s `Entry` API is SYNCHRONOUS, so the\n * credential store stays synchronous \u2014 no async ripple into the Inc-3a token\n * source that reads it.\n *\n * Why `createRequire` and not a static/dynamic `import`: a static import would\n * make the native module a HARD dependency (a missing prebuilt would crash the\n * MCP at startup); a dynamic `import()` is async (would force the whole read\n * path async). `createRequire(...)` inside a try/catch loads it lazily and\n * synchronously, and a load failure is just \"keychain unavailable\".\n */\nimport { createRequire } from 'node:module';\n\n/** Keychain service + account the single refresh credential is stored under. */\nconst SERVICE = 'vo-mcp';\nconst ACCOUNT = 'refresh-credential';\n\ninterface KeyringEntry {\n getPassword(): string | null;\n setPassword(password: string): void;\n deletePassword(): boolean;\n}\ninterface KeyringModule {\n Entry: new (service: string, account: string) => KeyringEntry;\n}\n\n// undefined = not yet attempted; null = attempted and unavailable.\nlet cached: KeyringModule | null | undefined;\n\nfunction loadKeyring(): KeyringModule | null {\n if (cached !== undefined) return cached;\n try {\n const req = createRequire(import.meta.url);\n const mod = req('@napi-rs/keyring') as Partial<KeyringModule>;\n cached = mod && typeof mod.Entry === 'function' ? (mod as KeyringModule) : null;\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** True when the OS keychain backend is usable in this runtime. */\nexport function keychainAvailable(): boolean {\n return loadKeyring() !== null;\n}\n\n/** Read the raw stored secret string from the OS keychain, or null. Never throws. */\nexport function keychainGet(): string | null {\n const k = loadKeyring();\n if (!k) return null;\n try {\n return new k.Entry(SERVICE, ACCOUNT).getPassword();\n } catch {\n return null;\n }\n}\n\n/** Store the raw secret string in the OS keychain. Returns true on success. Never throws. */\nexport function keychainSet(secret: string): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n new k.Entry(SERVICE, ACCOUNT).setPassword(secret);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Delete the stored secret from the OS keychain. Returns true if an entry was\n * removed. Never throws \u2014 a no-op (and `false`) when the backend is unavailable\n * or the entry is absent. Used to keep ONE source of truth: when the credential\n * is (re)written to the file, any stale keychain entry is cleared so it can't\n * shadow the newer file credential on read (and vice-versa).\n */\nexport function keychainDelete(): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n return new k.Entry(SERVICE, ACCOUNT).deletePassword();\n } catch {\n return false;\n }\n}\n\n/** Test-only seam to reset the memoised module load. */\nexport function __resetKeychainCache(): void {\n cached = undefined;\n}\n", "/**\n * Auth token sources for the cloud admin client \u2014 Increment 3 of the VO Command\n * Center unification (`docs/vo/vo-command-center-unification-spec-2026-06-05.md`\n * \u00A76): retire the shared god admin-token from the LOCAL install. Instead of a\n * single static `VO_CONTROL_PLANE_ADMIN_TOKEN`, the client can authenticate as\n * the USER with a short-lived Firebase ID token (the same credential the\n * dashboard sends; the control-plane already accepts it for an allow-listed\n * operator \u2014 `cloud-run/vo-control-plane/src/firebase-auth.ts`).\n *\n * Three sources, selected by env (per-user preferred):\n * 1. `VO_USER_REFRESH_TOKEN` (+ `VO_FIREBASE_API_KEY`) \u2192 auto-refreshing Firebase\n * user token (durable; exchanges the refresh token for fresh ID tokens via\n * the PUBLIC securetoken endpoint \u2014 no backend, no god-token, no model keys).\n * 2. `VO_USER_ID_TOKEN` \u2192 a raw Firebase ID token (simplest interim; expires ~1h).\n * 3. `VO_CONTROL_PLANE_ADMIN_TOKEN` \u2192 the legacy static god-token (back-compat).\n *\n * FAIL-CLOSED: a source that cannot produce a token returns `null`; the caller\n * (admin client) then refuses the request rather than sending an empty Bearer.\n *\n * NOTE: this slice removes the god-token from the *client config* (the user's\n * own credential is sent instead). Acquiring the initial refresh/ID token via a\n * browser/device-flow `login` command, and OS-keychain storage, are follow-up\n * slices (spec \u00A77); for now the token is supplied via env.\n */\n\n/** A source of a fresh bearer token for the control-plane. */\nexport interface AuthTokenSource {\n /** Stable label for diagnostics/logs \u2014 NEVER the token value. */\n readonly kind: 'admin-token' | 'firebase-id-token' | 'firebase-refresh' | 'vo-credential';\n /** Returns a fresh bearer token, or `null` when unavailable (fail-closed). */\n getToken(): Promise<string | null>;\n}\n\ntype FetchLike = (\n url: string,\n init?: { method?: string; headers?: Record<string, string>; body?: string },\n) => Promise<{ status: number; text: () => Promise<string> }>;\n\n/** Public Google endpoint that exchanges a Firebase refresh token for a fresh ID token. */\nexport const FIREBASE_SECURETOKEN_URL = 'https://securetoken.googleapis.com/v1/token';\n\n/**\n * The Algosuite Firebase Web API key is HTTP-referrer-restricted, and Google's\n * Identity Toolkit / securetoken endpoints reject key'd requests that arrive with\n * an EMPTY Referer (HTTP 403 \"Requests from referer <empty> are blocked\"). Browser\n * callers send one automatically; Node `fetch` sends none \u2014 so every server-side\n * exchange must pin an origin the key's restriction allows. Live-diagnosed\n * 2026-06-09: without this header the whole Inc 3a refresh flow (and the Inc 3b.4b\n * vocred_ exchange that builds on it) fail-opens silently.\n */\nexport const FIREBASE_TOKEN_REFERER = 'https://algosuite.ai/';\n\n/** Refresh this many ms BEFORE the ID token actually expires (clock-skew margin). */\nconst REFRESH_SKEW_MS = 60_000;\n\n/** A fixed, already-available token (the god-token or a pasted ID token). */\nexport function createStaticTokenSource(\n token: string,\n kind: AuthTokenSource['kind'] = 'admin-token',\n): AuthTokenSource {\n const value = token.trim();\n return { kind, getToken: async () => (value.length > 0 ? value : null) };\n}\n\nexport interface FirebaseRefreshTokenSourceOptions {\n readonly refreshToken: string;\n /** The Firebase Web API key (PUBLIC, not a secret) for the Algosuite project. */\n readonly apiKey: string;\n /** Injectable clock (ms) for tests; defaults to `Date.now`. */\n readonly now?: () => number;\n /** Injectable fetch for tests; defaults to the global `fetch`. */\n readonly fetchFn?: FetchLike;\n}\n\n/**\n * Auto-refreshing Firebase user token. Exchanges the long-lived refresh token\n * for short-lived ID tokens via the public securetoken endpoint and caches the\n * ID token until shortly before it expires. Fail-closed: any error \u21D2 `null`.\n */\nexport function createFirebaseRefreshTokenSource(\n opts: FirebaseRefreshTokenSourceOptions,\n): AuthTokenSource {\n const refreshToken = opts.refreshToken.trim();\n const apiKey = opts.apiKey.trim();\n const now = opts.now ?? (() => Date.now());\n const fetchFn: FetchLike = opts.fetchFn ?? (globalThis.fetch as unknown as FetchLike);\n\n let cachedToken: string | null = null;\n let expiresAtMs = 0;\n // Collapse concurrent refreshes so N parallel tool calls trigger ONE exchange.\n let inFlight: Promise<string | null> | null = null;\n\n async function refresh(): Promise<string | null> {\n try {\n const res = await fetchFn(`${FIREBASE_SECURETOKEN_URL}?key=${encodeURIComponent(apiKey)}`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n referer: FIREBASE_TOKEN_REFERER,\n },\n body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,\n });\n const text = await res.text();\n if (res.status < 200 || res.status >= 300) {\n cachedToken = null;\n return null;\n }\n const parsed = JSON.parse(text) as { id_token?: unknown; expires_in?: unknown };\n const idToken = typeof parsed.id_token === 'string' ? parsed.id_token : '';\n if (!idToken) {\n cachedToken = null;\n return null;\n }\n const expiresInSec = Number(parsed.expires_in);\n const ttlMs = Number.isFinite(expiresInSec) && expiresInSec > 0 ? expiresInSec * 1000 : 3_600_000;\n cachedToken = idToken;\n expiresAtMs = now() + ttlMs;\n return idToken;\n } catch {\n cachedToken = null;\n return null;\n }\n }\n\n return {\n kind: 'firebase-refresh',\n async getToken(): Promise<string | null> {\n if (cachedToken && now() < expiresAtMs - REFRESH_SKEW_MS) return cachedToken;\n if (!inFlight) {\n inFlight = refresh().finally(() => {\n inFlight = null;\n });\n }\n return inFlight;\n },\n };\n}\n\n/**\n * Select an auth token source from env. Per-user credentials win over the legacy\n * god-token, so a thin install configured with the user's token needs NO\n * `VO_CONTROL_PLANE_ADMIN_TOKEN`. Returns `null` when nothing is configured.\n * Throws on a half-configured refresh source (refresh token without API key).\n */\n/** Minimal stored-credential shape this selector consumes (from `vo-mcp login`). */\nexport interface StoredRefreshCredential {\n /** Firebase refresh token (optional once a scoped vocred_ is minted \u2014 Inc 3b.4b). */\n readonly refresh_token?: string;\n readonly api_key?: string;\n /** Scoped, revocable VO credential (Inc 3b.4b) \u2014 preferred over the raw refresh. */\n readonly vo_credential?: string;\n}\n\nexport function createAuthTokenSourceFromEnv(\n env: Readonly<Record<string, string | undefined>> = process.env,\n fetchFn?: FetchLike,\n /**\n * Inc 3b: a reader for a stored login credential (`vo-mcp login`). Injected so\n * the env-only callers (and tests) stay filesystem-free; production passes\n * `readStoredCredential`. A stored login credential is preferred over the legacy\n * god-token (so logging in retires it) but explicit env user-tokens still win.\n */\n readStoredCred: () => StoredRefreshCredential | null = () => null,\n): AuthTokenSource | null {\n const refreshToken = env['VO_USER_REFRESH_TOKEN']?.trim();\n const apiKey = env['VO_FIREBASE_API_KEY']?.trim();\n const idToken = env['VO_USER_ID_TOKEN']?.trim();\n const adminToken = env['VO_CONTROL_PLANE_ADMIN_TOKEN']?.trim();\n\n // 1. explicit env per-user refresh (CI / override).\n if (refreshToken || apiKey) {\n if (!refreshToken || !apiKey) {\n throw new Error(\n 'Per-user refresh auth requires BOTH VO_USER_REFRESH_TOKEN and VO_FIREBASE_API_KEY',\n );\n }\n return createFirebaseRefreshTokenSource({\n refreshToken,\n apiKey,\n ...(fetchFn ? { fetchFn } : {}),\n });\n }\n // 2. explicit env ID token.\n if (idToken) return createStaticTokenSource(idToken, 'firebase-id-token');\n // 3. stored login credential (`vo-mcp login`) \u2014 preferred over the god-token.\n const stored = readStoredCred();\n // 3a. a scoped vocred_ (Inc 3b.4b) wins \u2014 revocable, server-validated, and it\n // means no raw Firebase refresh token sits on disk. Sent as a static bearer;\n // the control-plane validates expiry/revocation (a stale one \u21D2 401 \u21D2 re-login).\n if (stored?.vo_credential && stored.vo_credential.trim()) {\n return createStaticTokenSource(stored.vo_credential.trim(), 'vo-credential');\n }\n // 3b. else the Firebase refresh credential.\n if (stored && stored.refresh_token?.trim() && stored.api_key?.trim()) {\n return createFirebaseRefreshTokenSource({\n refreshToken: stored.refresh_token.trim(),\n apiKey: stored.api_key.trim(),\n ...(fetchFn ? { fetchFn } : {}),\n });\n }\n // 4. legacy static god-token (back-compat).\n if (adminToken) return createStaticTokenSource(adminToken, 'admin-token');\n return null;\n}\n", "/**\n * Increment 3b.4b \u2014 exchange a captured Firebase refresh credential for a scoped,\n * revocable VO credential. The thin client mints ONCE via the control-plane\n * `POST /api/v1/auth/vo-credential` (authenticated with a fresh Firebase ID token\n * derived from the refresh token), then presents the `vocred_` instead of the raw\n * Firebase token thereafter \u2014 so the raw refresh token need not persist on disk\n * (spec \u00A76: kill the local secret).\n *\n * FAIL-OPEN to the refresh credential: ANY failure (an older control-plane without\n * the mint endpoint, a network error, a non-operator principal, a malformed\n * response) returns `null`, and the caller keeps using the Firebase refresh flow.\n * Never throws.\n */\nimport { createFirebaseRefreshTokenSource } from './auth-token-source.js';\n\n/** Minimal fetch shape (matches `auth-token-source`'s injectable fetch). */\ntype ExchangeFetch = (\n url: string,\n init?: { method?: string; headers?: Record<string, string>; body?: string },\n) => Promise<{ status: number; text: () => Promise<string> }>;\n\nexport interface VoCredentialResult {\n readonly vo_credential: string;\n readonly expires_at: string;\n}\n\nexport interface ExchangeOptions {\n readonly refreshToken: string;\n readonly apiKey: string;\n /** Control-plane base URL (e.g. `https://vo-control-plane-\u2026run.app`). */\n readonly controlPlaneUrl: string;\n /** Operator-friendly label stored with the credential (e.g. host + client). */\n readonly label?: string;\n /** Injectable fetch (tests); defaults to the global `fetch`. */\n readonly fetchFn?: ExchangeFetch;\n}\n\n/**\n * Mint a `vocred_` from a Firebase refresh credential. Returns the credential on\n * success, else `null` (fail-open). Never throws.\n */\nexport async function exchangeForVoCredential(opts: ExchangeOptions): Promise<VoCredentialResult | null> {\n const base = opts.controlPlaneUrl.replace(/\\/+$/, '');\n if (!base) return null;\n const fetchFn: ExchangeFetch = opts.fetchFn ?? (globalThis.fetch as unknown as ExchangeFetch);\n try {\n // 1. Derive a fresh Firebase ID token from the refresh credential (Inc 3a flow).\n const idToken = await createFirebaseRefreshTokenSource({\n refreshToken: opts.refreshToken,\n apiKey: opts.apiKey,\n ...(opts.fetchFn ? { fetchFn: opts.fetchFn } : {}),\n }).getToken();\n if (!idToken) return null;\n\n // 2. Mint the scoped credential with the ID token.\n const res = await fetchFn(`${base}/api/v1/auth/vo-credential`, {\n method: 'POST',\n headers: { 'content-type': 'application/json', authorization: `Bearer ${idToken}` },\n body: JSON.stringify(opts.label ? { label: opts.label } : {}),\n });\n if (res.status !== 200) return null;\n const parsed = JSON.parse(await res.text()) as { token?: unknown; expires_at?: unknown };\n const token = typeof parsed.token === 'string' ? parsed.token : '';\n const expiresAt = typeof parsed.expires_at === 'string' ? parsed.expires_at : '';\n if (!token.startsWith('vocred_') || !expiresAt) return null;\n return { vo_credential: token, expires_at: expiresAt };\n } catch {\n return null;\n }\n}\n", "#!/usr/bin/env node\n/**\n * `vo-mcp login` CLI entry point.\n *\n * Runs the interactive browser-callback login flow and stores the credential.\n */\nimport { runLogin } from './cloud/login.js';\nimport { exchangeForVoCredential } from './cloud/vo-credential-exchange.js';\nimport { platform } from 'node:os';\n\nasync function main(): Promise<void> {\n const env = process.env;\n const log = (m: string): void => console.error(m);\n\n const controlPlaneUrl = env['VO_CONTROL_PLANE_URL']?.trim() || 'https://vo-control-plane-bzjphrajaq-uc.a.run.app';\n const exchange = async (refreshToken: string, apiKey: string): Promise<{ vo_credential: string; expires_at: string } | null> => {\n return exchangeForVoCredential({\n refreshToken,\n apiKey,\n controlPlaneUrl,\n label: `vo-mcp login (${platform()})`,\n });\n };\n\n try {\n const result = await runLogin({ env, log, exchange });\n log(`\u2713 Signed in${result.email ? ` as ${result.email}` : ''}. Credential stored at: ${result.credentialPath}`);\n log('\\nNext steps:');\n log(' 1. Start the runner: vo-mcp runner');\n log(' 2. Dispatch agents from: https://algosuite.ai/virtualoffice');\n } catch (err: unknown) {\n log(`\u2717 Login failed: ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n }\n}\n\nmain().catch((err: unknown) => {\n console.error('[vo-mcp login] fatal:', err);\n process.exit(1);\n});\n"],
5
5
  "mappings": "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",
6
6
  "names": ["platform"]
7
7
  }
package/dist/pair-cli.js CHANGED
File without changes
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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"],
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", "/**\n * Local credential store for the thin-client `vo-mcp login` flow (Increment 3b,\n * Option A \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md`).\n *\n * Persists the per-user Firebase refresh token (the user's OWN credential, never\n * the god-token, never model keys) captured by `login`, so the auto-refreshing\n * token source (Inc 3a) can mint fresh ID tokens across MCP restarts.\n *\n * Storage precedence (Inc 3b.3):\n * 1. **OS keychain** (Windows Credential Manager / macOS Keychain / libsecret)\n * via the optional `@napi-rs/keyring` backend (`keychain.ts`). The DEFAULT\n * when available \u2014 the secret never lands in plaintext on disk.\n * 2. **0600 file** at `$VO_MCP_CREDENTIALS_PATH` or `~/.config/vo-mcp/credentials.json`.\n * The fallback when the keychain is unavailable or disabled\n * (`VO_MCP_DISABLE_KEYCHAIN`). `VO_MCP_CREDENTIALS_PATH` only sets the file\n * LOCATION; force file storage with `VO_MCP_DISABLE_KEYCHAIN`.\n *\n * `env`-supplied tokens (`VO_USER_REFRESH_TOKEN`, etc.) still win over BOTH\n * stores \u2014 that precedence lives upstream in `auth-token-source.ts`.\n *\n * **Single source of truth.** The credential lives in EITHER the keychain OR the\n * file, never both: a write to one store CLEARS the other, so a stale entry can\n * never shadow the current credential on read, and the secret never lingers in\n * plaintext after a migration to the keychain.\n *\n * **Keychain durability.** A keychain-stored credential is only readable while\n * the `@napi-rs/keyring` native module loads. If the module later becomes\n * unavailable (an ABI break across a Node upgrade, a corrupted install), the\n * credential can't be read and the user re-runs `vo-mcp login` \u2014 the same\n * behaviour as `gh` / `gcloud` / `firebase` keychain storage. We deliberately do\n * NOT mirror the secret to a plaintext file as a fallback: that would defeat the\n * entire point of keychain storage (keeping the secret off plaintext disk).\n */\nimport { homedir } from 'node:os';\nimport { join, dirname } from 'node:path';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n chmodSync,\n rmSync,\n} from 'node:fs';\n\nimport { keychainAvailable, keychainGet, keychainSet, keychainDelete } from './keychain.js';\n\nexport interface StoredCredential {\n /**\n * Firebase refresh token (long-lived; exchanged for short-lived ID tokens).\n * OPTIONAL since Inc 3b.4b: once a scoped `vo_credential` is minted, the raw\n * refresh token is dropped, so a stored credential may carry ONLY the vocred_.\n */\n readonly refresh_token?: string;\n /** Firebase Web API key (PUBLIC) needed for the securetoken refresh exchange. */\n readonly api_key?: string;\n /**\n * Scoped, revocable VO credential (`vocred_`) minted by the control-plane\n * (Inc 3b.4b). Preferred over the raw refresh token; lets the client present a\n * revocable, server-side credential instead of the Firebase refresh token.\n */\n readonly vo_credential?: string;\n /** ISO-8601 expiry of `vo_credential` (the client re-logs-in past this). */\n readonly vo_credential_expires_at?: string;\n /** The signed-in operator email (diagnostics only). */\n readonly email?: string;\n /** ISO timestamp the credential was stored. */\n readonly stored_at?: string;\n}\n\n/**\n * Pluggable OS-keychain backend. Defaults to the real `@napi-rs/keyring` wrapper;\n * tests inject a deterministic fake so they never touch the host keychain.\n */\nexport interface KeychainBackend {\n available(): boolean;\n get(): string | null;\n set(secret: string): boolean;\n delete(): boolean;\n}\n\nconst realKeychain: KeychainBackend = {\n available: keychainAvailable,\n get: keychainGet,\n set: keychainSet,\n delete: keychainDelete,\n};\n\n/** Human-readable \"location\" returned when the credential was stored in the OS keychain. */\nexport const KEYCHAIN_LOCATION = 'OS keychain (service \"vo-mcp\")';\n\n/** Resolve the credentials file path (env override \u2192 XDG-ish default under home). */\nexport function credentialPath(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const override = env['VO_MCP_CREDENTIALS_PATH']?.trim();\n if (override) return override;\n return join(homedir(), '.config', 'vo-mcp', 'credentials.json');\n}\n\n/**\n * Whether the keychain should be consulted at all (read OR write). False when the\n * native backend is unavailable or `VO_MCP_DISABLE_KEYCHAIN` is set (CI/headless).\n */\nfunction keychainEnabled(\n env: Readonly<Record<string, string | undefined>>,\n keychain: KeychainBackend,\n): boolean {\n const disabled = (env['VO_MCP_DISABLE_KEYCHAIN'] ?? '').trim().toLowerCase();\n if (disabled === '1' || disabled === 'true' || disabled === 'yes') return false;\n return keychain.available();\n}\n\n/** Parse + validate a stored credential blob. Returns null on any problem (never throws). */\nfunction deserialize(raw: string): StoredCredential | null {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredCredential>;\n const refresh = typeof parsed.refresh_token === 'string' ? parsed.refresh_token.trim() : '';\n const apiKey = typeof parsed.api_key === 'string' ? parsed.api_key.trim() : '';\n const voCred = typeof parsed.vo_credential === 'string' ? parsed.vo_credential.trim() : '';\n // Valid if it carries a scoped vocred_ OR a full Firebase refresh pair.\n if (!voCred && (!refresh || !apiKey)) return null;\n return {\n ...(refresh ? { refresh_token: refresh } : {}),\n ...(apiKey ? { api_key: apiKey } : {}),\n ...(voCred ? { vo_credential: voCred } : {}),\n ...(typeof parsed.vo_credential_expires_at === 'string' ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {}),\n ...(typeof parsed.email === 'string' ? { email: parsed.email } : {}),\n ...(typeof parsed.stored_at === 'string' ? { stored_at: parsed.stored_at } : {}),\n };\n } catch {\n return null;\n }\n}\n\nfunction readFromFile(env: Readonly<Record<string, string | undefined>>): StoredCredential | null {\n try {\n const p = credentialPath(env);\n if (!existsSync(p)) return null;\n return deserialize(readFileSync(p, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Read the stored credential, or `null` if absent/unreadable/invalid (never\n * throws). Consults an ENABLED keychain first (regardless of the write-target\n * flags, so a credential written to the keychain is found even if\n * `VO_MCP_CREDENTIALS_PATH` is later set), then the 0600 file.\n */\nexport function readStoredCredential(\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): StoredCredential | null {\n if (keychainEnabled(env, keychain)) {\n const raw = keychain.get();\n const fromKeychain = raw ? deserialize(raw) : null;\n if (fromKeychain) return fromKeychain;\n }\n return readFromFile(env);\n}\n\nfunction deleteFile(env: Readonly<Record<string, string | undefined>>): void {\n try {\n rmSync(credentialPath(env), { force: true });\n } catch {\n /* best-effort */\n }\n}\n\nfunction writeToFile(\n payload: StoredCredential,\n env: Readonly<Record<string, string | undefined>>,\n): string {\n const p = credentialPath(env);\n mkdirSync(dirname(p), { recursive: true });\n writeFileSync(p, `${JSON.stringify(payload, null, 2)}\\n`, { mode: 0o600 });\n // Best-effort tighten (no-op / throws on some Windows filesystems \u2014 ignore).\n try {\n chmodSync(p, 0o600);\n } catch {\n /* best-effort */\n }\n return p;\n}\n\n/**\n * Persist the credential. Prefers the OS keychain (secret never hits plaintext\n * disk); otherwise writes the 0600 file. Writing to one store CLEARS the other\n * (single source of truth \u2014 no stale shadow, no lingering plaintext). Returns the\n * location it was stored (`KEYCHAIN_LOCATION` or the file path).\n */\nexport function writeStoredCredential(\n cred: StoredCredential,\n storedAt: string,\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): string {\n const payload: StoredCredential = {\n ...(cred.refresh_token ? { refresh_token: cred.refresh_token } : {}),\n ...(cred.api_key ? { api_key: cred.api_key } : {}),\n ...(cred.vo_credential ? { vo_credential: cred.vo_credential } : {}),\n ...(cred.vo_credential_expires_at ? { vo_credential_expires_at: cred.vo_credential_expires_at } : {}),\n ...(cred.email ? { email: cred.email } : {}),\n stored_at: cred.stored_at ?? storedAt,\n };\n if (keychainEnabled(env, keychain) && keychain.set(JSON.stringify(payload))) {\n // Stored in the keychain \u2192 clear any stale plaintext file so the secret\n // doesn't linger on disk and can't shadow the keychain on read.\n deleteFile(env);\n return KEYCHAIN_LOCATION;\n }\n const p = writeToFile(payload, env);\n // Stored in the file \u2192 clear any stale keychain entry so it can't shadow the\n // newer file credential on read.\n if (keychainEnabled(env, keychain)) keychain.delete();\n return p;\n}\n", "/**\n * Optional OS-keychain backend for the thin-client credential store (Increment\n * 3b.3 \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md` \u00A75/\u00A76).\n *\n * Loads `@napi-rs/keyring` at runtime via `createRequire`, so it is a TRUE\n * optional dependency: if the native module is absent or fails to load\n * (unsupported platform, prebuilt binary missing, headless CI), every function\n * degrades to a no-op and the caller (`credential-store.ts`) falls back to the\n * 0600 file store. `@napi-rs/keyring`'s `Entry` API is SYNCHRONOUS, so the\n * credential store stays synchronous \u2014 no async ripple into the Inc-3a token\n * source that reads it.\n *\n * Why `createRequire` and not a static/dynamic `import`: a static import would\n * make the native module a HARD dependency (a missing prebuilt would crash the\n * MCP at startup); a dynamic `import()` is async (would force the whole read\n * path async). `createRequire(...)` inside a try/catch loads it lazily and\n * synchronously, and a load failure is just \"keychain unavailable\".\n */\nimport { createRequire } from 'node:module';\n\n/** Keychain service + account the single refresh credential is stored under. */\nconst SERVICE = 'vo-mcp';\nconst ACCOUNT = 'refresh-credential';\n\ninterface KeyringEntry {\n getPassword(): string | null;\n setPassword(password: string): void;\n deletePassword(): boolean;\n}\ninterface KeyringModule {\n Entry: new (service: string, account: string) => KeyringEntry;\n}\n\n// undefined = not yet attempted; null = attempted and unavailable.\nlet cached: KeyringModule | null | undefined;\n\nfunction loadKeyring(): KeyringModule | null {\n if (cached !== undefined) return cached;\n try {\n const req = createRequire(import.meta.url);\n const mod = req('@napi-rs/keyring') as Partial<KeyringModule>;\n cached = mod && typeof mod.Entry === 'function' ? (mod as KeyringModule) : null;\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** True when the OS keychain backend is usable in this runtime. */\nexport function keychainAvailable(): boolean {\n return loadKeyring() !== null;\n}\n\n/** Read the raw stored secret string from the OS keychain, or null. Never throws. */\nexport function keychainGet(): string | null {\n const k = loadKeyring();\n if (!k) return null;\n try {\n return new k.Entry(SERVICE, ACCOUNT).getPassword();\n } catch {\n return null;\n }\n}\n\n/** Store the raw secret string in the OS keychain. Returns true on success. Never throws. */\nexport function keychainSet(secret: string): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n new k.Entry(SERVICE, ACCOUNT).setPassword(secret);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Delete the stored secret from the OS keychain. Returns true if an entry was\n * removed. Never throws \u2014 a no-op (and `false`) when the backend is unavailable\n * or the entry is absent. Used to keep ONE source of truth: when the credential\n * is (re)written to the file, any stale keychain entry is cleared so it can't\n * shadow the newer file credential on read (and vice-versa).\n */\nexport function keychainDelete(): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n return new k.Entry(SERVICE, ACCOUNT).deletePassword();\n } catch {\n return false;\n }\n}\n\n/** Test-only seam to reset the memoised module load. */\nexport function __resetKeychainCache(): void {\n cached = undefined;\n}\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
5
  "mappings": "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",
6
6
  "names": ["credentialPath"]
7
7
  }