@12-apps/mcp 3.15.0 → 3.17.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.
Files changed (53) hide show
  1. package/ADOPTING.md +41 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-VDD4YRNP.js → chunk-EANHJLDH.js} +13 -4
  4. package/dist/chunk-EANHJLDH.js.map +1 -0
  5. package/dist/chunk-F3LMK6OL.js +25 -0
  6. package/dist/chunk-F3LMK6OL.js.map +1 -0
  7. package/dist/{chunk-UIILEGAC.js → chunk-WCZC4TPX.js} +193 -48
  8. package/dist/chunk-WCZC4TPX.js.map +1 -0
  9. package/dist/{create-api-mcp-oauth-CsC0jlH7.d.ts → create-api-mcp-oauth-BEvYLRBV.d.ts} +96 -4
  10. package/dist/e2e/index.d.ts +109 -0
  11. package/dist/e2e/index.js +27 -0
  12. package/dist/e2e/index.js.map +1 -0
  13. package/dist/e2e/steps/journey.steps.d.ts +2 -0
  14. package/dist/e2e/steps/journey.steps.js +79 -0
  15. package/dist/e2e/steps/journey.steps.js.map +1 -0
  16. package/dist/{guide-KQNcXlMG.d.ts → guide-CrzdsdNf.d.ts} +1 -1
  17. package/dist/hono/index.d.ts +1 -1
  18. package/dist/hono/index.js +1 -1
  19. package/dist/index.d.ts +102 -4
  20. package/dist/index.js +71 -6
  21. package/dist/index.js.map +1 -1
  22. package/dist/{locales-eKE_OJw4.d.ts → locales-Cv0Pecvu.d.ts} +1 -1
  23. package/dist/manifest/index.d.ts +29 -7
  24. package/dist/manifest/index.js +2 -1
  25. package/dist/manifest/index.js.map +1 -1
  26. package/dist/manifest/server.d.ts +1 -1
  27. package/dist/manifest/server.js +2 -2
  28. package/dist/oauth/index.d.ts +19 -4
  29. package/dist/oauth/index.js +4 -2
  30. package/dist/react/index.d.ts +3 -3
  31. package/features/ai-connect.feature +46 -0
  32. package/package.json +25 -8
  33. package/prisma/mcp.prisma +10 -0
  34. package/prisma/migrations/20260910120000_add_refresh_grace_seal/migration.sql +37 -0
  35. package/src/e2e/globs.ts +70 -0
  36. package/src/e2e/index.ts +16 -0
  37. package/src/e2e/steps/journey.steps.ts +136 -0
  38. package/src/e2e/world.ts +84 -0
  39. package/src/index.ts +11 -0
  40. package/src/manifest/index.ts +24 -7
  41. package/src/oauth/access-token.ts +72 -10
  42. package/src/oauth/context.ts +20 -0
  43. package/src/oauth/index.ts +2 -0
  44. package/src/oauth/prisma-stores.ts +16 -5
  45. package/src/oauth/refresh-lineage.ts +77 -0
  46. package/src/oauth/refresh.ts +169 -82
  47. package/src/oauth/rotation-grace.ts +216 -0
  48. package/src/oauth/stores.ts +45 -1
  49. package/src/oauth/token-grants.ts +4 -1
  50. package/src/server/auth-failure.ts +145 -0
  51. package/src/server/jsonrpc.ts +44 -5
  52. package/dist/chunk-UIILEGAC.js.map +0 -1
  53. package/dist/chunk-VDD4YRNP.js.map +0 -1
