@algosuite/vo-mcp 0.2.0-beta.12 → 0.2.0-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +2 -2
- package/dist/index.js +6 -0
- package/dist/index.js.map +2 -2
- package/dist/install-cli.js.map +2 -2
- package/dist/login-cli.js.map +2 -2
- package/dist/pair-cli.js.map +2 -2
- package/dist/runner-cli.js +6 -0
- package/dist/runner-cli.js.map +2 -2
- package/dist/supervisor-credential-helper.js +8 -2
- package/dist/supervisor-credential-helper.js.map +2 -2
- package/package.json +1 -1
|
@@ -113,6 +113,11 @@ function readStoredCredential(env = process.env, keychain = realKeychain) {
|
|
|
113
113
|
}
|
|
114
114
|
return readFromFile(env);
|
|
115
115
|
}
|
|
116
|
+
function readStoredCredentialKeychainOnly(env = process.env, keychain = realKeychain) {
|
|
117
|
+
if (!keychainEnabled(env, keychain)) return null;
|
|
118
|
+
const raw = keychain.get();
|
|
119
|
+
return raw ? deserialize(raw) : null;
|
|
120
|
+
}
|
|
116
121
|
|
|
117
122
|
// src/cloud/auth-token-source.ts
|
|
118
123
|
var FIREBASE_SECURETOKEN_URL = "https://securetoken.googleapis.com/v1/token";
|
|
@@ -208,8 +213,9 @@ function createAuthTokenSourceFromEnv(env = process.env, fetchFn, readStoredCred
|
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
// src/supervisor-credential-helper.mjs
|
|
211
|
-
var
|
|
212
|
-
|
|
216
|
+
var keychainOnly = process.argv.includes("--control-plane-auth-keychain-only");
|
|
217
|
+
var credential = keychainOnly ? readStoredCredentialKeychainOnly() : readStoredCredential();
|
|
218
|
+
if (process.argv.includes("--control-plane-auth") || keychainOnly) {
|
|
213
219
|
const source = createAuthTokenSourceFromEnv(process.env, void 0, () => credential);
|
|
214
220
|
const token = await source?.getToken();
|
|
215
221
|
if (!token) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/cloud/credential-store.ts", "../src/cloud/keychain.ts", "../src/cloud/auth-token-source.ts", "../src/supervisor-credential-helper.mjs"],
|
|
4
|
-
"sourcesContent": ["/**\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 * Read the paired runner credential in a disposable process.\n *\n * On Windows, @napi-rs/keyring loads a native DLL. Keeping that DLL loaded in\n * the long-lived supervisor prevents npm from replacing the globally installed\n * package during a self-update. This helper exits before maintenance starts, so\n * Windows releases the native module while the supervisor retains only the\n * parsed credential in memory.\n */\nimport { readStoredCredential } from './cloud/credential-store.js';\nimport { createAuthTokenSourceFromEnv } from './cloud/auth-token-source.js';\n\nconst credential = readStoredCredential();\nif (process.argv.includes('--control-plane-auth')) {\n const source = createAuthTokenSourceFromEnv(process.env, undefined, () => credential);\n const token = await source?.getToken();\n if (!token) {\n process.stderr.write('control-plane credential was not found\\n');\n process.exitCode = 2;\n } else {\n process.stdout.write(JSON.stringify({ token }));\n }\n} else if (!credential) {\n process.stderr.write('paired runner credential was not found\\n');\n process.exitCode = 2;\n} else {\n process.stdout.write(JSON.stringify(credential));\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\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\n/** Read only the OS keychain; never fall back to env-selected plaintext files. */\nexport function readStoredCredentialKeychainOnly(\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): StoredCredential | null {\n if (!keychainEnabled(env, keychain)) return null;\n const raw = keychain.get();\n return raw ? deserialize(raw) : null;\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 * Read the paired runner credential in a disposable process.\n *\n * On Windows, @napi-rs/keyring loads a native DLL. Keeping that DLL loaded in\n * the long-lived supervisor prevents npm from replacing the globally installed\n * package during a self-update. This helper exits before maintenance starts, so\n * Windows releases the native module while the supervisor retains only the\n * parsed credential in memory.\n */\nimport { readStoredCredential, readStoredCredentialKeychainOnly } from './cloud/credential-store.js';\nimport { createAuthTokenSourceFromEnv } from './cloud/auth-token-source.js';\n\nconst keychainOnly = process.argv.includes('--control-plane-auth-keychain-only');\nconst credential = keychainOnly ? readStoredCredentialKeychainOnly() : readStoredCredential();\nif (process.argv.includes('--control-plane-auth') || keychainOnly) {\n const source = createAuthTokenSourceFromEnv(process.env, undefined, () => credential);\n const token = await source?.getToken();\n if (!token) {\n process.stderr.write('control-plane credential was not found\\n');\n process.exitCode = 2;\n } else {\n process.stdout.write(JSON.stringify({ token }));\n }\n} else if (!credential) {\n process.stderr.write('paired runner credential was not found\\n');\n process.exitCode = 2;\n} else {\n process.stdout.write(JSON.stringify(credential));\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@algosuite/vo-mcp",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.13",
|
|
4
4
|
"description": "AlgoHQ MCP server — open protocol surface for the HQ consensus and ratchet tool family. Stdio transport, cross-vendor MCP client compatible (Claude Code, Claude Desktop, Codex, Cursor, Continue).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|