54 lines
2.5 KiB
TypeScript
54 lines
2.5 KiB
TypeScript
/**
|
|
* #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<string, string>,
|
|
lookup: ApiKeyLookup = getApiKeyMetadata
|
|
): Promise<string | undefined> {
|
|
// 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<string | undefined> {
|
|
return resolvePrincipalFromHeaders(getMcpHttpAuthHeadersForInternalFetch());
|
|
}
|