@@ -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/rotation-grace.ts","../src/oauth/refresh.ts","../src/oauth/refresh-lineage.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/**\n * WHY verification failed, at the granularity an operator and an agent can act on.\n *\n * `code` above is the RFC 6750 challenge and there are only three of those, so it\n * cannot tell \"your connection lapsed, refresh it\" from \"this token is not for\n * this server\". That distinction is the whole difference between an assistant\n * that tells its user to reconnect this server and one that reports a generic\n * failure on every tool call, so it is carried alongside rather than folded\n * into `code`.\n *\n * `unverified` stays deliberately COARSE. Signature, issuer and audience collapse\n * into it because naming which one failed is an oracle for the next attempt.\n * Expiry is the documented exception — RFC 6750 names it in `error_description`\n * precisely because a client must be told to refresh — and it leaks nothing: a\n * token's `exp` is readable by whoever holds the token.\n */\nexport type AccessTokenFailureReason =\n /** Valid in every other respect, but `exp` has passed. Refresh, do not re-consent. */\n | \"expired\"\n /** Signature, issuer or audience did not hold. Deliberately not narrowed further. */\n | \"unverified\"\n /** Verified, but missing the `sub`/`email` the identity is built from. */\n | \"incomplete\"\n /** No signing key is provisioned, so nothing can verify. An operator problem. */\n | \"not_provisioned\"\n /** A valid token that simply lacks the scope this call needs. */\n | \"insufficient_scope\";\n\n/**\n * A typed verification failure — `code` drives the `WWW-Authenticate` challenge,\n * {@link AccessTokenError.reason} drives what the caller is actually told.\n */\nexport class AccessTokenError extends Error {\n readonly code: AccessTokenErrorCode;\n\n readonly reason: AccessTokenFailureReason;\n\n constructor(code: AccessTokenErrorCode, reason: AccessTokenFailureReason, message?: string) {\n super(message ?? reason);\n this.name = \"AccessTokenError\";\n this.code = code;\n this.reason = reason;\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/** jose's code for a token that parsed and verified but whose `exp` has passed. */\nconst JWT_EXPIRED_CODE = \"ERR_JWT_EXPIRED\";\n\n/** Whether a thrown value is jose's expiry error, by its stable `code`. */\nfunction isExpiry(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n (error as { code?: unknown }).code === JWT_EXPIRED_CODE\n );\n}\n\n/**\n * The cryptographic half: signature, `iss`, `aud`, `exp`.\n *\n * Bad signature, wrong issuer, wrong audience, malformed token and unknown key\n * all collapse into ONE opaque `unverified`. A message naming the failed claim\n * would be an oracle for the next attempt.\n *\n * EXPIRY is separated out, and only expiry. It is the one failure a\n * well-behaved client is supposed to act on — refresh and retry — and it is the\n * one the RFC gives a description for, so collapsing it left every lapsed\n * connection indistinguishable from a broken one. It is not an oracle either:\n * `exp` is a readable claim of a token the caller already holds.\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) {\n throw new AccessTokenError(\"invalid_token\", \"not_provisioned\", \"no signing key configured\");\n }\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 (error) {\n if (isExpiry(error)) {\n throw new AccessTokenError(\"invalid_token\", \"expired\", \"access token expired\");\n }\n throw new AccessTokenError(\"invalid_token\", \"unverified\", \"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(\n \"invalid_token\",\n \"incomplete\",\n \"missing subject or email claim\",\n );\n }\n\n const scopes = parseScopes(payload.scope);\n if (options.requiredScope && !scopes.includes(options.requiredScope)) {\n throw new AccessTokenError(\n \"insufficient_scope\",\n \"insufficient_scope\",\n `token lacks required scope '${options.requiredScope}'`,\n );\n }\n\n return { email, subject, scopes };\n}\n","import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from \"node:crypto\";\n\n/**\n * The sealed successor that makes refresh rotation IDEMPOTENT for the length of a\n * grace window — the half of `refresh.ts` that lets a legitimate client survive a\n * lost response or a concurrent refresh without losing its session.\n *\n * ## The problem this exists to solve\n *\n * Rotation-on-use plus replay revocation is the OAuth 2.1 rule, and it is right:\n * a stolen refresh token is detected the moment BOTH the thief and the rightful\n * client use it, and the whole lineage dies. What that rule cannot tell apart is\n * a thief from a client that used its token twice for an innocent reason, and\n * there are two of those, both routine:\n *\n * - **the lost response.** The client rotates, the 200 never arrives (a proxy\n * timeout, a dropped connection), and it retries with the only token it\n * still has — the one the server already consumed.\n * - **the concurrent refresh.** Two of the client's own sessions notice an\n * expired access token at the same moment and both refresh.\n *\n * Without a grace window both are punished as theft: the lineage is revoked,\n * INCLUDING the successor just handed to whoever won, and the connection is dead\n * until a human re-runs the whole authorization flow. That is the failure this\n * module removes.\n *\n * ## Why a SEALED successor rather than a second one\n *\n * The obvious shortcut — mint a fresh successor for every in-window reuse — is\n * the one thing that must not happen. It leaves one parent with two live\n * successors and two independently rotating families, which is precisely the\n * state replay protection exists to prevent: an attacker holding a stolen token\n * would only have to fire it alongside the real client to walk away with a\n * family of its own. So the window returns *the same* successor to every caller\n * that presents the parent. Reuse becomes idempotent instead of forgiven, and\n * exactly one successor is ever written.\n *\n * ## What detection this actually costs — stated plainly\n *\n * It would be convenient to say detection is merely DEFERRED by one rotation.\n * It is not, and the code does not provide that. Two parties left holding one\n * successor take this same path again at the next rotation, and again after\n * that: whichever of them arrives second is inside a fresh window each time, so\n * they stay in lockstep indefinitely. The honest guarantee is narrower:\n *\n * **a collision is detected only when the two uses fall more than the window\n * apart.**\n *\n * A thief who replays a freshly stolen token within the window of the real\n * client's rotation is handed a live token and raises no signal — and that\n * timing is precisely what the window exists to forgive, so it cannot be\n * distinguished. This is the accepted cost, and it is why the window is short\n * by default, why it is configurable, and why `0` restores the strict rule for\n * a deployment that would rather pay in re-authentications.\n *\n * ## Why the key is derived from the parent, and nothing is stored in the clear\n *\n * Returning the same successor means recovering its plaintext, and the plaintext\n * is exactly what `refresh.ts` promises never to persist. So it is not persisted:\n * it is sealed under a key derived by HKDF from the PARENT's own plaintext, and\n * only the sealed blob reaches the store. The consequences are the point:\n *\n * - the database alone cannot open it. The parent's plaintext is never stored\n * either, so a dump of the tokens table yields ciphertext and no key — the\n * \"hashed, never plaintext\" invariant is unchanged;\n * - the only party that CAN open it is a caller presenting the parent, which is\n * the caller we mean to serve. It grants no capability that party lacks: it\n * already held the parent, and the parent is what mints the successor;\n * - the grace deadline is sealed INSIDE the blob rather than kept in a column,\n * so an attacker with write access to the row cannot extend the window\n * without also being able to forge the AES-GCM tag.\n *\n * One honest limit on that last point. The deadline is enforced by the server\n * when it opens a seal, not by the ciphertext, and a seal is cleared when its\n * token is consumed or revoked — not when its window lapses. So the ONE hop an\n * attacker holding a spent parent plaintext plus a table read can take is bounded\n * by when the successor is next used, which for an idle connection is the refresh\n * token's TTL rather than `graceMs`. Bounded to one hop either way, because every\n * consume and every revoke clears the parent's seal; sweeping lapsed seals would\n * tighten it to the window itself.\n */\n\n/** AEAD, so a tampered blob fails to open rather than decrypting to garbage. */\nconst ALGORITHM = \"aes-256-gcm\";\n\n/** 96-bit nonce — the size AES-GCM is specified for. */\nconst IV_BYTES = 12;\n\n/** AES-256. */\nconst KEY_BYTES = 32;\n\n/** GCM authentication tag length in bytes. */\nconst TAG_BYTES = 16;\n\n/** Domain separation for the HKDF expansion, so this key is only ever this key. */\nconst HKDF_INFO = \"12-apps/mcp:refresh-rotation-grace:v1\";\n\n/** Version prefix, so a future format change is recognisable rather than corrupt. */\nconst SEAL_VERSION = \"v1\";\n\n/** How long a just-rotated token keeps answering with its successor. */\nexport const DEFAULT_ROTATION_GRACE_MS = 30_000;\n\n/**\n * What a successfully opened seal yields.\n *\n * Not exported: `refresh.ts` is the only caller and reads it through inference,\n * so exporting it would only widen the package's public surface with a name\n * nobody imports.\n */\ninterface OpenedSuccessor {\n /** The successor's opaque plaintext — the token to hand back. */\n successor: string;\n /** Epoch milliseconds after which the seal must be refused. */\n graceUntil: number;\n}\n\n/**\n * Derive the sealing key from the parent's plaintext.\n *\n * No salt: an opaque refresh token is already 256 bits of CSPRNG output, so HKDF\n * is used here for domain separation and length adjustment rather than to\n * concentrate entropy that is not there.\n */\nfunction sealingKey(parentPlaintext: string): Buffer {\n const derived = hkdfSync(\n \"sha256\",\n Buffer.from(parentPlaintext, \"utf8\"),\n Buffer.alloc(0),\n Buffer.from(HKDF_INFO, \"utf8\"),\n KEY_BYTES,\n );\n return Buffer.from(derived);\n}\n\n/** base64url without padding, so the blob is safe in any column or URL. */\nfunction encode(value: Buffer): string {\n return value.toString(\"base64url\");\n}\n\n/**\n * Seal `successorPlaintext` so that only a caller holding `parentPlaintext` can\n * recover it, carrying `graceUntil` inside the sealed blob.\n *\n * The deadline is DATA here, not enforcement: {@link openSuccessor} returns it\n * rather than acting on it, and the caller (`refresh.ts`) is what refuses a\n * lapsed one. Sealing it inside the AEAD blob is what stops it being edited in\n * the row; it is not a claim that the ciphertext stops opening on its own. A\n * seal therefore stays openable-by-its-parent until the row is consumed or\n * revoked, which for an idle connection is the token's TTL rather than the\n * window — see the note in the module docblock.\n */\nexport function sealSuccessor(\n parentPlaintext: string,\n successorPlaintext: string,\n graceUntil: number,\n): string {\n const iv = randomBytes(IV_BYTES);\n const cipher = createCipheriv(ALGORITHM, sealingKey(parentPlaintext), iv);\n const payload = JSON.stringify({ successor: successorPlaintext, graceUntil });\n const sealed = Buffer.concat([cipher.update(payload, \"utf8\"), cipher.final()]);\n return [SEAL_VERSION, encode(iv), encode(cipher.getAuthTag()), encode(sealed)].join(\".\");\n}\n\n/** Parse the four-part wire form, or `null` when it is not one. */\nfunction parts(seal: string): { iv: Buffer; tag: Buffer; body: Buffer } | null {\n const segments = seal.split(\".\");\n if (segments.length !== 4) return null;\n const [version, iv, tag, body] = segments;\n if (version !== SEAL_VERSION) return null;\n\n const decoded = {\n iv: Buffer.from(iv ?? \"\", \"base64url\"),\n tag: Buffer.from(tag ?? \"\", \"base64url\"),\n body: Buffer.from(body ?? \"\", \"base64url\"),\n };\n // Lengths are fixed by the algorithm; a wrong one is a malformed blob, and\n // `createDecipheriv` would throw on it rather than return.\n if (decoded.iv.length !== IV_BYTES || decoded.tag.length !== TAG_BYTES) return null;\n return decoded;\n}\n\n/**\n * Open a seal with the parent's plaintext.\n *\n * `null` for every failure — a wrong parent, a tampered or truncated blob, an\n * unknown version, a payload that is not the expected shape. The caller treats\n * `null` as \"no grace applies\" and falls through to the replay rule, so a\n * failure here is never the difference between secure and insecure; it only\n * costs the client its retry.\n */\nexport function openSuccessor(parentPlaintext: string, seal: string): OpenedSuccessor | null {\n const parsed = parts(seal);\n if (!parsed) return null;\n\n try {\n const decipher = createDecipheriv(ALGORITHM, sealingKey(parentPlaintext), parsed.iv);\n decipher.setAuthTag(parsed.tag);\n const opened = Buffer.concat([decipher.update(parsed.body), decipher.final()]);\n const payload: unknown = JSON.parse(opened.toString(\"utf8\"));\n return readPayload(payload);\n } catch {\n // A wrong key fails the GCM tag check, which throws. That is the expected\n // path for \"this is not the parent that sealed it\", not an error to report.\n return null;\n }\n}\n\n/** Narrow the decrypted JSON to {@link OpenedSuccessor}, or `null`. */\nfunction readPayload(payload: unknown): OpenedSuccessor | null {\n if (payload === null || typeof payload !== \"object\") return null;\n const { successor, graceUntil } = payload as Record<string, unknown>;\n if (typeof successor !== \"string\" || successor === \"\") return null;\n if (typeof graceUntil !== \"number\" || !Number.isFinite(graceUntil)) return null;\n return { successor, graceUntil };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\nimport {\n DEFAULT_ROTATION_GRACE_MS,\n openSuccessor,\n sealSuccessor,\n} from \"./rotation-grace\";\nimport { revokeLineage } from \"./refresh-lineage\";\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 OUTSIDE the grace window is a\n * REPLAY: rejected, AND the whole lineage (every ancestor + descendant\n * reachable through `rotatedFrom`) is revoked — the OAuth 2.1 refresh-token\n * replay rule;\n * - reuse INSIDE the window is a RETRY, and answers with the successor that\n * rotation already minted rather than a second one (`./rotation-grace.ts`).\n * A lost response and two concurrent refreshes are the routine reasons one\n * client uses one token twice, and punishing them as theft is what cost a\n * connected user their session and sent them back through the whole\n * authorization flow;\n * - CONCURRENT reuse takes that same retry path. The store's `rotate` is a\n * claim-once write, so of two simultaneous rotations of one parent exactly\n * one successor is ever WRITTEN — that invariant is untouched, and without it\n * replay protection would be bypassable by WINNING a race instead of arriving\n * second (see `RefreshTokenStore.rotate`). The loser is now handed the\n * winner's token instead of destroying it;\n * - the cost is real and is NOT a one-rotation deferral: two parties left\n * holding one successor take the retry path again at every subsequent\n * rotation, so they stay in lockstep for as long as their uses keep falling\n * inside the window. What survives is: a collision is detected only when the\n * two uses fall more than `graceMs` apart. `./rotation-grace.ts` argues why\n * that is the accepted trade and `graceMs: 0` is the way back out;\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 * How long a just-rotated token keeps answering with the successor it minted,\n * instead of being treated as a replay. Default\n * {@link DEFAULT_ROTATION_GRACE_MS}; `0` restores the strict rule.\n *\n * This is what makes a rotation RETRYABLE. See `./rotation-grace.ts` for why the\n * window returns the same successor rather than minting a second one, and why\n * that keeps replay detection intact.\n */\n graceMs?: number;\n}\n\n/** The configured grace window, in milliseconds. `<= 0` disables it. */\nfunction graceWindowMs(context: RefreshTokenContext): number {\n return context.graceMs ?? DEFAULT_ROTATION_GRACE_MS;\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/** 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/** Set equality over scope lists, which are unordered and may repeat. */\nfunction sameScopes(left: readonly string[], right: readonly string[]): boolean {\n const wanted = new Set(left);\n const held = new Set(right);\n if (wanted.size !== held.size) return false;\n for (const scope of wanted) {\n if (!held.has(scope)) return false;\n }\n return true;\n}\n\n/** The one successor a retry may be answered with: its seal and what it grants. */\ninterface RetryTarget {\n seal: string;\n scopes: string[];\n}\n\n/**\n * Pick the successor a retry is entitled to, or `null` to fall through to the\n * replay rule.\n *\n * FILTER, not find. Two rows sharing one `rotatedFrom` cannot happen while\n * `rotate` honours its claim-once contract — but the lineage walk in\n * `./refresh-lineage.ts` already treats multiple children as possible, and\n * serving an arbitrary one of them would be the quiet half of a broken store, so\n * an ambiguous family fails closed.\n *\n * A REVOKED successor means the lineage already died to a real replay, and grace\n * must never resurrect it; an expired one is past its own TTL. Neither is a\n * retry the window was opened to forgive.\n */\nfunction retryableSuccessor(\n family: StoredRefreshToken[],\n tokenHash: string,\n now: number,\n): RetryTarget | null {\n const successors = family.filter((row) => row.rotatedFrom === tokenHash);\n if (successors.length !== 1) return null;\n const [successor] = successors;\n if (!successor?.graceSeal || successor.revokedAt) return null;\n if (successor.expiresAt.getTime() <= now) return null;\n return { seal: successor.graceSeal, scopes: successor.scopes };\n}\n\n/**\n * The grace path: a token that was already consumed is being presented again.\n *\n * Returns the successor that consumption minted — the SAME one, recovered by\n * opening the seal with the parent the caller just presented — when every\n * condition for a retry holds, and `null` when any of them does not, in which\n * case the caller falls through to the replay rule unchanged.\n *\n * The conditions are the security argument, so each is checked rather than\n * assumed:\n *\n * - exactly one unrevoked, unexpired successor exists ({@link retryableSuccessor});\n * - the seal opens with THIS parent, which is what proves the caller held the\n * token it claims to be retrying rather than merely knowing its hash;\n * - the sealed deadline has not passed. It rides inside the AEAD blob, so it\n * cannot be extended by editing the row;\n * - the request asks for the same scopes. A retry repeats its original\n * request; a different scope set is a NEW decision, and answering it with a\n * token minted for the old one would silently ignore what was asked.\n */\nasync function graceReissue(\n context: RefreshTokenContext,\n current: StoredRefreshToken,\n tokenHash: string,\n parentPlaintext: string,\n requestedScopes?: string[],\n): Promise<IssuedRefreshToken | null> {\n if (graceWindowMs(context) <= 0) return null;\n\n const now = Date.now();\n const family = await context.store.listFamily(current.userEmail, current.clientId);\n const target = retryableSuccessor(family, tokenHash, now);\n if (!target) return null;\n\n const opened = openSuccessor(parentPlaintext, target.seal);\n if (!opened || opened.graceUntil <= now) return null;\n\n // Inside the window and the seal opened, so this IS the retry it looks like —\n // but it asks for something else. Refusing is right; refusing as a REPLAY is\n // not, because that revokes the whole lineage and destroys a live session for\n // the innocent double-use this window exists to forgive. Say `invalid_scope`\n // and leave the family alone.\n if (requestedScopes && !sameScopes(requestedScopes, target.scopes)) {\n throw new RefreshTokenError(\n \"invalid_scope\",\n \"a retry inside the rotation grace window cannot change scope\",\n );\n }\n\n return { refreshToken: opened.successor, scopes: target.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. Inside the grace\n // window this is a RETRY and answers with the successor that consumption\n // already minted; outside it, it is the replay it looks like — rejected, with\n // the whole lineage revoked.\n if (current.revokedAt || (await context.store.hasSuccessor(tokenHash))) {\n const retried = await graceReissue(context, current, tokenHash, plaintext, newScopes);\n if (retried) return retried;\n await replay(context, current, tokenHash);\n }\n\n const scopes = narrowedScopes(current, newScopes);\n const successorPlaintext = generateToken();\n const grace = graceWindowMs(context);\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 // Sealed under the PARENT the caller just presented, so a retry of this\n // very rotation can be answered with this same token and nothing else can\n // read it. Omitted entirely when the window is off, so the strict rule\n // stores nothing extra.\n graceSeal:\n grace > 0 ? sealSuccessor(plaintext, successorPlaintext, Date.now() + grace) : null,\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. Exactly one successor is therefore ever written — that\n // part is unchanged, and it is the invariant replay protection rests on.\n //\n // What the loser is TOLD changed. It used to be the replay answer: reject, and\n // revoke the lineage including the successor just handed to the winner. That\n // is correct against an attacker racing the client, and catastrophic for the\n // far more common case of one client refreshing twice — it destroyed a working\n // session and forced a human back through the authorization flow. So the loser\n // now takes the same grace path as a sequential retry and receives the WINNER's\n // token: one successor, two callers holding it, no second family. What that\n // costs is stated honestly in `./rotation-grace.ts` — not a one-rotation\n // deferral, but detection only once two uses fall more than the window apart.\n if (!claimed) {\n const retried = await graceReissue(context, current, tokenHash, plaintext, newScopes);\n if (retried) return retried;\n await replay(context, current, tokenHash);\n }\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.store, 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 type { RefreshTokenStore, StoredRefreshToken } from \"./stores\";\n\n/**\n * The walk over `rotatedFrom`, and the revocation the replay rule spends it on.\n *\n * Split out of `./refresh.ts` because it is the one part of that file with no\n * opinion about tokens: it takes a family of rows, follows the links between\n * them, and revokes what it reaches. It knows nothing about grace windows,\n * scopes, error codes or the request being served — which is also why it takes a\n * {@link RefreshTokenStore} rather than the refresh context, keeping the\n * dependency pointing one way.\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 */\nexport async function revokeLineage(\n store: RefreshTokenStore,\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 store.listFamily(scopedTo.userEmail, scopedTo.clientId);\n const lineage = collectLineage(buildLineageIndex(family), seedHash);\n await store.revokeHashes([...lineage], new Date());\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 { DEFAULT_ROTATION_GRACE_MS } from \"./rotation-grace\";\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 * How long a just-rotated refresh token keeps answering with the successor it\n * minted, instead of being treated as a replay. Default\n * {@link DEFAULT_ROTATION_GRACE_MS}; `0` restores the strict single-use rule.\n *\n * It exists because one client using one token twice is routine — a response\n * lost to a proxy timeout, or two of its own sessions refreshing at once — and\n * the strict rule cannot tell either from theft, so it revoked the lineage and\n * cost a connected user their session. Inside the window the retry is answered\n * with the SAME successor, so no second family is ever created. It does NOT\n * merely defer detection by one rotation: two parties left holding one\n * successor take the retry path again at every rotation, so a collision is\n * detected only once two uses fall more than this window apart. That trade is\n * argued in full in `./rotation-grace.ts`.\n */\n refreshRotationGraceMs?: 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 refreshRotationGraceMs: 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 | \"refreshRotationGraceMs\"\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 refreshRotationGraceMs: config.refreshRotationGraceMs ?? DEFAULT_ROTATION_GRACE_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 * with a grace window in which re-presenting a just-consumed token is a\n * RETRY answered with the same successor, not a replay (`./rotation-grace.ts`).\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 graceMs: context.refreshRotationGraceMs,\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","randomBytes","randomBytes","createHash","randomBytes","createHash","randomBytes","DEFAULT_GRANT_TYPES","createHash","JSON_HEADERS","createHash"]}
@@ -150,6 +150,38 @@ interface StoredRefreshToken {
150
150
  /** The prior token's hash — the rotation lineage. `null` for a root token. */
151
151
  rotatedFrom: string | null;
152
152
  revokedAt: Date | null;
153
+ /**
154
+ * This token's own plaintext, SEALED under a key derived from the plaintext of
155
+ * the token it was rotated from (`./rotation-grace.ts`), and readable only by a
156
+ * caller presenting that parent.
157
+ *
158
+ * It is what lets a rotation be retried: within the grace window, re-presenting
159
+ * the consumed parent returns THIS successor again instead of destroying the
160
+ * lineage, so a lost response or two concurrent refreshes no longer force the
161
+ * user through the whole authorization flow again.
162
+ *
163
+ * REQUIRED of a store from this version on, even though the type is optional
164
+ * for the root token that has no parent to seal under. `rotate` writes it on
165
+ * every successor and CLEARS it on every parent it consumes.
166
+ *
167
+ * A PRISMA host that raises the version without the column fails loudly, on
168
+ * every rotation, because the delegate rejects the unknown key — a dead token
169
+ * endpoint rather than a degraded one, and the reason to land the migration in
170
+ * the SAME change as the version raise. A hand-written store has no such
171
+ * backstop: drop the field there and the window silently never applies, so
172
+ * implementing this field and its clearing is part of meeting the port, not an
173
+ * optional extra. `harness/backend/src/mcp-oauth-db.ts` is the worked example.
174
+ *
175
+ * Clearing it on consumption is what bounds the exposure, and it is the whole
176
+ * reason the field is safe to store at all: a seal is openable only by the
177
+ * plaintext of the token it was rotated from, so leaving spent seals in place
178
+ * would let anyone holding ONE historical plaintext plus a copy of this table
179
+ * walk the chain forward offline — hop by hop, with no server call and so no
180
+ * replay detection — all the way to the live token. With the parent's seal
181
+ * cleared as it is consumed, at most one hop is ever open, and only while the
182
+ * successor it points at is still the live token.
183
+ */
184
+ graceSeal?: string | null;
153
185
  }
