@12-apps/mcp 3.2.0 → 3.3.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/oauth/config.ts","../src/oauth/keys.ts","../src/oauth/authorization-code.ts","../src/oauth/clients.ts","../src/oauth/pkce.ts","../src/oauth/code-replay.ts","../src/oauth/access-token.ts","../src/oauth/refresh.ts","../src/oauth/context.ts","../src/oauth/authorize.ts","../src/oauth/register.ts","../src/oauth/token-response.ts","../src/oauth/token-grants.ts","../src/oauth/create-api-mcp-oauth.ts"],"sourcesContent":["/**\n * The OAuth 2.1 authorization-server foundation: the shared scope source, the\n * issuer/audience derivation, and the trusted-origin resolver every URL in the\n * surface is built from (12-23, ported from the origin host's\n * `lib/mcp/oauth/config.ts`).\n *\n * Keeping the scopes and the origin resolution in ONE place is what stops the\n * two discovery documents — RFC 8414 `/.well-known/oauth-authorization-server`\n * and RFC 9728 `/.well-known/oauth-protected-resource` — from drifting apart,\n * and what makes a token minted for an origin verify against that same origin.\n *\n * What was env-reading in the host is CONFIG here (the package must not learn a\n * host's variable names); `trustedOriginsFromEnv` is the one-line helper that\n * keeps the origin host's wiring identical.\n */\n\n/** Scopes advertised by both discovery documents. `mcp:write` gates mutating tools. */\nexport const MCP_SUPPORTED_SCOPES = [\"mcp:read\", \"mcp:write\"] as const;\n\nexport type McpScope = (typeof MCP_SUPPORTED_SCOPES)[number];\n\n/** Path the MCP JSON-RPC endpoint is mounted at — the access token's audience. */\nexport const DEFAULT_MCP_RESOURCE_PATH = \"/api/mcp\";\n\n/** The OAuth `iss` — the deployment origin, used verbatim. */\nexport function issuer(origin: string): string {\n return origin;\n}\n\n/** The access-token `aud` — the MCP resource URL (`${origin}${resourcePath}`). */\nexport function resourceAudience(\n origin: string,\n resourcePath: string = DEFAULT_MCP_RESOURCE_PATH,\n): string {\n return `${origin}${resourcePath}`;\n}\n\n/**\n * A single bare `host` or `host:port` — no scheme, path, userinfo (`@`), or\n * whitespace. A syntactic guard so a forwarded host cannot smuggle anything but\n * a hostname; it does NOT by itself decide trust (the allowlist does).\n */\nconst HOST_ONLY =\n /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::\\d{1,5})?$/i;\n\n/** ASCII `/`. */\nconst SLASH = 0x2f;\n\n/**\n * Strip trailing slashes by index, not by regex.\n *\n * `replace(/\\/+$/, \"\")` is quadratic on a string of many slashes — the classic\n * anchored-quantifier backtrack — and this function is reached from an operator's\n * env var AND (through `resolveTrustedOrigin`) from values that arrive with a\n * request. A backwards walk is linear and needs no reasoning about the engine.\n */\nfunction stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === SLASH) end -= 1;\n return value.slice(0, end);\n}\n\n/** Normalize an allowlist entry (trailing `/` stripped, blanks dropped). */\nfunction normalizeOrigins(origins: readonly string[]): string[] {\n return origins.map((origin) => stripTrailingSlashes(origin.trim())).filter(Boolean);\n}\n\n/**\n * Read a comma-separated allowlist out of an environment variable — the origin host\n * passes `trustedOriginsFromEnv('MCP_OAUTH_TRUSTED_ORIGINS')`, so the behaviour\n * is identical while the variable's NAME stays the host's.\n */\nexport function trustedOriginsFromEnv(name: string): string[] {\n const raw = typeof process === \"undefined\" ? undefined : process.env?.[name];\n return raw ? normalizeOrigins(raw.split(\",\")) : [];\n}\n\n/**\n * The origin a request's forwarded headers CLAIM, or `null` when\n * absent/malformed. Only the first hop of a comma-separated list is taken, the\n * host must be a bare host[:port], and the proto is restricted to http/https.\n * This is a claim to be checked against the allowlist — never trusted alone.\n */\nfunction claimedForwardedOrigin(getHeader: (name: string) => string | null): string | null {\n const host = getHeader(\"x-forwarded-host\")?.split(\",\")[0]?.trim();\n if (!host || !HOST_ONLY.test(host)) return null;\n const proto = getHeader(\"x-forwarded-proto\")?.split(\",\")[0]?.trim();\n return `${proto === \"http\" ? \"http\" : \"https\"}://${host}`;\n}\n\n/**\n * THE single trusted-origin resolver, shared by token ISSUANCE (the `iss`/`aud` a\n * token is minted with) and bearer VERIFICATION (the expected `aud` a protected\n * route checks). Because both sides pass the SAME request's headers, a token\n * minted for the allowlisted origin verifies against that same origin — they\n * cannot drift (e.g. mint `https://app.example.com` but verify\n * `http://0.0.0.0:3000` and reject a valid token).\n *\n * The origin must NEVER be attacker-controllable. Behind a reverse proxy the\n * server sees only its internal bind on `request.url`, so the public origin comes\n * from the proxy's `X-Forwarded-Host` — but a forwarded host is honored ONLY when\n * it is on the operator-configured allowlist; ANY other value (a spoofed/foreign\n * host, or an absent header) resolves to the canonical (FIRST) allowlisted origin.\n * So even if the edge forwards `X-Forwarded-Host: evil.example.com`, the origin\n * stays the trusted one.\n *\n * With NO allowlist configured a forwarded host is NEVER trusted — a spoofed\n * header must not be able to choose the issuer — so `fallbackOrigin` (the\n * request's OWN origin) is used instead. A proxied deployment therefore REQUIRES\n * the allowlist; until it is set the surface fails closed to the internal origin\n * rather than a foreign one.\n */\nexport function resolveTrustedOrigin(\n getHeader: (name: string) => string | null,\n fallbackOrigin: string | undefined,\n trustedOrigins: readonly string[] = [],\n): string | undefined {\n const [canonical, ...rest] = normalizeOrigins(trustedOrigins);\n // No allowlist → the forwarded host is untrusted; fall back to the request's\n // own origin so a spoofed X-Forwarded-Host can never become the issuer.\n if (!canonical) return fallbackOrigin;\n\n const allowed = [canonical, ...rest];\n const claimed = claimedForwardedOrigin(getHeader);\n return claimed && allowed.includes(claimed) ? claimed : canonical;\n}\n\n/**\n * The PUBLIC origin every URL in the surface derives from (issuer, endpoint URLs,\n * access-token `iss`/`aud`). A thin wrapper over {@link resolveTrustedOrigin}\n * bound to a `Request`, falling back to the request URL's origin when no\n * allowlist is configured.\n */\nexport function originFromRequest(\n request: Request,\n trustedOrigins: readonly string[] = [],\n): string {\n const fallback = new URL(request.url).origin;\n return (\n resolveTrustedOrigin((name) => request.headers.get(name), fallback, trustedOrigins) ?? fallback\n );\n}\n","import { exportJWK, importPKCS8, type CryptoKey, type JWK } from \"jose\";\n\n/**\n * Signing-key / JWK loading for the OAuth authorization server (12-23, ported\n * from the origin host's `lib/mcp/oauth/keys.ts`).\n *\n * ES256 (P-256) from PEM material, the published public JWK (with `kid` for\n * rotation), and a safe-by-default absence signal (`null`) when no key is\n * configured — callers then refuse to issue tokens and serve the JWKS as 503\n * rather than falling back to a weaker mode while the surface is mounted.\n *\n * WHERE the PEM comes from is the host's business: `loadSigningKeyFromEnv` keeps\n * the origin host's env-var wiring, and any other provider (a secrets manager, a KMS\n * export) satisfies the same `McpSigningKeyProvider` shape.\n */\n\n/** JWS algorithm for the signing key pair (asymmetric, self-validated via JWKS). */\nexport const SIGNING_ALG = \"ES256\";\n\n/** A public JWK safe to publish at the JWKS endpoint (never carries `d`). */\nexport interface PublicSigningJwk extends JWK {\n kid: string;\n kty: \"EC\";\n crv: \"P-256\";\n alg: typeof SIGNING_ALG;\n use: \"sig\";\n}\n\n/** The loaded signing material: the private key for signing + its public JWK. */\nexport interface McpSigningKey {\n privateKey: CryptoKey;\n publicJwk: PublicSigningJwk;\n kid: string;\n}\n\n/**\n * How the surface obtains signing material. Returning `null` means \"not\n * provisioned\": the AS then mints nothing and the JWKS answers 503.\n */\nexport type McpSigningKeyProvider = () => Promise<McpSigningKey | null>;\n\nasync function parseSigningKey(pem: string, kid: string): Promise<McpSigningKey> {\n // `extractable: true` is required so `exportJWK` can derive the public JWK;\n // jose imports keys as non-extractable by default, which blocks the export.\n const privateKey = await importPKCS8(pem, SIGNING_ALG, { extractable: true });\n const jwk = await exportJWK(privateKey);\n // Strip the private component; publish only the public half.\n const { d: _private, ...publicHalf } = jwk;\n void _private;\n\n const publicJwk: PublicSigningJwk = {\n ...publicHalf,\n kty: \"EC\",\n crv: \"P-256\",\n alg: SIGNING_ALG,\n use: \"sig\",\n kid,\n };\n\n return { privateKey, publicJwk, kid };\n}\n\n/**\n * Build a provider over a PKCS#8 PEM + `kid` pair, with a per-process cache.\n *\n * Parsing PKCS#8 and exporting the JWK is pure for a given (pem, kid), so the\n * promise is cached keyed on the material itself. A rotated key (different pem or\n * kid) produces a different cache key and re-parses — the cache never masks a\n * rotation.\n *\n * Rotation is BY `kid`: each key is published in the JWKS and selected by the\n * `kid` header on issued JWTs, so publishing old + new during an overlap window\n * lets both verify.\n */\nexport function signingKeyProvider(\n read: () => { pem: string | undefined; kid: string | undefined },\n): McpSigningKeyProvider {\n let cache: { key: string; promise: Promise<McpSigningKey> } | null = null;\n return async () => {\n const { pem, kid } = read();\n if (!pem || !kid) return null;\n const cacheKey = `${kid} ${pem}`;\n if (cache?.key === cacheKey) return cache.promise;\n const promise = parseSigningKey(pem, kid);\n cache = { key: cacheKey, promise };\n return promise;\n };\n}\n\n/** Env var carrying the ES256 private key as a PKCS#8 PEM (the origin host's name). */\nexport const DEFAULT_SIGNING_KEY_ENV = \"MCP_OAUTH_SIGNING_KEY\";\n/** Env var carrying the key id (`kid`) used to select the key during rotation. */\nexport const DEFAULT_SIGNING_KEY_ID_ENV = \"MCP_OAUTH_SIGNING_KEY_ID\";\n\n/**\n * The env-backed provider — the origin host's wiring, kept identical, with the\n * variable names as arguments so the package states no host's vocabulary.\n */\nexport function loadSigningKeyFromEnv(\n keyEnv: string = DEFAULT_SIGNING_KEY_ENV,\n kidEnv: string = DEFAULT_SIGNING_KEY_ID_ENV,\n): McpSigningKeyProvider {\n return signingKeyProvider(() => ({\n pem: typeof process === \"undefined\" ? undefined : process.env?.[keyEnv],\n kid: typeof process === \"undefined\" ? undefined : process.env?.[kidEnv],\n }));\n}\n","import { SignJWT, jwtVerify, importJWK, type JWTPayload } from \"jose\";\n\nimport { issuer } from \"./config\";\nimport { SIGNING_ALG, type McpSigningKeyProvider } from \"./keys\";\n\n/**\n * Stateless authorization-code mint/verify (12-23, ported from the origin host's\n * `lib/mcp/oauth/authorization-code.ts` — behaviour unchanged; the signing key\n * arrives through a provider instead of an env read).\n *\n * The authorization code is a short-lived (<=60s) ES256-signed JWT — no DB table,\n * no cleanup job. It binds the signed-in user (`sub`/`email`), the `client_id`,\n * the `redirect_uri`, the PKCE `code_challenge`, and the requested `scope`, plus a\n * unique `jti` the token endpoint records once to enforce single-use (replay)\n * semantics on top of the short expiry.\n *\n * The code carries a DISTINCT audience (`oauth:code`) from the access token\n * (`${origin}/api/mcp`), so a code can never be presented to the resource server\n * as a bearer access token (and vice versa): {@link verifyCode} pins\n * `audience: \"oauth:code\"`, and the access-token verifier pins the resource\n * audience — each rejects the other's blobs.\n */\n\n/**\n * Audience pinning the code to the OAuth code-exchange step only. Distinct from\n * the access-token audience so a code cannot be replayed as an access token.\n */\nexport const AUTHORIZATION_CODE_AUDIENCE = \"oauth:code\";\n\n/** Authorization-code lifetime — single-use and short-lived (<=60s per spec). */\nexport const AUTHORIZATION_CODE_TTL_SECONDS = 60;\n\n/** Clock skew tolerated on `exp`/`iat` validation, in seconds. */\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\n/** Fields bound into a minted authorization code. */\nexport interface MintCodeInput {\n /** The OAuth subject bound to the code (identity from the cookie session). */\n sub: string;\n /** The signed-in user's email (the identity all downstream tokens bind to). */\n email: string;\n /** The OAuth client the code is issued to. */\n clientId: string;\n /** The exact registered redirect URI the flow started with. */\n redirectUri: string;\n /** The PKCE S256 `code_challenge` the token endpoint verifies against. */\n codeChallenge: string;\n /** The requested scope (space-delimited), carried through to the token. */\n scope: string;\n /** The deployment origin — derives the code's `iss`. */\n origin: string;\n}\n\n/** The bound fields a verified authorization code resolves to. */\nexport interface VerifiedAuthorizationCode {\n sub: string;\n email: string;\n clientId: string;\n redirectUri: string;\n codeChallenge: string;\n scope: string;\n /** The one-time identifier the token endpoint records to enforce single-use. */\n jti: string;\n}\n\n/** The single failure discriminator for the OAuth token endpoint. */\nexport type AuthorizationCodeErrorCode = \"invalid_grant\";\n\n/**\n * A typed authorization-code failure. Every rejection (expired, wrong-audience,\n * wrong-issuer, tampered, bad-signature, unconfigured key) surfaces as\n * `invalid_grant` per RFC 6749 §5.2 for the token endpoint.\n */\nexport class AuthorizationCodeError extends Error {\n readonly code: AuthorizationCodeErrorCode;\n\n constructor(message?: string) {\n super(message ?? \"invalid_grant\");\n this.name = \"AuthorizationCodeError\";\n this.code = \"invalid_grant\";\n }\n}\n\n/** Deterministic-clock option shared by mint + verify. */\ninterface ClockOption {\n /** Epoch milliseconds; defaults to `Date.now()`. Injected for deterministic tests. */\n now?: number;\n}\n\n/** The JWT claim shape of an authorization code. */\ninterface AuthorizationCodeClaims extends JWTPayload {\n email: string;\n client_id: string;\n redirect_uri: string;\n code_challenge: string;\n scope: string;\n}\n\nfunction nowSeconds(now?: number): number {\n return Math.floor((now ?? Date.now()) / 1000);\n}\n\n/** Read a required string claim, or `null` when absent/wrong-typed. */\nfunction stringClaim(payload: JWTPayload, key: string): string | null {\n const value = payload[key];\n return typeof value === \"string\" ? value : null;\n}\n\n/**\n * Extract the bound fields from an already-signature/iss/aud/exp-validated code\n * payload, enforcing that every required claim is a present string (`scope` may be\n * the empty string but must be present). Throws {@link AuthorizationCodeError}\n * when any required bound field is missing or wrong-typed.\n */\nfunction extractBoundFields(payload: JWTPayload): VerifiedAuthorizationCode {\n const sub = stringClaim(payload, \"sub\");\n const email = stringClaim(payload, \"email\");\n const clientId = stringClaim(payload, \"client_id\");\n const redirectUri = stringClaim(payload, \"redirect_uri\");\n const codeChallenge = stringClaim(payload, \"code_challenge\");\n const scope = stringClaim(payload, \"scope\");\n const jti = stringClaim(payload, \"jti\");\n\n if (!sub || !email || !clientId || !redirectUri || !codeChallenge || scope === null || !jti) {\n throw new AuthorizationCodeError(\"code is missing required bound fields\");\n }\n\n return { sub, email, clientId, redirectUri, codeChallenge, scope, jti };\n}\n\n/**\n * Mint a single-use, stateless authorization code bound to the flow inputs.\n *\n * Returns `null` when no signing key is configured (safe-by-default: the AS\n * refuses to issue rather than falling back to a weaker mode). Sets the `kid`\n * header so the same key resolves the code at verify time.\n */\nexport async function mintCode(\n loadSigningKey: McpSigningKeyProvider,\n input: MintCodeInput,\n options?: ClockOption,\n): Promise<string | null> {\n const key = await loadSigningKey();\n if (!key) return null;\n\n const iat = nowSeconds(options?.now);\n const exp = iat + AUTHORIZATION_CODE_TTL_SECONDS;\n\n const claims: AuthorizationCodeClaims = {\n email: input.email,\n client_id: input.clientId,\n redirect_uri: input.redirectUri,\n code_challenge: input.codeChallenge,\n scope: input.scope,\n };\n\n return new SignJWT(claims)\n .setProtectedHeader({ alg: SIGNING_ALG, kid: key.kid })\n .setIssuer(issuer(input.origin))\n .setAudience(AUTHORIZATION_CODE_AUDIENCE)\n .setSubject(input.sub)\n .setIssuedAt(iat)\n .setExpirationTime(exp)\n .setJti(crypto.randomUUID())\n .sign(key.privateKey);\n}\n\n/** Options for {@link verifyCode}. */\nexport interface VerifyCodeOptions extends ClockOption {\n /** The deployment origin — derives the expected `iss`. */\n origin: string;\n}\n\n/**\n * Verify a stateless authorization code and return its bound fields.\n *\n * Validates signature (via the public JWK selected by `kid`), `iss`, the\n * `oauth:code` audience, and `exp`. Every failure — expired, wrong-audience (e.g.\n * an access token), wrong-issuer, tampered, bad-signature, or no configured key —\n * throws an {@link AuthorizationCodeError} (`invalid_grant`).\n *\n * The returned `jti` is the one-time identifier the token endpoint records to\n * enforce single-use on top of the short expiry (replay guard).\n */\nexport async function verifyCode(\n loadSigningKey: McpSigningKeyProvider,\n code: string,\n options: VerifyCodeOptions,\n): Promise<VerifiedAuthorizationCode> {\n const key = await loadSigningKey();\n if (!key) {\n // No signing key configured → nothing can verify (safe-by-default).\n throw new AuthorizationCodeError(\"no signing key configured\");\n }\n\n const publicKey = await importJWK(key.publicJwk, SIGNING_ALG);\n\n let payload: JWTPayload;\n try {\n const result = await jwtVerify(code, publicKey, {\n algorithms: [SIGNING_ALG],\n issuer: issuer(options.origin),\n audience: AUTHORIZATION_CODE_AUDIENCE,\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n currentDate: options.now === undefined ? undefined : new Date(options.now),\n });\n payload = result.payload;\n } catch {\n // jose throws on bad signature, wrong iss/aud, expiry, malformed token,\n // unknown key — all map to a single opaque `invalid_grant`.\n throw new AuthorizationCodeError(\"code verification failed\");\n }\n\n return extractBoundFields(payload);\n}\n","import { createHash, randomBytes, randomUUID } from \"node:crypto\";\n\nimport type {\n OAuthClientStore,\n StoredOAuthClient,\n TokenEndpointAuthMethod,\n} from \"./stores\";\n\n/**\n * Client registration and the open-redirect guard (12-23, ported from\n * the origin host's `lib/mcp/oauth/clients.ts`).\n *\n * A registered client is an external host (a Claude.ai / ChatGPT connector) from\n * RFC 7591 dynamic client registration, or a static registration an operator\n * created out of band. A confidential client's secret is generated HERE, returned\n * exactly once, and stored only as a SHA-256 hash — it is never persisted in\n * plaintext, never logged, and never re-derivable.\n */\n\n/** Default grant types for a registered client (OAuth 2.1 code + refresh). */\nconst DEFAULT_GRANT_TYPES = [\"authorization_code\", \"refresh_token\"] as const;\n\n/** Bytes of entropy for a generated confidential-client secret (→ 64 hex). */\nconst CLIENT_SECRET_BYTES = 32;\n\n/** RFC 7591 registration input (the durable subset the store persists). */\nexport interface RegisterClientInput {\n /** The exact-match redirect-uri allowlist (open-redirect guard). */\n redirectUris: string[];\n clientName?: string | null;\n /**\n * `none` (public PKCE client, the default) or `client_secret_basic`\n * (confidential — a secret is generated and its hash stored).\n */\n tokenEndpointAuthMethod?: TokenEndpointAuthMethod;\n /** Grant types; defaults to authorization_code + refresh_token. */\n grantTypes?: string[];\n scopes: string[];\n}\n\n/**\n * The registration RESULT. `clientSecret` is present (plaintext, ONCE) only for a\n * confidential client — it is never stored and never returned again.\n */\nexport interface RegisteredClient {\n clientId: string;\n clientSecret?: string;\n redirectUris: string[];\n clientName: string | null;\n tokenEndpointAuthMethod: TokenEndpointAuthMethod;\n grantTypes: string[];\n scopes: string[];\n}\n\n/** SHA-256 hex digest — the at-rest form of the client secret. */\nexport function hashSecret(secret: string): string {\n return createHash(\"sha256\").update(secret).digest(\"hex\");\n}\n\n/**\n * Register an OAuth client under a generated `clientId`. For a confidential\n * client a random secret is generated and its hash stored; the plaintext is\n * returned once.\n */\nexport async function registerClient(\n store: OAuthClientStore,\n input: RegisterClientInput,\n): Promise<RegisteredClient> {\n const clientId = randomUUID();\n const authMethod: TokenEndpointAuthMethod = input.tokenEndpointAuthMethod ?? \"none\";\n const grantTypes = input.grantTypes ?? [...DEFAULT_GRANT_TYPES];\n\n // Only a confidential client gets a secret; a public PKCE client has none.\n const clientSecret =\n authMethod === \"client_secret_basic\"\n ? randomBytes(CLIENT_SECRET_BYTES).toString(\"hex\")\n : undefined;\n\n const row = await store.create({\n clientId,\n clientSecretHash: clientSecret ? hashSecret(clientSecret) : null,\n redirectUris: input.redirectUris,\n clientName: input.clientName ?? null,\n tokenEndpointAuthMethod: authMethod,\n grantTypes,\n scopes: input.scopes,\n });\n\n return {\n clientId: row.clientId,\n ...(clientSecret ? { clientSecret } : {}),\n redirectUris: row.redirectUris,\n clientName: row.clientName,\n tokenEndpointAuthMethod: row.tokenEndpointAuthMethod as TokenEndpointAuthMethod,\n grantTypes: row.grantTypes,\n scopes: row.scopes,\n };\n}\n\n/**\n * Open-redirect guard: a redirect target is accepted ONLY when it EXACTLY equals a\n * registered `redirect_uri`. No normalization, no prefix, no trailing-slash\n * leniency — an intercepted authorization request must not be steerable to any URI\n * the client did not register.\n */\nexport function matchesRedirectUri(\n client: Pick<StoredOAuthClient, \"redirectUris\">,\n redirectUri: string,\n): boolean {\n if (!redirectUri) return false;\n return client.redirectUris.includes(redirectUri);\n}\n\n/**\n * Provider attribution rules: the canonical root domains that own each host's\n * OAuth callback. A redirect host matches a root only as the exact domain or a\n * real (dot-guarded) subdomain — never a suffix, so `evilchatgpt.com` never\n * matches `chatgpt.com`.\n */\nexport interface ProviderAttributionRule {\n roots: readonly string[];\n provider: string;\n}\n\n/** The origin host's rules, and a sane default for any host talking to the same two. */\nexport const DEFAULT_PROVIDER_ROOTS: readonly ProviderAttributionRule[] = [\n { roots: [\"claude.ai\", \"anthropic.com\"], provider: \"claude\" },\n { roots: [\"chatgpt.com\", \"openai.com\"], provider: \"chatgpt\" },\n];\n\n/** Whether `host` is exactly `root` or a real subdomain of it (dot-guarded). */\nfunction hostMatchesRoot(host: string, root: string): boolean {\n return host === root || host.endsWith(`.${root}`);\n}\n\n/**\n * Best-effort provider attribution from a client's redirect URIs: the host that\n * owns the callback (`claude.ai` → claude, `chatgpt.com` → chatgpt). Returns\n * `null` when nothing matches — the UI then falls back to what the owner\n * completed the flow with, and a self-report (the `announce` path) can attribute\n * it later.\n */\nexport function providerFromRedirectUris(\n redirectUris: readonly string[],\n rules: readonly ProviderAttributionRule[] = DEFAULT_PROVIDER_ROOTS,\n): string | null {\n for (const uri of redirectUris) {\n let host: string;\n try {\n host = new URL(uri).host.toLowerCase();\n } catch {\n continue;\n }\n const match = rules.find((rule) => rule.roots.some((root) => hostMatchesRoot(host, root)));\n if (match) return match.provider;\n }\n return null;\n}\n","/**\n * PKCE (RFC 7636) S256 challenge helpers for the OAuth authorization server\n * (12-23, ported verbatim from the origin host's `lib/mcp/oauth/pkce.ts`).\n *\n * OAuth 2.1 mandates the `S256` code-challenge method and forbids `plain`, so\n * this module computes `BASE64URL(SHA-256(code_verifier))` and compares it to\n * the stored `code_challenge` in constant time. The authorization endpoint\n * binds a `code_challenge` into the stateless authorization code; the token\n * endpoint calls {@link verifyChallenge} with the presented `code_verifier` to\n * prove the redeeming client is the one that started the flow.\n *\n * `plain` is refused (throws {@link UnsupportedChallengeMethodError}) rather\n * than silently accepted: `plain` offers no protection against an intercepted\n * authorization code, which is the exact threat PKCE exists to close.\n */\n\n/** The only PKCE method this server accepts (OAuth 2.1 requires S256). */\nexport const SUPPORTED_CHALLENGE_METHOD = \"S256\";\n\n/**\n * PKCE code-challenge methods, including the rejected legacy `plain`.\n *\n * @public exported because it is a parameter type of the exported\n * {@link verifyChallenge}.\n */\nexport type CodeChallengeMethod = \"S256\" | \"plain\";\n\n/** Thrown when a caller supplies a challenge method other than `S256`. */\nexport class UnsupportedChallengeMethodError extends Error {\n readonly method: string;\n\n constructor(method: string) {\n super(\n `unsupported code_challenge_method '${method}' — only ${SUPPORTED_CHALLENGE_METHOD} is allowed`,\n );\n this.name = \"UnsupportedChallengeMethodError\";\n this.method = method;\n }\n}\n\n/** Encode raw bytes as unpadded base64url (RFC 7636 challenge encoding). */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n // Buffer.toString(\"base64url\") emits the URL-safe alphabet with no padding.\n return Buffer.from(bytes).toString(\"base64url\");\n}\n\n/**\n * Compute the RFC 7636 S256 challenge for a `code_verifier`:\n * `BASE64URL(SHA-256(ASCII(verifier)))`.\n */\nexport async function computeChallenge(verifier: string): Promise<string> {\n const data = new TextEncoder().encode(verifier);\n const digest = await crypto.subtle.digest(\"SHA-256\", data);\n return base64UrlEncode(new Uint8Array(digest));\n}\n\n/**\n * Constant-time string comparison over the base64url challenge bytes.\n *\n * Returns `false` immediately on a length mismatch (lengths are not secret);\n * for equal-length inputs every byte is compared so the timing does not reveal\n * how many leading characters matched.\n */\nfunction constantTimeEquals(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let mismatch = 0;\n for (let i = 0; i < a.length; i += 1) {\n mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return mismatch === 0;\n}\n\n/**\n * Verify a presented `code_verifier` against a stored `code_challenge`.\n *\n * Recomputes the S256 challenge from `verifier` and constant-time-compares it\n * to `storedChallenge`. Returns `true` on a match, `false` on a mismatch (or an\n * empty stored challenge). Any method other than `S256` throws\n * {@link UnsupportedChallengeMethodError} — `plain` is never accepted.\n */\nexport async function verifyChallenge(\n verifier: string,\n storedChallenge: string,\n method: CodeChallengeMethod | string = SUPPORTED_CHALLENGE_METHOD,\n): Promise<boolean> {\n if (method !== SUPPORTED_CHALLENGE_METHOD) {\n throw new UnsupportedChallengeMethodError(method);\n }\n if (!storedChallenge) return false;\n\n const computed = await computeChallenge(verifier);\n return constantTimeEquals(computed, storedChallenge);\n}\n","/**\n * Single-use guard for the stateless authorization codes (12-23, ported from\n * the origin host's `lib/mcp/oauth/token-replay.ts`).\n *\n * A code is a signed blob with a `jti`, so \"already redeemed\" has to be remembered\n * somewhere. The in-process option remembers it IN THIS PROCESS: a small map of\n * `jti → expiry`, self-pruning once the code that carried it would have expired\n * anyway, so the set never grows without bound.\n *\n * ⚠️ MULTI-INSTANCE LIMITATION (best-effort single-use): that map lives in ONE\n * process. On a horizontally-scaled deployment a code could be replayed against a\n * second instance that has not yet seen the `jti`, within the ≤60s code lifetime.\n * Single-use is therefore strictly guaranteed only on a SINGLE instance — which is\n * why choosing it is explicit and cannot happen by omission: `codeReplay` has no\n * default, so a host either names a shared store or types `'in-process'`. BEFORE\n * running this surface on more than one instance, pass a `codeReplay` store backed\n * by something shared and atomic — a short-TTL row with a unique constraint, or a\n * distributed cache with an atomic set-if-absent. The port exists precisely so\n * that is a config change rather than a patch to the grant handler.\n */\n\nexport interface CodeReplayStore {\n /**\n * Record a code's `jti` as consumed. `false` means it was ALREADY recorded (a\n * replay); `true` is the first redemption. Must be atomic to be a real guard.\n */\n consume(jti: string, nowMs: number): Promise<boolean> | boolean;\n}\n\n/** Retain slightly beyond the 60s code TTL to cover the verify clock tolerance. */\nconst RETENTION_MS = 90_000;\n\n/**\n * The in-process store — correct on ONE instance, see the caveat above. Reached by\n * passing `codeReplay: 'in-process'`, which is a required acknowledgement rather\n * than a default: the config has no default for this field precisely because the\n * only possible one would fail open on a multi-pod deployment.\n */\nexport function inProcessCodeReplayStore(): CodeReplayStore {\n const usedJtis = new Map<string, number>();\n return {\n consume(jti: string, nowMs: number): boolean {\n for (const [seen, expiresAt] of usedJtis) {\n if (expiresAt <= nowMs) usedJtis.delete(seen);\n }\n if (usedJtis.has(jti)) return false;\n usedJtis.set(jti, nowMs + RETENTION_MS);\n return true;\n },\n };\n}\n","import { SignJWT, jwtVerify, importJWK, type JWTPayload } from \"jose\";\n\nimport { issuer, resourceAudience, DEFAULT_MCP_RESOURCE_PATH, type McpScope } from \"./config\";\nimport { SIGNING_ALG, type McpSigningKeyProvider } from \"./keys\";\n\n/**\n * JWT access-token issuer + verifier (12-23, ported from the origin host's\n * `lib/mcp/oauth/jwt.ts` — behaviour unchanged; the signing key arrives through a\n * provider and the resource path is config).\n *\n * The access token is a short-lived, ES256-signed JWT bound to the signed-in\n * user. It carries the claims the resource server checks LOCALLY against the\n * published JWKS (no introspection round-trip): `iss` (the issuer origin), `aud`\n * (`${origin}${resourcePath}`), `sub`, `email`, `scope` (space-delimited), `iat`,\n * `exp` (short TTL), and `jti`; the JWT header carries `kid` so the verifier can\n * select the public key during rotation.\n *\n * Failures are typed so the caller maps them to the right OAuth challenge\n * (`invalid_token` vs `insufficient_scope`).\n */\n\n/** Access-token lifetime — short-lived (15 min) per the spec. */\nexport const ACCESS_TOKEN_TTL_SECONDS = 15 * 60;\n\n/** Clock skew tolerated on `exp`/`iat` validation, in seconds. */\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\n/** The identity a verified access token resolves to. */\nexport interface VerifiedAccessToken {\n email: string;\n subject: string;\n scopes: string[];\n}\n\n/** Distinct verification failure reasons the caller maps to OAuth challenges. */\nexport type AccessTokenErrorCode = \"invalid_token\" | \"insufficient_scope\";\n\n/** A typed verification failure — `code` drives the `WWW-Authenticate` challenge. */\nexport class AccessTokenError extends Error {\n readonly code: AccessTokenErrorCode;\n\n constructor(code: AccessTokenErrorCode, message?: string) {\n super(message ?? code);\n this.name = \"AccessTokenError\";\n this.code = code;\n }\n}\n\n/** Inputs bound into a minted access token. */\nexport interface SignAccessTokenInput {\n email: string;\n subject: string;\n scopes: readonly McpScope[] | readonly string[];\n origin: string;\n /** Where the MCP resource is mounted. Default `/api/mcp`. */\n resourcePath?: string;\n /** Token lifetime in seconds. Default 15 minutes. */\n ttlSeconds?: number;\n}\n\n/** Deterministic-clock option shared by mint + verify. */\ninterface ClockOption {\n /** Epoch milliseconds; defaults to `Date.now()`. Injected for deterministic tests. */\n now?: number;\n}\n\n/** The full access-token claim set (beyond the registered JWT claims). */\ninterface AccessTokenClaims extends JWTPayload {\n email: string;\n scope: string;\n}\n\nfunction nowSeconds(now?: number): number {\n return Math.floor((now ?? Date.now()) / 1000);\n}\n\n/**\n * Mint an ES256-signed access token bound to the user.\n *\n * Returns `null` when no signing key is configured (safe-by-default: the AS\n * refuses to issue rather than falling back to a weaker mode). Sets the `kid`\n * header from the loaded key so the verifier can resolve the public JWK during\n * rotation.\n */\nexport async function signAccessToken(\n loadSigningKey: McpSigningKeyProvider,\n input: SignAccessTokenInput,\n options?: ClockOption,\n): Promise<string | null> {\n const key = await loadSigningKey();\n if (!key) return null;\n\n const iat = nowSeconds(options?.now);\n const exp = iat + (input.ttlSeconds ?? ACCESS_TOKEN_TTL_SECONDS);\n const scope = input.scopes.join(\" \");\n\n return new SignJWT({ email: input.email, scope } satisfies AccessTokenClaims)\n .setProtectedHeader({ alg: SIGNING_ALG, kid: key.kid })\n .setIssuer(issuer(input.origin))\n .setAudience(resourceAudience(input.origin, input.resourcePath ?? DEFAULT_MCP_RESOURCE_PATH))\n .setSubject(input.subject)\n .setIssuedAt(iat)\n .setExpirationTime(exp)\n .setJti(crypto.randomUUID())\n .sign(key.privateKey);\n}\n\n/** Options for {@link verifyAccessToken}. */\nexport interface VerifyAccessTokenOptions extends ClockOption {\n /** The deployment origin — derives the expected `iss` and `aud`. */\n origin: string;\n /** Where the MCP resource is mounted. Default `/api/mcp`. */\n resourcePath?: string;\n /** When set, the token must carry this scope or verification fails `insufficient_scope`. */\n requiredScope?: McpScope | string;\n}\n\n/** Parse the space-delimited `scope` claim into a de-duplicated string array. */\nfunction parseScopes(scope: unknown): string[] {\n if (typeof scope !== \"string\" || scope.trim() === \"\") return [];\n return [...new Set(scope.trim().split(/\\s+/))];\n}\n\n/**\n * Verify a bearer access token locally against the published JWKS public key.\n *\n * Checks signature (via the public JWK selected by the token's `kid`), `iss`,\n * `aud`, and `exp`. When `requiredScope` is supplied, also enforces scope. On\n * success returns `{ email, subject, scopes }`; on failure throws an\n * {@link AccessTokenError} whose `code` distinguishes `invalid_token` (bad\n * signature / wrong issuer / wrong audience / expired / malformed / unconfigured\n * key) from `insufficient_scope` (a valid token lacking the required scope).\n */\n/**\n * The cryptographic half: signature, `iss`, `aud`, `exp`.\n *\n * Every jose failure — bad signature, wrong issuer, wrong audience, expiry,\n * malformed token, unknown key — collapses into ONE opaque `invalid_token`. A\n * message naming the failed claim would be an oracle for the next attempt.\n */\nasync function verifiedPayload(\n loadSigningKey: McpSigningKeyProvider,\n token: string,\n options: VerifyAccessTokenOptions,\n): Promise<JWTPayload> {\n const key = await loadSigningKey();\n // No signing key configured → nothing can verify (safe-by-default).\n if (!key) throw new AccessTokenError(\"invalid_token\", \"no signing key configured\");\n\n try {\n const { payload } = await jwtVerify(token, await importJWK(key.publicJwk, SIGNING_ALG), {\n algorithms: [SIGNING_ALG],\n issuer: issuer(options.origin),\n audience: resourceAudience(options.origin, options.resourcePath ?? DEFAULT_MCP_RESOURCE_PATH),\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n currentDate: options.now === undefined ? undefined : new Date(options.now),\n });\n return payload;\n } catch {\n throw new AccessTokenError(\"invalid_token\", \"token verification failed\");\n }\n}\n\nexport async function verifyAccessToken(\n loadSigningKey: McpSigningKeyProvider,\n token: string,\n options: VerifyAccessTokenOptions,\n): Promise<VerifiedAccessToken> {\n const payload = await verifiedPayload(loadSigningKey, token, options);\n\n const email = typeof payload.email === \"string\" ? payload.email : null;\n const subject = typeof payload.sub === \"string\" ? payload.sub : null;\n if (!email || !subject) {\n throw new AccessTokenError(\"invalid_token\", \"missing subject or email claim\");\n }\n\n const scopes = parseScopes(payload.scope);\n if (options.requiredScope && !scopes.includes(options.requiredScope)) {\n throw new AccessTokenError(\n \"insufficient_scope\",\n `token lacks required scope '${options.requiredScope}'`,\n );\n }\n\n return { email, subject, scopes };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\nimport type { NewRefreshToken, RefreshTokenStore, StoredRefreshToken } from \"./stores\";\n\n/**\n * Refresh-token issue + rotation (12-23, ported from the origin host's\n * `lib/mcp/oauth/refresh.ts` — behaviour unchanged; Prisma calls became the\n * `RefreshTokenStore` port).\n *\n * Refresh tokens are opaque high-entropy strings; only their SHA-256 HASH is ever\n * stored — the plaintext is returned once at issue/rotate time and never\n * persisted, never logged.\n *\n * Rotation-on-use with replay protection:\n * - {@link issueRefreshToken} mints a root token bound to email + sub + client\n * + scopes;\n * - {@link rotateRefreshToken} consumes a token: it issues a NEW token chained\n * via `rotatedFrom` and revokes the parent, so a token is single-use;\n * - reuse of an already-rotated/revoked token is a REPLAY: rejected, AND the\n * whole lineage (every ancestor + descendant reachable through `rotatedFrom`)\n * is revoked — the OAuth 2.1 refresh-token replay rule;\n * - CONCURRENT reuse is the same event and gets the same answer. The store's\n * `rotate` is a claim-once write, so of two simultaneous rotations of one\n * parent exactly one is issued a successor and the other is treated as the\n * replay it is. Without that, replay protection would be bypassable by\n * WINNING a race instead of arriving second (see `RefreshTokenStore.rotate`);\n * - rotate may only NARROW scope (new ⊆ original); broadening is rejected and\n * nothing new is stored.\n */\n\n/** Bytes of entropy per opaque refresh token (→ 64 hex chars). */\nconst REFRESH_TOKEN_BYTES = 32;\n\n/** Refresh-token lifetime — long-lived relative to the 15-min access token. */\nexport const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days\n\n/** The single failure discriminator surfaced to the token endpoint. */\nexport type RefreshTokenErrorCode = \"invalid_grant\" | \"invalid_scope\";\n\n/**\n * A typed refresh-token failure. Every rejection — unknown, expired, revoked,\n * already-rotated (replay), wrong client, or a scope-broadening request —\n * surfaces as a discriminated error the token endpoint maps to the RFC 6749\n * error JSON.\n */\nexport class RefreshTokenError extends Error {\n readonly code: RefreshTokenErrorCode;\n\n constructor(code: RefreshTokenErrorCode, message?: string) {\n super(message ?? code);\n this.name = \"RefreshTokenError\";\n this.code = code;\n }\n}\n\n/** The result of issuing/rotating: the plaintext token (once) + bound scopes. */\nexport interface IssuedRefreshToken {\n /** The opaque plaintext refresh token — returned once, never persisted. */\n refreshToken: string;\n scopes: string[];\n}\n\n/** SHA-256 hex digest — the at-rest form of an opaque refresh token. */\nexport function hashToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\n/** Generate a fresh opaque refresh token (high-entropy hex). */\nfunction generateToken(): string {\n return randomBytes(REFRESH_TOKEN_BYTES).toString(\"hex\");\n}\n\nexport interface RefreshTokenContext {\n store: RefreshTokenStore;\n /** Lifetime of a newly stored token. Default 30 days. */\n ttlMs?: number;\n}\n\nfunction expiryOf(context: RefreshTokenContext): Date {\n return new Date(Date.now() + (context.ttlMs ?? REFRESH_TOKEN_TTL_MS));\n}\n\n/**\n * Issue a fresh (root) refresh token bound to a user (email + OAuth `sub`) +\n * client + scopes. The plaintext is returned once; only its hash is stored.\n */\nexport async function issueRefreshToken(\n context: RefreshTokenContext,\n binding: { userEmail: string; userSub: string; clientId: string; scopes: string[] },\n): Promise<IssuedRefreshToken> {\n const refreshToken = generateToken();\n const row: NewRefreshToken = {\n tokenHash: hashToken(refreshToken),\n userEmail: binding.userEmail,\n userSub: binding.userSub,\n clientId: binding.clientId,\n scopes: binding.scopes,\n expiresAt: expiryOf(context),\n rotatedFrom: null,\n };\n await context.store.create(row);\n return { refreshToken, scopes: binding.scopes };\n}\n\n/**\n * A pre-built O(1)-lookup index of one `(userEmail, clientId)` token family:\n * `byHash` resolves a hash to its row (to walk ancestors via `rotatedFrom`), and\n * `childrenOf` is the reverse index mapping a parent hash to its direct successor\n * hashes (to walk descendants). Both are built in a single pass so the lineage\n * traversal never re-scans the family (no O(n²) inner loop).\n */\ninterface LineageIndex {\n byHash: Map<string, StoredRefreshToken>;\n childrenOf: Map<string, string[]>;\n}\n\nfunction buildLineageIndex(family: StoredRefreshToken[]): LineageIndex {\n const byHash = new Map<string, StoredRefreshToken>();\n const childrenOf = new Map<string, string[]>();\n for (const row of family) {\n byHash.set(row.tokenHash, row);\n if (!row.rotatedFrom) continue;\n const siblings = childrenOf.get(row.rotatedFrom) ?? [];\n siblings.push(row.tokenHash);\n childrenOf.set(row.rotatedFrom, siblings);\n }\n return { byHash, childrenOf };\n}\n\n/**\n * Collect every token hash reachable from `seedHash` — its ancestors (via\n * `rotatedFrom`) and its descendants (via the reverse index) — by a BFS over the\n * pre-built index. Each neighbour lookup is O(1), so the walk is linear in the\n * family size.\n */\nfunction collectLineage(index: LineageIndex, seedHash: string): Set<string> {\n const lineage = new Set<string>();\n const queue = [seedHash];\n while (queue.length > 0) {\n const hash = queue.shift();\n if (!hash || lineage.has(hash)) continue;\n lineage.add(hash);\n\n const parent = index.byHash.get(hash)?.rotatedFrom ?? null;\n if (parent && !lineage.has(parent)) queue.push(parent);\n\n const children = (index.childrenOf.get(hash) ?? []).filter((child) => !lineage.has(child));\n queue.push(...children);\n }\n return lineage;\n}\n\n/**\n * Walk a token's rotation lineage (both directions) and revoke every token in it.\n * Called on replay detection, so a leaked refresh token — once reused —\n * invalidates the entire chain it belongs to.\n */\nasync function revokeLineage(\n context: RefreshTokenContext,\n scopedTo: Pick<StoredRefreshToken, \"userEmail\" | \"clientId\">,\n seedHash: string,\n): Promise<void> {\n // The lineage is confined to one (userEmail, clientId) pair, so load that set\n // once and walk the `rotatedFrom` links in memory — a small, bounded chain.\n const family = await context.store.listFamily(scopedTo.userEmail, scopedTo.clientId);\n const lineage = collectLineage(buildLineageIndex(family), seedHash);\n await context.store.revokeHashes([...lineage], new Date());\n}\n\n/** Reject any requested scope not already on the token (narrow-only). */\nfunction narrowedScopes(current: StoredRefreshToken, requested?: string[]): string[] {\n const scopes = requested ?? current.scopes;\n const original = new Set(current.scopes);\n for (const scope of scopes) {\n if (!original.has(scope)) {\n throw new RefreshTokenError(\n \"invalid_scope\",\n `scope '${scope}' broadens the refresh token grant`,\n );\n }\n }\n return scopes;\n}\n\n/**\n * Rotate a refresh token on use: validate it (must exist, be BOUND to the\n * presenting client, be unexpired, unrevoked and un-rotated), then issue a NEW\n * token chained via `rotatedFrom` and revoke the consumed one. Optionally NARROW\n * scope; a broadening request is `invalid_scope`.\n *\n * Client binding (OAuth 2.1 §4.3 / RFC 6749 §10.4) is checked BEFORE any rotation\n * or revocation, so client A can never redeem client B's refresh token — nor\n * silently consume B's token by trying: the token stays live for its rightful\n * owner.\n */\nexport async function rotateRefreshToken(\n context: RefreshTokenContext,\n plaintext: string,\n expectedClientId: string,\n newScopes?: string[],\n): Promise<IssuedRefreshToken> {\n const tokenHash = hashToken(plaintext);\n const current = await context.store.findByHash(tokenHash);\n\n if (!current) {\n throw new RefreshTokenError(\"invalid_grant\", \"unknown refresh token\");\n }\n if (current.clientId !== expectedClientId) {\n throw new RefreshTokenError(\n \"invalid_grant\",\n \"refresh token was not issued to this client\",\n );\n }\n // Expired → reject (not a replay; no lineage revocation needed beyond the\n // expiry itself).\n if (current.expiresAt.getTime() <= Date.now()) {\n throw new RefreshTokenError(\"invalid_grant\", \"refresh token expired\");\n }\n // Already revoked OR already used as the parent of a rotation → REPLAY. Revoke\n // the whole lineage and reject.\n if (current.revokedAt || (await context.store.hasSuccessor(tokenHash))) {\n await replay(context, current, tokenHash);\n }\n\n const scopes = narrowedScopes(current, newScopes);\n const successorPlaintext = generateToken();\n const claimed = await context.store.rotate(\n {\n tokenHash: hashToken(successorPlaintext),\n userEmail: current.userEmail,\n userSub: current.userSub,\n clientId: current.clientId,\n scopes,\n expiresAt: expiryOf(context),\n rotatedFrom: tokenHash,\n },\n tokenHash,\n new Date(),\n );\n // The checks above are a READ, so a concurrent rotation of the same parent can\n // pass them too; `rotate` is the serialization point and it hands the claim to\n // exactly one caller. Losing it is the SAME event as the replay branch above —\n // one token used twice — so it gets the same answer, deliberately: reject, and\n // revoke the lineage including the winner's fresh successor. Rejecting without\n // revoking would leave a race-winning attacker holding a live family, which is\n // the whole attack; and a client that legitimately double-submits already loses\n // its family in the sequential case, so this is consistent rather than harsher.\n if (!claimed) await replay(context, current, tokenHash);\n\n return { refreshToken: successorPlaintext, scopes };\n}\n\n/** Detected reuse: revoke the whole lineage and reject. Never returns. */\nasync function replay(\n context: RefreshTokenContext,\n current: StoredRefreshToken,\n tokenHash: string,\n): Promise<never> {\n await revokeLineage(context, current, tokenHash);\n throw new RefreshTokenError(\n \"invalid_grant\",\n \"refresh token already used (replay) — lineage revoked\",\n );\n}\n\n/** The stable identity a refresh token is bound to. */\nexport interface RefreshTokenIdentity {\n /** The user's email — the identity the AS binds to and route guards resolve by. */\n userEmail: string;\n /** The original OAuth subject, kept stable across every rotation. */\n userSub: string;\n}\n\n/**\n * Resolve the identity (`email` + original OAuth `sub`) a refresh token is bound\n * to. The token endpoint uses this after rotation to mint the successor access\n * token with the correct email AND the SAME stable `sub` as the initial token (no\n * re-consent, no `sub` drift). `null` if the row is unexpectedly absent.\n */\nexport async function getRefreshTokenIdentity(\n context: RefreshTokenContext,\n plaintext: string,\n): Promise<RefreshTokenIdentity | null> {\n const row = await context.store.findByHash(hashToken(plaintext));\n return row ? { userEmail: row.userEmail, userSub: row.userSub } : null;\n}\n","import {\n DEFAULT_MCP_RESOURCE_PATH,\n MCP_SUPPORTED_SCOPES,\n originFromRequest,\n} from \"./config\";\nimport {\n inProcessCodeReplayStore,\n type CodeReplayStore,\n} from \"./code-replay\";\nimport { ACCESS_TOKEN_TTL_SECONDS } from \"./access-token\";\nimport { REFRESH_TOKEN_TTL_MS } from \"./refresh\";\nimport { loadSigningKeyFromEnv, type McpSigningKeyProvider } from \"./keys\";\nimport type { ProviderAttributionRule } from \"./clients\";\nimport type { McpOauthStores, StoredOAuthClient } from \"./stores\";\n\n/**\n * The config seam of the authorization server, and its resolved form (12-23).\n *\n * Everything a HOST knows and the package cannot: who the signed-in caller is,\n * where the data lives, which origins are trusted, whether the surface is turned\n * on at all, and where its endpoints are mounted. Everything else — the RFC wire,\n * PKCE, rotation, replay, the discovery documents — is the package's.\n */\n\n/** The identity an authorize request binds a code to. From the SESSION only. */\nexport interface McpOauthSession {\n /**\n * The OAuth subject (the origin host passes the Google `sub`, falling back to the\n * email). Carried through every rotation so a refreshed token keeps the same\n * stable `sub`.\n */\n subject: string;\n /** The signed-in user's email — the identity the AS binds to. */\n email: string;\n}\n\n/** Where each endpoint of the surface lives, from the origin root. */\nexport interface McpOauthPaths {\n authorize: string;\n token: string;\n register: string;\n jwks: string;\n authorizationServerMetadata: string;\n protectedResourceMetadata: string;\n}\n\nexport const DEFAULT_OAUTH_PATHS: McpOauthPaths = {\n // the origin host's paths, and the ones the RFC 8414 document has always advertised.\n authorize: \"/api/oauth/authorize\",\n token: \"/api/oauth/token\",\n register: \"/api/oauth/register\",\n jwks: \"/.well-known/jwks.json\",\n authorizationServerMetadata: \"/.well-known/oauth-authorization-server\",\n protectedResourceMetadata: \"/.well-known/oauth-protected-resource\",\n};\n\n/** How a connection's liveness is recorded on a successful grant. */\nexport interface McpConnectionRecording {\n /**\n * The host's DB user id for a token's email, or `null` when there is no user row\n * yet (recording is then skipped — email is the identity, not the id).\n */\n resolveUserId: (email: string) => Promise<string | null> | string | null;\n /** Provider attribution rules; defaults to claude/chatgpt roots. */\n providerRules?: readonly ProviderAttributionRule[];\n /** Don't rewrite on every grant — refresh liveness at most this often. */\n activityThrottleMs?: number;\n}\n\nexport interface McpOauthConfig {\n /** Where the three owned tables live (see `./stores.ts`). */\n stores: McpOauthStores;\n /**\n * Resolve the caller's COOKIE SESSION for the authorize endpoint. `null` sends\n * the caller through the host's sign-in flow; no code is ever minted for an\n * unauthenticated request, and a client can never supply the identity itself.\n */\n resolveSession: (request: Request) => Promise<McpOauthSession | null> | McpOauthSession | null;\n /**\n * The operator gate. `false` makes the whole surface inert — authorize/token/jwks\n * answer 404 and registration answers 403 — which is how the origin host ships it OFF\n * by default (`MCP_BEARER_ENABLED`). Default: enabled (mounting is the opt-in).\n */\n enabled?: boolean | (() => boolean);\n /**\n * Signing material. Default: the env-backed provider with the origin host's variable\n * names. `null` from the provider means \"not provisioned\": nothing is minted and\n * the JWKS answers 503 rather than falling back to a weaker mode.\n */\n signingKey?: McpSigningKeyProvider;\n /**\n * The trusted PUBLIC origin allowlist — REQUIRED behind a reverse proxy, where\n * the server sees only its internal bind. The FIRST entry is canonical. With\n * none configured a forwarded host is never trusted (see `resolveTrustedOrigin`).\n */\n trustedOrigins?: readonly string[];\n /** Scopes the AS advertises and validates against. Default `mcp:read mcp:write`. */\n scopes?: readonly string[];\n /** Where the MCP resource is mounted — the token audience. Default `/api/mcp`. */\n resourcePath?: string;\n /** Endpoint paths, if the host mounts them somewhere else. */\n paths?: Partial<McpOauthPaths>;\n /** Where an unauthenticated authorize request is sent. Default `/login`. */\n loginPath?: string;\n /**\n * The query parameter carrying the post-login return path. Default\n * `callbackUrl` (Auth.js's name).\n */\n loginCallbackParam?: string;\n accessTokenTtlSeconds?: number;\n refreshTokenTtlMs?: number;\n /**\n * The single-use guard for authorization codes — REQUIRED, and required on\n * purpose. Pass a shared atomic store, or the literal `'in-process'` to accept\n * the single-instance limitation explicitly.\n *\n * There is deliberately NO default, because a default here would be the only one\n * in this config that fails OPEN. Every other one fails closed: no signing key\n * mints nothing and answers JWKS 503; `enabled: false` is 404 everywhere; an\n * empty `trustedOrigins` never trusts a forwarded host. An in-process default\n * instead silently permits cross-instance code replay — against an OAuth 2.1\n * MUST, on the very deployment shape a reusable package exists for (two pods\n * behind one load balancer), with nothing in the types to notice. Scaling out\n * must not be able to weaken the guard without somebody having typed something.\n */\n codeReplay: CodeReplayStore | \"in-process\";\n /**\n * Approve an authorize request before a code is minted — the CONSENT step.\n *\n * Registration is open whenever `enabled` is true (RFC 7591), so without an\n * approval step anyone may register a client carrying their OWN redirect URI and\n * their OWN scope ceiling, send a signed-in admin one link, and have the endpoint\n * mint them a code with no interaction: the redirect URI is exact-matched against\n * the attacker's own registration and the scope ceiling is the attacker's too, so\n * every other guard here holds and none of them helps.\n *\n * Until a host supplies this, `authorize` REFUSES any client it cannot see the\n * operator behind — i.e. any client not named in {@link preApprovedClientIds}.\n * Return `false` to deny (the caller gets an `access_denied` redirect, exactly as\n * a human refusal would).\n */\n resolveApproval?: (\n request: Request,\n client: StoredOAuthClient,\n scopes: readonly string[],\n ) => Promise<boolean> | boolean;\n /**\n * Client ids the OPERATOR registered, exempt from the approval gate above — the\n * escape hatch for a host that ships its own first-party clients and has no\n * consent screen to offer. Anything NOT listed here is treated as dynamically\n * registered, i.e. as attacker-controllable.\n */\n preApprovedClientIds?: readonly string[];\n /** Liveness recording on a grant; omit to record nothing. */\n connections?: McpConnectionRecording;\n}\n\n/** The config with every default applied — what the handlers actually read. */\nexport interface McpOauthContext {\n stores: McpOauthStores;\n resolveSession: McpOauthConfig[\"resolveSession\"];\n enabled: () => boolean;\n signingKey: McpSigningKeyProvider;\n trustedOrigins: readonly string[];\n scopes: readonly string[];\n resourcePath: string;\n paths: McpOauthPaths;\n loginPath: string;\n loginCallbackParam: string;\n accessTokenTtlSeconds: number;\n refreshTokenTtlMs: number;\n codeReplay: CodeReplayStore;\n /**\n * The resolved consent decision for one authorize request. Always present: with\n * no host seam it refuses every client the operator did not pre-approve, so the\n * handler has no \"unset\" case to forget.\n */\n approve: (\n request: Request,\n client: StoredOAuthClient,\n scopes: readonly string[],\n ) => Promise<boolean>;\n connections?: McpConnectionRecording;\n /** The trusted public origin for THIS request (issuance and verification agree). */\n originOf: (request: Request) => string;\n}\n\n/** The surface's own shape: what it advertises, where it lives, how long it lasts. */\nfunction resolveSurface(\n config: McpOauthConfig,\n): Pick<\n McpOauthContext,\n | \"scopes\"\n | \"resourcePath\"\n | \"paths\"\n | \"loginPath\"\n | \"loginCallbackParam\"\n | \"accessTokenTtlSeconds\"\n | \"refreshTokenTtlMs\"\n> {\n return {\n scopes: config.scopes ?? [...MCP_SUPPORTED_SCOPES],\n resourcePath: config.resourcePath ?? DEFAULT_MCP_RESOURCE_PATH,\n paths: { ...DEFAULT_OAUTH_PATHS, ...config.paths },\n loginPath: config.loginPath ?? \"/login\",\n loginCallbackParam: config.loginCallbackParam ?? \"callbackUrl\",\n accessTokenTtlSeconds: config.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS,\n refreshTokenTtlMs: config.refreshTokenTtlMs ?? REFRESH_TOKEN_TTL_MS,\n };\n}\n\nexport function resolveMcpOauthConfig(config: McpOauthConfig): McpOauthContext {\n const enabled = config.enabled ?? true;\n const trustedOrigins = config.trustedOrigins ?? [];\n return {\n stores: config.stores,\n resolveSession: config.resolveSession,\n // Mounting is the opt-in, so the gate defaults to ON; a host that ships the\n // surface dark passes its own flag (the origin host: `MCP_BEARER_ENABLED`).\n enabled: typeof enabled === \"function\" ? enabled : () => enabled,\n // `null` from the provider means \"not provisioned\": nothing is minted and the\n // JWKS answers 503 rather than falling back to a weaker mode.\n signingKey: config.signingKey ?? loadSigningKeyFromEnv(),\n trustedOrigins,\n ...resolveSurface(config),\n // `'in-process'` is an ACKNOWLEDGEMENT, not a default — see the field's docs.\n codeReplay:\n config.codeReplay === \"in-process\" ? inProcessCodeReplayStore() : config.codeReplay,\n approve: resolveApprover(config),\n ...(config.connections ? { connections: config.connections } : {}),\n originOf: (request) => originFromRequest(request, trustedOrigins),\n };\n}\n\n/**\n * The consent decision, resolved once. A host seam wins; otherwise only a client\n * the OPERATOR named may proceed, so an open registration endpoint cannot mint a\n * code for a client nobody approved.\n */\nfunction resolveApprover(config: McpOauthConfig): McpOauthContext[\"approve\"] {\n const preApproved = new Set(config.preApprovedClientIds ?? []);\n const { resolveApproval } = config;\n return async (request, client, scopes) => {\n if (preApproved.has(client.clientId)) return true;\n if (!resolveApproval) return false;\n return resolveApproval(request, client, scopes);\n };\n}\n\n/** The gate's own answer: 404, so a disabled surface looks like no surface. */\nexport function notFound(): Response {\n return new Response(\"Not Found\", { status: 404 });\n}\n","import { mintCode } from \"./authorization-code\";\nimport { matchesRedirectUri } from \"./clients\";\nimport type { McpOauthContext } from \"./context\";\nimport { SUPPORTED_CHALLENGE_METHOD } from \"./pkce\";\nimport type { StoredOAuthClient } from \"./stores\";\n\n/**\n * The OAuth 2.1 Authorization Code + PKCE authorization endpoint (12-23, ported\n * from the origin host's `app/api/oauth/authorize/route.ts`).\n *\n * It renders no UI: it authenticates the caller against the host's cookie session\n * (through `resolveSession`), validates the request, and either 302-redirects an\n * unauthenticated caller into the host's sign-in flow (so the flow resumes\n * post-login) or, for a signed-in caller with a valid request, mints a stateless\n * authorization code bound to the SESSION identity and 302-redirects back to the\n * client's registered `redirect_uri` with the code and echoed `state`.\n *\n * Security invariants, unchanged:\n * - **Open-redirect prevention:** `client_id` + `redirect_uri` are validated\n * against the registered client BEFORE anything else; an unknown client or a\n * `redirect_uri` that is not an EXACT registered match yields a 400 plain-text\n * response — the endpoint NEVER redirects an error to an unvalidated URI. Only\n * once the URI is validated do other failures redirect back to it.\n * - **Mandatory PKCE S256:** a missing `code_challenge`, or a method other than\n * `S256` (incl. `plain`), is rejected.\n * - **Identity from the session only:** `sub`/`email` come solely from the\n * verified session; a client can never supply the identity via a query param.\n * - **No key, no code:** an unprovisioned signing key is a `server_error`\n * redirect, never a weaker mode.\n */\n\n/** OAuth 2.1 error codes this endpoint can emit on a validated redirect_uri. */\ntype AuthorizeErrorCode =\n | \"invalid_request\"\n | \"unsupported_response_type\"\n | \"invalid_scope\"\n /** The resource owner said no — or nobody was asked and nobody approved. */\n | \"access_denied\"\n | \"server_error\";\n\n/** The parsed, still-untrusted query parameters of an authorize request. */\ninterface AuthorizeParams {\n responseType: string | null;\n clientId: string | null;\n redirectUri: string | null;\n codeChallenge: string | null;\n codeChallengeMethod: string | null;\n scope: string | null;\n state: string | null;\n}\n\nfunction parseParams(url: URL): AuthorizeParams {\n const q = url.searchParams;\n return {\n responseType: q.get(\"response_type\"),\n clientId: q.get(\"client_id\"),\n redirectUri: q.get(\"redirect_uri\"),\n codeChallenge: q.get(\"code_challenge\"),\n codeChallengeMethod: q.get(\"code_challenge_method\"),\n scope: q.get(\"scope\"),\n state: q.get(\"state\"),\n };\n}\n\n/** A 302 response to `location` with no body. */\nfunction redirectTo(location: string): Response {\n return new Response(null, { status: 302, headers: { location } });\n}\n\n/**\n * A 400 plain-text refusal used ONLY when the `redirect_uri`/`client_id` are\n * themselves invalid — i.e. there is no validated URI to safely redirect an error\n * to (the open-redirect guard).\n */\nfunction badRequest(message: string): Response {\n return new Response(message, {\n status: 400,\n headers: { \"content-type\": \"text/plain; charset=utf-8\" },\n });\n}\n\n/**\n * Build an error redirect back to the (already-validated) `redirect_uri`, carrying\n * the OAuth `error` and the echoed `state` per OAuth 2.1 §4.1.2.1.\n */\nfunction errorRedirect(\n redirectUri: string,\n error: AuthorizeErrorCode,\n state: string | null,\n): Response {\n const target = new URL(redirectUri);\n target.searchParams.set(\"error\", error);\n if (state !== null) target.searchParams.set(\"state\", state);\n return redirectTo(target.toString());\n}\n\n/**\n * Whether every space-delimited requested scope is within `allowed`. An\n * empty/absent scope is permitted (the server applies its default), but any present\n * scope must be in `allowed` — and `allowed` is the SPECIFIC CLIENT's registered\n * scopes, so a client that registered for only `mcp:read` cannot request\n * `mcp:write` and be issued a code for it (\"no privilege escalation via metadata\",\n * enforced at authorize rather than trusted at registration).\n */\nfunction scopeIsSupported(scope: string | null, allowed: readonly string[]): boolean {\n if (!scope) return true;\n const requested = scope.split(/\\s+/).filter(Boolean);\n const allowedSet = new Set<string>(allowed);\n return requested.every((candidate) => allowedSet.has(candidate));\n}\n\n/**\n * Resolve + validate the client and its `redirect_uri` FIRST (the open-redirect\n * guard). Returns the validated URI AND the client's registered scopes, or a plain\n * 400 — NEVER a redirect — when the client or URI is unknown/unregistered, so an\n * error is never steered to an unvalidated URI.\n */\nasync function validateClientAndRedirect(\n context: McpOauthContext,\n params: AuthorizeParams,\n): Promise<{ client: StoredOAuthClient; redirectUri: string } | Response> {\n if (!params.clientId) return badRequest(\"invalid_request: missing client_id\");\n if (!params.redirectUri) return badRequest(\"invalid_request: missing redirect_uri\");\n\n const client = await context.stores.clients.findByClientId(params.clientId);\n if (!client) return badRequest(\"invalid_client: unknown client_id\");\n if (!matchesRedirectUri(client, params.redirectUri)) {\n return badRequest(\"invalid_request: redirect_uri is not registered\");\n }\n\n return { client, redirectUri: params.redirectUri };\n}\n\n/**\n * Validate the response_type, mandatory PKCE S256, and the requested scope against\n * the already-validated `redirectUri`. Returns `null` when the request passes, or an\n * error redirect back to the validated URI on the first failure.\n */\nfunction validateAuthorizeRequest(\n params: AuthorizeParams,\n redirectUri: string,\n clientScopes: readonly string[],\n): Response | null {\n const { state } = params;\n\n if (params.responseType !== \"code\") {\n return errorRedirect(redirectUri, \"unsupported_response_type\", state);\n }\n // Mandatory PKCE S256: reject a missing challenge or any non-S256 method.\n if (!params.codeChallenge || params.codeChallengeMethod !== SUPPORTED_CHALLENGE_METHOD) {\n return errorRedirect(redirectUri, \"invalid_request\", state);\n }\n if (!scopeIsSupported(params.scope, clientScopes)) {\n return errorRedirect(redirectUri, \"invalid_scope\", state);\n }\n return null;\n}\n\n/** The already-validated inputs an authorize request resolves to before minting. */\ninterface ValidatedAuthorize {\n /** The registered client — needed by the approval seam, not just its id. */\n client: StoredOAuthClient;\n clientId: string;\n redirectUri: string;\n codeChallenge: string;\n scope: string;\n state: string | null;\n}\n\n/**\n * With a validated request, resolve the authenticated session (identity from the\n * session ONLY) and either send the caller through sign-in, mint the code, or\n * `server_error` when no signing key is configured.\n */\nasync function authenticateAndMint(\n context: McpOauthContext,\n request: Request,\n url: URL,\n origin: string,\n validated: ValidatedAuthorize,\n): Promise<Response> {\n const { redirectUri, state } = validated;\n\n const session = await context.resolveSession(request);\n if (!session?.email) {\n // No session: send the caller through the host's sign-in flow with a callback\n // back to THIS authorize URL so the flow resumes post-login. No code is minted\n // for an unauthenticated request.\n const loginUrl = new URL(context.loginPath, origin);\n loginUrl.searchParams.set(context.loginCallbackParam, url.pathname + url.search);\n return redirectTo(loginUrl.toString());\n }\n\n // CONSENT. A session proves WHO is asking; it never proves they agreed to THIS\n // client holding THESE scopes. Registration is open (RFC 7591), so without this\n // step anyone may register a client carrying their own redirect URI and their own\n // scope ceiling, send a signed-in admin a single link, and have that admin's\n // browser mint them a code — and the two guards that look like they would stop it,\n // exact redirect-URI matching and the per-client scope ceiling, are both checked\n // against the ATTACKER'S OWN registration. Refuses by default; see\n // `resolveApproval` / `preApprovedClientIds`.\n const scopes = validated.scope.split(/\\s+/).filter(Boolean);\n if (!(await context.approve(request, validated.client, scopes))) {\n // The answer a human refusal gives, at the URI validated further up.\n return errorRedirect(redirectUri, \"access_denied\", state);\n }\n\n const code = await mintCode(context.signingKey, {\n // The subject is the OAuth `sub` the host resolved, NOT a DB id: downstream\n // guards resolve the user by EMAIL, and the code carries only what the session\n // verified.\n sub: session.subject || session.email,\n email: session.email,\n clientId: validated.clientId,\n redirectUri,\n codeChallenge: validated.codeChallenge,\n scope: validated.scope,\n origin,\n });\n\n if (!code) {\n // No signing key configured while the surface is on — refuse to issue rather\n // than fall back to a weaker mode (safe-by-default).\n return errorRedirect(redirectUri, \"server_error\", state);\n }\n\n const success = new URL(redirectUri);\n success.searchParams.set(\"code\", code);\n if (state !== null) success.searchParams.set(\"state\", state);\n return redirectTo(success.toString());\n}\n\n/** `GET <authorize>` — the whole endpoint. */\nexport async function authorizeEndpoint(\n context: McpOauthContext,\n request: Request,\n): Promise<Response> {\n const url = new URL(request.url);\n const origin = context.originOf(request);\n const params = parseParams(url);\n\n // --- Validate client + redirect_uri FIRST (the open-redirect guard) --------\n const clientResult = await validateClientAndRedirect(context, params);\n if (clientResult instanceof Response) return clientResult;\n const { client, redirectUri } = clientResult;\n\n // --- Validate the rest (scope checked against the CLIENT's own registration)\n const requestError = validateAuthorizeRequest(params, redirectUri, client.scopes);\n if (requestError) return requestError;\n\n // The guards above guarantee a present client_id + PKCE challenge; narrow them.\n return authenticateAndMint(context, request, url, origin, {\n client,\n clientId: params.clientId as string,\n redirectUri,\n codeChallenge: params.codeChallenge as string,\n scope: params.scope ?? \"\",\n state: params.state,\n });\n}\n","import { registerClient, type RegisterClientInput } from \"./clients\";\nimport type { McpOauthContext } from \"./context\";\nimport type { TokenEndpointAuthMethod } from \"./stores\";\n\n/**\n * RFC 7591 Dynamic Client Registration (12-23, ported from the origin host's\n * `app/api/oauth/register/route.ts`).\n *\n * An external host (a Claude.ai / ChatGPT connector) self-registers by POSTing RFC\n * 7591 client metadata as JSON; on success a public `client_id` (and, for a\n * confidential client, a one-time `client_secret`) is returned so the host can run\n * the Authorization Code + PKCE flow.\n *\n * Security, unchanged:\n * - **The gate answers 403 here, not 404.** Open DCR is an operator opt-in, and\n * RFC 7591 registration explicitly refuses with `access_denied` so a probing\n * host learns the endpoint exists but registration is closed — the documented\n * static-client path is used instead.\n * - **No privilege escalation via metadata:** registration can only set\n * `redirect_uris`, an auth method the token endpoint actually supports, the\n * supported grant types, and a scope SUBSET of the AS's advertised scopes. Any\n * attempt to widen is rejected, never silently coerced. Identity is never\n * client-supplied.\n * - **Secret hygiene:** a confidential client's secret is generated server-side,\n * returned once, and stored only as a SHA-256 hash.\n */\n\n/** RFC 7591 §3.2.2 registration error codes this endpoint can emit. */\ntype RegistrationErrorCode = \"invalid_redirect_uri\" | \"invalid_client_metadata\";\n\n/** The RFC 7591 §3.2.1 client-information success response. */\ninterface RegistrationSuccessResponse {\n client_id: string;\n client_secret?: string;\n client_id_issued_at: number;\n token_endpoint_auth_method: TokenEndpointAuthMethod;\n redirect_uris: string[];\n grant_types: string[];\n scope: string;\n client_name?: string;\n}\n\n/** Auth methods the token endpoint can actually enforce (RFC 7591 §2). */\nconst SUPPORTED_AUTH_METHODS: readonly TokenEndpointAuthMethod[] = [\n \"none\",\n \"client_secret_basic\",\n];\n\n/** Grant types the AS supports (mirrors the AS discovery metadata). */\nconst SUPPORTED_GRANT_TYPES: readonly string[] = [\"authorization_code\", \"refresh_token\"];\n\nconst DEFAULT_GRANT_TYPES = [\"authorization_code\", \"refresh_token\"] as const;\nconst DEFAULT_AUTH_METHOD: TokenEndpointAuthMethod = \"none\";\n\nconst JSON_HEADERS = {\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n} as const;\n\n/** A JSON error response in the RFC 7591 §3.2.2 shape. */\nfunction registrationError(\n error: RegistrationErrorCode,\n status: number,\n description?: string,\n): Response {\n const body: { error: RegistrationErrorCode; error_description?: string } = { error };\n if (description) body.error_description = description;\n return new Response(JSON.stringify(body), { status, headers: { ...JSON_HEADERS } });\n}\n\n/** Whether a value is a syntactically valid absolute URI (scheme + authority). */\nfunction isAbsoluteUri(value: string): boolean {\n try {\n const url = new URL(value);\n // An absolute redirect target must carry a scheme AND an authority — reject\n // opaque/relative forms so an intercepted request can never be re-steered.\n return Boolean(url.protocol) && Boolean(url.host);\n } catch {\n return false;\n }\n}\n\n/** The RFC 7591 client-metadata fields this endpoint reads. */\ninterface ClientMetadata {\n redirect_uris?: unknown;\n token_endpoint_auth_method?: unknown;\n grant_types?: unknown;\n scope?: unknown;\n client_name?: unknown;\n}\n\n/** A validated registration input, or a typed rejection to return verbatim. */\ntype ValidationResult =\n | { ok: true; input: RegisterClientInput }\n | { ok: false; response: Response };\n\n/** A per-field validator result: the accepted value, or a rejection response. */\ntype FieldResult<T> = { ok: true; value: T } | { ok: false; response: Response };\n\nfunction accept<T>(value: T): FieldResult<T> {\n return { ok: true, value };\n}\n\nfunction reject<T>(response: Response): FieldResult<T> {\n return { ok: false, response };\n}\n\n/**\n * `redirect_uris` — REQUIRED, a non-empty array whose every entry is an absolute\n * URI. Any failure maps to `invalid_redirect_uri` (RFC 7591 §3.2.2).\n */\nfunction validateRedirectUris(raw: unknown): FieldResult<string[]> {\n if (\n !Array.isArray(raw) ||\n raw.length === 0 ||\n !raw.every((uri): uri is string => typeof uri === \"string\" && isAbsoluteUri(uri))\n ) {\n return reject(\n registrationError(\n \"invalid_redirect_uri\",\n 400,\n \"redirect_uris must be a non-empty array of absolute URIs\",\n ),\n );\n }\n return accept([...raw]);\n}\n\n/** `token_endpoint_auth_method` — optional; defaults to `none`. */\nfunction validateAuthMethod(raw: unknown): FieldResult<TokenEndpointAuthMethod> {\n if (raw === undefined || raw === null) return accept(DEFAULT_AUTH_METHOD);\n if (\n typeof raw !== \"string\" ||\n !SUPPORTED_AUTH_METHODS.includes(raw as TokenEndpointAuthMethod)\n ) {\n return reject(\n registrationError(\n \"invalid_client_metadata\",\n 400,\n `unsupported token_endpoint_auth_method (supported: ${SUPPORTED_AUTH_METHODS.join(\", \")})`,\n ),\n );\n }\n return accept(raw as TokenEndpointAuthMethod);\n}\n\n/** `grant_types` — optional; defaults to code+refresh. */\nfunction validateGrantTypes(raw: unknown): FieldResult<string[]> {\n if (raw === undefined || raw === null) return accept([...DEFAULT_GRANT_TYPES]);\n if (\n !Array.isArray(raw) ||\n raw.length === 0 ||\n !raw.every(\n (grant): grant is string =>\n typeof grant === \"string\" && SUPPORTED_GRANT_TYPES.includes(grant),\n )\n ) {\n return reject(\n registrationError(\n \"invalid_client_metadata\",\n 400,\n `unsupported grant_types (supported: ${SUPPORTED_GRANT_TYPES.join(\", \")})`,\n ),\n );\n }\n return accept([...raw]);\n}\n\n/**\n * `scope` — optional space-delimited string; every requested scope must be in the\n * AS's supported set. An explicit empty request falls back to the full set.\n */\nfunction validateScopes(raw: unknown, supportedScopes: readonly string[]): FieldResult<string[]> {\n if (raw === undefined || raw === null) return accept([...supportedScopes]);\n if (typeof raw !== \"string\") {\n return reject(\n registrationError(\"invalid_client_metadata\", 400, \"scope must be a space-delimited string\"),\n );\n }\n const requested = raw.split(/\\s+/).filter(Boolean);\n const supported = new Set<string>(supportedScopes);\n if (!requested.every((scope) => supported.has(scope))) {\n return reject(\n registrationError(\n \"invalid_client_metadata\",\n 400,\n `scope must be a subset of: ${supportedScopes.join(\" \")}`,\n ),\n );\n }\n return accept(requested.length > 0 ? requested : [...supportedScopes]);\n}\n\n/**\n * Validate RFC 7591 client metadata strictly, by composing the per-field\n * validators. `redirect_uris` failures map to `invalid_redirect_uri`; every other\n * unsupported-metadata failure maps to `invalid_client_metadata`.\n */\nfunction validateMetadata(\n metadata: ClientMetadata,\n supportedScopes: readonly string[],\n): ValidationResult {\n const redirectUris = validateRedirectUris(metadata.redirect_uris);\n if (!redirectUris.ok) return redirectUris;\n\n const authMethod = validateAuthMethod(metadata.token_endpoint_auth_method);\n if (!authMethod.ok) return authMethod;\n\n const grantTypes = validateGrantTypes(metadata.grant_types);\n if (!grantTypes.ok) return grantTypes;\n\n const scopes = validateScopes(metadata.scope, supportedScopes);\n if (!scopes.ok) return scopes;\n\n const clientNameRaw = metadata.client_name;\n const clientName = typeof clientNameRaw === \"string\" ? clientNameRaw : null;\n\n return {\n ok: true,\n input: {\n redirectUris: redirectUris.value,\n clientName,\n tokenEndpointAuthMethod: authMethod.value,\n grantTypes: grantTypes.value,\n scopes: scopes.value,\n },\n };\n}\n\n/** The refusal a closed registration endpoint answers with. */\nexport function registrationDisabled(): Response {\n return new Response(\n JSON.stringify({\n error: \"access_denied\",\n error_description: \"dynamic client registration is disabled\",\n }),\n { status: 403, headers: { ...JSON_HEADERS } },\n );\n}\n\n/** `POST <register>` — the whole endpoint. */\nexport async function registerEndpoint(\n context: McpOauthContext,\n request: Request,\n): Promise<Response> {\n // Parse the JSON body. A malformed body is unusable metadata → 400.\n let metadata: ClientMetadata;\n try {\n const parsed: unknown = await request.json();\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n return registrationError(\n \"invalid_client_metadata\",\n 400,\n \"request body must be a JSON object\",\n );\n }\n metadata = parsed as ClientMetadata;\n } catch {\n return registrationError(\"invalid_client_metadata\", 400, \"request body must be valid JSON\");\n }\n\n const validated = validateMetadata(metadata, context.scopes);\n if (!validated.ok) return validated.response;\n\n const registered = await registerClient(context.stores.clients, validated.input);\n\n const responseBody: RegistrationSuccessResponse = {\n client_id: registered.clientId,\n ...(registered.clientSecret ? { client_secret: registered.clientSecret } : {}),\n client_id_issued_at: Math.floor(Date.now() / 1000),\n token_endpoint_auth_method: registered.tokenEndpointAuthMethod,\n redirect_uris: registered.redirectUris,\n grant_types: registered.grantTypes,\n scope: registered.scopes.join(\" \"),\n ...(registered.clientName ? { client_name: registered.clientName } : {}),\n };\n\n return new Response(JSON.stringify(responseBody), {\n status: 201,\n headers: { ...JSON_HEADERS },\n });\n}\n","import { createHash, timingSafeEqual } from \"node:crypto\";\n\nimport type { OAuthClientStore } from \"./stores\";\n\n/**\n * The token endpoint's wire helpers: the RFC 6749 §5.1/§5.2 bodies and client\n * authentication (12-23, split out of the grant handlers so each file stays under\n * the size gate — the same split the origin host made).\n *\n * These bodies are NOT the house `{ data }` envelope, deliberately: they are read\n * by OAuth clients that expect the RFC shapes at the top level, and `Cache-Control:\n * no-store` is required on every one of them because they carry credentials.\n */\n\n/** OAuth 2.1 / RFC 6749 §5.2 error codes the token endpoint can emit. */\ntype TokenErrorCode =\n | \"invalid_request\"\n | \"invalid_client\"\n | \"invalid_grant\"\n | \"invalid_scope\"\n | \"unsupported_grant_type\";\n\n/** The RFC 6749 §5.1 successful token response. */\ninterface TokenSuccessResponse {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n refresh_token: string;\n scope: string;\n}\n\nconst JSON_HEADERS = {\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n} as const;\n\n/** A JSON error response in the RFC 6749 §5.2 shape. */\nexport function tokenError(\n error: TokenErrorCode,\n status: number,\n description?: string,\n headers: Record<string, string> = {},\n): Response {\n const body: { error: TokenErrorCode; error_description?: string } = { error };\n if (description) body.error_description = description;\n return new Response(JSON.stringify(body), {\n status,\n headers: { ...JSON_HEADERS, ...headers },\n });\n}\n\n/** A JSON success response with `Cache-Control: no-store` (RFC 6749 §5.1). */\nexport function tokenSuccess(payload: TokenSuccessResponse): Response {\n return new Response(JSON.stringify(payload), { status: 200, headers: { ...JSON_HEADERS } });\n}\n\n/** Constant-time equality of two SHA-256 hex digests. */\nfunction hashesEqual(a: string, b: string): boolean {\n const bufA = Buffer.from(a, \"hex\");\n const bufB = Buffer.from(b, \"hex\");\n if (bufA.length !== bufB.length || bufA.length === 0) return false;\n return timingSafeEqual(bufA, bufB);\n}\n\n/** SHA-256 hex digest — matches the at-rest client-secret hashing convention. */\nfunction sha256Hex(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\n/** Client credentials extracted from HTTP Basic auth or the form body. */\nexport interface ClientCredentials {\n clientId: string | null;\n clientSecret: string | null;\n}\n\n/**\n * Resolve the presented client credentials. HTTP Basic (`client_secret_basic`)\n * takes precedence over the form-body `client_id` per RFC 6749 §2.3.1; a malformed\n * Basic header is treated as absent (the form body still applies).\n */\nexport function readClientCredentials(\n request: Request,\n form: URLSearchParams,\n): ClientCredentials {\n const authorization = request.headers.get(\"authorization\");\n if (authorization && authorization.startsWith(\"Basic \")) {\n const decoded = Buffer.from(authorization.slice(6), \"base64\").toString(\"utf8\");\n const separator = decoded.indexOf(\":\");\n if (separator !== -1) {\n return {\n clientId: decoded.slice(0, separator),\n clientSecret: decoded.slice(separator + 1),\n };\n }\n }\n return { clientId: form.get(\"client_id\"), clientSecret: form.get(\"client_secret\") };\n}\n\n/** The 401 challenge issued on a client-authentication failure. */\nconst CLIENT_AUTH_CHALLENGE = { \"www-authenticate\": 'Basic realm=\"oauth-token\"' };\n\n/**\n * Authenticate the client that presents the request. A public client is identified\n * by `client_id` alone (which must equal `expectedClientId` when provided). A\n * confidential client (`client_secret_basic`) MUST present a secret whose SHA-256\n * matches the stored hash. Returns `null` on success, or a 401 `invalid_client`\n * response on failure — never a body that says which half was wrong.\n */\nexport async function authenticateClient(\n clients: OAuthClientStore,\n credentials: ClientCredentials,\n expectedClientId?: string,\n): Promise<Response | null> {\n const clientId = credentials.clientId;\n if (!clientId) {\n return tokenError(\"invalid_client\", 401, \"missing client_id\", CLIENT_AUTH_CHALLENGE);\n }\n if (expectedClientId && clientId !== expectedClientId) {\n // The presenting client does not match the client the code was issued to.\n return tokenError(\n \"invalid_client\",\n 401,\n \"client_id does not match the grant\",\n CLIENT_AUTH_CHALLENGE,\n );\n }\n\n const client = await clients.findByClientId(clientId);\n if (!client) {\n return tokenError(\"invalid_client\", 401, \"unknown client\", CLIENT_AUTH_CHALLENGE);\n }\n\n if (client.tokenEndpointAuthMethod === \"client_secret_basic\") {\n const secret = credentials.clientSecret;\n if (!secret || !client.clientSecretHash) {\n return tokenError(\n \"invalid_client\",\n 401,\n \"client authentication required\",\n CLIENT_AUTH_CHALLENGE,\n );\n }\n if (!hashesEqual(sha256Hex(secret), client.clientSecretHash)) {\n return tokenError(\n \"invalid_client\",\n 401,\n \"invalid client credentials\",\n CLIENT_AUTH_CHALLENGE,\n );\n }\n }\n\n return null;\n}\n","import { ACCESS_TOKEN_TTL_SECONDS, signAccessToken } from \"./access-token\";\nimport { AuthorizationCodeError, verifyCode } from \"./authorization-code\";\nimport { providerFromRedirectUris } from \"./clients\";\nimport type { McpConnectionRecording, McpOauthContext } from \"./context\";\nimport { verifyChallenge } from \"./pkce\";\nimport {\n RefreshTokenError,\n getRefreshTokenIdentity,\n issueRefreshToken,\n rotateRefreshToken,\n} from \"./refresh\";\nimport type { McpConnectionStore } from \"./stores\";\nimport {\n authenticateClient,\n readClientCredentials,\n tokenError,\n tokenSuccess,\n type ClientCredentials,\n} from \"./token-response\";\n\n/**\n * The two grant handlers of the token endpoint (12-23, ported from the origin host's\n * `lib/mcp/oauth/token-grants.ts`).\n *\n * Security invariants enforced here, unchanged:\n * - **Single-use codes:** the code's `jti` is consumed the moment it is\n * redeemed; a replay of the same code is `invalid_grant`.\n * - **PKCE:** a `code_verifier` that does not S256-match the code's\n * `code_challenge` is `invalid_grant`.\n * - **Client auth:** a public client's `client_id` must equal the code's bound\n * client; a confidential client must present a secret whose SHA-256 matches\n * the stored hash, else `invalid_client` (401).\n * - **Bound `redirect_uri`:** it must equal the one the code was minted with\n * (RFC 6749 §4.1.3).\n * - **Refresh rotation:** client-bound, replay-revoking, narrow-only scope.\n */\n\n/** Throttle default: don't rewrite liveness on every grant. */\nconst DEFAULT_ACTIVITY_THROTTLE_MS = 60_000;\n\n/**\n * Best-effort: record that this user's AI host (OAuth client) is live, so an\n * account page can show \"connected via Claude · active 2 min ago\". Runs on the\n * token grant, not the per-request hot path; hosts refresh every ~15 min so\n * liveness stays fresh.\n *\n * NEVER lets a failure break token issuance — a recording error is swallowed, and\n * nothing about it is logged, because the only interesting values here are an\n * email and a client id. Skipped when the host resolves no user row (email is the\n * identity) or when no connection recording is configured at all.\n */\nasync function recordHostConnection(\n context: McpOauthContext,\n email: string,\n clientId: string,\n): Promise<void> {\n const recording = context.connections;\n const store = context.stores.connections;\n if (!recording || !store) return;\n try {\n await writeConnectionActivity(context, { recording, store }, email, clientId);\n } catch {\n // Liveness is non-critical — never fail the grant on it. Nothing is logged\n // either: the only values here are an email and a client id.\n }\n}\n\n/** The write itself, once the recording ports are known to exist. */\nasync function writeConnectionActivity(\n context: McpOauthContext,\n ports: { recording: McpConnectionRecording; store: McpConnectionStore },\n email: string,\n clientId: string,\n): Promise<void> {\n const { recording, store } = ports;\n const [userId, client] = await Promise.all([\n recording.resolveUserId(email),\n context.stores.clients.findByClientId(clientId),\n ]);\n // No user row yet: email is the identity the AS binds to, so the grant stands\n // and there is simply nothing to attribute it to.\n if (!userId) return;\n\n const throttleMs = recording.activityThrottleMs ?? DEFAULT_ACTIVITY_THROTTLE_MS;\n const lastActiveAt = await store.lastActiveAt(userId, clientId);\n const now = new Date();\n if (lastActiveAt && now.getTime() - lastActiveAt.getTime() < throttleMs) return;\n\n await store.recordActivity({\n userId,\n oauthClientId: clientId,\n clientName: client?.clientName ?? null,\n // Attribute to a provider from the client's redirect URIs (claude.ai →\n // claude, chatgpt.com → chatgpt) so an account page lights the right card.\n host: client ? providerFromRedirectUris(client.redirectUris, recording.providerRules) : null,\n at: now,\n });\n}\n\n/** The presented `authorization_code` grant parameters, once validated present. */\ninterface AuthorizationCodeParams {\n code: string;\n redirectUri: string;\n codeVerifier: string;\n}\n\n/**\n * Read + presence-check the `authorization_code` form parameters. Returns the three\n * required values, or an `invalid_request` (400) naming the first missing field.\n */\nfunction readAuthorizationCodeParams(form: URLSearchParams): AuthorizationCodeParams | Response {\n const code = form.get(\"code\");\n const redirectUri = form.get(\"redirect_uri\");\n const codeVerifier = form.get(\"code_verifier\");\n\n if (!code) return tokenError(\"invalid_request\", 400, \"missing code\");\n if (!redirectUri) return tokenError(\"invalid_request\", 400, \"missing redirect_uri\");\n if (!codeVerifier) return tokenError(\"invalid_request\", 400, \"missing code_verifier\");\n\n return { code, redirectUri, codeVerifier };\n}\n\n/**\n * Redeem a presented code, or refuse.\n *\n * The ORDER is the security contract, and it is the order the origin host established:\n * verify the code's signature, authenticate the presenting client against the\n * client the code was bound to, check the bound `redirect_uri`, check PKCE — and\n * only THEN consume the single-use `jti`. Consuming earlier would let a failed\n * attempt (a wrong secret, a mismatched verifier) burn a legitimate code.\n */\nasync function redeemCode(\n context: McpOauthContext,\n params: AuthorizationCodeParams,\n credentials: ClientCredentials,\n origin: string,\n): Promise<Awaited<ReturnType<typeof verifyCode>> | Response> {\n const { code, redirectUri, codeVerifier } = params;\n\n // Every code-level failure (expired, tampered, wrong-audience) is invalid_grant.\n let verified;\n try {\n verified = await verifyCode(context.signingKey, code, { origin });\n } catch (error) {\n if (error instanceof AuthorizationCodeError) {\n return tokenError(\"invalid_grant\", 400, \"invalid or expired authorization code\");\n }\n throw error;\n }\n\n const authError = await authenticateClient(\n context.stores.clients,\n credentials,\n verified.clientId,\n );\n if (authError) return authError;\n\n // The redirect_uri MUST match the one the code was bound to (RFC 6749 §4.1.3).\n if (redirectUri !== verified.redirectUri) {\n return tokenError(\"invalid_grant\", 400, \"redirect_uri mismatch\");\n }\n\n // PKCE: the presented verifier must S256-match the bound challenge.\n if (!(await verifyChallenge(codeVerifier, verified.codeChallenge))) {\n return tokenError(\"invalid_grant\", 400, \"PKCE verification failed\");\n }\n\n // Single-use: consume the code's jti; a replay of the same code fails.\n if (!(await context.codeReplay.consume(verified.jti, Date.now()))) {\n return tokenError(\"invalid_grant\", 400, \"authorization code already used\");\n }\n\n return verified;\n}\n\n/** Handle the `authorization_code` grant. */\nasync function handleAuthorizationCode(\n context: McpOauthContext,\n form: URLSearchParams,\n credentials: ClientCredentials,\n origin: string,\n): Promise<Response> {\n const params = readAuthorizationCodeParams(form);\n if (params instanceof Response) return params;\n\n const verified = await redeemCode(context, params, credentials, origin);\n if (verified instanceof Response) return verified;\n\n const scopes = verified.scope.split(/\\s+/).filter(Boolean);\n\n const accessToken = await signAccessToken(context.signingKey, {\n email: verified.email,\n subject: verified.sub,\n scopes,\n origin,\n resourcePath: context.resourcePath,\n ttlSeconds: context.accessTokenTtlSeconds,\n });\n if (!accessToken) {\n // No signing key configured while the surface is on — refuse rather than fall\n // back to a weaker mode (safe-by-default).\n return tokenError(\"invalid_request\", 400, \"token issuance unavailable\");\n }\n\n const refresh = await issueRefreshToken(\n { store: context.stores.refreshTokens, ttlMs: context.refreshTokenTtlMs },\n {\n userEmail: verified.email,\n userSub: verified.sub,\n clientId: verified.clientId,\n scopes,\n },\n );\n\n await recordHostConnection(context, verified.email, verified.clientId);\n\n return tokenSuccess({\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: context.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS,\n refresh_token: refresh.refreshToken,\n scope: refresh.scopes.join(\" \"),\n });\n}\n\n/** Handle the `refresh_token` grant. */\nasync function handleRefreshToken(\n context: McpOauthContext,\n form: URLSearchParams,\n credentials: ClientCredentials,\n origin: string,\n): Promise<Response> {\n const refreshToken = form.get(\"refresh_token\");\n const requestedScope = form.get(\"scope\");\n\n if (!refreshToken) return tokenError(\"invalid_request\", 400, \"missing refresh_token\");\n\n // Authenticate the presenting client (public: client_id present; confidential:\n // secret checked). `authenticateClient` rejects a missing client_id, so on\n // success `credentials.clientId` is non-null and is the identity the rotation is\n // bound to below.\n const authError = await authenticateClient(context.stores.clients, credentials);\n if (authError) return authError;\n const clientId = credentials.clientId as string;\n\n const newScopes = requestedScope ? requestedScope.split(/\\s+/).filter(Boolean) : undefined;\n const refreshContext = {\n store: context.stores.refreshTokens,\n ttlMs: context.refreshTokenTtlMs,\n };\n\n // Rotation enforces client binding (the token's stored clientId must equal the\n // authenticated one, else invalid_grant — OAuth 2.1 §4.3), plus\n // replay-revocation and scope-narrowing.\n let rotated;\n try {\n rotated = await rotateRefreshToken(refreshContext, refreshToken, clientId, newScopes);\n } catch (error) {\n if (error instanceof RefreshTokenError) {\n return tokenError(error.code, 400, error.message);\n }\n throw error;\n }\n\n // The refresh token binds the user's email AND original OAuth `sub`; recover both\n // so the successor access token carries the SAME stable `sub` as the initial\n // token (RFC 6749 §5.1 / OIDC §2), not the email.\n const identity = await getRefreshTokenIdentity(refreshContext, rotated.refreshToken);\n if (!identity) return tokenError(\"invalid_grant\", 400, \"refresh token binding not found\");\n\n const accessToken = await signAccessToken(context.signingKey, {\n email: identity.userEmail,\n subject: identity.userSub,\n scopes: rotated.scopes,\n origin,\n resourcePath: context.resourcePath,\n ttlSeconds: context.accessTokenTtlSeconds,\n });\n if (!accessToken) return tokenError(\"invalid_request\", 400, \"token issuance unavailable\");\n\n await recordHostConnection(context, identity.userEmail, clientId);\n\n return tokenSuccess({\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: context.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS,\n refresh_token: rotated.refreshToken,\n scope: rotated.scopes.join(\" \"),\n });\n}\n\n/**\n * `POST <token>` — the endpoint itself: gate → parse form → dispatch by\n * `grant_type`. Thin on purpose; the flows above are where the invariants live.\n */\nexport async function tokenEndpoint(\n context: McpOauthContext,\n request: Request,\n): Promise<Response> {\n const origin = context.originOf(request);\n\n let form: URLSearchParams;\n try {\n form = new URLSearchParams(await request.text());\n } catch {\n return tokenError(\"invalid_request\", 400, \"malformed request body\");\n }\n\n const grantType = form.get(\"grant_type\");\n if (!grantType) return tokenError(\"invalid_request\", 400, \"missing grant_type\");\n\n const credentials = readClientCredentials(request, form);\n\n switch (grantType) {\n case \"authorization_code\":\n return handleAuthorizationCode(context, form, credentials, origin);\n case \"refresh_token\":\n return handleRefreshToken(context, form, credentials, origin);\n default:\n return tokenError(\n \"unsupported_grant_type\",\n 400,\n `grant_type '${grantType}' is not supported`,\n );\n }\n}\n","import { buildAuthorizationServerMetadata } from \"../auth/authorization-server-metadata\";\nimport { buildProtectedResourceMetadata } from \"../auth/resource-metadata\";\n\nimport { authorizeEndpoint } from \"./authorize\";\nimport { issuer, resourceAudience } from \"./config\";\nimport {\n notFound,\n resolveMcpOauthConfig,\n type McpOauthConfig,\n type McpOauthContext,\n} from \"./context\";\nimport { registerEndpoint, registrationDisabled } from \"./register\";\nimport { tokenEndpoint } from \"./token-grants\";\nimport {\n verifyAccessToken,\n type VerifiedAccessToken,\n type VerifyAccessTokenOptions,\n} from \"./access-token\";\n\n/**\n * The OAuth 2.1 authorization server, as one mount (12-23).\n *\n * `@12-apps/mcp` shipped the OpenAPI→tools generator, the bearer proxy and the two\n * discovery BUILDERS, and held zero authorization logic — which meant every new app\n * still wrote the AS itself: ~1.5k LOC of authorize/token/register plus the code,\n * token, PKCE, rotation and replay machinery under them. All of that is the\n * surface's contract, not a host's, so it lives here.\n *\n * Routes are FRAMEWORK-NEUTRAL descriptors whose handler takes a Fetch `Request`\n * and answers a Fetch `Response`. Unlike the report-builder-shaped surfaces there\n * is no `{ data }` envelope to adapt: an OAuth response is a 302 with a `Location`,\n * a form-encoded exchange answering RFC 6749 §5.1/§5.2 JSON, or an RFC 8414/9728\n * document — shapes fixed by specification that a wrapper would only break. So the\n * adapters are one line each, and a host with a file-per-route layout can export\n * the named handlers directly:\n *\n * export const GET = mcpOauth.handlers.authorize; // app/api/oauth/authorize\n * export const POST = mcpOauth.handlers.token; // app/api/oauth/token\n *\n * What stays the HOST's: the cookie session (`resolveSession`), where the data\n * lives (`stores`), which origins are trusted, the operator gate, and its sign-in\n * path. Everything else is the RFCs'.\n */\n\nexport interface McpOauthRoute {\n method: \"GET\" | \"POST\";\n /** Absolute path from the ORIGIN ROOT — `.well-known/*` cannot live under a prefix. */\n path: string;\n handle(request: Request): Promise<Response>;\n}\n\nexport interface McpOauthHandlers {\n /** `GET` — Authorization Code + PKCE, identity from the session only. */\n authorize: (request: Request) => Promise<Response>;\n /** `POST` — the two grants, form-encoded, RFC 6749 bodies. */\n token: (request: Request) => Promise<Response>;\n /** `POST` — RFC 7591 dynamic client registration (403 when the gate is off). */\n register: (request: Request) => Promise<Response>;\n /** `GET` — the public JWKS (503 while no key is provisioned). */\n jwks: (request: Request) => Promise<Response>;\n /** `GET` — RFC 8414 authorization-server metadata. */\n authorizationServerMetadata: (request: Request) => Promise<Response>;\n /** `GET` — RFC 9728 protected-resource metadata. */\n protectedResourceMetadata: (request: Request) => Promise<Response>;\n}\n\nexport interface ApiMcpOauth {\n /** Every endpoint, in mount order. */\n routes: McpOauthRoute[];\n /** The same handlers by name, for a host whose router is its file tree. */\n handlers: McpOauthHandlers;\n /**\n * Verify a bearer token the way THIS surface mints them — the resource server's\n * half. Bound to the same signing key, resource path and trusted-origin\n * resolution, which is what stops \"minted for origin A, verified against origin\n * B\" from rejecting valid tokens.\n */\n verifyBearer: (\n token: string,\n request: Request,\n options?: Omit<VerifyAccessTokenOptions, \"origin\" | \"resourcePath\">,\n ) => Promise<VerifiedAccessToken>;\n /** The resolved config, for a host that needs the same origin/audience answers. */\n context: McpOauthContext;\n}\n\n/** JSON, with the status and cache policy each document wants. */\nfunction jsonResponse(\n body: unknown,\n status = 200,\n headers: Record<string, string> = {},\n): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json; charset=utf-8\", ...headers },\n });\n}\n\n/**\n * The public key set, or a 503.\n *\n * Safe-by-default: an unprovisioned AS answers 503 rather than an empty or\n * partial key set, so a client never mistakes \"no key yet\" for \"a usable key\".\n */\nasync function jwksResponse(context: McpOauthContext): Promise<Response> {\n const key = await context.signingKey();\n if (!key) return jsonResponse({ error: \"signing_key_unavailable\" }, 503);\n return jsonResponse({ keys: [key.publicJwk] }, 200, {\n // Public, cacheable key set; hosts may cache it and re-fetch on a `kid` miss\n // (rotation). A short max-age keeps the rotation overlap tight.\n \"cache-control\": \"public, max-age=300\",\n });\n}\n\n/** The two discovery documents, built from ONE origin and ONE scope source. */\nfunction discoveryHandlers(\n context: McpOauthContext,\n): Pick<McpOauthHandlers, \"authorizationServerMetadata\" | \"protectedResourceMetadata\"> {\n return {\n authorizationServerMetadata: async (request) =>\n jsonResponse(\n buildAuthorizationServerMetadata({\n issuer: issuer(context.originOf(request)),\n scopesSupported: [...context.scopes],\n // The RESOLVED paths, so what a connector reads before its first request\n // is where the endpoints actually are.\n paths: context.paths,\n }),\n ),\n protectedResourceMetadata: async (request) => {\n const origin = context.originOf(request);\n return jsonResponse(\n buildProtectedResourceMetadata({\n resource: resourceAudience(origin, context.resourcePath),\n authorizationServers: [issuer(origin)],\n scopesSupported: [...context.scopes],\n }),\n );\n },\n };\n}\n\n/**\n * Every endpoint, behind the operator gate.\n *\n * With the gate off the surface is INERT and answers 404, so a probe cannot tell\n * a disabled AS from an app that has none. Registration is the one exception: it\n * answers 403, because RFC 7591 has a code for \"the endpoint is here, but\n * registration is closed\" and the documented static-client path is the answer.\n */\nfunction buildHandlers(context: McpOauthContext): McpOauthHandlers {\n const gated =\n (handler: (request: Request) => Promise<Response>) =>\n async (request: Request): Promise<Response> =>\n context.enabled() ? handler(request) : notFound();\n const discovery = discoveryHandlers(context);\n\n return {\n authorize: gated((request) => authorizeEndpoint(context, request)),\n token: gated((request) => tokenEndpoint(context, request)),\n register: async (request) =>\n context.enabled() ? registerEndpoint(context, request) : registrationDisabled(),\n jwks: gated(() => jwksResponse(context)),\n authorizationServerMetadata: gated(discovery.authorizationServerMetadata),\n protectedResourceMetadata: gated(discovery.protectedResourceMetadata),\n };\n}\n\n/** Mount order, and the paths a host may have moved. */\nfunction buildRoutes(context: McpOauthContext, handlers: McpOauthHandlers): McpOauthRoute[] {\n const { paths } = context;\n return [\n {\n method: \"GET\",\n path: paths.authorizationServerMetadata,\n handle: handlers.authorizationServerMetadata,\n },\n {\n method: \"GET\",\n path: paths.protectedResourceMetadata,\n handle: handlers.protectedResourceMetadata,\n },\n { method: \"GET\", path: paths.jwks, handle: handlers.jwks },\n { method: \"GET\", path: paths.authorize, handle: handlers.authorize },\n { method: \"POST\", path: paths.token, handle: handlers.token },\n { method: \"POST\", path: paths.register, handle: handlers.register },\n ];\n}\n\nexport function createApiMcpOauth(config: McpOauthConfig): ApiMcpOauth {\n const context = resolveMcpOauthConfig(config);\n const handlers = buildHandlers(context);\n\n return {\n routes: buildRoutes(context, handlers),\n handlers,\n verifyBearer: (token, request, options) =>\n verifyAccessToken(context.signingKey, token, {\n ...options,\n origin: context.originOf(request),\n resourcePath: context.resourcePath,\n }),\n context,\n };\n}\n"],"mappings":"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","names":["SignJWT","jwtVerify","importJWK","CLOCK_TOLERANCE_SECONDS","nowSeconds","SignJWT","jwtVerify","importJWK","createHash","randomBytes","createHash","randomBytes","DEFAULT_GRANT_TYPES","createHash","JSON_HEADERS","createHash"]}
@@ -0,0 +1,63 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-7QVYU63E.js";
4
+
5
+ // src/auth/resource-metadata.ts
6
+ function buildProtectedResourceMetadata(input) {
7
+ return {
8
+ resource: input.resource,
9
+ authorization_servers: input.authorizationServers,
10
+ // MCP clients present the token in the Authorization header only.
11
+ bearer_methods_supported: ["header"],
12
+ ...input.scopesSupported ? { scopes_supported: input.scopesSupported } : {},
13
+ ...input.resourceDocumentation ? { resource_documentation: input.resourceDocumentation } : {}
14
+ };
15
+ }
16
+ __name(buildProtectedResourceMetadata, "buildProtectedResourceMetadata");
17
+ function bearerChallenge(params) {
18
+ const parts = [`Bearer resource_metadata="${params.resourceMetadataUrl}"`];
19
+ if (params.error) parts.push(`error="${params.error}"`);
20
+ if (params.errorDescription) {
21
+ parts.push(`error_description="${params.errorDescription}"`);
22
+ }
23
+ return parts.join(", ");
24
+ }
25
+ __name(bearerChallenge, "bearerChallenge");
26
+ var PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
27
+
28
+ // src/auth/authorization-server-metadata.ts
29
+ var DEFAULT_PATHS = {
30
+ authorize: "/api/oauth/authorize",
31
+ token: "/api/oauth/token",
32
+ register: "/api/oauth/register",
33
+ jwks: "/.well-known/jwks.json"
34
+ };
35
+ var DEFAULT_TOKEN_ENDPOINT_AUTH_METHODS = [
36
+ "none",
37
+ "client_secret_basic"
38
+ ];
39
+ function buildAuthorizationServerMetadata(input) {
40
+ const origin = input.issuer;
41
+ const paths = { ...DEFAULT_PATHS, ...input.paths };
42
+ return {
43
+ issuer: origin,
44
+ authorization_endpoint: `${origin}${paths.authorize}`,
45
+ token_endpoint: `${origin}${paths.token}`,
46
+ registration_endpoint: `${origin}${paths.register}`,
47
+ jwks_uri: `${origin}${paths.jwks}`,
48
+ scopes_supported: input.scopesSupported,
49
+ response_types_supported: ["code"],
50
+ grant_types_supported: ["authorization_code", "refresh_token"],
51
+ code_challenge_methods_supported: ["S256"],
52
+ token_endpoint_auth_methods_supported: input.tokenEndpointAuthMethods ?? [...DEFAULT_TOKEN_ENDPOINT_AUTH_METHODS]
53
+ };
54
+ }
55
+ __name(buildAuthorizationServerMetadata, "buildAuthorizationServerMetadata");
56
+
57
+ export {
58
+ buildProtectedResourceMetadata,
59
+ bearerChallenge,
60
+ PROTECTED_RESOURCE_METADATA_PATH,
61
+ buildAuthorizationServerMetadata
62
+ };
63
+ //# sourceMappingURL=chunk-WJJNKKNS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/auth/resource-metadata.ts","../src/auth/authorization-server-metadata.ts"],"sourcesContent":["/**\n * OAuth 2.0 Protected Resource Metadata (RFC 9728), as required by the MCP\n * authorization spec: the MCP endpoint is an OAuth *resource server*. Agent hosts\n * (Claude.ai / ChatGPT connectors) discover where to obtain a token by reading\n * `/.well-known/oauth-protected-resource`, and on a 401 the resource server points\n * them at that document via a `WWW-Authenticate` challenge.\n *\n * This module only builds the discovery documents/headers — validating the\n * resulting access token is the app's job (the {@link import(\"../types\").AuthResolver}),\n * because it depends on the app's authorization server and key material.\n */\n\nexport interface ProtectedResourceMetadataInput {\n /** Canonical resource identifier — the MCP endpoint URL (the token audience). */\n resource: string;\n /** Authorization server issuer URLs that can mint tokens for this resource. */\n authorizationServers: string[];\n /** Scopes the resource server understands (advertised to clients). */\n scopesSupported?: string[];\n /** Human-facing docs URL for the protected resource, if any. */\n resourceDocumentation?: string;\n}\n\n/** The RFC 9728 metadata document served at `/.well-known/oauth-protected-resource`. */\nexport interface ProtectedResourceMetadata {\n resource: string;\n authorization_servers: string[];\n bearer_methods_supported: string[];\n scopes_supported?: string[];\n resource_documentation?: string;\n}\n\nexport function buildProtectedResourceMetadata(\n input: ProtectedResourceMetadataInput,\n): ProtectedResourceMetadata {\n return {\n resource: input.resource,\n authorization_servers: input.authorizationServers,\n // MCP clients present the token in the Authorization header only.\n bearer_methods_supported: [\"header\"],\n ...(input.scopesSupported ? { scopes_supported: input.scopesSupported } : {}),\n ...(input.resourceDocumentation\n ? { resource_documentation: input.resourceDocumentation }\n : {}),\n };\n}\n\n/**\n * Build the `WWW-Authenticate` value for an unauthorized MCP response, pointing\n * the client at the protected-resource metadata so it can start the OAuth flow.\n * Per RFC 9728 §5.1 the challenge carries a `resource_metadata` parameter.\n */\nexport function bearerChallenge(params: {\n resourceMetadataUrl: string;\n error?: \"invalid_token\" | \"insufficient_scope\";\n errorDescription?: string;\n}): string {\n const parts = [`Bearer resource_metadata=\"${params.resourceMetadataUrl}\"`];\n if (params.error) parts.push(`error=\"${params.error}\"`);\n if (params.errorDescription) {\n parts.push(`error_description=\"${params.errorDescription}\"`);\n }\n return parts.join(\", \");\n}\n\n/** Standard path for the protected-resource metadata document. */\nexport const PROTECTED_RESOURCE_METADATA_PATH =\n \"/.well-known/oauth-protected-resource\";\n","/**\n * OAuth 2.0 Authorization Server Metadata (RFC 8414), the discovery half that\n * complements the RFC 9728 protected-resource metadata in `resource-metadata.ts`.\n * Agent hosts (Claude.ai / ChatGPT connectors) read\n * `/.well-known/oauth-authorization-server` to learn where to start the OAuth\n * 2.1 Authorization Code + PKCE flow.\n *\n * This builder is a pure function of `(issuer, scopes)` with no Next.js/request\n * coupling, so it can move verbatim into the future `@12-apps/mcp` extraction.\n * It derives every endpoint from the same issuer origin the resource metadata\n * advertises, so the two discovery documents cannot drift.\n */\n\nexport interface AuthorizationServerMetadataInput {\n /** Authorization server issuer URL (origin) — also the resource issuer. */\n issuer: string;\n /** Scopes the authorization server advertises (from the shared scope source). */\n scopesSupported: string[];\n /**\n * Client authentication methods the token endpoint accepts. Defaults to\n * public PKCE clients (`none`) plus HTTP Basic client-secret auth.\n */\n tokenEndpointAuthMethods?: string[];\n /**\n * Where the endpoints are actually mounted, if not at the defaults below. A\n * host that moves an endpoint MUST move it here too: this document is the only\n * thing a connector reads before its first request, so a path that lies here is\n * a flow that fails at the first hop (12-23 — `createApiMcpOauth` passes its\n * resolved paths, so the two cannot disagree).\n */\n paths?: Partial<AuthorizationServerPaths>;\n}\n\n/** The endpoint paths this document advertises, relative to the issuer origin. */\nexport interface AuthorizationServerPaths {\n authorize: string;\n token: string;\n register: string;\n jwks: string;\n}\n\n/** The RFC 8414 document served at `/.well-known/oauth-authorization-server`. */\nexport interface AuthorizationServerMetadata {\n issuer: string;\n authorization_endpoint: string;\n token_endpoint: string;\n registration_endpoint: string;\n jwks_uri: string;\n scopes_supported: string[];\n response_types_supported: string[];\n grant_types_supported: string[];\n code_challenge_methods_supported: string[];\n token_endpoint_auth_methods_supported: string[];\n}\n\nconst DEFAULT_PATHS: AuthorizationServerPaths = {\n authorize: \"/api/oauth/authorize\",\n token: \"/api/oauth/token\",\n register: \"/api/oauth/register\",\n jwks: \"/.well-known/jwks.json\",\n};\n\nconst DEFAULT_TOKEN_ENDPOINT_AUTH_METHODS = [\n \"none\",\n \"client_secret_basic\",\n] as const;\n\n/**\n * Build the RFC 8414 authorization-server metadata document from an issuer\n * origin and the supported scopes. Endpoints are derived from `issuer`; the\n * OAuth 2.1 + PKCE contract fixes `response_types_supported`,\n * `grant_types_supported`, and `code_challenge_methods_supported`.\n */\nexport function buildAuthorizationServerMetadata(\n input: AuthorizationServerMetadataInput,\n): AuthorizationServerMetadata {\n const origin = input.issuer;\n const paths = { ...DEFAULT_PATHS, ...input.paths };\n return {\n issuer: origin,\n authorization_endpoint: `${origin}${paths.authorize}`,\n token_endpoint: `${origin}${paths.token}`,\n registration_endpoint: `${origin}${paths.register}`,\n jwks_uri: `${origin}${paths.jwks}`,\n scopes_supported: input.scopesSupported,\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported:\n input.tokenEndpointAuthMethods ?? [...DEFAULT_TOKEN_ENDPOINT_AUTH_METHODS],\n };\n}\n"],"mappings":";;;;;AAgCO,SAAS,+BACd,OAC2B;AAC3B,SAAO;AAAA,IACL,UAAU,MAAM;AAAA,IAChB,uBAAuB,MAAM;AAAA;AAAA,IAE7B,0BAA0B,CAAC,QAAQ;AAAA,IACnC,GAAI,MAAM,kBAAkB,EAAE,kBAAkB,MAAM,gBAAgB,IAAI,CAAC;AAAA,IAC3E,GAAI,MAAM,wBACN,EAAE,wBAAwB,MAAM,sBAAsB,IACtD,CAAC;AAAA,EACP;AACF;AAbgB;AAoBT,SAAS,gBAAgB,QAIrB;AACT,QAAM,QAAQ,CAAC,6BAA6B,OAAO,mBAAmB,GAAG;AACzE,MAAI,OAAO,MAAO,OAAM,KAAK,UAAU,OAAO,KAAK,GAAG;AACtD,MAAI,OAAO,kBAAkB;AAC3B,UAAM,KAAK,sBAAsB,OAAO,gBAAgB,GAAG;AAAA,EAC7D;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAXgB;AAcT,IAAM,mCACX;;;ACZF,IAAM,gBAA0C;AAAA,EAC9C,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV,MAAM;AACR;AAEA,IAAM,sCAAsC;AAAA,EAC1C;AAAA,EACA;AACF;AAQO,SAAS,iCACd,OAC6B;AAC7B,QAAM,SAAS,MAAM;AACrB,QAAM,QAAQ,EAAE,GAAG,eAAe,GAAG,MAAM,MAAM;AACjD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,wBAAwB,GAAG,MAAM,GAAG,MAAM,SAAS;AAAA,IACnD,gBAAgB,GAAG,MAAM,GAAG,MAAM,KAAK;AAAA,IACvC,uBAAuB,GAAG,MAAM,GAAG,MAAM,QAAQ;AAAA,IACjD,UAAU,GAAG,MAAM,GAAG,MAAM,IAAI;AAAA,IAChC,kBAAkB,MAAM;AAAA,IACxB,0BAA0B,CAAC,MAAM;AAAA,IACjC,uBAAuB,CAAC,sBAAsB,eAAe;AAAA,IAC7D,kCAAkC,CAAC,MAAM;AAAA,IACzC,uCACE,MAAM,4BAA4B,CAAC,GAAG,mCAAmC;AAAA,EAC7E;AACF;AAlBgB;","names":[]}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The route-METHOD half of the surface scan (12-23) — what `mcp:coverage` needs
3
+ * on top of what `rbac:coverage` already ships.
4
+ *
5
+ * The WALK is imported from `@12-apps/rbac/coverage` rather than copied — the file
6
+ * walk (`walkRouteFiles`), the URL mapping (`urlPathOf`) AND the export-head
7
+ * parser (`exportedNamesOf`) — and that is deliberate: both gates assert a
8
+ * COMPLETENESS property over the same two surfaces (`app/**` route files and
9
+ * `*actions.ts` modules), and the origin host's own comment on the shared scanner says
10
+ * why they must share it — "so the two gates can never disagree about what the
11
+ * surface is". Two copies would agree on the day they were written and drift
12
+ * silently after, in the direction of not looking. What is left here is the one
13
+ * thing that genuinely differs: the GRAMMAR (see {@link exportedMethodsOf}).
14
+ *
15
+ * THE SCAN ROOT IS THE WHOLE `app` FOLDER, never `app/api`: a completeness gate
16
+ * rooted below the surface it claims to cover does not fail when it misses
17
+ * something, it simply never looks. Three OAuth/JWKS discovery routes shipped
18
+ * unregistered for exactly as long as the walk was rooted at `app/api`.
19
+ *
20
+ * Detection is over SOURCE, with no TS compiler: fast, dependency-free, and it
21
+ * matches how the framework itself keys routes off file paths plus exported names.
22
+ */
23
+ /** Every method a route file can serve — the scan must see them all. */
24
+ declare const HTTP_METHODS: readonly ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
25
+ /** One exported HTTP handler discovered on a route file. */
26
+ interface RouteMethod {
27
+ /** URL path with `[param]` → `{param}` (e.g. `/api/checkout/{id}`). */
28
+ urlPath: string;
29
+ /** The HTTP method exported (GET/POST/…). */
30
+ method: string;
31
+ /** The route file, relative to the web root. */
32
+ file: string;
33
+ }
34
+ /**
35
+ * Exported HTTP methods, across every form the app router serves:
36
+ * `export const GET`, `export function GET`, `export async function GET`,
37
+ * `export const { GET, POST } = handlers`, `export { handler as GET }`. For brace
38
+ * lists the exported name is the last identifier of each item (after `as`, or
39
+ * after `:` for destructuring renames). `export type { … }` never matches — a type
40
+ * is never a handler.
41
+ *
42
+ * The two grammar knobs are the whole difference from `exportedActionsOf`, and both
43
+ * are load-bearing: a route handler may be a SYNC `export function` (a server
44
+ * action may not — it must be async), and only the seven HTTP methods count, where
45
+ * every runtime export of a use-server module is an action.
46
+ *
47
+ * The shared walk is a linear hand-parse, not a regex: the `\s+`-joined patterns
48
+ * this gate first shipped with backtracked polynomially on adversarial input
49
+ * (CodeQL js/polynomial-redos), and a COMPLETENESS gate must stay O(n) on whatever
50
+ * source it is pointed at — it is run over files a contributor supplies.
51
+ */
52
+ declare function exportedMethodsOf(source: string): string[];
53
+ /** Every exported HTTP handler across all route files under `appDir`. */
54
+ declare function collectRouteMethods(appDir: string, webRoot: string): RouteMethod[];
55
+
56
+ /**
57
+ * `@12-apps/mcp/coverage` — the MCP route/action coverage gate (12-23), moved out
58
+ * of the origin host's `apps/web/scripts/mcp/coverage.ts` so a host's own script is a
59
+ * one-line re-export and the CI workflow that shells out to the consumer's
60
+ * `mcp:coverage` package script (`12-apps/ci`'s `mcp-contract.yml`) keeps working
61
+ * unchanged.
62
+ *
63
+ * `mcp:check` only proves the REGISTRY matches the committed manifest; nothing
64
+ * stops a new route file, or a new server action, from shipping outside the
65
+ * agent-exposable surface. This gate closes both:
66
+ *
67
+ * 1. **Route coverage** — every HTTP method exported by a route file must be
68
+ * registered in the host's MCP registry (or its path listed under `routes` in
69
+ * the exclusions file), and every registry entry must map back to a real route
70
+ * file exporting that method. A tool the manifest advertises but no route
71
+ * serves is a promise an agent cannot cash.
72
+ * 2. **Action coverage** — every exported server action must be mapped to a
73
+ * registry operationId in the action map, or listed under `actions` in the
74
+ * exclusions file with a reason. New actions fail until mapped; stale entries
75
+ * fail until pruned, so neither file can rot.
76
+ *
77
+ * The exclusions file is the ONLY escape hatch, and keeping it a separate,
78
+ * human-protected file is the point: an agent cannot silently exclude a new
79
+ * route/action — it has to justify to a human why the capability is not exposed.
80
+ */
81
+ /** One registry entry, as the host's MCP registry describes an endpoint. */
82
+ interface McpRegistryEndpoint {
83
+ method: string;
84
+ /** URL path in `{param}` form — the same shape the scan produces. */
85
+ path: string;
86
+ operationId: string;
87
+ }
88
+ /** The protected exclusions file: every deliberate gate escape hatch. */
89
+ interface McpCoverageExclusions {
90
+ /** Server actions kept off the surface, name → reason. */
91
+ actions: Record<string, string>;
92
+ /** Route path prefixes kept off the surface, prefix → reason. */
93
+ routes: Record<string, string>;
94
+ }
95
+ /** The action map: server action name → registry operationId. */
96
+ interface McpActionMap {
97
+ mapped: Record<string, string>;
98
+ }
99
+ interface McpCoverageOptions {
100
+ /** The framework routes folder (the WHOLE `app`, never `app/api`). */
101
+ appDir: string;
102
+ /** Root for relative paths in failure messages. Default: `appDir`. */
103
+ webRoot?: string;
104
+ /** The host's registry entries (its `endpoints` array). */
105
+ endpoints: readonly McpRegistryEndpoint[];
106
+ /** Path to the exclusions JSON ({@link McpCoverageExclusions}). */
107
+ exclusionsPath: string;
108
+ /**
109
+ * Path to the action-map JSON ({@link McpActionMap}). Omit for a host with no
110
+ * server actions at all — action coverage is then vacuous rather than a crash on
111
+ * a file that was never written.
112
+ */
113
+ actionMapPath?: string;
114
+ }
115
+ interface McpCoverageResult {
116
+ failures: string[];
117
+ routeMethodCount: number;
118
+ actionCount: number;
119
+ }
120
+ /** Run the gate and return every violation (empty = green). */
121
+ declare function runMcpCoverage(options: McpCoverageOptions): McpCoverageResult;
122
+ /**
123
+ * The CLI face: print the verdict and exit non-zero on violations. A host's
124
+ * `scripts/mcp/coverage.ts` is then one import + one call, and the CI workflow that
125
+ * runs `pnpm mcp:coverage` needs no change at all.
126
+ */
127
+ declare function mcpCoverageCli(options: McpCoverageOptions): void;
128
+
129
+ export { HTTP_METHODS, type McpActionMap, type McpCoverageExclusions, type McpCoverageOptions, type McpCoverageResult, type McpRegistryEndpoint, type RouteMethod, collectRouteMethods, exportedMethodsOf, mcpCoverageCli, runMcpCoverage };