/** * #5649 — resolve the MCP caller's API-key **principal id** for content stores * (CCR) that are keyed by principal. * * The CCR store keys blocks by `String(apiKeyInfo.id)` at compression time * (`chatCore` → `apiKeyInfo = getApiKeyMetadata(rawKey)`). MCP tool retrieval must * resolve the SAME id or the block is not found. On the MCP HTTP transports * (SSE / Streamable HTTP) the raw key lives in `httpAuthContext`'s * AsyncLocalStorage (set by `withMcpHttpAuthContext`), NOT in the tool handler's * `extra.authInfo` (OmniRoute authenticates with API keys, not OAuth client ids — * so `extra.authInfo.clientId` is never populated and the caller resolved to * "anonymous", producing a cross-principal store-key miss). * * Resolving through the same `getApiKeyMetadata` lookup keeps cross-tenant IDOR * isolation intact: a different key → a different id → a miss; no key → undefined * → the anonymous (`__anon__`) bucket, which only matches unauthenticated stores. */ import { getMcpHttpAuthHeadersForInternalFetch } from "./httpAuthContext.ts"; import { extractApiKey } from "../../src/sse/services/auth.ts"; import { getApiKeyMetadata } from "../../src/lib/db/apiKeys.ts"; type ApiKeyLookup = (rawKey: string) => Promise<{ id?: string | number | null } | null>; /** * Pure resolver: given the request auth headers and a key→metadata lookup, return * the principal id (as a string) or `undefined`. Separated from the AsyncLocalStorage * read so it is unit-testable without a live transport or DB. */ export async function resolvePrincipalFromHeaders( headers: Record, lookup: ApiKeyLookup = getApiKeyMetadata ): Promise { // Nothing to resolve without an Authorization / x-api-key header. if (!headers.Authorization && !headers["x-api-key"]) return undefined; const rawKey = extractApiKey({ headers: new Headers(headers) }, { allowUrl: false }); if (!rawKey) return undefined; try { const meta = await lookup(rawKey); return meta?.id != null && meta.id !== "" ? String(meta.id) : undefined; } catch { // Fail closed: an unresolved principal can only reach the anonymous bucket. return undefined; } } /** * Resolve the current MCP HTTP caller's API-key principal id from the ambient * `httpAuthContext`. Returns `undefined` off the HTTP transport (stdio) or when the * request carries no API key. */ export function resolveMcpCallerApiKeyId(): Promise { return resolvePrincipalFromHeaders(getMcpHttpAuthHeadersForInternalFetch()); }