154
186
  /** A token about to be stored (the plaintext never is). */
155
187
  type NewRefreshToken = Omit<StoredRefreshToken, "revokedAt">;
@@ -160,7 +192,13 @@ interface RefreshTokenStore {
160
192
  hasSuccessor(tokenHash: string): Promise<boolean>;
161
193
  /** Every token of one `(userEmail, clientId)` family — the lineage walk's input. */
162
194
  listFamily(userEmail: string, clientId: string): Promise<StoredRefreshToken[]>;
163
- /** Revoke exactly these hashes (idempotent). */
195
+ /**
196
+ * Revoke exactly these hashes (idempotent), CLEARING each row's `graceSeal`.
197
+ *
198
+ * A revoked lineage must leave nothing openable behind it — otherwise the
199
+ * revocation that replay detection exists to perform would still leave the
200
+ * chain readable to anyone holding one of its plaintexts.
201
+ */
164
202
  revokeHashes(tokenHashes: readonly string[], at: Date): Promise<void>;
165
203
  /**
166
204
  * CLAIM the parent and store the successor, atomically. The whole of OAuth 2.1
@@ -187,6 +225,12 @@ interface RefreshTokenStore {
187
225
  * live parent AND a live child) but it is not sufficient, and it is the easier
188
226
  * half to satisfy by accident.
189
227
  */
228
+ /**
229
+ * The claim MUST also clear the parent's own `graceSeal`. It is not tidiness:
230
+ * an uncleared seal is permanently openable by the plaintext it was sealed
231
+ * under, so a chain of them is an offline path from any historical token to
232
+ * the live one. Clearing on consumption keeps at most one hop readable.
233
+ */
190
234
  rotate(successor: NewRefreshToken, parentHash: string, at: Date): Promise<boolean>;
191
235
  /**
192
236
  * Revoke every LIVE token a user holds for one client; returns how many were
@@ -388,6 +432,22 @@ interface McpOauthConfig {
388
432
  loginCallbackParam?: string;
389
433
  accessTokenTtlSeconds?: number;
390
434
  refreshTokenTtlMs?: number;
435
+ /**
436
+ * How long a just-rotated refresh token keeps answering with the successor it
437
+ * minted, instead of being treated as a replay. Default
438
+ * {@link DEFAULT_ROTATION_GRACE_MS}; `0` restores the strict single-use rule.
439
+ *
440
+ * It exists because one client using one token twice is routine — a response
441
+ * lost to a proxy timeout, or two of its own sessions refreshing at once — and
442
+ * the strict rule cannot tell either from theft, so it revoked the lineage and
443
+ * cost a connected user their session. Inside the window the retry is answered
444
+ * with the SAME successor, so no second family is ever created. It does NOT
445
+ * merely defer detection by one rotation: two parties left holding one
446
+ * successor take the retry path again at every rotation, so a collision is
447
+ * detected only once two uses fall more than this window apart. That trade is
448
+ * argued in full in `./rotation-grace.ts`.
449
+ */
450
+ refreshRotationGraceMs?: number;
391
451
  /**
392
452
  * The single-use guard for authorization codes — REQUIRED, and required on
393
453
  * purpose. Pass a shared atomic store, or the literal `'in-process'` to accept
@@ -443,6 +503,7 @@ interface McpOauthContext {
443
503
  loginCallbackParam: string;
444
504
  accessTokenTtlSeconds: number;
445
505
  refreshTokenTtlMs: number;
506
+ refreshRotationGraceMs: number;
446
507
  codeReplay: CodeReplayStore;
447
508
  /**
448
509
  * The resolved consent decision for one authorize request. Always present: with
@@ -542,10 +603,41 @@ interface VerifiedAccessToken {
542
603
  }
543
604
  /** Distinct verification failure reasons the caller maps to OAuth challenges. */
544
605
  type AccessTokenErrorCode = "invalid_token" | "insufficient_scope";
545
- /** A typed verification failure — `code` drives the `WWW-Authenticate` challenge. */
606
+ /**
607
+ * WHY verification failed, at the granularity an operator and an agent can act on.
608
+ *
609
+ * `code` above is the RFC 6750 challenge and there are only three of those, so it
610
+ * cannot tell "your connection lapsed, refresh it" from "this token is not for
611
+ * this server". That distinction is the whole difference between an assistant
612
+ * that tells its user to reconnect this server and one that reports a generic
613
+ * failure on every tool call, so it is carried alongside rather than folded
614
+ * into `code`.
615
+ *
616
+ * `unverified` stays deliberately COARSE. Signature, issuer and audience collapse
617
+ * into it because naming which one failed is an oracle for the next attempt.
618
+ * Expiry is the documented exception — RFC 6750 names it in `error_description`
619
+ * precisely because a client must be told to refresh — and it leaks nothing: a
620
+ * token's `exp` is readable by whoever holds the token.
621
+ */
622
+ type AccessTokenFailureReason =
623
+ /** Valid in every other respect, but `exp` has passed. Refresh, do not re-consent. */
624
+ "expired"
625
+ /** Signature, issuer or audience did not hold. Deliberately not narrowed further. */
626
+ | "unverified"
627
+ /** Verified, but missing the `sub`/`email` the identity is built from. */
628
+ | "incomplete"
629
+ /** No signing key is provisioned, so nothing can verify. An operator problem. */
630
+ | "not_provisioned"
631
+ /** A valid token that simply lacks the scope this call needs. */
632
+ | "insufficient_scope";
633
+ /**
634
+ * A typed verification failure — `code` drives the `WWW-Authenticate` challenge,
635
+ * {@link AccessTokenError.reason} drives what the caller is actually told.
636
+ */
546
637
  declare class AccessTokenError extends Error {
547
638
  readonly code: AccessTokenErrorCode;
548
- constructor(code: AccessTokenErrorCode, message?: string);
639
+ readonly reason: AccessTokenFailureReason;
640
+ constructor(code: AccessTokenErrorCode, reason: AccessTokenFailureReason, message?: string);
549
641
  }
550
642
  /** Inputs bound into a minted access token. */
551
643
  interface SignAccessTokenInput {
@@ -644,4 +736,4 @@ interface ApiMcpOauth {
644
736
  }
645
737
  declare function createApiMcpOauth(config: McpOauthConfig): ApiMcpOauth;
646
738
 
647
- export { trustedOriginsFromEnv as $, type ApiMcpOauth as A, type StoredMcpConnection as B, type CodeReplayStore as C, DEFAULT_MCP_RESOURCE_PATH as D, type VerifyAccessTokenOptions as E, createApiMcpOauth as F, hashSecret as G, inProcessCodeReplayStore as H, issuer as I, loadSigningKeyFromEnv as J, matchesRedirectUri as K, originFromRequest as L, type McpOauthConfig as M, type NewOAuthClient as N, type OAuthClientStore as O, type ProviderAttributionRule as P, providerFromRedirectUris as Q, type RefreshTokenStore as R, type StoredOAuthClient as S, type TokenEndpointAuthMethod as T, registerClient as U, type VerifiedAccessToken as V, resolveMcpOauthConfig as W, resolveTrustedOrigin as X, resourceAudience as Y, signAccessToken as Z, signingKeyProvider as _, type McpOauthRoute as a, verifyAccessToken as a0, type McpSigningKeyProvider as b, type NewRefreshToken as c, type StoredRefreshToken as d, type McpOauthStores as e, type McpConnectionStore as f, ACCESS_TOKEN_TTL_SECONDS as g, AccessTokenError as h, type AccessTokenErrorCode as i, DEFAULT_OAUTH_PATHS as j, DEFAULT_PROVIDER_ROOTS as k, DEFAULT_SIGNING_KEY_ENV as l, DEFAULT_SIGNING_KEY_ID_ENV as m, MCP_SUPPORTED_SCOPES as n, type McpConnectionRecording as o, type McpOauthContext as p, type McpOauthHandlers as q, type McpOauthPaths as r, type McpOauthSession as s, type McpScope as t, type McpSigningKey as u, type PublicSigningJwk as v, type RegisterClientInput as w, type RegisteredClient as x, SIGNING_ALG as y, type SignAccessTokenInput as z };
739
+ export { signingKeyProvider as $, type ApiMcpOauth as A, type SignAccessTokenInput as B, type CodeReplayStore as C, DEFAULT_MCP_RESOURCE_PATH as D, type StoredMcpConnection as E, type VerifyAccessTokenOptions as F, createApiMcpOauth as G, hashSecret as H, inProcessCodeReplayStore as I, issuer as J, loadSigningKeyFromEnv as K, matchesRedirectUri as L, type McpOauthConfig as M, type NewOAuthClient as N, type OAuthClientStore as O, type ProviderAttributionRule as P, originFromRequest as Q, type RefreshTokenStore as R, type StoredOAuthClient as S, type TokenEndpointAuthMethod as T, providerFromRedirectUris as U, type VerifiedAccessToken as V, registerClient as W, resolveMcpOauthConfig as X, resolveTrustedOrigin as Y, resourceAudience as Z, signAccessToken as _, type McpOauthRoute as a, trustedOriginsFromEnv as a0, verifyAccessToken as a1, type McpSigningKeyProvider as b, type NewRefreshToken as c, type StoredRefreshToken as d, type McpOauthStores as e, type McpConnectionStore as f, ACCESS_TOKEN_TTL_SECONDS as g, AccessTokenError as h, type AccessTokenErrorCode as i, type AccessTokenFailureReason as j, DEFAULT_OAUTH_PATHS as k, DEFAULT_PROVIDER_ROOTS as l, DEFAULT_SIGNING_KEY_ENV as m, DEFAULT_SIGNING_KEY_ID_ENV as n, MCP_SUPPORTED_SCOPES as o, type McpConnectionRecording as p, type McpOauthContext as q, type McpOauthHandlers as r, type McpOauthPaths as s, type McpOauthSession as t, type McpScope as u, type McpSigningKey as v, type PublicSigningJwk as w, type RegisterClientInput as x, type RegisteredClient as y, SIGNING_ALG as z };