import { b as McpSigningKeyProvider, R as RefreshTokenStore, N as NewOAuthClient, S as StoredOAuthClient, c as NewRefreshToken, d as StoredRefreshToken, e as McpOauthStores, f as McpConnectionStore } from '../create-api-mcp-oauth-BEvYLRBV.js'; export { g as ACCESS_TOKEN_TTL_SECONDS, h as AccessTokenError, i as AccessTokenErrorCode, j as AccessTokenFailureReason, A as ApiMcpOauth, C as CodeReplayStore, D as DEFAULT_MCP_RESOURCE_PATH, k as DEFAULT_OAUTH_PATHS, l as DEFAULT_PROVIDER_ROOTS, m as DEFAULT_SIGNING_KEY_ENV, n as DEFAULT_SIGNING_KEY_ID_ENV, o as MCP_SUPPORTED_SCOPES, p as McpConnectionRecording, M as McpOauthConfig, q as McpOauthContext, r as McpOauthHandlers, s as McpOauthPaths, a as McpOauthRoute, t as McpOauthSession, u as McpScope, v as McpSigningKey, O as OAuthClientStore, P as ProviderAttributionRule, w as PublicSigningJwk, x as RegisterClientInput, y as RegisteredClient, z as SIGNING_ALG, B as SignAccessTokenInput, E as StoredMcpConnection, T as TokenEndpointAuthMethod, V as VerifiedAccessToken, F as VerifyAccessTokenOptions, G as createApiMcpOauth, H as hashSecret, I as inProcessCodeReplayStore, J as issuer, K as loadSigningKeyFromEnv, L as matchesRedirectUri, Q as originFromRequest, U as providerFromRedirectUris, W as registerClient, X as resolveMcpOauthConfig, Y as resolveTrustedOrigin, Z as resourceAudience, _ as signAccessToken, $ as signingKeyProvider, a0 as trustedOriginsFromEnv, a1 as verifyAccessToken } from '../create-api-mcp-oauth-BEvYLRBV.js'; import { d as AiProvider } from '../guide-CrzdsdNf.js'; import 'jose'; /** * Stateless authorization-code mint/verify (12-23, ported from the origin host's * `lib/mcp/oauth/authorization-code.ts` — behaviour unchanged; the signing key * arrives through a provider instead of an env read). * * The authorization code is a short-lived (<=60s) ES256-signed JWT — no DB table, * no cleanup job. It binds the signed-in user (`sub`/`email`), the `client_id`, * the `redirect_uri`, the PKCE `code_challenge`, and the requested `scope`, plus a * unique `jti` the token endpoint records once to enforce single-use (replay) * semantics on top of the short expiry. * * The code carries a DISTINCT audience (`oauth:code`) from the access token * (`${origin}/api/mcp`), so a code can never be presented to the resource server * as a bearer access token (and vice versa): {@link verifyCode} pins * `audience: "oauth:code"`, and the access-token verifier pins the resource * audience — each rejects the other's blobs. */ /** * Audience pinning the code to the OAuth code-exchange step only. Distinct from * the access-token audience so a code cannot be replayed as an access token. */ declare const AUTHORIZATION_CODE_AUDIENCE = "oauth:code"; /** Authorization-code lifetime — single-use and short-lived (<=60s per spec). */ declare const AUTHORIZATION_CODE_TTL_SECONDS = 60; /** Fields bound into a minted authorization code. */ interface MintCodeInput { /** The OAuth subject bound to the code (identity from the cookie session). */ sub: string; /** The signed-in user's email (the identity all downstream tokens bind to). */ email: string; /** The OAuth client the code is issued to. */ clientId: string; /** The exact registered redirect URI the flow started with. */ redirectUri: string; /** The PKCE S256 `code_challenge` the token endpoint verifies against. */ codeChallenge: string; /** The requested scope (space-delimited), carried through to the token. */ scope: string; /** The deployment origin — derives the code's `iss`. */ origin: string; } /** The bound fields a verified authorization code resolves to. */ interface VerifiedAuthorizationCode { sub: string; email: string; clientId: string; redirectUri: string; codeChallenge: string; scope: string; /** The one-time identifier the token endpoint records to enforce single-use. */ jti: string; } /** The single failure discriminator for the OAuth token endpoint. */ type AuthorizationCodeErrorCode = "invalid_grant"; /** * A typed authorization-code failure. Every rejection (expired, wrong-audience, * wrong-issuer, tampered, bad-signature, unconfigured key) surfaces as * `invalid_grant` per RFC 6749 §5.2 for the token endpoint. */ declare class AuthorizationCodeError extends Error { readonly code: AuthorizationCodeErrorCode; constructor(message?: string); } /** Deterministic-clock option shared by mint + verify. */ interface ClockOption { /** Epoch milliseconds; defaults to `Date.now()`. Injected for deterministic tests. */ now?: number; } /** * Mint a single-use, stateless authorization code bound to the flow inputs. * * Returns `null` when no signing key is configured (safe-by-default: the AS * refuses to issue rather than falling back to a weaker mode). Sets the `kid` * header so the same key resolves the code at verify time. */ declare function mintCode(loadSigningKey: McpSigningKeyProvider, input: MintCodeInput, options?: ClockOption): Promise; /** Options for {@link verifyCode}. */ interface VerifyCodeOptions extends ClockOption { /** The deployment origin — derives the expected `iss`. */ origin: string; } /** * Verify a stateless authorization code and return its bound fields. * * Validates signature (via the public JWK selected by `kid`), `iss`, the * `oauth:code` audience, and `exp`. Every failure — expired, wrong-audience (e.g. * an access token), wrong-issuer, tampered, bad-signature, or no configured key — * throws an {@link AuthorizationCodeError} (`invalid_grant`). * * The returned `jti` is the one-time identifier the token endpoint records to * enforce single-use on top of the short expiry (replay guard). */ declare function verifyCode(loadSigningKey: McpSigningKeyProvider, code: string, options: VerifyCodeOptions): Promise; /** * PKCE (RFC 7636) S256 challenge helpers for the OAuth authorization server * (12-23, ported verbatim from the origin host's `lib/mcp/oauth/pkce.ts`). * * OAuth 2.1 mandates the `S256` code-challenge method and forbids `plain`, so * this module computes `BASE64URL(SHA-256(code_verifier))` and compares it to * the stored `code_challenge` in constant time. The authorization endpoint * binds a `code_challenge` into the stateless authorization code; the token * endpoint calls {@link verifyChallenge} with the presented `code_verifier` to * prove the redeeming client is the one that started the flow. * * `plain` is refused (throws {@link UnsupportedChallengeMethodError}) rather * than silently accepted: `plain` offers no protection against an intercepted * authorization code, which is the exact threat PKCE exists to close. */ /** The only PKCE method this server accepts (OAuth 2.1 requires S256). */ declare const SUPPORTED_CHALLENGE_METHOD = "S256"; /** * PKCE code-challenge methods, including the rejected legacy `plain`. * * @public exported because it is a parameter type of the exported * {@link verifyChallenge}. */ type CodeChallengeMethod = "S256" | "plain"; /** Thrown when a caller supplies a challenge method other than `S256`. */ declare class UnsupportedChallengeMethodError extends Error { readonly method: string; constructor(method: string); } /** * Compute the RFC 7636 S256 challenge for a `code_verifier`: * `BASE64URL(SHA-256(ASCII(verifier)))`. */ declare function computeChallenge(verifier: string): Promise; /** * Verify a presented `code_verifier` against a stored `code_challenge`. * * Recomputes the S256 challenge from `verifier` and constant-time-compares it * to `storedChallenge`. Returns `true` on a match, `false` on a mismatch (or an * empty stored challenge). Any method other than `S256` throws * {@link UnsupportedChallengeMethodError} — `plain` is never accepted. */ declare function verifyChallenge(verifier: string, storedChallenge: string, method?: CodeChallengeMethod | string): Promise; /** Refresh-token lifetime — long-lived relative to the 15-min access token. */ declare const REFRESH_TOKEN_TTL_MS: number; /** The single failure discriminator surfaced to the token endpoint. */ type RefreshTokenErrorCode = "invalid_grant" | "invalid_scope"; /** * A typed refresh-token failure. Every rejection — unknown, expired, revoked, * already-rotated (replay), wrong client, or a scope-broadening request — * surfaces as a discriminated error the token endpoint maps to the RFC 6749 * error JSON. */ declare class RefreshTokenError extends Error { readonly code: RefreshTokenErrorCode; constructor(code: RefreshTokenErrorCode, message?: string); } /** The result of issuing/rotating: the plaintext token (once) + bound scopes. */ interface IssuedRefreshToken { /** The opaque plaintext refresh token — returned once, never persisted. */ refreshToken: string; scopes: string[]; } /** SHA-256 hex digest — the at-rest form of an opaque refresh token. */ declare function hashToken(token: string): string; interface RefreshTokenContext { store: RefreshTokenStore; /** Lifetime of a newly stored token. Default 30 days. */ ttlMs?: number; /** * How long a just-rotated token keeps answering with the successor it minted, * instead of being treated as a replay. Default * {@link DEFAULT_ROTATION_GRACE_MS}; `0` restores the strict rule. * * This is what makes a rotation RETRYABLE. See `./rotation-grace.ts` for why the * window returns the same successor rather than minting a second one, and why * that keeps replay detection intact. */ graceMs?: number; } /** * Issue a fresh (root) refresh token bound to a user (email + OAuth `sub`) + * client + scopes. The plaintext is returned once; only its hash is stored. */ declare function issueRefreshToken(context: RefreshTokenContext, binding: { userEmail: string; userSub: string; clientId: string; scopes: string[]; }): Promise; /** * Rotate a refresh token on use: validate it (must exist, be BOUND to the * presenting client, be unexpired, unrevoked and un-rotated), then issue a NEW * token chained via `rotatedFrom` and revoke the consumed one. Optionally NARROW * scope; a broadening request is `invalid_scope`. * * Client binding (OAuth 2.1 §4.3 / RFC 6749 §10.4) is checked BEFORE any rotation * or revocation, so client A can never redeem client B's refresh token — nor * silently consume B's token by trying: the token stays live for its rightful * owner. */ declare function rotateRefreshToken(context: RefreshTokenContext, plaintext: string, expectedClientId: string, newScopes?: string[]): Promise; /** The stable identity a refresh token is bound to. */ interface RefreshTokenIdentity { /** The user's email — the identity the AS binds to and route guards resolve by. */ userEmail: string; /** The original OAuth subject, kept stable across every rotation. */ userSub: string; } /** * Resolve the identity (`email` + original OAuth `sub`) a refresh token is bound * to. The token endpoint uses this after rotation to mint the successor access * token with the correct email AND the SAME stable `sub` as the initial token (no * re-consent, no `sub` drift). `null` if the row is unexpectedly absent. */ declare function getRefreshTokenIdentity(context: RefreshTokenContext, plaintext: string): Promise; /** How long a just-rotated token keeps answering with its successor. */ declare const DEFAULT_ROTATION_GRACE_MS = 30000; /** * The ports of `./stores.ts`, filled by Prisma (12-23). * * The package owns the three models (`prisma/mcp.prisma`), so their delegate * shapes are known and this adapter can be exact. A host with Prisma therefore * writes ONE line — * * stores: createPrismaMcpStores(async () => getPrismaClient() as unknown as McpOauthPrisma) * * — and no host code at all beyond it. The client is duck-typed (only the * delegates used, only the arguments used) so this file never imports a project's * generated client, and a non-Prisma host fills the ports directly instead. */ /** A `where` on the composite unique of `mcp_connections`. */ interface ConnectionKey { userId_oauthClientId: { userId: string; oauthClientId: string; }; } /** The minimal Prisma surface the AS needs. Every field is one the surface writes. */ interface McpOauthPrisma { oAuthClient: { create(args: { data: NewOAuthClient; }): Promise; findUnique(args: { where: { clientId: string; }; }): Promise; }; oAuthRefreshToken: { create(args: { data: NewRefreshToken; }): Promise; findUnique(args: { where: { tokenHash: string; }; }): Promise; findFirst(args: { where: { rotatedFrom: string; }; }): Promise<{ tokenHash: string; } | null>; findMany(args: { where: { userEmail: string; clientId: string; }; }): Promise; updateMany(args: { where: { tokenHash: { in: string[]; }; } | { userEmail: string; clientId: string; revokedAt: null; }; data: { revokedAt: Date; graceSeal?: null; }; }): Promise<{ count: number; }>; }; mcpConnection: { findUnique(args: { where: ConnectionKey; select: { lastActiveAt: true; }; }): Promise<{ lastActiveAt: Date; } | null>; findFirst(args: { where: { userId: string; revokedAt: null; host: null; }; orderBy: { lastActiveAt: "desc"; }; select: { id: true; }; }): Promise<{ id: string; } | null>; findMany(args: { where: { userId: string; revokedAt: null; host?: string | null; }; orderBy?: { lastActiveAt: "desc"; }; select: Record; }): Promise[]>; upsert(args: { where: ConnectionKey; create: Record; update: Record; }): Promise; update(args: { where: { id: string; }; data: Record; }): Promise; updateMany(args: { where: { id: { in: string[]; }; } | { userId: string; revokedAt: null; host: string; }; data: Record; }): Promise<{ count: number; }>; }; /** * Prisma's INTERACTIVE transaction, used for the rotation's claim + write. The * callback form (not the array form) is required: the successor may only be * created once the conditional revoke has reported that it, and not a concurrent * sibling, claimed the parent — see `RefreshTokenStore.rotate`. */ $transaction(fn: (tx: McpOauthTx) => Promise): Promise; } /** * The delegate subset used INSIDE the rotation transaction. Not exported: it is * reachable structurally through `McpOauthPrisma.$transaction`, so no host ever * needs to name it, and exporting a type nobody imports is what knip flags. */ interface McpOauthTx { oAuthRefreshToken: { create(args: { data: NewRefreshToken; }): Promise; updateMany(args: { where: { tokenHash: string; revokedAt: null; }; data: { revokedAt: Date; graceSeal?: null; }; }): Promise<{ count: number; }>; }; } /** A lazily-resolved client, so a host's singleton is awaited per call. */ type McpOauthPrismaProvider = () => Promise; /** Every port, over one lazily-resolved Prisma client. */ declare function createPrismaMcpStores(getPrisma: McpOauthPrismaProvider): McpOauthStores; /** * The account surface's connection OPERATIONS (12-48) — the half of the * `GET/DELETE /api/account/mcp-connections` endpoints that is contract rather * than host vocabulary. * * The ROUTE stays in the host on purpose: it mixes the host's session * resolution, its response envelope, its published plugin URLs and its logger, * and injecting all four here would make the config surface bigger than the * handler it replaces. What must NOT stay in each host is the disconnect's * both-halves rule, because getting it half right LOOKS right: * * `connections.revokeByHost` ends the connection rows and returns the OAuth * client ids behind them — and a host that stops there has revoked nothing that * matters. The assistant still holds a live refresh token for each of those * clients, rotates it on schedule, and the very next grant records fresh * activity: the card the user just disconnected lights green again on its own. * So the rule is one function: revoke the rows AND end every live refresh token * of each returned client, in the same call, with no way to import one half * without the other. * * Deliberately NOT invalidated here: the assistant's current ACCESS token. * Those are self-contained JWTs the server does not track; a just-disconnected * host keeps working for at most their TTL (15 minutes by default) and can then * obtain nothing further. */ /** An active AI connection, narrowed for display. */ interface AiConnectionSnapshot { oauthClientId: string; clientName: string | null; /** The provider this connection is attributed to (`null` = pre-attribution). */ host: AiProvider | null; connectedAt: Date; lastActiveAt: Date; } /** The caller the operations act for — always the session's own user. */ interface AiConnectionCaller { /** The host's user id — what `mcp_connections` rows are keyed by. */ userId: string; /** The identity refresh tokens are bound to (the AS binds by email). */ email: string; } /** What one disconnect actually ended, for the host's log and response. */ interface AiDisconnectResult { /** OAuth client ids whose connection rows were revoked. */ disconnectedClientIds: string[]; /** Live refresh tokens ended across those clients — the half that cuts access. */ revokedRefreshTokens: number; } /** * A user's active connections, most-recently-active first, with the stored open * `host` string narrowed to the package's closed {@link AiProvider} union — the * store cannot know which assistants have screens, but the union is this * package's own vocabulary (`guide.ts`), so the narrowing lives beside it * rather than being re-derived in every host. */ declare function listAiConnections(connections: McpConnectionStore, userId: string): Promise; /** * Disconnect one provider for this user — BOTH halves, atomically from the * caller's point of view (see the module doc for why one half alone is a * disconnect that undoes itself). * * Idempotent: disconnecting a provider that was never connected returns zero * counts rather than failing, so a double-click is harmless. Repeat calls also * report zero — `revokeLiveForClient` skips already-revoked tokens by contract. */ declare function disconnectAiHost(stores: { connections: McpConnectionStore; refreshTokens: RefreshTokenStore; }, caller: AiConnectionCaller, host: AiProvider): Promise; export { AUTHORIZATION_CODE_AUDIENCE, AUTHORIZATION_CODE_TTL_SECONDS, type AiConnectionCaller, type AiConnectionSnapshot, type AiDisconnectResult, AuthorizationCodeError, type AuthorizationCodeErrorCode, type CodeChallengeMethod, DEFAULT_ROTATION_GRACE_MS, type IssuedRefreshToken, McpConnectionStore, type McpOauthPrisma, type McpOauthPrismaProvider, McpOauthStores, McpSigningKeyProvider, type MintCodeInput, NewOAuthClient, NewRefreshToken, REFRESH_TOKEN_TTL_MS, type RefreshTokenContext, RefreshTokenError, type RefreshTokenErrorCode, type RefreshTokenIdentity, RefreshTokenStore, SUPPORTED_CHALLENGE_METHOD, StoredOAuthClient, StoredRefreshToken, UnsupportedChallengeMethodError, type VerifiedAuthorizationCode, type VerifyCodeOptions, computeChallenge, createPrismaMcpStores, disconnectAiHost, getRefreshTokenIdentity, hashToken, issueRefreshToken, listAiConnections, mintCode, rotateRefreshToken, verifyChallenge, verifyCode };