@12-apps/mcp 3.16.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.
- package/ADOPTING.md +41 -0
- package/README.md +1 -1
- package/dist/{chunk-VDD4YRNP.js → chunk-EANHJLDH.js} +13 -4
- package/dist/chunk-EANHJLDH.js.map +1 -0
- package/dist/{chunk-UIILEGAC.js → chunk-WCZC4TPX.js} +193 -48
- package/dist/chunk-WCZC4TPX.js.map +1 -0
- package/dist/{create-api-mcp-oauth-CsC0jlH7.d.ts → create-api-mcp-oauth-BEvYLRBV.d.ts} +96 -4
- package/dist/hono/index.d.ts +1 -1
- package/dist/hono/index.js +1 -1
- package/dist/index.d.ts +100 -2
- package/dist/index.js +71 -6
- package/dist/index.js.map +1 -1
- package/dist/manifest/server.d.ts +1 -1
- package/dist/manifest/server.js +2 -2
- package/dist/oauth/index.d.ts +18 -3
- package/dist/oauth/index.js +4 -2
- package/package.json +5 -5
- package/prisma/mcp.prisma +10 -0
- package/prisma/migrations/20260910120000_add_refresh_grace_seal/migration.sql +37 -0
- package/src/index.ts +11 -0
- package/src/oauth/access-token.ts +72 -10
- package/src/oauth/context.ts +20 -0
- package/src/oauth/index.ts +2 -0
- package/src/oauth/prisma-stores.ts +16 -5
- package/src/oauth/refresh-lineage.ts +77 -0
- package/src/oauth/refresh.ts +169 -82
- package/src/oauth/rotation-grace.ts +216 -0
- package/src/oauth/stores.ts +45 -1
- package/src/oauth/token-grants.ts +4 -1
- package/src/server/auth-failure.ts +145 -0
- package/src/server/jsonrpc.ts +44 -5
- package/dist/chunk-UIILEGAC.js.map +0 -1
- package/dist/chunk-VDD4YRNP.js.map +0 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an unauthorized MCP call is TOLD, as opposed to what it is refused with.
|
|
3
|
+
*
|
|
4
|
+
* ## The failure this module exists to remove
|
|
5
|
+
*
|
|
6
|
+
* A resource server has three RFC 6750 challenge codes and a boolean's worth of
|
|
7
|
+
* expressiveness, so every way a bearer can fail arrives at the agent as one
|
|
8
|
+
* opaque refusal. `Authentication required` is what a lapsed connection, a token
|
|
9
|
+
* minted for a different origin, a surface an operator never switched on, and a
|
|
10
|
+
* missing scope all look like — identical, and none of them actionable.
|
|
11
|
+
*
|
|
12
|
+
* The cost is not cosmetic. An agent that cannot tell those apart cannot tell the
|
|
13
|
+
* user anything useful either: every tool call fails, the connector still reports
|
|
14
|
+
* itself connected, and the one thing that would fix the common case — reconnect
|
|
15
|
+
* it — is the one thing nobody is told to do. Reported from a live deployment as
|
|
16
|
+
* "every tool call failing, even the ones that read nothing".
|
|
17
|
+
*
|
|
18
|
+
* ## The shape of the answer
|
|
19
|
+
*
|
|
20
|
+
* Each reason resolves to three things, and they are deliberately separate:
|
|
21
|
+
*
|
|
22
|
+
* - `challenge` — the RFC 6750 code for the `WWW-Authenticate` header. Only
|
|
23
|
+
* ever one of the two the spec defines for this situation, because a host's
|
|
24
|
+
* OAuth machinery keys off it;
|
|
25
|
+
* - `action` — what would actually fix it, for a client that automates;
|
|
26
|
+
* - `message` — one sentence an agent can relay to a person. English, like
|
|
27
|
+
* everything else a developer or a model reads here; a host that wants its
|
|
28
|
+
* own wording supplies it.
|
|
29
|
+
*
|
|
30
|
+
* Nothing here narrows `unverified`. Signature, issuer and audience stay
|
|
31
|
+
* collapsed into one answer on purpose — see `../oauth/access-token.ts` for why
|
|
32
|
+
* expiry is the single documented exception.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Why a call was refused, across both the transport and the token verifier. */
|
|
36
|
+
export type McpAuthFailureReason =
|
|
37
|
+
/** No `Authorization` header at all — the client has not connected yet. */
|
|
38
|
+
| "no_token"
|
|
39
|
+
/** The token was fine until its `exp` passed. The common one, and recoverable. */
|
|
40
|
+
| "expired"
|
|
41
|
+
/** Signature, issuer or audience did not hold. Deliberately not narrowed. */
|
|
42
|
+
| "unverified"
|
|
43
|
+
/** Verified, but carrying no usable identity. */
|
|
44
|
+
| "incomplete"
|
|
45
|
+
/** The operator has not provisioned signing material, so nothing can verify. */
|
|
46
|
+
| "not_provisioned"
|
|
47
|
+
/** The whole MCP surface is switched off for this deployment. */
|
|
48
|
+
| "surface_disabled"
|
|
49
|
+
/** A valid token that lacks the scope this particular call needs. */
|
|
50
|
+
| "insufficient_scope";
|
|
51
|
+
|
|
52
|
+
/** What a client should do about it. */
|
|
53
|
+
export type McpAuthRecovery =
|
|
54
|
+
/** Exchange the refresh token for a new access token, then retry. */
|
|
55
|
+
| "refresh"
|
|
56
|
+
/** Re-run the authorization flow — a human has to approve it again. */
|
|
57
|
+
| "reconnect"
|
|
58
|
+
/** Nothing the client can do; the deployment has to change. */
|
|
59
|
+
| "contact_operator";
|
|
60
|
+
|
|
61
|
+
/** The resolved answer for one refusal. */
|
|
62
|
+
export interface McpAuthFailure {
|
|
63
|
+
reason: McpAuthFailureReason;
|
|
64
|
+
/** The RFC 6750 code for the `WWW-Authenticate` challenge. */
|
|
65
|
+
challenge: "invalid_token" | "insufficient_scope";
|
|
66
|
+
action: McpAuthRecovery;
|
|
67
|
+
/** One sentence, written to be relayed to a person by an agent. */
|
|
68
|
+
message: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const FAILURES: Record<McpAuthFailureReason, Omit<McpAuthFailure, "reason">> = {
|
|
72
|
+
no_token: {
|
|
73
|
+
challenge: "invalid_token",
|
|
74
|
+
action: "reconnect",
|
|
75
|
+
message:
|
|
76
|
+
"This request carried no access token. Ask the user to connect this MCP server in their assistant's connector settings, then retry.",
|
|
77
|
+
},
|
|
78
|
+
expired: {
|
|
79
|
+
challenge: "invalid_token",
|
|
80
|
+
action: "refresh",
|
|
81
|
+
message:
|
|
82
|
+
"The access token has expired. A client that holds a refresh token should renew it and retry; if renewal also fails, ask the user to reconnect this MCP server in their assistant's connector settings.",
|
|
83
|
+
},
|
|
84
|
+
unverified: {
|
|
85
|
+
challenge: "invalid_token",
|
|
86
|
+
action: "reconnect",
|
|
87
|
+
message:
|
|
88
|
+
"The access token could not be verified for this server. Ask the user to reconnect this MCP server in their assistant's connector settings — a token issued for a different deployment will never verify here.",
|
|
89
|
+
},
|
|
90
|
+
incomplete: {
|
|
91
|
+
challenge: "invalid_token",
|
|
92
|
+
action: "reconnect",
|
|
93
|
+
message:
|
|
94
|
+
"The access token verified but carries no usable identity. Ask the user to reconnect this MCP server in their assistant's connector settings.",
|
|
95
|
+
},
|
|
96
|
+
not_provisioned: {
|
|
97
|
+
challenge: "invalid_token",
|
|
98
|
+
action: "contact_operator",
|
|
99
|
+
message:
|
|
100
|
+
"This server has no signing key provisioned, so no access token can be verified. Reconnecting will not help; the deployment's operator has to configure it.",
|
|
101
|
+
},
|
|
102
|
+
surface_disabled: {
|
|
103
|
+
challenge: "invalid_token",
|
|
104
|
+
action: "contact_operator",
|
|
105
|
+
message:
|
|
106
|
+
"The MCP surface is switched off on this deployment. Reconnecting will not help; the deployment's operator has to enable it.",
|
|
107
|
+
},
|
|
108
|
+
insufficient_scope: {
|
|
109
|
+
challenge: "insufficient_scope",
|
|
110
|
+
action: "reconnect",
|
|
111
|
+
message:
|
|
112
|
+
"The access token does not grant the scope this tool needs. Ask the user to reconnect this MCP server and approve the wider scope.",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Resolve a reason to its challenge code, recovery and human-relayable message. */
|
|
117
|
+
export function describeAuthFailure(reason: McpAuthFailureReason): McpAuthFailure {
|
|
118
|
+
return { reason, ...FAILURES[reason] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The machine-readable half, carried in the JSON-RPC error's `data` member.
|
|
123
|
+
*
|
|
124
|
+
* A model reads `message`; a host that automates its connection lifecycle reads
|
|
125
|
+
* this. Both travel together so neither has to be inferred from the other.
|
|
126
|
+
*/
|
|
127
|
+
export interface McpAuthFailureData {
|
|
128
|
+
reason: McpAuthFailureReason;
|
|
129
|
+
action: McpAuthRecovery;
|
|
130
|
+
/**
|
|
131
|
+
* Whether presenting a NEW token could succeed. `false` means the deployment
|
|
132
|
+
* itself is the problem, so a client that retries forever is wasting its time
|
|
133
|
+
* and the user's — and should say so rather than loop.
|
|
134
|
+
*/
|
|
135
|
+
recoverable: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Build the `data` payload for a refusal. */
|
|
139
|
+
export function authFailureData(failure: McpAuthFailure): McpAuthFailureData {
|
|
140
|
+
return {
|
|
141
|
+
reason: failure.reason,
|
|
142
|
+
action: failure.action,
|
|
143
|
+
recoverable: failure.action !== "contact_operator",
|
|
144
|
+
};
|
|
145
|
+
}
|
package/src/server/jsonrpc.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { RequestAuth } from "../types";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
authFailureData,
|
|
5
|
+
describeAuthFailure,
|
|
6
|
+
type McpAuthFailureReason,
|
|
7
|
+
} from "./auth-failure";
|
|
3
8
|
import type { ToolRegistry } from "./registry";
|
|
4
9
|
|
|
5
10
|
/**
|
|
@@ -52,7 +57,12 @@ export interface JsonRpcResponse {
|
|
|
52
57
|
jsonrpc: "2.0";
|
|
53
58
|
id: string | number | null;
|
|
54
59
|
result?: unknown;
|
|
55
|
-
|
|
60
|
+
/**
|
|
61
|
+
* `data` is the JSON-RPC 2.0 optional member, and it is what carries the
|
|
62
|
+
* machine-readable half of a refusal ({@link McpAuthFailureData}) while
|
|
63
|
+
* `message` carries the half a model relays to a person.
|
|
64
|
+
*/
|
|
65
|
+
error?: { code: number; message: string; data?: unknown };
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
/** What a client is told it connected to, in `initialize`'s `serverInfo`. */
|
|
@@ -91,8 +101,17 @@ function ok(id: JsonRpcRequest["id"], result: unknown): JsonRpcResponse {
|
|
|
91
101
|
return { jsonrpc: "2.0", id: id ?? null, result };
|
|
92
102
|
}
|
|
93
103
|
|
|
94
|
-
function fail(
|
|
95
|
-
|
|
104
|
+
function fail(
|
|
105
|
+
id: JsonRpcRequest["id"],
|
|
106
|
+
code: number,
|
|
107
|
+
message: string,
|
|
108
|
+
data?: unknown,
|
|
109
|
+
): JsonRpcResponse {
|
|
110
|
+
return {
|
|
111
|
+
jsonrpc: "2.0",
|
|
112
|
+
id: id ?? null,
|
|
113
|
+
error: { code, message, ...(data === undefined ? {} : { data }) },
|
|
114
|
+
};
|
|
96
115
|
}
|
|
97
116
|
|
|
98
117
|
/** A parsed body is a usable request only if it's an object carrying a string `method`. */
|
|
@@ -104,8 +123,22 @@ async function handleToolsCall(
|
|
|
104
123
|
request: JsonRpcRequest,
|
|
105
124
|
registry: ToolRegistry,
|
|
106
125
|
auth: RequestAuth | null,
|
|
126
|
+
failure?: McpAuthFailureReason,
|
|
107
127
|
): Promise<JsonRpcResponse> {
|
|
108
|
-
if (!auth)
|
|
128
|
+
if (!auth) {
|
|
129
|
+
// It used to be the bare string "Authentication required", which is true of
|
|
130
|
+
// every refusal and useful for none of them: an agent reading it cannot tell
|
|
131
|
+
// a lapsed connection from a misconfigured server, so it cannot tell the user
|
|
132
|
+
// to reconnect. The host resolves WHY and passes it in; `no_token` is the
|
|
133
|
+
// honest default when it says nothing.
|
|
134
|
+
const described = describeAuthFailure(failure ?? "no_token");
|
|
135
|
+
return fail(
|
|
136
|
+
request.id,
|
|
137
|
+
UNAUTHORIZED_CODE,
|
|
138
|
+
described.message,
|
|
139
|
+
authFailureData(described),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
109
142
|
const params = (request.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };
|
|
110
143
|
if (!params.name) return fail(request.id, INVALID_PARAMS_CODE, "Missing tool name");
|
|
111
144
|
const result = await registry.callTool(params.name, params.arguments ?? {}, auth);
|
|
@@ -135,12 +168,18 @@ function handleInitialize(
|
|
|
135
168
|
* `tools/call` then returns {@link UNAUTHORIZED_CODE}, which the host surfaces as
|
|
136
169
|
* HTTP 401. Discovery (`initialize`, `ping`, `tools/list`) stays open, so a client
|
|
137
170
|
* can read the surface before it has a token.
|
|
171
|
+
*
|
|
172
|
+
* `failure` is WHY `auth` is null, which only the host's verifier knows. It is
|
|
173
|
+
* optional so an existing caller keeps compiling, and passing it is what turns a
|
|
174
|
+
* refusal an agent can only report into one it can act on — see
|
|
175
|
+
* `./auth-failure.ts`.
|
|
138
176
|
*/
|
|
139
177
|
export async function handleMcpJsonRpc(
|
|
140
178
|
request: JsonRpcRequest,
|
|
141
179
|
registry: ToolRegistry,
|
|
142
180
|
auth: RequestAuth | null,
|
|
143
181
|
options: McpJsonRpcOptions,
|
|
182
|
+
failure?: McpAuthFailureReason,
|
|
144
183
|
): Promise<JsonRpcResponse | null> {
|
|
145
184
|
// A host casts the parsed body to JsonRpcRequest without validating it, so a
|
|
146
185
|
// malformed payload can arrive here: a `null` body/batch element, a non-object,
|
|
@@ -157,7 +196,7 @@ export async function handleMcpJsonRpc(
|
|
|
157
196
|
case "tools/list":
|
|
158
197
|
return ok(request.id, { tools: registry.listTools(auth ?? undefined) });
|
|
159
198
|
case "tools/call":
|
|
160
|
-
return handleToolsCall(request, registry, auth);
|
|
199
|
+
return handleToolsCall(request, registry, auth, failure);
|
|
161
200
|
default:
|
|
162
201
|
// JSON-RPC notifications (`notifications/*`) expect no reply — silently
|
|
163
202
|
// ignore any we don't explicitly handle, rather than returning an error.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/oauth/config.ts","../src/oauth/keys.ts","../src/oauth/authorization-code.ts","../src/oauth/clients.ts","../src/oauth/pkce.ts","../src/oauth/code-replay.ts","../src/oauth/access-token.ts","../src/oauth/refresh.ts","../src/oauth/context.ts","../src/oauth/authorize.ts","../src/oauth/register.ts","../src/oauth/token-response.ts","../src/oauth/token-grants.ts","../src/oauth/create-api-mcp-oauth.ts"],"sourcesContent":["/**\n * The OAuth 2.1 authorization-server foundation: the shared scope source, the\n * issuer/audience derivation, and the trusted-origin resolver every URL in the\n * surface is built from (12-23, ported from the origin host's\n * `lib/mcp/oauth/config.ts`).\n *\n * Keeping the scopes and the origin resolution in ONE place is what stops the\n * two discovery documents — RFC 8414 `/.well-known/oauth-authorization-server`\n * and RFC 9728 `/.well-known/oauth-protected-resource` — from drifting apart,\n * and what makes a token minted for an origin verify against that same origin.\n *\n * What was env-reading in the host is CONFIG here (the package must not learn a\n * host's variable names); `trustedOriginsFromEnv` is the one-line helper that\n * keeps the origin host's wiring identical.\n */\n\n/** Scopes advertised by both discovery documents. `mcp:write` gates mutating tools. */\nexport const MCP_SUPPORTED_SCOPES = [\"mcp:read\", \"mcp:write\"] as const;\n\nexport type McpScope = (typeof MCP_SUPPORTED_SCOPES)[number];\n\n/** Path the MCP JSON-RPC endpoint is mounted at — the access token's audience. */\nexport const DEFAULT_MCP_RESOURCE_PATH = \"/api/mcp\";\n\n/** The OAuth `iss` — the deployment origin, used verbatim. */\nexport function issuer(origin: string): string {\n return origin;\n}\n\n/** The access-token `aud` — the MCP resource URL (`${origin}${resourcePath}`). */\nexport function resourceAudience(\n origin: string,\n resourcePath: string = DEFAULT_MCP_RESOURCE_PATH,\n): string {\n return `${origin}${resourcePath}`;\n}\n\n/**\n * A single bare `host` or `host:port` — no scheme, path, userinfo (`@`), or\n * whitespace. A syntactic guard so a forwarded host cannot smuggle anything but\n * a hostname; it does NOT by itself decide trust (the allowlist does).\n */\nconst HOST_ONLY =\n /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?::\\d{1,5})?$/i;\n\n/** ASCII `/`. */\nconst SLASH = 0x2f;\n\n/**\n * Strip trailing slashes by index, not by regex.\n *\n * `replace(/\\/+$/, \"\")` is quadratic on a string of many slashes — the classic\n * anchored-quantifier backtrack — and this function is reached from an operator's\n * env var AND (through `resolveTrustedOrigin`) from values that arrive with a\n * request. A backwards walk is linear and needs no reasoning about the engine.\n */\nfunction stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === SLASH) end -= 1;\n return value.slice(0, end);\n}\n\n/** Normalize an allowlist entry (trailing `/` stripped, blanks dropped). */\nfunction normalizeOrigins(origins: readonly string[]): string[] {\n return origins.map((origin) => stripTrailingSlashes(origin.trim())).filter(Boolean);\n}\n\n/**\n * Read a comma-separated allowlist out of an environment variable — the origin host\n * passes `trustedOriginsFromEnv('MCP_OAUTH_TRUSTED_ORIGINS')`, so the behaviour\n * is identical while the variable's NAME stays the host's.\n */\nexport function trustedOriginsFromEnv(name: string): string[] {\n const raw = typeof process === \"undefined\" ? undefined : process.env?.[name];\n return raw ? normalizeOrigins(raw.split(\",\")) : [];\n}\n\n/**\n * The origin a request's forwarded headers CLAIM, or `null` when\n * absent/malformed. Only the first hop of a comma-separated list is taken, the\n * host must be a bare host[:port], and the proto is restricted to http/https.\n * This is a claim to be checked against the allowlist — never trusted alone.\n */\nfunction claimedForwardedOrigin(getHeader: (name: string) => string | null): string | null {\n const host = getHeader(\"x-forwarded-host\")?.split(\",\")[0]?.trim();\n if (!host || !HOST_ONLY.test(host)) return null;\n const proto = getHeader(\"x-forwarded-proto\")?.split(\",\")[0]?.trim();\n return `${proto === \"http\" ? \"http\" : \"https\"}://${host}`;\n}\n\n/**\n * THE single trusted-origin resolver, shared by token ISSUANCE (the `iss`/`aud` a\n * token is minted with) and bearer VERIFICATION (the expected `aud` a protected\n * route checks). Because both sides pass the SAME request's headers, a token\n * minted for the allowlisted origin verifies against that same origin — they\n * cannot drift (e.g. mint `https://app.example.com` but verify\n * `http://0.0.0.0:3000` and reject a valid token).\n *\n * The origin must NEVER be attacker-controllable. Behind a reverse proxy the\n * server sees only its internal bind on `request.url`, so the public origin comes\n * from the proxy's `X-Forwarded-Host` — but a forwarded host is honored ONLY when\n * it is on the operator-configured allowlist; ANY other value (a spoofed/foreign\n * host, or an absent header) resolves to the canonical (FIRST) allowlisted origin.\n * So even if the edge forwards `X-Forwarded-Host: evil.example.com`, the origin\n * stays the trusted one.\n *\n * With NO allowlist configured a forwarded host is NEVER trusted — a spoofed\n * header must not be able to choose the issuer — so `fallbackOrigin` (the\n * request's OWN origin) is used instead. A proxied deployment therefore REQUIRES\n * the allowlist; until it is set the surface fails closed to the internal origin\n * rather than a foreign one.\n */\nexport function resolveTrustedOrigin(\n getHeader: (name: string) => string | null,\n fallbackOrigin: string | undefined,\n trustedOrigins: readonly string[] = [],\n): string | undefined {\n const [canonical, ...rest] = normalizeOrigins(trustedOrigins);\n // No allowlist → the forwarded host is untrusted; fall back to the request's\n // own origin so a spoofed X-Forwarded-Host can never become the issuer.\n if (!canonical) return fallbackOrigin;\n\n const allowed = [canonical, ...rest];\n const claimed = claimedForwardedOrigin(getHeader);\n return claimed && allowed.includes(claimed) ? claimed : canonical;\n}\n\n/**\n * The PUBLIC origin every URL in the surface derives from (issuer, endpoint URLs,\n * access-token `iss`/`aud`). A thin wrapper over {@link resolveTrustedOrigin}\n * bound to a `Request`, falling back to the request URL's origin when no\n * allowlist is configured.\n */\nexport function originFromRequest(\n request: Request,\n trustedOrigins: readonly string[] = [],\n): string {\n const fallback = new URL(request.url).origin;\n return (\n resolveTrustedOrigin((name) => request.headers.get(name), fallback, trustedOrigins) ?? fallback\n );\n}\n","import { exportJWK, importPKCS8, type CryptoKey, type JWK } from \"jose\";\n\n/**\n * Signing-key / JWK loading for the OAuth authorization server (12-23, ported\n * from the origin host's `lib/mcp/oauth/keys.ts`).\n *\n * ES256 (P-256) from PEM material, the published public JWK (with `kid` for\n * rotation), and a safe-by-default absence signal (`null`) when no key is\n * configured — callers then refuse to issue tokens and serve the JWKS as 503\n * rather than falling back to a weaker mode while the surface is mounted.\n *\n * WHERE the PEM comes from is the host's business: `loadSigningKeyFromEnv` keeps\n * the origin host's env-var wiring, and any other provider (a secrets manager, a KMS\n * export) satisfies the same `McpSigningKeyProvider` shape.\n */\n\n/** JWS algorithm for the signing key pair (asymmetric, self-validated via JWKS). */\nexport const SIGNING_ALG = \"ES256\";\n\n/** A public JWK safe to publish at the JWKS endpoint (never carries `d`). */\nexport interface PublicSigningJwk extends JWK {\n kid: string;\n kty: \"EC\";\n crv: \"P-256\";\n alg: typeof SIGNING_ALG;\n use: \"sig\";\n}\n\n/** The loaded signing material: the private key for signing + its public JWK. */\nexport interface McpSigningKey {\n privateKey: CryptoKey;\n publicJwk: PublicSigningJwk;\n kid: string;\n}\n\n/**\n * How the surface obtains signing material. Returning `null` means \"not\n * provisioned\": the AS then mints nothing and the JWKS answers 503.\n */\nexport type McpSigningKeyProvider = () => Promise<McpSigningKey | null>;\n\nasync function parseSigningKey(pem: string, kid: string): Promise<McpSigningKey> {\n // `extractable: true` is required so `exportJWK` can derive the public JWK;\n // jose imports keys as non-extractable by default, which blocks the export.\n const privateKey = await importPKCS8(pem, SIGNING_ALG, { extractable: true });\n const jwk = await exportJWK(privateKey);\n // Strip the private component; publish only the public half.\n const { d: _private, ...publicHalf } = jwk;\n void _private;\n\n const publicJwk: PublicSigningJwk = {\n ...publicHalf,\n kty: \"EC\",\n crv: \"P-256\",\n alg: SIGNING_ALG,\n use: \"sig\",\n kid,\n };\n\n return { privateKey, publicJwk, kid };\n}\n\n/**\n * Build a provider over a PKCS#8 PEM + `kid` pair, with a per-process cache.\n *\n * Parsing PKCS#8 and exporting the JWK is pure for a given (pem, kid), so the\n * promise is cached keyed on the material itself. A rotated key (different pem or\n * kid) produces a different cache key and re-parses — the cache never masks a\n * rotation.\n *\n * Rotation is BY `kid`: each key is published in the JWKS and selected by the\n * `kid` header on issued JWTs, so publishing old + new during an overlap window\n * lets both verify.\n */\nexport function signingKeyProvider(\n read: () => { pem: string | undefined; kid: string | undefined },\n): McpSigningKeyProvider {\n let cache: { key: string; promise: Promise<McpSigningKey> } | null = null;\n return async () => {\n const { pem, kid } = read();\n if (!pem || !kid) return null;\n const cacheKey = `${kid} ${pem}`;\n if (cache?.key === cacheKey) return cache.promise;\n const promise = parseSigningKey(pem, kid);\n cache = { key: cacheKey, promise };\n return promise;\n };\n}\n\n/** Env var carrying the ES256 private key as a PKCS#8 PEM (the origin host's name). */\nexport const DEFAULT_SIGNING_KEY_ENV = \"MCP_OAUTH_SIGNING_KEY\";\n/** Env var carrying the key id (`kid`) used to select the key during rotation. */\nexport const DEFAULT_SIGNING_KEY_ID_ENV = \"MCP_OAUTH_SIGNING_KEY_ID\";\n\n/**\n * The env-backed provider — the origin host's wiring, kept identical, with the\n * variable names as arguments so the package states no host's vocabulary.\n */\nexport function loadSigningKeyFromEnv(\n keyEnv: string = DEFAULT_SIGNING_KEY_ENV,\n kidEnv: string = DEFAULT_SIGNING_KEY_ID_ENV,\n): McpSigningKeyProvider {\n return signingKeyProvider(() => ({\n pem: typeof process === \"undefined\" ? undefined : process.env?.[keyEnv],\n kid: typeof process === \"undefined\" ? undefined : process.env?.[kidEnv],\n }));\n}\n","import { SignJWT, jwtVerify, importJWK, type JWTPayload } from \"jose\";\n\nimport { issuer } from \"./config\";\nimport { SIGNING_ALG, type McpSigningKeyProvider } from \"./keys\";\n\n/**\n * Stateless authorization-code mint/verify (12-23, ported from the origin host's\n * `lib/mcp/oauth/authorization-code.ts` — behaviour unchanged; the signing key\n * arrives through a provider instead of an env read).\n *\n * The authorization code is a short-lived (<=60s) ES256-signed JWT — no DB table,\n * no cleanup job. It binds the signed-in user (`sub`/`email`), the `client_id`,\n * the `redirect_uri`, the PKCE `code_challenge`, and the requested `scope`, plus a\n * unique `jti` the token endpoint records once to enforce single-use (replay)\n * semantics on top of the short expiry.\n *\n * The code carries a DISTINCT audience (`oauth:code`) from the access token\n * (`${origin}/api/mcp`), so a code can never be presented to the resource server\n * as a bearer access token (and vice versa): {@link verifyCode} pins\n * `audience: \"oauth:code\"`, and the access-token verifier pins the resource\n * audience — each rejects the other's blobs.\n */\n\n/**\n * Audience pinning the code to the OAuth code-exchange step only. Distinct from\n * the access-token audience so a code cannot be replayed as an access token.\n */\nexport const AUTHORIZATION_CODE_AUDIENCE = \"oauth:code\";\n\n/** Authorization-code lifetime — single-use and short-lived (<=60s per spec). */\nexport const AUTHORIZATION_CODE_TTL_SECONDS = 60;\n\n/** Clock skew tolerated on `exp`/`iat` validation, in seconds. */\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\n/** Fields bound into a minted authorization code. */\nexport interface MintCodeInput {\n /** The OAuth subject bound to the code (identity from the cookie session). */\n sub: string;\n /** The signed-in user's email (the identity all downstream tokens bind to). */\n email: string;\n /** The OAuth client the code is issued to. */\n clientId: string;\n /** The exact registered redirect URI the flow started with. */\n redirectUri: string;\n /** The PKCE S256 `code_challenge` the token endpoint verifies against. */\n codeChallenge: string;\n /** The requested scope (space-delimited), carried through to the token. */\n scope: string;\n /** The deployment origin — derives the code's `iss`. */\n origin: string;\n}\n\n/** The bound fields a verified authorization code resolves to. */\nexport interface VerifiedAuthorizationCode {\n sub: string;\n email: string;\n clientId: string;\n redirectUri: string;\n codeChallenge: string;\n scope: string;\n /** The one-time identifier the token endpoint records to enforce single-use. */\n jti: string;\n}\n\n/** The single failure discriminator for the OAuth token endpoint. */\nexport type AuthorizationCodeErrorCode = \"invalid_grant\";\n\n/**\n * A typed authorization-code failure. Every rejection (expired, wrong-audience,\n * wrong-issuer, tampered, bad-signature, unconfigured key) surfaces as\n * `invalid_grant` per RFC 6749 §5.2 for the token endpoint.\n */\nexport class AuthorizationCodeError extends Error {\n readonly code: AuthorizationCodeErrorCode;\n\n constructor(message?: string) {\n super(message ?? \"invalid_grant\");\n this.name = \"AuthorizationCodeError\";\n this.code = \"invalid_grant\";\n }\n}\n\n/** Deterministic-clock option shared by mint + verify. */\ninterface ClockOption {\n /** Epoch milliseconds; defaults to `Date.now()`. Injected for deterministic tests. */\n now?: number;\n}\n\n/** The JWT claim shape of an authorization code. */\ninterface AuthorizationCodeClaims extends JWTPayload {\n email: string;\n client_id: string;\n redirect_uri: string;\n code_challenge: string;\n scope: string;\n}\n\nfunction nowSeconds(now?: number): number {\n return Math.floor((now ?? Date.now()) / 1000);\n}\n\n/** Read a required string claim, or `null` when absent/wrong-typed. */\nfunction stringClaim(payload: JWTPayload, key: string): string | null {\n const value = payload[key];\n return typeof value === \"string\" ? value : null;\n}\n\n/**\n * Extract the bound fields from an already-signature/iss/aud/exp-validated code\n * payload, enforcing that every required claim is a present string (`scope` may be\n * the empty string but must be present). Throws {@link AuthorizationCodeError}\n * when any required bound field is missing or wrong-typed.\n */\nfunction extractBoundFields(payload: JWTPayload): VerifiedAuthorizationCode {\n const sub = stringClaim(payload, \"sub\");\n const email = stringClaim(payload, \"email\");\n const clientId = stringClaim(payload, \"client_id\");\n const redirectUri = stringClaim(payload, \"redirect_uri\");\n const codeChallenge = stringClaim(payload, \"code_challenge\");\n const scope = stringClaim(payload, \"scope\");\n const jti = stringClaim(payload, \"jti\");\n\n if (!sub || !email || !clientId || !redirectUri || !codeChallenge || scope === null || !jti) {\n throw new AuthorizationCodeError(\"code is missing required bound fields\");\n }\n\n return { sub, email, clientId, redirectUri, codeChallenge, scope, jti };\n}\n\n/**\n * Mint a single-use, stateless authorization code bound to the flow inputs.\n *\n * Returns `null` when no signing key is configured (safe-by-default: the AS\n * refuses to issue rather than falling back to a weaker mode). Sets the `kid`\n * header so the same key resolves the code at verify time.\n */\nexport async function mintCode(\n loadSigningKey: McpSigningKeyProvider,\n input: MintCodeInput,\n options?: ClockOption,\n): Promise<string | null> {\n const key = await loadSigningKey();\n if (!key) return null;\n\n const iat = nowSeconds(options?.now);\n const exp = iat + AUTHORIZATION_CODE_TTL_SECONDS;\n\n const claims: AuthorizationCodeClaims = {\n email: input.email,\n client_id: input.clientId,\n redirect_uri: input.redirectUri,\n code_challenge: input.codeChallenge,\n scope: input.scope,\n };\n\n return new SignJWT(claims)\n .setProtectedHeader({ alg: SIGNING_ALG, kid: key.kid })\n .setIssuer(issuer(input.origin))\n .setAudience(AUTHORIZATION_CODE_AUDIENCE)\n .setSubject(input.sub)\n .setIssuedAt(iat)\n .setExpirationTime(exp)\n .setJti(crypto.randomUUID())\n .sign(key.privateKey);\n}\n\n/** Options for {@link verifyCode}. */\nexport interface VerifyCodeOptions extends ClockOption {\n /** The deployment origin — derives the expected `iss`. */\n origin: string;\n}\n\n/**\n * Verify a stateless authorization code and return its bound fields.\n *\n * Validates signature (via the public JWK selected by `kid`), `iss`, the\n * `oauth:code` audience, and `exp`. Every failure — expired, wrong-audience (e.g.\n * an access token), wrong-issuer, tampered, bad-signature, or no configured key —\n * throws an {@link AuthorizationCodeError} (`invalid_grant`).\n *\n * The returned `jti` is the one-time identifier the token endpoint records to\n * enforce single-use on top of the short expiry (replay guard).\n */\nexport async function verifyCode(\n loadSigningKey: McpSigningKeyProvider,\n code: string,\n options: VerifyCodeOptions,\n): Promise<VerifiedAuthorizationCode> {\n const key = await loadSigningKey();\n if (!key) {\n // No signing key configured → nothing can verify (safe-by-default).\n throw new AuthorizationCodeError(\"no signing key configured\");\n }\n\n const publicKey = await importJWK(key.publicJwk, SIGNING_ALG);\n\n let payload: JWTPayload;\n try {\n const result = await jwtVerify(code, publicKey, {\n algorithms: [SIGNING_ALG],\n issuer: issuer(options.origin),\n audience: AUTHORIZATION_CODE_AUDIENCE,\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n currentDate: options.now === undefined ? undefined : new Date(options.now),\n });\n payload = result.payload;\n } catch {\n // jose throws on bad signature, wrong iss/aud, expiry, malformed token,\n // unknown key — all map to a single opaque `invalid_grant`.\n throw new AuthorizationCodeError(\"code verification failed\");\n }\n\n return extractBoundFields(payload);\n}\n","import { createHash, randomBytes, randomUUID } from \"node:crypto\";\n\nimport type {\n OAuthClientStore,\n StoredOAuthClient,\n TokenEndpointAuthMethod,\n} from \"./stores\";\n\n/**\n * Client registration and the open-redirect guard (12-23, ported from\n * the origin host's `lib/mcp/oauth/clients.ts`).\n *\n * A registered client is an external host (a Claude.ai / ChatGPT connector) from\n * RFC 7591 dynamic client registration, or a static registration an operator\n * created out of band. A confidential client's secret is generated HERE, returned\n * exactly once, and stored only as a SHA-256 hash — it is never persisted in\n * plaintext, never logged, and never re-derivable.\n */\n\n/** Default grant types for a registered client (OAuth 2.1 code + refresh). */\nconst DEFAULT_GRANT_TYPES = [\"authorization_code\", \"refresh_token\"] as const;\n\n/** Bytes of entropy for a generated confidential-client secret (→ 64 hex). */\nconst CLIENT_SECRET_BYTES = 32;\n\n/** RFC 7591 registration input (the durable subset the store persists). */\nexport interface RegisterClientInput {\n /** The exact-match redirect-uri allowlist (open-redirect guard). */\n redirectUris: string[];\n clientName?: string | null;\n /**\n * `none` (public PKCE client, the default) or `client_secret_basic`\n * (confidential — a secret is generated and its hash stored).\n */\n tokenEndpointAuthMethod?: TokenEndpointAuthMethod;\n /** Grant types; defaults to authorization_code + refresh_token. */\n grantTypes?: string[];\n scopes: string[];\n}\n\n/**\n * The registration RESULT. `clientSecret` is present (plaintext, ONCE) only for a\n * confidential client — it is never stored and never returned again.\n */\nexport interface RegisteredClient {\n clientId: string;\n clientSecret?: string;\n redirectUris: string[];\n clientName: string | null;\n tokenEndpointAuthMethod: TokenEndpointAuthMethod;\n grantTypes: string[];\n scopes: string[];\n}\n\n/** SHA-256 hex digest — the at-rest form of the client secret. */\nexport function hashSecret(secret: string): string {\n return createHash(\"sha256\").update(secret).digest(\"hex\");\n}\n\n/**\n * Register an OAuth client under a generated `clientId`. For a confidential\n * client a random secret is generated and its hash stored; the plaintext is\n * returned once.\n */\nexport async function registerClient(\n store: OAuthClientStore,\n input: RegisterClientInput,\n): Promise<RegisteredClient> {\n const clientId = randomUUID();\n const authMethod: TokenEndpointAuthMethod = input.tokenEndpointAuthMethod ?? \"none\";\n const grantTypes = input.grantTypes ?? [...DEFAULT_GRANT_TYPES];\n\n // Only a confidential client gets a secret; a public PKCE client has none.\n const clientSecret =\n authMethod === \"client_secret_basic\"\n ? randomBytes(CLIENT_SECRET_BYTES).toString(\"hex\")\n : undefined;\n\n const row = await store.create({\n clientId,\n clientSecretHash: clientSecret ? hashSecret(clientSecret) : null,\n redirectUris: input.redirectUris,\n clientName: input.clientName ?? null,\n tokenEndpointAuthMethod: authMethod,\n grantTypes,\n scopes: input.scopes,\n });\n\n return {\n clientId: row.clientId,\n ...(clientSecret ? { clientSecret } : {}),\n redirectUris: row.redirectUris,\n clientName: row.clientName,\n tokenEndpointAuthMethod: row.tokenEndpointAuthMethod as TokenEndpointAuthMethod,\n grantTypes: row.grantTypes,\n scopes: row.scopes,\n };\n}\n\n/**\n * Open-redirect guard: a redirect target is accepted ONLY when it EXACTLY equals a\n * registered `redirect_uri`. No normalization, no prefix, no trailing-slash\n * leniency — an intercepted authorization request must not be steerable to any URI\n * the client did not register.\n */\nexport function matchesRedirectUri(\n client: Pick<StoredOAuthClient, \"redirectUris\">,\n redirectUri: string,\n): boolean {\n if (!redirectUri) return false;\n return client.redirectUris.includes(redirectUri);\n}\n\n/**\n * Provider attribution rules: the canonical root domains that own each host's\n * OAuth callback. A redirect host matches a root only as the exact domain or a\n * real (dot-guarded) subdomain — never a suffix, so `evilchatgpt.com` never\n * matches `chatgpt.com`.\n */\nexport interface ProviderAttributionRule {\n roots: readonly string[];\n provider: string;\n}\n\n/** The origin host's rules, and a sane default for any host talking to the same two. */\nexport const DEFAULT_PROVIDER_ROOTS: readonly ProviderAttributionRule[] = [\n { roots: [\"claude.ai\", \"anthropic.com\"], provider: \"claude\" },\n { roots: [\"chatgpt.com\", \"openai.com\"], provider: \"chatgpt\" },\n];\n\n/** Whether `host` is exactly `root` or a real subdomain of it (dot-guarded). */\nfunction hostMatchesRoot(host: string, root: string): boolean {\n return host === root || host.endsWith(`.${root}`);\n}\n\n/**\n * Best-effort provider attribution from a client's redirect URIs: the host that\n * owns the callback (`claude.ai` → claude, `chatgpt.com` → chatgpt). Returns\n * `null` when nothing matches — the UI then falls back to what the owner\n * completed the flow with, and a self-report (the `announce` path) can attribute\n * it later.\n */\nexport function providerFromRedirectUris(\n redirectUris: readonly string[],\n rules: readonly ProviderAttributionRule[] = DEFAULT_PROVIDER_ROOTS,\n): string | null {\n for (const uri of redirectUris) {\n let host: string;\n try {\n host = new URL(uri).host.toLowerCase();\n } catch {\n continue;\n }\n const match = rules.find((rule) => rule.roots.some((root) => hostMatchesRoot(host, root)));\n if (match) return match.provider;\n }\n return null;\n}\n","/**\n * PKCE (RFC 7636) S256 challenge helpers for the OAuth authorization server\n * (12-23, ported verbatim from the origin host's `lib/mcp/oauth/pkce.ts`).\n *\n * OAuth 2.1 mandates the `S256` code-challenge method and forbids `plain`, so\n * this module computes `BASE64URL(SHA-256(code_verifier))` and compares it to\n * the stored `code_challenge` in constant time. The authorization endpoint\n * binds a `code_challenge` into the stateless authorization code; the token\n * endpoint calls {@link verifyChallenge} with the presented `code_verifier` to\n * prove the redeeming client is the one that started the flow.\n *\n * `plain` is refused (throws {@link UnsupportedChallengeMethodError}) rather\n * than silently accepted: `plain` offers no protection against an intercepted\n * authorization code, which is the exact threat PKCE exists to close.\n */\n\n/** The only PKCE method this server accepts (OAuth 2.1 requires S256). */\nexport const SUPPORTED_CHALLENGE_METHOD = \"S256\";\n\n/**\n * PKCE code-challenge methods, including the rejected legacy `plain`.\n *\n * @public exported because it is a parameter type of the exported\n * {@link verifyChallenge}.\n */\nexport type CodeChallengeMethod = \"S256\" | \"plain\";\n\n/** Thrown when a caller supplies a challenge method other than `S256`. */\nexport class UnsupportedChallengeMethodError extends Error {\n readonly method: string;\n\n constructor(method: string) {\n super(\n `unsupported code_challenge_method '${method}' — only ${SUPPORTED_CHALLENGE_METHOD} is allowed`,\n );\n this.name = \"UnsupportedChallengeMethodError\";\n this.method = method;\n }\n}\n\n/** Encode raw bytes as unpadded base64url (RFC 7636 challenge encoding). */\nfunction base64UrlEncode(bytes: Uint8Array): string {\n // Buffer.toString(\"base64url\") emits the URL-safe alphabet with no padding.\n return Buffer.from(bytes).toString(\"base64url\");\n}\n\n/**\n * Compute the RFC 7636 S256 challenge for a `code_verifier`:\n * `BASE64URL(SHA-256(ASCII(verifier)))`.\n */\nexport async function computeChallenge(verifier: string): Promise<string> {\n const data = new TextEncoder().encode(verifier);\n const digest = await crypto.subtle.digest(\"SHA-256\", data);\n return base64UrlEncode(new Uint8Array(digest));\n}\n\n/**\n * Constant-time string comparison over the base64url challenge bytes.\n *\n * Returns `false` immediately on a length mismatch (lengths are not secret);\n * for equal-length inputs every byte is compared so the timing does not reveal\n * how many leading characters matched.\n */\nfunction constantTimeEquals(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n let mismatch = 0;\n for (let i = 0; i < a.length; i += 1) {\n mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);\n }\n return mismatch === 0;\n}\n\n/**\n * Verify a presented `code_verifier` against a stored `code_challenge`.\n *\n * Recomputes the S256 challenge from `verifier` and constant-time-compares it\n * to `storedChallenge`. Returns `true` on a match, `false` on a mismatch (or an\n * empty stored challenge). Any method other than `S256` throws\n * {@link UnsupportedChallengeMethodError} — `plain` is never accepted.\n */\nexport async function verifyChallenge(\n verifier: string,\n storedChallenge: string,\n method: CodeChallengeMethod | string = SUPPORTED_CHALLENGE_METHOD,\n): Promise<boolean> {\n if (method !== SUPPORTED_CHALLENGE_METHOD) {\n throw new UnsupportedChallengeMethodError(method);\n }\n if (!storedChallenge) return false;\n\n const computed = await computeChallenge(verifier);\n return constantTimeEquals(computed, storedChallenge);\n}\n","/**\n * Single-use guard for the stateless authorization codes (12-23, ported from\n * the origin host's `lib/mcp/oauth/token-replay.ts`).\n *\n * A code is a signed blob with a `jti`, so \"already redeemed\" has to be remembered\n * somewhere. The in-process option remembers it IN THIS PROCESS: a small map of\n * `jti → expiry`, self-pruning once the code that carried it would have expired\n * anyway, so the set never grows without bound.\n *\n * ⚠️ MULTI-INSTANCE LIMITATION (best-effort single-use): that map lives in ONE\n * process. On a horizontally-scaled deployment a code could be replayed against a\n * second instance that has not yet seen the `jti`, within the ≤60s code lifetime.\n * Single-use is therefore strictly guaranteed only on a SINGLE instance — which is\n * why choosing it is explicit and cannot happen by omission: `codeReplay` has no\n * default, so a host either names a shared store or types `'in-process'`. BEFORE\n * running this surface on more than one instance, pass a `codeReplay` store backed\n * by something shared and atomic — a short-TTL row with a unique constraint, or a\n * distributed cache with an atomic set-if-absent. The port exists precisely so\n * that is a config change rather than a patch to the grant handler.\n */\n\nexport interface CodeReplayStore {\n /**\n * Record a code's `jti` as consumed. `false` means it was ALREADY recorded (a\n * replay); `true` is the first redemption. Must be atomic to be a real guard.\n */\n consume(jti: string, nowMs: number): Promise<boolean> | boolean;\n}\n\n/** Retain slightly beyond the 60s code TTL to cover the verify clock tolerance. */\nconst RETENTION_MS = 90_000;\n\n/**\n * The in-process store — correct on ONE instance, see the caveat above. Reached by\n * passing `codeReplay: 'in-process'`, which is a required acknowledgement rather\n * than a default: the config has no default for this field precisely because the\n * only possible one would fail open on a multi-pod deployment.\n */\nexport function inProcessCodeReplayStore(): CodeReplayStore {\n const usedJtis = new Map<string, number>();\n return {\n consume(jti: string, nowMs: number): boolean {\n for (const [seen, expiresAt] of usedJtis) {\n if (expiresAt <= nowMs) usedJtis.delete(seen);\n }\n if (usedJtis.has(jti)) return false;\n usedJtis.set(jti, nowMs + RETENTION_MS);\n return true;\n },\n };\n}\n","import { SignJWT, jwtVerify, importJWK, type JWTPayload } from \"jose\";\n\nimport { issuer, resourceAudience, DEFAULT_MCP_RESOURCE_PATH, type McpScope } from \"./config\";\nimport { SIGNING_ALG, type McpSigningKeyProvider } from \"./keys\";\n\n/**\n * JWT access-token issuer + verifier (12-23, ported from the origin host's\n * `lib/mcp/oauth/jwt.ts` — behaviour unchanged; the signing key arrives through a\n * provider and the resource path is config).\n *\n * The access token is a short-lived, ES256-signed JWT bound to the signed-in\n * user. It carries the claims the resource server checks LOCALLY against the\n * published JWKS (no introspection round-trip): `iss` (the issuer origin), `aud`\n * (`${origin}${resourcePath}`), `sub`, `email`, `scope` (space-delimited), `iat`,\n * `exp` (short TTL), and `jti`; the JWT header carries `kid` so the verifier can\n * select the public key during rotation.\n *\n * Failures are typed so the caller maps them to the right OAuth challenge\n * (`invalid_token` vs `insufficient_scope`).\n */\n\n/** Access-token lifetime — short-lived (15 min) per the spec. */\nexport const ACCESS_TOKEN_TTL_SECONDS = 15 * 60;\n\n/** Clock skew tolerated on `exp`/`iat` validation, in seconds. */\nconst CLOCK_TOLERANCE_SECONDS = 5;\n\n/** The identity a verified access token resolves to. */\nexport interface VerifiedAccessToken {\n email: string;\n subject: string;\n scopes: string[];\n}\n\n/** Distinct verification failure reasons the caller maps to OAuth challenges. */\nexport type AccessTokenErrorCode = \"invalid_token\" | \"insufficient_scope\";\n\n/** A typed verification failure — `code` drives the `WWW-Authenticate` challenge. */\nexport class AccessTokenError extends Error {\n readonly code: AccessTokenErrorCode;\n\n constructor(code: AccessTokenErrorCode, message?: string) {\n super(message ?? code);\n this.name = \"AccessTokenError\";\n this.code = code;\n }\n}\n\n/** Inputs bound into a minted access token. */\nexport interface SignAccessTokenInput {\n email: string;\n subject: string;\n scopes: readonly McpScope[] | readonly string[];\n origin: string;\n /** Where the MCP resource is mounted. Default `/api/mcp`. */\n resourcePath?: string;\n /** Token lifetime in seconds. Default 15 minutes. */\n ttlSeconds?: number;\n}\n\n/** Deterministic-clock option shared by mint + verify. */\ninterface ClockOption {\n /** Epoch milliseconds; defaults to `Date.now()`. Injected for deterministic tests. */\n now?: number;\n}\n\n/** The full access-token claim set (beyond the registered JWT claims). */\ninterface AccessTokenClaims extends JWTPayload {\n email: string;\n scope: string;\n}\n\nfunction nowSeconds(now?: number): number {\n return Math.floor((now ?? Date.now()) / 1000);\n}\n\n/**\n * Mint an ES256-signed access token bound to the user.\n *\n * Returns `null` when no signing key is configured (safe-by-default: the AS\n * refuses to issue rather than falling back to a weaker mode). Sets the `kid`\n * header from the loaded key so the verifier can resolve the public JWK during\n * rotation.\n */\nexport async function signAccessToken(\n loadSigningKey: McpSigningKeyProvider,\n input: SignAccessTokenInput,\n options?: ClockOption,\n): Promise<string | null> {\n const key = await loadSigningKey();\n if (!key) return null;\n\n const iat = nowSeconds(options?.now);\n const exp = iat + (input.ttlSeconds ?? ACCESS_TOKEN_TTL_SECONDS);\n const scope = input.scopes.join(\" \");\n\n return new SignJWT({ email: input.email, scope } satisfies AccessTokenClaims)\n .setProtectedHeader({ alg: SIGNING_ALG, kid: key.kid })\n .setIssuer(issuer(input.origin))\n .setAudience(resourceAudience(input.origin, input.resourcePath ?? DEFAULT_MCP_RESOURCE_PATH))\n .setSubject(input.subject)\n .setIssuedAt(iat)\n .setExpirationTime(exp)\n .setJti(crypto.randomUUID())\n .sign(key.privateKey);\n}\n\n/** Options for {@link verifyAccessToken}. */\nexport interface VerifyAccessTokenOptions extends ClockOption {\n /** The deployment origin — derives the expected `iss` and `aud`. */\n origin: string;\n /** Where the MCP resource is mounted. Default `/api/mcp`. */\n resourcePath?: string;\n /** When set, the token must carry this scope or verification fails `insufficient_scope`. */\n requiredScope?: McpScope | string;\n}\n\n/** Parse the space-delimited `scope` claim into a de-duplicated string array. */\nfunction parseScopes(scope: unknown): string[] {\n if (typeof scope !== \"string\" || scope.trim() === \"\") return [];\n return [...new Set(scope.trim().split(/\\s+/))];\n}\n\n/**\n * Verify a bearer access token locally against the published JWKS public key.\n *\n * Checks signature (via the public JWK selected by the token's `kid`), `iss`,\n * `aud`, and `exp`. When `requiredScope` is supplied, also enforces scope. On\n * success returns `{ email, subject, scopes }`; on failure throws an\n * {@link AccessTokenError} whose `code` distinguishes `invalid_token` (bad\n * signature / wrong issuer / wrong audience / expired / malformed / unconfigured\n * key) from `insufficient_scope` (a valid token lacking the required scope).\n */\n/**\n * The cryptographic half: signature, `iss`, `aud`, `exp`.\n *\n * Every jose failure — bad signature, wrong issuer, wrong audience, expiry,\n * malformed token, unknown key — collapses into ONE opaque `invalid_token`. A\n * message naming the failed claim would be an oracle for the next attempt.\n */\nasync function verifiedPayload(\n loadSigningKey: McpSigningKeyProvider,\n token: string,\n options: VerifyAccessTokenOptions,\n): Promise<JWTPayload> {\n const key = await loadSigningKey();\n // No signing key configured → nothing can verify (safe-by-default).\n if (!key) throw new AccessTokenError(\"invalid_token\", \"no signing key configured\");\n\n try {\n const { payload } = await jwtVerify(token, await importJWK(key.publicJwk, SIGNING_ALG), {\n algorithms: [SIGNING_ALG],\n issuer: issuer(options.origin),\n audience: resourceAudience(options.origin, options.resourcePath ?? DEFAULT_MCP_RESOURCE_PATH),\n clockTolerance: CLOCK_TOLERANCE_SECONDS,\n currentDate: options.now === undefined ? undefined : new Date(options.now),\n });\n return payload;\n } catch {\n throw new AccessTokenError(\"invalid_token\", \"token verification failed\");\n }\n}\n\nexport async function verifyAccessToken(\n loadSigningKey: McpSigningKeyProvider,\n token: string,\n options: VerifyAccessTokenOptions,\n): Promise<VerifiedAccessToken> {\n const payload = await verifiedPayload(loadSigningKey, token, options);\n\n const email = typeof payload.email === \"string\" ? payload.email : null;\n const subject = typeof payload.sub === \"string\" ? payload.sub : null;\n if (!email || !subject) {\n throw new AccessTokenError(\"invalid_token\", \"missing subject or email claim\");\n }\n\n const scopes = parseScopes(payload.scope);\n if (options.requiredScope && !scopes.includes(options.requiredScope)) {\n throw new AccessTokenError(\n \"insufficient_scope\",\n `token lacks required scope '${options.requiredScope}'`,\n );\n }\n\n return { email, subject, scopes };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\nimport type { NewRefreshToken, RefreshTokenStore, StoredRefreshToken } from \"./stores\";\n\n/**\n * Refresh-token issue + rotation (12-23, ported from the origin host's\n * `lib/mcp/oauth/refresh.ts` — behaviour unchanged; Prisma calls became the\n * `RefreshTokenStore` port).\n *\n * Refresh tokens are opaque high-entropy strings; only their SHA-256 HASH is ever\n * stored — the plaintext is returned once at issue/rotate time and never\n * persisted, never logged.\n *\n * Rotation-on-use with replay protection:\n * - {@link issueRefreshToken} mints a root token bound to email + sub + client\n * + scopes;\n * - {@link rotateRefreshToken} consumes a token: it issues a NEW token chained\n * via `rotatedFrom` and revokes the parent, so a token is single-use;\n * - reuse of an already-rotated/revoked token is a REPLAY: rejected, AND the\n * whole lineage (every ancestor + descendant reachable through `rotatedFrom`)\n * is revoked — the OAuth 2.1 refresh-token replay rule;\n * - CONCURRENT reuse is the same event and gets the same answer. The store's\n * `rotate` is a claim-once write, so of two simultaneous rotations of one\n * parent exactly one is issued a successor and the other is treated as the\n * replay it is. Without that, replay protection would be bypassable by\n * WINNING a race instead of arriving second (see `RefreshTokenStore.rotate`);\n * - rotate may only NARROW scope (new ⊆ original); broadening is rejected and\n * nothing new is stored.\n */\n\n/** Bytes of entropy per opaque refresh token (→ 64 hex chars). */\nconst REFRESH_TOKEN_BYTES = 32;\n\n/** Refresh-token lifetime — long-lived relative to the 15-min access token. */\nexport const REFRESH_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days\n\n/** The single failure discriminator surfaced to the token endpoint. */\nexport type RefreshTokenErrorCode = \"invalid_grant\" | \"invalid_scope\";\n\n/**\n * A typed refresh-token failure. Every rejection — unknown, expired, revoked,\n * already-rotated (replay), wrong client, or a scope-broadening request —\n * surfaces as a discriminated error the token endpoint maps to the RFC 6749\n * error JSON.\n */\nexport class RefreshTokenError extends Error {\n readonly code: RefreshTokenErrorCode;\n\n constructor(code: RefreshTokenErrorCode, message?: string) {\n super(message ?? code);\n this.name = \"RefreshTokenError\";\n this.code = code;\n }\n}\n\n/** The result of issuing/rotating: the plaintext token (once) + bound scopes. */\nexport interface IssuedRefreshToken {\n /** The opaque plaintext refresh token — returned once, never persisted. */\n refreshToken: string;\n scopes: string[];\n}\n\n/** SHA-256 hex digest — the at-rest form of an opaque refresh token. */\nexport function hashToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\n/** Generate a fresh opaque refresh token (high-entropy hex). */\nfunction generateToken(): string {\n return randomBytes(REFRESH_TOKEN_BYTES).toString(\"hex\");\n}\n\nexport interface RefreshTokenContext {\n store: RefreshTokenStore;\n /** Lifetime of a newly stored token. Default 30 days. */\n ttlMs?: number;\n}\n\nfunction expiryOf(context: RefreshTokenContext): Date {\n return new Date(Date.now() + (context.ttlMs ?? REFRESH_TOKEN_TTL_MS));\n}\n\n/**\n * Issue a fresh (root) refresh token bound to a user (email + OAuth `sub`) +\n * client + scopes. The plaintext is returned once; only its hash is stored.\n */\nexport async function issueRefreshToken(\n context: RefreshTokenContext,\n binding: { userEmail: string; userSub: string; clientId: string; scopes: string[] },\n): Promise<IssuedRefreshToken> {\n const refreshToken = generateToken();\n const row: NewRefreshToken = {\n tokenHash: hashToken(refreshToken),\n userEmail: binding.userEmail,\n userSub: binding.userSub,\n clientId: binding.clientId,\n scopes: binding.scopes,\n expiresAt: expiryOf(context),\n rotatedFrom: null,\n };\n await context.store.create(row);\n return { refreshToken, scopes: binding.scopes };\n}\n\n/**\n * A pre-built O(1)-lookup index of one `(userEmail, clientId)` token family:\n * `byHash` resolves a hash to its row (to walk ancestors via `rotatedFrom`), and\n * `childrenOf` is the reverse index mapping a parent hash to its direct successor\n * hashes (to walk descendants). Both are built in a single pass so the lineage\n * traversal never re-scans the family (no O(n²) inner loop).\n */\ninterface LineageIndex {\n byHash: Map<string, StoredRefreshToken>;\n childrenOf: Map<string, string[]>;\n}\n\nfunction buildLineageIndex(family: StoredRefreshToken[]): LineageIndex {\n const byHash = new Map<string, StoredRefreshToken>();\n const childrenOf = new Map<string, string[]>();\n for (const row of family) {\n byHash.set(row.tokenHash, row);\n if (!row.rotatedFrom) continue;\n const siblings = childrenOf.get(row.rotatedFrom) ?? [];\n siblings.push(row.tokenHash);\n childrenOf.set(row.rotatedFrom, siblings);\n }\n return { byHash, childrenOf };\n}\n\n/**\n * Collect every token hash reachable from `seedHash` — its ancestors (via\n * `rotatedFrom`) and its descendants (via the reverse index) — by a BFS over the\n * pre-built index. Each neighbour lookup is O(1), so the walk is linear in the\n * family size.\n */\nfunction collectLineage(index: LineageIndex, seedHash: string): Set<string> {\n const lineage = new Set<string>();\n const queue = [seedHash];\n while (queue.length > 0) {\n const hash = queue.shift();\n if (!hash || lineage.has(hash)) continue;\n lineage.add(hash);\n\n const parent = index.byHash.get(hash)?.rotatedFrom ?? null;\n if (parent && !lineage.has(parent)) queue.push(parent);\n\n const children = (index.childrenOf.get(hash) ?? []).filter((child) => !lineage.has(child));\n queue.push(...children);\n }\n return lineage;\n}\n\n/**\n * Walk a token's rotation lineage (both directions) and revoke every token in it.\n * Called on replay detection, so a leaked refresh token — once reused —\n * invalidates the entire chain it belongs to.\n */\nasync function revokeLineage(\n context: RefreshTokenContext,\n scopedTo: Pick<StoredRefreshToken, \"userEmail\" | \"clientId\">,\n seedHash: string,\n): Promise<void> {\n // The lineage is confined to one (userEmail, clientId) pair, so load that set\n // once and walk the `rotatedFrom` links in memory — a small, bounded chain.\n const family = await context.store.listFamily(scopedTo.userEmail, scopedTo.clientId);\n const lineage = collectLineage(buildLineageIndex(family), seedHash);\n await context.store.revokeHashes([...lineage], new Date());\n}\n\n/** Reject any requested scope not already on the token (narrow-only). */\nfunction narrowedScopes(current: StoredRefreshToken, requested?: string[]): string[] {\n const scopes = requested ?? current.scopes;\n const original = new Set(current.scopes);\n for (const scope of scopes) {\n if (!original.has(scope)) {\n throw new RefreshTokenError(\n \"invalid_scope\",\n `scope '${scope}' broadens the refresh token grant`,\n );\n }\n }\n return scopes;\n}\n\n/**\n * Rotate a refresh token on use: validate it (must exist, be BOUND to the\n * presenting client, be unexpired, unrevoked and un-rotated), then issue a NEW\n * token chained via `rotatedFrom` and revoke the consumed one. Optionally NARROW\n * scope; a broadening request is `invalid_scope`.\n *\n * Client binding (OAuth 2.1 §4.3 / RFC 6749 §10.4) is checked BEFORE any rotation\n * or revocation, so client A can never redeem client B's refresh token — nor\n * silently consume B's token by trying: the token stays live for its rightful\n * owner.\n */\nexport async function rotateRefreshToken(\n context: RefreshTokenContext,\n plaintext: string,\n expectedClientId: string,\n newScopes?: string[],\n): Promise<IssuedRefreshToken> {\n const tokenHash = hashToken(plaintext);\n const current = await context.store.findByHash(tokenHash);\n\n if (!current) {\n throw new RefreshTokenError(\"invalid_grant\", \"unknown refresh token\");\n }\n if (current.clientId !== expectedClientId) {\n throw new RefreshTokenError(\n \"invalid_grant\",\n \"refresh token was not issued to this client\",\n );\n }\n // Expired → reject (not a replay; no lineage revocation needed beyond the\n // expiry itself).\n if (current.expiresAt.getTime() <= Date.now()) {\n throw new RefreshTokenError(\"invalid_grant\", \"refresh token expired\");\n }\n // Already revoked OR already used as the parent of a rotation → REPLAY. Revoke\n // the whole lineage and reject.\n if (current.revokedAt || (await context.store.hasSuccessor(tokenHash))) {\n await replay(context, current, tokenHash);\n }\n\n const scopes = narrowedScopes(current, newScopes);\n const successorPlaintext = generateToken();\n const claimed = await context.store.rotate(\n {\n tokenHash: hashToken(successorPlaintext),\n userEmail: current.userEmail,\n userSub: current.userSub,\n clientId: current.clientId,\n scopes,\n expiresAt: expiryOf(context),\n rotatedFrom: tokenHash,\n },\n tokenHash,\n new Date(),\n );\n // The checks above are a READ, so a concurrent rotation of the same parent can\n // pass them too; `rotate` is the serialization point and it hands the claim to\n // exactly one caller. Losing it is the SAME event as the replay branch above —\n // one token used twice — so it gets the same answer, deliberately: reject, and\n // revoke the lineage including the winner's fresh successor. Rejecting without\n // revoking would leave a race-winning attacker holding a live family, which is\n // the whole attack; and a client that legitimately double-submits already loses\n // its family in the sequential case, so this is consistent rather than harsher.\n if (!claimed) await replay(context, current, tokenHash);\n\n return { refreshToken: successorPlaintext, scopes };\n}\n\n/** Detected reuse: revoke the whole lineage and reject. Never returns. */\nasync function replay(\n context: RefreshTokenContext,\n current: StoredRefreshToken,\n tokenHash: string,\n): Promise<never> {\n await revokeLineage(context, current, tokenHash);\n throw new RefreshTokenError(\n \"invalid_grant\",\n \"refresh token already used (replay) — lineage revoked\",\n );\n}\n\n/** The stable identity a refresh token is bound to. */\nexport interface RefreshTokenIdentity {\n /** The user's email — the identity the AS binds to and route guards resolve by. */\n userEmail: string;\n /** The original OAuth subject, kept stable across every rotation. */\n userSub: string;\n}\n\n/**\n * Resolve the identity (`email` + original OAuth `sub`) a refresh token is bound\n * to. The token endpoint uses this after rotation to mint the successor access\n * token with the correct email AND the SAME stable `sub` as the initial token (no\n * re-consent, no `sub` drift). `null` if the row is unexpectedly absent.\n */\nexport async function getRefreshTokenIdentity(\n context: RefreshTokenContext,\n plaintext: string,\n): Promise<RefreshTokenIdentity | null> {\n const row = await context.store.findByHash(hashToken(plaintext));\n return row ? { userEmail: row.userEmail, userSub: row.userSub } : null;\n}\n","import {\n DEFAULT_MCP_RESOURCE_PATH,\n MCP_SUPPORTED_SCOPES,\n originFromRequest,\n} from \"./config\";\nimport {\n inProcessCodeReplayStore,\n type CodeReplayStore,\n} from \"./code-replay\";\nimport { ACCESS_TOKEN_TTL_SECONDS } from \"./access-token\";\nimport { REFRESH_TOKEN_TTL_MS } from \"./refresh\";\nimport { loadSigningKeyFromEnv, type McpSigningKeyProvider } from \"./keys\";\nimport type { ProviderAttributionRule } from \"./clients\";\nimport type { McpOauthStores, StoredOAuthClient } from \"./stores\";\n\n/**\n * The config seam of the authorization server, and its resolved form (12-23).\n *\n * Everything a HOST knows and the package cannot: who the signed-in caller is,\n * where the data lives, which origins are trusted, whether the surface is turned\n * on at all, and where its endpoints are mounted. Everything else — the RFC wire,\n * PKCE, rotation, replay, the discovery documents — is the package's.\n */\n\n/** The identity an authorize request binds a code to. From the SESSION only. */\nexport interface McpOauthSession {\n /**\n * The OAuth subject (the origin host passes the Google `sub`, falling back to the\n * email). Carried through every rotation so a refreshed token keeps the same\n * stable `sub`.\n */\n subject: string;\n /** The signed-in user's email — the identity the AS binds to. */\n email: string;\n}\n\n/** Where each endpoint of the surface lives, from the origin root. */\nexport interface McpOauthPaths {\n authorize: string;\n token: string;\n register: string;\n jwks: string;\n authorizationServerMetadata: string;\n protectedResourceMetadata: string;\n}\n\nexport const DEFAULT_OAUTH_PATHS: McpOauthPaths = {\n // the origin host's paths, and the ones the RFC 8414 document has always advertised.\n authorize: \"/api/oauth/authorize\",\n token: \"/api/oauth/token\",\n register: \"/api/oauth/register\",\n jwks: \"/.well-known/jwks.json\",\n authorizationServerMetadata: \"/.well-known/oauth-authorization-server\",\n protectedResourceMetadata: \"/.well-known/oauth-protected-resource\",\n};\n\n/** How a connection's liveness is recorded on a successful grant. */\nexport interface McpConnectionRecording {\n /**\n * The host's DB user id for a token's email, or `null` when there is no user row\n * yet (recording is then skipped — email is the identity, not the id).\n */\n resolveUserId: (email: string) => Promise<string | null> | string | null;\n /** Provider attribution rules; defaults to claude/chatgpt roots. */\n providerRules?: readonly ProviderAttributionRule[];\n /** Don't rewrite on every grant — refresh liveness at most this often. */\n activityThrottleMs?: number;\n}\n\nexport interface McpOauthConfig {\n /** Where the three owned tables live (see `./stores.ts`). */\n stores: McpOauthStores;\n /**\n * Resolve the caller's COOKIE SESSION for the authorize endpoint. `null` sends\n * the caller through the host's sign-in flow; no code is ever minted for an\n * unauthenticated request, and a client can never supply the identity itself.\n */\n resolveSession: (request: Request) => Promise<McpOauthSession | null> | McpOauthSession | null;\n /**\n * The operator gate. `false` makes the whole surface inert — authorize/token/jwks\n * answer 404 and registration answers 403 — which is how the origin host ships it OFF\n * by default (`MCP_BEARER_ENABLED`). Default: enabled (mounting is the opt-in).\n */\n enabled?: boolean | (() => boolean);\n /**\n * Signing material. Default: the env-backed provider with the origin host's variable\n * names. `null` from the provider means \"not provisioned\": nothing is minted and\n * the JWKS answers 503 rather than falling back to a weaker mode.\n */\n signingKey?: McpSigningKeyProvider;\n /**\n * The trusted PUBLIC origin allowlist — REQUIRED behind a reverse proxy, where\n * the server sees only its internal bind. The FIRST entry is canonical. With\n * none configured a forwarded host is never trusted (see `resolveTrustedOrigin`).\n */\n trustedOrigins?: readonly string[];\n /** Scopes the AS advertises and validates against. Default `mcp:read mcp:write`. */\n scopes?: readonly string[];\n /** Where the MCP resource is mounted — the token audience. Default `/api/mcp`. */\n resourcePath?: string;\n /** Endpoint paths, if the host mounts them somewhere else. */\n paths?: Partial<McpOauthPaths>;\n /** Where an unauthenticated authorize request is sent. Default `/login`. */\n loginPath?: string;\n /**\n * The query parameter carrying the post-login return path. Default\n * `callbackUrl` (Auth.js's name).\n */\n loginCallbackParam?: string;\n accessTokenTtlSeconds?: number;\n refreshTokenTtlMs?: number;\n /**\n * The single-use guard for authorization codes — REQUIRED, and required on\n * purpose. Pass a shared atomic store, or the literal `'in-process'` to accept\n * the single-instance limitation explicitly.\n *\n * There is deliberately NO default, because a default here would be the only one\n * in this config that fails OPEN. Every other one fails closed: no signing key\n * mints nothing and answers JWKS 503; `enabled: false` is 404 everywhere; an\n * empty `trustedOrigins` never trusts a forwarded host. An in-process default\n * instead silently permits cross-instance code replay — against an OAuth 2.1\n * MUST, on the very deployment shape a reusable package exists for (two pods\n * behind one load balancer), with nothing in the types to notice. Scaling out\n * must not be able to weaken the guard without somebody having typed something.\n */\n codeReplay: CodeReplayStore | \"in-process\";\n /**\n * Approve an authorize request before a code is minted — the CONSENT step.\n *\n * Registration is open whenever `enabled` is true (RFC 7591), so without an\n * approval step anyone may register a client carrying their OWN redirect URI and\n * their OWN scope ceiling, send a signed-in admin one link, and have the endpoint\n * mint them a code with no interaction: the redirect URI is exact-matched against\n * the attacker's own registration and the scope ceiling is the attacker's too, so\n * every other guard here holds and none of them helps.\n *\n * Until a host supplies this, `authorize` REFUSES any client it cannot see the\n * operator behind — i.e. any client not named in {@link preApprovedClientIds}.\n * Return `false` to deny (the caller gets an `access_denied` redirect, exactly as\n * a human refusal would).\n */\n resolveApproval?: (\n request: Request,\n client: StoredOAuthClient,\n scopes: readonly string[],\n ) => Promise<boolean> | boolean;\n /**\n * Client ids the OPERATOR registered, exempt from the approval gate above — the\n * escape hatch for a host that ships its own first-party clients and has no\n * consent screen to offer. Anything NOT listed here is treated as dynamically\n * registered, i.e. as attacker-controllable.\n */\n preApprovedClientIds?: readonly string[];\n /** Liveness recording on a grant; omit to record nothing. */\n connections?: McpConnectionRecording;\n}\n\n/** The config with every default applied — what the handlers actually read. */\nexport interface McpOauthContext {\n stores: McpOauthStores;\n resolveSession: McpOauthConfig[\"resolveSession\"];\n enabled: () => boolean;\n signingKey: McpSigningKeyProvider;\n trustedOrigins: readonly string[];\n scopes: readonly string[];\n resourcePath: string;\n paths: McpOauthPaths;\n loginPath: string;\n loginCallbackParam: string;\n accessTokenTtlSeconds: number;\n refreshTokenTtlMs: number;\n codeReplay: CodeReplayStore;\n /**\n * The resolved consent decision for one authorize request. Always present: with\n * no host seam it refuses every client the operator did not pre-approve, so the\n * handler has no \"unset\" case to forget.\n */\n approve: (\n request: Request,\n client: StoredOAuthClient,\n scopes: readonly string[],\n ) => Promise<boolean>;\n connections?: McpConnectionRecording;\n /** The trusted public origin for THIS request (issuance and verification agree). */\n originOf: (request: Request) => string;\n}\n\n/** The surface's own shape: what it advertises, where it lives, how long it lasts. */\nfunction resolveSurface(\n config: McpOauthConfig,\n): Pick<\n McpOauthContext,\n | \"scopes\"\n | \"resourcePath\"\n | \"paths\"\n | \"loginPath\"\n | \"loginCallbackParam\"\n | \"accessTokenTtlSeconds\"\n | \"refreshTokenTtlMs\"\n> {\n return {\n scopes: config.scopes ?? [...MCP_SUPPORTED_SCOPES],\n resourcePath: config.resourcePath ?? DEFAULT_MCP_RESOURCE_PATH,\n paths: { ...DEFAULT_OAUTH_PATHS, ...config.paths },\n loginPath: config.loginPath ?? \"/login\",\n loginCallbackParam: config.loginCallbackParam ?? \"callbackUrl\",\n accessTokenTtlSeconds: config.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS,\n refreshTokenTtlMs: config.refreshTokenTtlMs ?? REFRESH_TOKEN_TTL_MS,\n };\n}\n\nexport function resolveMcpOauthConfig(config: McpOauthConfig): McpOauthContext {\n const enabled = config.enabled ?? true;\n const trustedOrigins = config.trustedOrigins ?? [];\n return {\n stores: config.stores,\n resolveSession: config.resolveSession,\n // Mounting is the opt-in, so the gate defaults to ON; a host that ships the\n // surface dark passes its own flag (the origin host: `MCP_BEARER_ENABLED`).\n enabled: typeof enabled === \"function\" ? enabled : () => enabled,\n // `null` from the provider means \"not provisioned\": nothing is minted and the\n // JWKS answers 503 rather than falling back to a weaker mode.\n signingKey: config.signingKey ?? loadSigningKeyFromEnv(),\n trustedOrigins,\n ...resolveSurface(config),\n // `'in-process'` is an ACKNOWLEDGEMENT, not a default — see the field's docs.\n codeReplay:\n config.codeReplay === \"in-process\" ? inProcessCodeReplayStore() : config.codeReplay,\n approve: resolveApprover(config),\n ...(config.connections ? { connections: config.connections } : {}),\n originOf: (request) => originFromRequest(request, trustedOrigins),\n };\n}\n\n/**\n * The consent decision, resolved once. A host seam wins; otherwise only a client\n * the OPERATOR named may proceed, so an open registration endpoint cannot mint a\n * code for a client nobody approved.\n */\nfunction resolveApprover(config: McpOauthConfig): McpOauthContext[\"approve\"] {\n const preApproved = new Set(config.preApprovedClientIds ?? []);\n const { resolveApproval } = config;\n return async (request, client, scopes) => {\n if (preApproved.has(client.clientId)) return true;\n if (!resolveApproval) return false;\n return resolveApproval(request, client, scopes);\n };\n}\n\n/** The gate's own answer: 404, so a disabled surface looks like no surface. */\nexport function notFound(): Response {\n return new Response(\"Not Found\", { status: 404 });\n}\n","import { mintCode } from \"./authorization-code\";\nimport { matchesRedirectUri } from \"./clients\";\nimport type { McpOauthContext } from \"./context\";\nimport { SUPPORTED_CHALLENGE_METHOD } from \"./pkce\";\nimport type { StoredOAuthClient } from \"./stores\";\n\n/**\n * The OAuth 2.1 Authorization Code + PKCE authorization endpoint (12-23, ported\n * from the origin host's `app/api/oauth/authorize/route.ts`).\n *\n * It renders no UI: it authenticates the caller against the host's cookie session\n * (through `resolveSession`), validates the request, and either 302-redirects an\n * unauthenticated caller into the host's sign-in flow (so the flow resumes\n * post-login) or, for a signed-in caller with a valid request, mints a stateless\n * authorization code bound to the SESSION identity and 302-redirects back to the\n * client's registered `redirect_uri` with the code and echoed `state`.\n *\n * Security invariants, unchanged:\n * - **Open-redirect prevention:** `client_id` + `redirect_uri` are validated\n * against the registered client BEFORE anything else; an unknown client or a\n * `redirect_uri` that is not an EXACT registered match yields a 400 plain-text\n * response — the endpoint NEVER redirects an error to an unvalidated URI. Only\n * once the URI is validated do other failures redirect back to it.\n * - **Mandatory PKCE S256:** a missing `code_challenge`, or a method other than\n * `S256` (incl. `plain`), is rejected.\n * - **Identity from the session only:** `sub`/`email` come solely from the\n * verified session; a client can never supply the identity via a query param.\n * - **No key, no code:** an unprovisioned signing key is a `server_error`\n * redirect, never a weaker mode.\n */\n\n/** OAuth 2.1 error codes this endpoint can emit on a validated redirect_uri. */\ntype AuthorizeErrorCode =\n | \"invalid_request\"\n | \"unsupported_response_type\"\n | \"invalid_scope\"\n /** The resource owner said no — or nobody was asked and nobody approved. */\n | \"access_denied\"\n | \"server_error\";\n\n/** The parsed, still-untrusted query parameters of an authorize request. */\ninterface AuthorizeParams {\n responseType: string | null;\n clientId: string | null;\n redirectUri: string | null;\n codeChallenge: string | null;\n codeChallengeMethod: string | null;\n scope: string | null;\n state: string | null;\n}\n\nfunction parseParams(url: URL): AuthorizeParams {\n const q = url.searchParams;\n return {\n responseType: q.get(\"response_type\"),\n clientId: q.get(\"client_id\"),\n redirectUri: q.get(\"redirect_uri\"),\n codeChallenge: q.get(\"code_challenge\"),\n codeChallengeMethod: q.get(\"code_challenge_method\"),\n scope: q.get(\"scope\"),\n state: q.get(\"state\"),\n };\n}\n\n/** A 302 response to `location` with no body. */\nfunction redirectTo(location: string): Response {\n return new Response(null, { status: 302, headers: { location } });\n}\n\n/**\n * A 400 plain-text refusal used ONLY when the `redirect_uri`/`client_id` are\n * themselves invalid — i.e. there is no validated URI to safely redirect an error\n * to (the open-redirect guard).\n */\nfunction badRequest(message: string): Response {\n return new Response(message, {\n status: 400,\n headers: { \"content-type\": \"text/plain; charset=utf-8\" },\n });\n}\n\n/**\n * Build an error redirect back to the (already-validated) `redirect_uri`, carrying\n * the OAuth `error` and the echoed `state` per OAuth 2.1 §4.1.2.1.\n */\nfunction errorRedirect(\n redirectUri: string,\n error: AuthorizeErrorCode,\n state: string | null,\n): Response {\n const target = new URL(redirectUri);\n target.searchParams.set(\"error\", error);\n if (state !== null) target.searchParams.set(\"state\", state);\n return redirectTo(target.toString());\n}\n\n/**\n * Whether every space-delimited requested scope is within `allowed`. An\n * empty/absent scope is permitted (the server applies its default), but any present\n * scope must be in `allowed` — and `allowed` is the SPECIFIC CLIENT's registered\n * scopes, so a client that registered for only `mcp:read` cannot request\n * `mcp:write` and be issued a code for it (\"no privilege escalation via metadata\",\n * enforced at authorize rather than trusted at registration).\n */\nfunction scopeIsSupported(scope: string | null, allowed: readonly string[]): boolean {\n if (!scope) return true;\n const requested = scope.split(/\\s+/).filter(Boolean);\n const allowedSet = new Set<string>(allowed);\n return requested.every((candidate) => allowedSet.has(candidate));\n}\n\n/**\n * Resolve + validate the client and its `redirect_uri` FIRST (the open-redirect\n * guard). Returns the validated URI AND the client's registered scopes, or a plain\n * 400 — NEVER a redirect — when the client or URI is unknown/unregistered, so an\n * error is never steered to an unvalidated URI.\n */\nasync function validateClientAndRedirect(\n context: McpOauthContext,\n params: AuthorizeParams,\n): Promise<{ client: StoredOAuthClient; redirectUri: string } | Response> {\n if (!params.clientId) return badRequest(\"invalid_request: missing client_id\");\n if (!params.redirectUri) return badRequest(\"invalid_request: missing redirect_uri\");\n\n const client = await context.stores.clients.findByClientId(params.clientId);\n if (!client) return badRequest(\"invalid_client: unknown client_id\");\n if (!matchesRedirectUri(client, params.redirectUri)) {\n return badRequest(\"invalid_request: redirect_uri is not registered\");\n }\n\n return { client, redirectUri: params.redirectUri };\n}\n\n/**\n * Validate the response_type, mandatory PKCE S256, and the requested scope against\n * the already-validated `redirectUri`. Returns `null` when the request passes, or an\n * error redirect back to the validated URI on the first failure.\n */\nfunction validateAuthorizeRequest(\n params: AuthorizeParams,\n redirectUri: string,\n clientScopes: readonly string[],\n): Response | null {\n const { state } = params;\n\n if (params.responseType !== \"code\") {\n return errorRedirect(redirectUri, \"unsupported_response_type\", state);\n }\n // Mandatory PKCE S256: reject a missing challenge or any non-S256 method.\n if (!params.codeChallenge || params.codeChallengeMethod !== SUPPORTED_CHALLENGE_METHOD) {\n return errorRedirect(redirectUri, \"invalid_request\", state);\n }\n if (!scopeIsSupported(params.scope, clientScopes)) {\n return errorRedirect(redirectUri, \"invalid_scope\", state);\n }\n return null;\n}\n\n/** The already-validated inputs an authorize request resolves to before minting. */\ninterface ValidatedAuthorize {\n /** The registered client — needed by the approval seam, not just its id. */\n client: StoredOAuthClient;\n clientId: string;\n redirectUri: string;\n codeChallenge: string;\n scope: string;\n state: string | null;\n}\n\n/**\n * With a validated request, resolve the authenticated session (identity from the\n * session ONLY) and either send the caller through sign-in, mint the code, or\n * `server_error` when no signing key is configured.\n */\nasync function authenticateAndMint(\n context: McpOauthContext,\n request: Request,\n url: URL,\n origin: string,\n validated: ValidatedAuthorize,\n): Promise<Response> {\n const { redirectUri, state } = validated;\n\n const session = await context.resolveSession(request);\n if (!session?.email) {\n // No session: send the caller through the host's sign-in flow with a callback\n // back to THIS authorize URL so the flow resumes post-login. No code is minted\n // for an unauthenticated request.\n const loginUrl = new URL(context.loginPath, origin);\n loginUrl.searchParams.set(context.loginCallbackParam, url.pathname + url.search);\n return redirectTo(loginUrl.toString());\n }\n\n // CONSENT. A session proves WHO is asking; it never proves they agreed to THIS\n // client holding THESE scopes. Registration is open (RFC 7591), so without this\n // step anyone may register a client carrying their own redirect URI and their own\n // scope ceiling, send a signed-in admin a single link, and have that admin's\n // browser mint them a code — and the two guards that look like they would stop it,\n // exact redirect-URI matching and the per-client scope ceiling, are both checked\n // against the ATTACKER'S OWN registration. Refuses by default; see\n // `resolveApproval` / `preApprovedClientIds`.\n const scopes = validated.scope.split(/\\s+/).filter(Boolean);\n if (!(await context.approve(request, validated.client, scopes))) {\n // The answer a human refusal gives, at the URI validated further up.\n return errorRedirect(redirectUri, \"access_denied\", state);\n }\n\n const code = await mintCode(context.signingKey, {\n // The subject is the OAuth `sub` the host resolved, NOT a DB id: downstream\n // guards resolve the user by EMAIL, and the code carries only what the session\n // verified.\n sub: session.subject || session.email,\n email: session.email,\n clientId: validated.clientId,\n redirectUri,\n codeChallenge: validated.codeChallenge,\n scope: validated.scope,\n origin,\n });\n\n if (!code) {\n // No signing key configured while the surface is on — refuse to issue rather\n // than fall back to a weaker mode (safe-by-default).\n return errorRedirect(redirectUri, \"server_error\", state);\n }\n\n const success = new URL(redirectUri);\n success.searchParams.set(\"code\", code);\n if (state !== null) success.searchParams.set(\"state\", state);\n return redirectTo(success.toString());\n}\n\n/** `GET <authorize>` — the whole endpoint. */\nexport async function authorizeEndpoint(\n context: McpOauthContext,\n request: Request,\n): Promise<Response> {\n const url = new URL(request.url);\n const origin = context.originOf(request);\n const params = parseParams(url);\n\n // --- Validate client + redirect_uri FIRST (the open-redirect guard) --------\n const clientResult = await validateClientAndRedirect(context, params);\n if (clientResult instanceof Response) return clientResult;\n const { client, redirectUri } = clientResult;\n\n // --- Validate the rest (scope checked against the CLIENT's own registration)\n const requestError = validateAuthorizeRequest(params, redirectUri, client.scopes);\n if (requestError) return requestError;\n\n // The guards above guarantee a present client_id + PKCE challenge; narrow them.\n return authenticateAndMint(context, request, url, origin, {\n client,\n clientId: params.clientId as string,\n redirectUri,\n codeChallenge: params.codeChallenge as string,\n scope: params.scope ?? \"\",\n state: params.state,\n });\n}\n","import { registerClient, type RegisterClientInput } from \"./clients\";\nimport type { McpOauthContext } from \"./context\";\nimport type { TokenEndpointAuthMethod } from \"./stores\";\n\n/**\n * RFC 7591 Dynamic Client Registration (12-23, ported from the origin host's\n * `app/api/oauth/register/route.ts`).\n *\n * An external host (a Claude.ai / ChatGPT connector) self-registers by POSTing RFC\n * 7591 client metadata as JSON; on success a public `client_id` (and, for a\n * confidential client, a one-time `client_secret`) is returned so the host can run\n * the Authorization Code + PKCE flow.\n *\n * Security, unchanged:\n * - **The gate answers 403 here, not 404.** Open DCR is an operator opt-in, and\n * RFC 7591 registration explicitly refuses with `access_denied` so a probing\n * host learns the endpoint exists but registration is closed — the documented\n * static-client path is used instead.\n * - **No privilege escalation via metadata:** registration can only set\n * `redirect_uris`, an auth method the token endpoint actually supports, the\n * supported grant types, and a scope SUBSET of the AS's advertised scopes. Any\n * attempt to widen is rejected, never silently coerced. Identity is never\n * client-supplied.\n * - **Secret hygiene:** a confidential client's secret is generated server-side,\n * returned once, and stored only as a SHA-256 hash.\n */\n\n/** RFC 7591 §3.2.2 registration error codes this endpoint can emit. */\ntype RegistrationErrorCode = \"invalid_redirect_uri\" | \"invalid_client_metadata\";\n\n/** The RFC 7591 §3.2.1 client-information success response. */\ninterface RegistrationSuccessResponse {\n client_id: string;\n client_secret?: string;\n client_id_issued_at: number;\n token_endpoint_auth_method: TokenEndpointAuthMethod;\n redirect_uris: string[];\n grant_types: string[];\n scope: string;\n client_name?: string;\n}\n\n/** Auth methods the token endpoint can actually enforce (RFC 7591 §2). */\nconst SUPPORTED_AUTH_METHODS: readonly TokenEndpointAuthMethod[] = [\n \"none\",\n \"client_secret_basic\",\n];\n\n/** Grant types the AS supports (mirrors the AS discovery metadata). */\nconst SUPPORTED_GRANT_TYPES: readonly string[] = [\"authorization_code\", \"refresh_token\"];\n\nconst DEFAULT_GRANT_TYPES = [\"authorization_code\", \"refresh_token\"] as const;\nconst DEFAULT_AUTH_METHOD: TokenEndpointAuthMethod = \"none\";\n\nconst JSON_HEADERS = {\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n} as const;\n\n/** A JSON error response in the RFC 7591 §3.2.2 shape. */\nfunction registrationError(\n error: RegistrationErrorCode,\n status: number,\n description?: string,\n): Response {\n const body: { error: RegistrationErrorCode; error_description?: string } = { error };\n if (description) body.error_description = description;\n return new Response(JSON.stringify(body), { status, headers: { ...JSON_HEADERS } });\n}\n\n/** Whether a value is a syntactically valid absolute URI (scheme + authority). */\nfunction isAbsoluteUri(value: string): boolean {\n try {\n const url = new URL(value);\n // An absolute redirect target must carry a scheme AND an authority — reject\n // opaque/relative forms so an intercepted request can never be re-steered.\n return Boolean(url.protocol) && Boolean(url.host);\n } catch {\n return false;\n }\n}\n\n/** The RFC 7591 client-metadata fields this endpoint reads. */\ninterface ClientMetadata {\n redirect_uris?: unknown;\n token_endpoint_auth_method?: unknown;\n grant_types?: unknown;\n scope?: unknown;\n client_name?: unknown;\n}\n\n/** A validated registration input, or a typed rejection to return verbatim. */\ntype ValidationResult =\n | { ok: true; input: RegisterClientInput }\n | { ok: false; response: Response };\n\n/** A per-field validator result: the accepted value, or a rejection response. */\ntype FieldResult<T> = { ok: true; value: T } | { ok: false; response: Response };\n\nfunction accept<T>(value: T): FieldResult<T> {\n return { ok: true, value };\n}\n\nfunction reject<T>(response: Response): FieldResult<T> {\n return { ok: false, response };\n}\n\n/**\n * `redirect_uris` — REQUIRED, a non-empty array whose every entry is an absolute\n * URI. Any failure maps to `invalid_redirect_uri` (RFC 7591 §3.2.2).\n */\nfunction validateRedirectUris(raw: unknown): FieldResult<string[]> {\n if (\n !Array.isArray(raw) ||\n raw.length === 0 ||\n !raw.every((uri): uri is string => typeof uri === \"string\" && isAbsoluteUri(uri))\n ) {\n return reject(\n registrationError(\n \"invalid_redirect_uri\",\n 400,\n \"redirect_uris must be a non-empty array of absolute URIs\",\n ),\n );\n }\n return accept([...raw]);\n}\n\n/** `token_endpoint_auth_method` — optional; defaults to `none`. */\nfunction validateAuthMethod(raw: unknown): FieldResult<TokenEndpointAuthMethod> {\n if (raw === undefined || raw === null) return accept(DEFAULT_AUTH_METHOD);\n if (\n typeof raw !== \"string\" ||\n !SUPPORTED_AUTH_METHODS.includes(raw as TokenEndpointAuthMethod)\n ) {\n return reject(\n registrationError(\n \"invalid_client_metadata\",\n 400,\n `unsupported token_endpoint_auth_method (supported: ${SUPPORTED_AUTH_METHODS.join(\", \")})`,\n ),\n );\n }\n return accept(raw as TokenEndpointAuthMethod);\n}\n\n/** `grant_types` — optional; defaults to code+refresh. */\nfunction validateGrantTypes(raw: unknown): FieldResult<string[]> {\n if (raw === undefined || raw === null) return accept([...DEFAULT_GRANT_TYPES]);\n if (\n !Array.isArray(raw) ||\n raw.length === 0 ||\n !raw.every(\n (grant): grant is string =>\n typeof grant === \"string\" && SUPPORTED_GRANT_TYPES.includes(grant),\n )\n ) {\n return reject(\n registrationError(\n \"invalid_client_metadata\",\n 400,\n `unsupported grant_types (supported: ${SUPPORTED_GRANT_TYPES.join(\", \")})`,\n ),\n );\n }\n return accept([...raw]);\n}\n\n/**\n * `scope` — optional space-delimited string; every requested scope must be in the\n * AS's supported set. An explicit empty request falls back to the full set.\n */\nfunction validateScopes(raw: unknown, supportedScopes: readonly string[]): FieldResult<string[]> {\n if (raw === undefined || raw === null) return accept([...supportedScopes]);\n if (typeof raw !== \"string\") {\n return reject(\n registrationError(\"invalid_client_metadata\", 400, \"scope must be a space-delimited string\"),\n );\n }\n const requested = raw.split(/\\s+/).filter(Boolean);\n const supported = new Set<string>(supportedScopes);\n if (!requested.every((scope) => supported.has(scope))) {\n return reject(\n registrationError(\n \"invalid_client_metadata\",\n 400,\n `scope must be a subset of: ${supportedScopes.join(\" \")}`,\n ),\n );\n }\n return accept(requested.length > 0 ? requested : [...supportedScopes]);\n}\n\n/**\n * Validate RFC 7591 client metadata strictly, by composing the per-field\n * validators. `redirect_uris` failures map to `invalid_redirect_uri`; every other\n * unsupported-metadata failure maps to `invalid_client_metadata`.\n */\nfunction validateMetadata(\n metadata: ClientMetadata,\n supportedScopes: readonly string[],\n): ValidationResult {\n const redirectUris = validateRedirectUris(metadata.redirect_uris);\n if (!redirectUris.ok) return redirectUris;\n\n const authMethod = validateAuthMethod(metadata.token_endpoint_auth_method);\n if (!authMethod.ok) return authMethod;\n\n const grantTypes = validateGrantTypes(metadata.grant_types);\n if (!grantTypes.ok) return grantTypes;\n\n const scopes = validateScopes(metadata.scope, supportedScopes);\n if (!scopes.ok) return scopes;\n\n const clientNameRaw = metadata.client_name;\n const clientName = typeof clientNameRaw === \"string\" ? clientNameRaw : null;\n\n return {\n ok: true,\n input: {\n redirectUris: redirectUris.value,\n clientName,\n tokenEndpointAuthMethod: authMethod.value,\n grantTypes: grantTypes.value,\n scopes: scopes.value,\n },\n };\n}\n\n/** The refusal a closed registration endpoint answers with. */\nexport function registrationDisabled(): Response {\n return new Response(\n JSON.stringify({\n error: \"access_denied\",\n error_description: \"dynamic client registration is disabled\",\n }),\n { status: 403, headers: { ...JSON_HEADERS } },\n );\n}\n\n/** `POST <register>` — the whole endpoint. */\nexport async function registerEndpoint(\n context: McpOauthContext,\n request: Request,\n): Promise<Response> {\n // Parse the JSON body. A malformed body is unusable metadata → 400.\n let metadata: ClientMetadata;\n try {\n const parsed: unknown = await request.json();\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n return registrationError(\n \"invalid_client_metadata\",\n 400,\n \"request body must be a JSON object\",\n );\n }\n metadata = parsed as ClientMetadata;\n } catch {\n return registrationError(\"invalid_client_metadata\", 400, \"request body must be valid JSON\");\n }\n\n const validated = validateMetadata(metadata, context.scopes);\n if (!validated.ok) return validated.response;\n\n const registered = await registerClient(context.stores.clients, validated.input);\n\n const responseBody: RegistrationSuccessResponse = {\n client_id: registered.clientId,\n ...(registered.clientSecret ? { client_secret: registered.clientSecret } : {}),\n client_id_issued_at: Math.floor(Date.now() / 1000),\n token_endpoint_auth_method: registered.tokenEndpointAuthMethod,\n redirect_uris: registered.redirectUris,\n grant_types: registered.grantTypes,\n scope: registered.scopes.join(\" \"),\n ...(registered.clientName ? { client_name: registered.clientName } : {}),\n };\n\n return new Response(JSON.stringify(responseBody), {\n status: 201,\n headers: { ...JSON_HEADERS },\n });\n}\n","import { createHash, timingSafeEqual } from \"node:crypto\";\n\nimport type { OAuthClientStore } from \"./stores\";\n\n/**\n * The token endpoint's wire helpers: the RFC 6749 §5.1/§5.2 bodies and client\n * authentication (12-23, split out of the grant handlers so each file stays under\n * the size gate — the same split the origin host made).\n *\n * These bodies are NOT the house `{ data }` envelope, deliberately: they are read\n * by OAuth clients that expect the RFC shapes at the top level, and `Cache-Control:\n * no-store` is required on every one of them because they carry credentials.\n */\n\n/** OAuth 2.1 / RFC 6749 §5.2 error codes the token endpoint can emit. */\ntype TokenErrorCode =\n | \"invalid_request\"\n | \"invalid_client\"\n | \"invalid_grant\"\n | \"invalid_scope\"\n | \"unsupported_grant_type\";\n\n/** The RFC 6749 §5.1 successful token response. */\ninterface TokenSuccessResponse {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n refresh_token: string;\n scope: string;\n}\n\nconst JSON_HEADERS = {\n \"content-type\": \"application/json; charset=utf-8\",\n \"cache-control\": \"no-store\",\n} as const;\n\n/** A JSON error response in the RFC 6749 §5.2 shape. */\nexport function tokenError(\n error: TokenErrorCode,\n status: number,\n description?: string,\n headers: Record<string, string> = {},\n): Response {\n const body: { error: TokenErrorCode; error_description?: string } = { error };\n if (description) body.error_description = description;\n return new Response(JSON.stringify(body), {\n status,\n headers: { ...JSON_HEADERS, ...headers },\n });\n}\n\n/** A JSON success response with `Cache-Control: no-store` (RFC 6749 §5.1). */\nexport function tokenSuccess(payload: TokenSuccessResponse): Response {\n return new Response(JSON.stringify(payload), { status: 200, headers: { ...JSON_HEADERS } });\n}\n\n/** Constant-time equality of two SHA-256 hex digests. */\nfunction hashesEqual(a: string, b: string): boolean {\n const bufA = Buffer.from(a, \"hex\");\n const bufB = Buffer.from(b, \"hex\");\n if (bufA.length !== bufB.length || bufA.length === 0) return false;\n return timingSafeEqual(bufA, bufB);\n}\n\n/** SHA-256 hex digest — matches the at-rest client-secret hashing convention. */\nfunction sha256Hex(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\n/** Client credentials extracted from HTTP Basic auth or the form body. */\nexport interface ClientCredentials {\n clientId: string | null;\n clientSecret: string | null;\n}\n\n/**\n * Resolve the presented client credentials. HTTP Basic (`client_secret_basic`)\n * takes precedence over the form-body `client_id` per RFC 6749 §2.3.1; a malformed\n * Basic header is treated as absent (the form body still applies).\n */\nexport function readClientCredentials(\n request: Request,\n form: URLSearchParams,\n): ClientCredentials {\n const authorization = request.headers.get(\"authorization\");\n if (authorization && authorization.startsWith(\"Basic \")) {\n const decoded = Buffer.from(authorization.slice(6), \"base64\").toString(\"utf8\");\n const separator = decoded.indexOf(\":\");\n if (separator !== -1) {\n return {\n clientId: decoded.slice(0, separator),\n clientSecret: decoded.slice(separator + 1),\n };\n }\n }\n return { clientId: form.get(\"client_id\"), clientSecret: form.get(\"client_secret\") };\n}\n\n/** The 401 challenge issued on a client-authentication failure. */\nconst CLIENT_AUTH_CHALLENGE = { \"www-authenticate\": 'Basic realm=\"oauth-token\"' };\n\n/**\n * Authenticate the client that presents the request. A public client is identified\n * by `client_id` alone (which must equal `expectedClientId` when provided). A\n * confidential client (`client_secret_basic`) MUST present a secret whose SHA-256\n * matches the stored hash. Returns `null` on success, or a 401 `invalid_client`\n * response on failure — never a body that says which half was wrong.\n */\nexport async function authenticateClient(\n clients: OAuthClientStore,\n credentials: ClientCredentials,\n expectedClientId?: string,\n): Promise<Response | null> {\n const clientId = credentials.clientId;\n if (!clientId) {\n return tokenError(\"invalid_client\", 401, \"missing client_id\", CLIENT_AUTH_CHALLENGE);\n }\n if (expectedClientId && clientId !== expectedClientId) {\n // The presenting client does not match the client the code was issued to.\n return tokenError(\n \"invalid_client\",\n 401,\n \"client_id does not match the grant\",\n CLIENT_AUTH_CHALLENGE,\n );\n }\n\n const client = await clients.findByClientId(clientId);\n if (!client) {\n return tokenError(\"invalid_client\", 401, \"unknown client\", CLIENT_AUTH_CHALLENGE);\n }\n\n if (client.tokenEndpointAuthMethod === \"client_secret_basic\") {\n const secret = credentials.clientSecret;\n if (!secret || !client.clientSecretHash) {\n return tokenError(\n \"invalid_client\",\n 401,\n \"client authentication required\",\n CLIENT_AUTH_CHALLENGE,\n );\n }\n if (!hashesEqual(sha256Hex(secret), client.clientSecretHash)) {\n return tokenError(\n \"invalid_client\",\n 401,\n \"invalid client credentials\",\n CLIENT_AUTH_CHALLENGE,\n );\n }\n }\n\n return null;\n}\n","import { ACCESS_TOKEN_TTL_SECONDS, signAccessToken } from \"./access-token\";\nimport { AuthorizationCodeError, verifyCode } from \"./authorization-code\";\nimport { providerFromRedirectUris } from \"./clients\";\nimport type { McpConnectionRecording, McpOauthContext } from \"./context\";\nimport { verifyChallenge } from \"./pkce\";\nimport {\n RefreshTokenError,\n getRefreshTokenIdentity,\n issueRefreshToken,\n rotateRefreshToken,\n} from \"./refresh\";\nimport type { McpConnectionStore } from \"./stores\";\nimport {\n authenticateClient,\n readClientCredentials,\n tokenError,\n tokenSuccess,\n type ClientCredentials,\n} from \"./token-response\";\n\n/**\n * The two grant handlers of the token endpoint (12-23, ported from the origin host's\n * `lib/mcp/oauth/token-grants.ts`).\n *\n * Security invariants enforced here, unchanged:\n * - **Single-use codes:** the code's `jti` is consumed the moment it is\n * redeemed; a replay of the same code is `invalid_grant`.\n * - **PKCE:** a `code_verifier` that does not S256-match the code's\n * `code_challenge` is `invalid_grant`.\n * - **Client auth:** a public client's `client_id` must equal the code's bound\n * client; a confidential client must present a secret whose SHA-256 matches\n * the stored hash, else `invalid_client` (401).\n * - **Bound `redirect_uri`:** it must equal the one the code was minted with\n * (RFC 6749 §4.1.3).\n * - **Refresh rotation:** client-bound, replay-revoking, narrow-only scope.\n */\n\n/** Throttle default: don't rewrite liveness on every grant. */\nconst DEFAULT_ACTIVITY_THROTTLE_MS = 60_000;\n\n/**\n * Best-effort: record that this user's AI host (OAuth client) is live, so an\n * account page can show \"connected via Claude · active 2 min ago\". Runs on the\n * token grant, not the per-request hot path; hosts refresh every ~15 min so\n * liveness stays fresh.\n *\n * NEVER lets a failure break token issuance — a recording error is swallowed, and\n * nothing about it is logged, because the only interesting values here are an\n * email and a client id. Skipped when the host resolves no user row (email is the\n * identity) or when no connection recording is configured at all.\n */\nasync function recordHostConnection(\n context: McpOauthContext,\n email: string,\n clientId: string,\n): Promise<void> {\n const recording = context.connections;\n const store = context.stores.connections;\n if (!recording || !store) return;\n try {\n await writeConnectionActivity(context, { recording, store }, email, clientId);\n } catch {\n // Liveness is non-critical — never fail the grant on it. Nothing is logged\n // either: the only values here are an email and a client id.\n }\n}\n\n/** The write itself, once the recording ports are known to exist. */\nasync function writeConnectionActivity(\n context: McpOauthContext,\n ports: { recording: McpConnectionRecording; store: McpConnectionStore },\n email: string,\n clientId: string,\n): Promise<void> {\n const { recording, store } = ports;\n const [userId, client] = await Promise.all([\n recording.resolveUserId(email),\n context.stores.clients.findByClientId(clientId),\n ]);\n // No user row yet: email is the identity the AS binds to, so the grant stands\n // and there is simply nothing to attribute it to.\n if (!userId) return;\n\n const throttleMs = recording.activityThrottleMs ?? DEFAULT_ACTIVITY_THROTTLE_MS;\n const lastActiveAt = await store.lastActiveAt(userId, clientId);\n const now = new Date();\n if (lastActiveAt && now.getTime() - lastActiveAt.getTime() < throttleMs) return;\n\n await store.recordActivity({\n userId,\n oauthClientId: clientId,\n clientName: client?.clientName ?? null,\n // Attribute to a provider from the client's redirect URIs (claude.ai →\n // claude, chatgpt.com → chatgpt) so an account page lights the right card.\n host: client ? providerFromRedirectUris(client.redirectUris, recording.providerRules) : null,\n at: now,\n });\n}\n\n/** The presented `authorization_code` grant parameters, once validated present. */\ninterface AuthorizationCodeParams {\n code: string;\n redirectUri: string;\n codeVerifier: string;\n}\n\n/**\n * Read + presence-check the `authorization_code` form parameters. Returns the three\n * required values, or an `invalid_request` (400) naming the first missing field.\n */\nfunction readAuthorizationCodeParams(form: URLSearchParams): AuthorizationCodeParams | Response {\n const code = form.get(\"code\");\n const redirectUri = form.get(\"redirect_uri\");\n const codeVerifier = form.get(\"code_verifier\");\n\n if (!code) return tokenError(\"invalid_request\", 400, \"missing code\");\n if (!redirectUri) return tokenError(\"invalid_request\", 400, \"missing redirect_uri\");\n if (!codeVerifier) return tokenError(\"invalid_request\", 400, \"missing code_verifier\");\n\n return { code, redirectUri, codeVerifier };\n}\n\n/**\n * Redeem a presented code, or refuse.\n *\n * The ORDER is the security contract, and it is the order the origin host established:\n * verify the code's signature, authenticate the presenting client against the\n * client the code was bound to, check the bound `redirect_uri`, check PKCE — and\n * only THEN consume the single-use `jti`. Consuming earlier would let a failed\n * attempt (a wrong secret, a mismatched verifier) burn a legitimate code.\n */\nasync function redeemCode(\n context: McpOauthContext,\n params: AuthorizationCodeParams,\n credentials: ClientCredentials,\n origin: string,\n): Promise<Awaited<ReturnType<typeof verifyCode>> | Response> {\n const { code, redirectUri, codeVerifier } = params;\n\n // Every code-level failure (expired, tampered, wrong-audience) is invalid_grant.\n let verified;\n try {\n verified = await verifyCode(context.signingKey, code, { origin });\n } catch (error) {\n if (error instanceof AuthorizationCodeError) {\n return tokenError(\"invalid_grant\", 400, \"invalid or expired authorization code\");\n }\n throw error;\n }\n\n const authError = await authenticateClient(\n context.stores.clients,\n credentials,\n verified.clientId,\n );\n if (authError) return authError;\n\n // The redirect_uri MUST match the one the code was bound to (RFC 6749 §4.1.3).\n if (redirectUri !== verified.redirectUri) {\n return tokenError(\"invalid_grant\", 400, \"redirect_uri mismatch\");\n }\n\n // PKCE: the presented verifier must S256-match the bound challenge.\n if (!(await verifyChallenge(codeVerifier, verified.codeChallenge))) {\n return tokenError(\"invalid_grant\", 400, \"PKCE verification failed\");\n }\n\n // Single-use: consume the code's jti; a replay of the same code fails.\n if (!(await context.codeReplay.consume(verified.jti, Date.now()))) {\n return tokenError(\"invalid_grant\", 400, \"authorization code already used\");\n }\n\n return verified;\n}\n\n/** Handle the `authorization_code` grant. */\nasync function handleAuthorizationCode(\n context: McpOauthContext,\n form: URLSearchParams,\n credentials: ClientCredentials,\n origin: string,\n): Promise<Response> {\n const params = readAuthorizationCodeParams(form);\n if (params instanceof Response) return params;\n\n const verified = await redeemCode(context, params, credentials, origin);\n if (verified instanceof Response) return verified;\n\n const scopes = verified.scope.split(/\\s+/).filter(Boolean);\n\n const accessToken = await signAccessToken(context.signingKey, {\n email: verified.email,\n subject: verified.sub,\n scopes,\n origin,\n resourcePath: context.resourcePath,\n ttlSeconds: context.accessTokenTtlSeconds,\n });\n if (!accessToken) {\n // No signing key configured while the surface is on — refuse rather than fall\n // back to a weaker mode (safe-by-default).\n return tokenError(\"invalid_request\", 400, \"token issuance unavailable\");\n }\n\n const refresh = await issueRefreshToken(\n { store: context.stores.refreshTokens, ttlMs: context.refreshTokenTtlMs },\n {\n userEmail: verified.email,\n userSub: verified.sub,\n clientId: verified.clientId,\n scopes,\n },\n );\n\n await recordHostConnection(context, verified.email, verified.clientId);\n\n return tokenSuccess({\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: context.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS,\n refresh_token: refresh.refreshToken,\n scope: refresh.scopes.join(\" \"),\n });\n}\n\n/** Handle the `refresh_token` grant. */\nasync function handleRefreshToken(\n context: McpOauthContext,\n form: URLSearchParams,\n credentials: ClientCredentials,\n origin: string,\n): Promise<Response> {\n const refreshToken = form.get(\"refresh_token\");\n const requestedScope = form.get(\"scope\");\n\n if (!refreshToken) return tokenError(\"invalid_request\", 400, \"missing refresh_token\");\n\n // Authenticate the presenting client (public: client_id present; confidential:\n // secret checked). `authenticateClient` rejects a missing client_id, so on\n // success `credentials.clientId` is non-null and is the identity the rotation is\n // bound to below.\n const authError = await authenticateClient(context.stores.clients, credentials);\n if (authError) return authError;\n const clientId = credentials.clientId as string;\n\n const newScopes = requestedScope ? requestedScope.split(/\\s+/).filter(Boolean) : undefined;\n const refreshContext = {\n store: context.stores.refreshTokens,\n ttlMs: context.refreshTokenTtlMs,\n };\n\n // Rotation enforces client binding (the token's stored clientId must equal the\n // authenticated one, else invalid_grant — OAuth 2.1 §4.3), plus\n // replay-revocation and scope-narrowing.\n let rotated;\n try {\n rotated = await rotateRefreshToken(refreshContext, refreshToken, clientId, newScopes);\n } catch (error) {\n if (error instanceof RefreshTokenError) {\n return tokenError(error.code, 400, error.message);\n }\n throw error;\n }\n\n // The refresh token binds the user's email AND original OAuth `sub`; recover both\n // so the successor access token carries the SAME stable `sub` as the initial\n // token (RFC 6749 §5.1 / OIDC §2), not the email.\n const identity = await getRefreshTokenIdentity(refreshContext, rotated.refreshToken);\n if (!identity) return tokenError(\"invalid_grant\", 400, \"refresh token binding not found\");\n\n const accessToken = await signAccessToken(context.signingKey, {\n email: identity.userEmail,\n subject: identity.userSub,\n scopes: rotated.scopes,\n origin,\n resourcePath: context.resourcePath,\n ttlSeconds: context.accessTokenTtlSeconds,\n });\n if (!accessToken) return tokenError(\"invalid_request\", 400, \"token issuance unavailable\");\n\n await recordHostConnection(context, identity.userEmail, clientId);\n\n return tokenSuccess({\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: context.accessTokenTtlSeconds ?? ACCESS_TOKEN_TTL_SECONDS,\n refresh_token: rotated.refreshToken,\n scope: rotated.scopes.join(\" \"),\n });\n}\n\n/**\n * `POST <token>` — the endpoint itself: gate → parse form → dispatch by\n * `grant_type`. Thin on purpose; the flows above are where the invariants live.\n */\nexport async function tokenEndpoint(\n context: McpOauthContext,\n request: Request,\n): Promise<Response> {\n const origin = context.originOf(request);\n\n let form: URLSearchParams;\n try {\n form = new URLSearchParams(await request.text());\n } catch {\n return tokenError(\"invalid_request\", 400, \"malformed request body\");\n }\n\n const grantType = form.get(\"grant_type\");\n if (!grantType) return tokenError(\"invalid_request\", 400, \"missing grant_type\");\n\n const credentials = readClientCredentials(request, form);\n\n switch (grantType) {\n case \"authorization_code\":\n return handleAuthorizationCode(context, form, credentials, origin);\n case \"refresh_token\":\n return handleRefreshToken(context, form, credentials, origin);\n default:\n return tokenError(\n \"unsupported_grant_type\",\n 400,\n `grant_type '${grantType}' is not supported`,\n );\n }\n}\n","import { buildAuthorizationServerMetadata } from \"../auth/authorization-server-metadata\";\nimport { buildProtectedResourceMetadata } from \"../auth/resource-metadata\";\n\nimport { authorizeEndpoint } from \"./authorize\";\nimport { issuer, resourceAudience } from \"./config\";\nimport {\n notFound,\n resolveMcpOauthConfig,\n type McpOauthConfig,\n type McpOauthContext,\n} from \"./context\";\nimport { registerEndpoint, registrationDisabled } from \"./register\";\nimport { tokenEndpoint } from \"./token-grants\";\nimport {\n verifyAccessToken,\n type VerifiedAccessToken,\n type VerifyAccessTokenOptions,\n} from \"./access-token\";\n\n/**\n * The OAuth 2.1 authorization server, as one mount (12-23).\n *\n * `@12-apps/mcp` shipped the OpenAPI→tools generator, the bearer proxy and the two\n * discovery BUILDERS, and held zero authorization logic — which meant every new app\n * still wrote the AS itself: ~1.5k LOC of authorize/token/register plus the code,\n * token, PKCE, rotation and replay machinery under them. All of that is the\n * surface's contract, not a host's, so it lives here.\n *\n * Routes are FRAMEWORK-NEUTRAL descriptors whose handler takes a Fetch `Request`\n * and answers a Fetch `Response`. Unlike the report-builder-shaped surfaces there\n * is no `{ data }` envelope to adapt: an OAuth response is a 302 with a `Location`,\n * a form-encoded exchange answering RFC 6749 §5.1/§5.2 JSON, or an RFC 8414/9728\n * document — shapes fixed by specification that a wrapper would only break. So the\n * adapters are one line each, and a host with a file-per-route layout can export\n * the named handlers directly:\n *\n * export const GET = mcpOauth.handlers.authorize; // app/api/oauth/authorize\n * export const POST = mcpOauth.handlers.token; // app/api/oauth/token\n *\n * What stays the HOST's: the cookie session (`resolveSession`), where the data\n * lives (`stores`), which origins are trusted, the operator gate, and its sign-in\n * path. Everything else is the RFCs'.\n */\n\nexport interface McpOauthRoute {\n method: \"GET\" | \"POST\";\n /** Absolute path from the ORIGIN ROOT — `.well-known/*` cannot live under a prefix. */\n path: string;\n handle(request: Request): Promise<Response>;\n}\n\nexport interface McpOauthHandlers {\n /** `GET` — Authorization Code + PKCE, identity from the session only. */\n authorize: (request: Request) => Promise<Response>;\n /** `POST` — the two grants, form-encoded, RFC 6749 bodies. */\n token: (request: Request) => Promise<Response>;\n /** `POST` — RFC 7591 dynamic client registration (403 when the gate is off). */\n register: (request: Request) => Promise<Response>;\n /** `GET` — the public JWKS (503 while no key is provisioned). */\n jwks: (request: Request) => Promise<Response>;\n /** `GET` — RFC 8414 authorization-server metadata. */\n authorizationServerMetadata: (request: Request) => Promise<Response>;\n /** `GET` — RFC 9728 protected-resource metadata. */\n protectedResourceMetadata: (request: Request) => Promise<Response>;\n}\n\nexport interface ApiMcpOauth {\n /** Every endpoint, in mount order. */\n routes: McpOauthRoute[];\n /** The same handlers by name, for a host whose router is its file tree. */\n handlers: McpOauthHandlers;\n /**\n * Verify a bearer token the way THIS surface mints them — the resource server's\n * half. Bound to the same signing key, resource path and trusted-origin\n * resolution, which is what stops \"minted for origin A, verified against origin\n * B\" from rejecting valid tokens.\n */\n verifyBearer: (\n token: string,\n request: Request,\n options?: Omit<VerifyAccessTokenOptions, \"origin\" | \"resourcePath\">,\n ) => Promise<VerifiedAccessToken>;\n /** The resolved config, for a host that needs the same origin/audience answers. */\n context: McpOauthContext;\n}\n\n/** JSON, with the status and cache policy each document wants. */\nfunction jsonResponse(\n body: unknown,\n status = 200,\n headers: Record<string, string> = {},\n): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json; charset=utf-8\", ...headers },\n });\n}\n\n/**\n * The public key set, or a 503.\n *\n * Safe-by-default: an unprovisioned AS answers 503 rather than an empty or\n * partial key set, so a client never mistakes \"no key yet\" for \"a usable key\".\n */\nasync function jwksResponse(context: McpOauthContext): Promise<Response> {\n const key = await context.signingKey();\n if (!key) return jsonResponse({ error: \"signing_key_unavailable\" }, 503);\n return jsonResponse({ keys: [key.publicJwk] }, 200, {\n // Public, cacheable key set; hosts may cache it and re-fetch on a `kid` miss\n // (rotation). A short max-age keeps the rotation overlap tight.\n \"cache-control\": \"public, max-age=300\",\n });\n}\n\n/** The two discovery documents, built from ONE origin and ONE scope source. */\nfunction discoveryHandlers(\n context: McpOauthContext,\n): Pick<McpOauthHandlers, \"authorizationServerMetadata\" | \"protectedResourceMetadata\"> {\n return {\n authorizationServerMetadata: async (request) =>\n jsonResponse(\n buildAuthorizationServerMetadata({\n issuer: issuer(context.originOf(request)),\n scopesSupported: [...context.scopes],\n // The RESOLVED paths, so what a connector reads before its first request\n // is where the endpoints actually are.\n paths: context.paths,\n }),\n ),\n protectedResourceMetadata: async (request) => {\n const origin = context.originOf(request);\n return jsonResponse(\n buildProtectedResourceMetadata({\n resource: resourceAudience(origin, context.resourcePath),\n authorizationServers: [issuer(origin)],\n scopesSupported: [...context.scopes],\n }),\n );\n },\n };\n}\n\n/**\n * Every endpoint, behind the operator gate.\n *\n * With the gate off the surface is INERT and answers 404, so a probe cannot tell\n * a disabled AS from an app that has none. Registration is the one exception: it\n * answers 403, because RFC 7591 has a code for \"the endpoint is here, but\n * registration is closed\" and the documented static-client path is the answer.\n */\nfunction buildHandlers(context: McpOauthContext): McpOauthHandlers {\n const gated =\n (handler: (request: Request) => Promise<Response>) =>\n async (request: Request): Promise<Response> =>\n context.enabled() ? handler(request) : notFound();\n const discovery = discoveryHandlers(context);\n\n return {\n authorize: gated((request) => authorizeEndpoint(context, request)),\n token: gated((request) => tokenEndpoint(context, request)),\n register: async (request) =>\n context.enabled() ? registerEndpoint(context, request) : registrationDisabled(),\n jwks: gated(() => jwksResponse(context)),\n authorizationServerMetadata: gated(discovery.authorizationServerMetadata),\n protectedResourceMetadata: gated(discovery.protectedResourceMetadata),\n };\n}\n\n/** Mount order, and the paths a host may have moved. */\nfunction buildRoutes(context: McpOauthContext, handlers: McpOauthHandlers): McpOauthRoute[] {\n const { paths } = context;\n return [\n {\n method: \"GET\",\n path: paths.authorizationServerMetadata,\n handle: handlers.authorizationServerMetadata,\n },\n {\n method: \"GET\",\n path: paths.protectedResourceMetadata,\n handle: handlers.protectedResourceMetadata,\n },\n { method: \"GET\", path: paths.jwks, handle: handlers.jwks },\n { method: \"GET\", path: paths.authorize, handle: handlers.authorize },\n { method: \"POST\", path: paths.token, handle: handlers.token },\n { method: \"POST\", path: paths.register, handle: handlers.register },\n ];\n}\n\nexport function createApiMcpOauth(config: McpOauthConfig): ApiMcpOauth {\n const context = resolveMcpOauthConfig(config);\n const handlers = buildHandlers(context);\n\n return {\n routes: buildRoutes(context, handlers),\n handlers,\n verifyBearer: (token, request, options) =>\n verifyAccessToken(context.signingKey, token, {\n ...options,\n origin: context.originOf(request),\n resourcePath: context.resourcePath,\n }),\n context,\n };\n}\n"],"mappings":";;;;;;;;;AAiBO,IAAM,uBAAuB,CAAC,YAAY,WAAW;AAKrD,IAAM,4BAA4B;AAGlC,SAAS,OAAO,QAAwB;AAC7C,SAAO;AACT;AAFgB;AAKT,SAAS,iBACd,QACA,eAAuB,2BACf;AACR,SAAO,GAAG,MAAM,GAAG,YAAY;AACjC;AALgB;AAYhB,IAAM,YACJ;AAGF,IAAM,QAAQ;AAUd,SAAS,qBAAqB,OAAuB;AACnD,MAAI,MAAM,MAAM;AAChB,SAAO,MAAM,KAAK,MAAM,WAAW,MAAM,CAAC,MAAM,MAAO,QAAO;AAC9D,SAAO,MAAM,MAAM,GAAG,GAAG;AAC3B;AAJS;AAOT,SAAS,iBAAiB,SAAsC;AAC9D,SAAO,QAAQ,IAAI,CAAC,WAAW,qBAAqB,OAAO,KAAK,CAAC,CAAC,EAAE,OAAO,OAAO;AACpF;AAFS;AASF,SAAS,sBAAsB,MAAwB;AAC5D,QAAM,MAAM,OAAO,YAAY,cAAc,SAAY,QAAQ,MAAM,IAAI;AAC3E,SAAO,MAAM,iBAAiB,IAAI,MAAM,GAAG,CAAC,IAAI,CAAC;AACnD;AAHgB;AAWhB,SAAS,uBAAuB,WAA2D;AACzF,QAAM,OAAO,UAAU,kBAAkB,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAChE,MAAI,CAAC,QAAQ,CAAC,UAAU,KAAK,IAAI,EAAG,QAAO;AAC3C,QAAM,QAAQ,UAAU,mBAAmB,GAAG,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK;AAClE,SAAO,GAAG,UAAU,SAAS,SAAS,OAAO,MAAM,IAAI;AACzD;AALS;AA6BF,SAAS,qBACd,WACA,gBACA,iBAAoC,CAAC,GACjB;AACpB,QAAM,CAAC,WAAW,GAAG,IAAI,IAAI,iBAAiB,cAAc;AAG5D,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,UAAU,CAAC,WAAW,GAAG,IAAI;AACnC,QAAM,UAAU,uBAAuB,SAAS;AAChD,SAAO,WAAW,QAAQ,SAAS,OAAO,IAAI,UAAU;AAC1D;AAbgB;AAqBT,SAAS,kBACd,SACA,iBAAoC,CAAC,GAC7B;AACR,QAAM,WAAW,IAAI,IAAI,QAAQ,GAAG,EAAE;AACtC,SACE,qBAAqB,CAAC,SAAS,QAAQ,QAAQ,IAAI,IAAI,GAAG,UAAU,cAAc,KAAK;AAE3F;AARgB;;;ACrIhB,SAAS,WAAW,mBAA6C;AAiB1D,IAAM,cAAc;AAwB3B,eAAe,gBAAgB,KAAa,KAAqC;AAG/E,QAAM,aAAa,MAAM,YAAY,KAAK,aAAa,EAAE,aAAa,KAAK,CAAC;AAC5E,QAAM,MAAM,MAAM,UAAU,UAAU;AAEtC,QAAM,EAAE,GAAG,UAAU,GAAG,WAAW,IAAI;AACvC,OAAK;AAEL,QAAM,YAA8B;AAAA,IAClC,GAAG;AAAA,IACH,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL;AAAA,EACF;AAEA,SAAO,EAAE,YAAY,WAAW,IAAI;AACtC;AAnBe;AAiCR,SAAS,mBACd,MACuB;AACvB,MAAI,QAAiE;AACrE,SAAO,YAAY;AACjB,UAAM,EAAE,KAAK,IAAI,IAAI,KAAK;AAC1B,QAAI,CAAC,OAAO,CAAC,IAAK,QAAO;AACzB,UAAM,WAAW,GAAG,GAAG,IAAI,GAAG;AAC9B,QAAI,OAAO,QAAQ,SAAU,QAAO,MAAM;AAC1C,UAAM,UAAU,gBAAgB,KAAK,GAAG;AACxC,YAAQ,EAAE,KAAK,UAAU,QAAQ;AACjC,WAAO;AAAA,EACT;AACF;AAbgB;AAgBT,IAAM,0BAA0B;AAEhC,IAAM,6BAA6B;AAMnC,SAAS,sBACd,SAAiB,yBACjB,SAAiB,4BACM;AACvB,SAAO,mBAAmB,OAAO;AAAA,IAC/B,KAAK,OAAO,YAAY,cAAc,SAAY,QAAQ,MAAM,MAAM;AAAA,IACtE,KAAK,OAAO,YAAY,cAAc,SAAY,QAAQ,MAAM,MAAM;AAAA,EACxE,EAAE;AACJ;AARgB;;;AClGhB,SAAS,SAAS,WAAW,iBAAkC;AA2BxD,IAAM,8BAA8B;AAGpC,IAAM,iCAAiC;AAG9C,IAAM,0BAA0B;AAwCzB,IAAM,yBAAN,cAAqC,MAAM;AAAA,EAzElD,OAyEkD;AAAA;AAAA;AAAA,EACvC;AAAA,EAET,YAAY,SAAkB;AAC5B,UAAM,WAAW,eAAe;AAChC,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAiBA,SAAS,WAAW,KAAsB;AACxC,SAAO,KAAK,OAAO,OAAO,KAAK,IAAI,KAAK,GAAI;AAC9C;AAFS;AAKT,SAAS,YAAY,SAAqB,KAA4B;AACpE,QAAM,QAAQ,QAAQ,GAAG;AACzB,SAAO,OAAO,UAAU,WAAW,QAAQ;AAC7C;AAHS;AAWT,SAAS,mBAAmB,SAAgD;AAC1E,QAAM,MAAM,YAAY,SAAS,KAAK;AACtC,QAAM,QAAQ,YAAY,SAAS,OAAO;AAC1C,QAAM,WAAW,YAAY,SAAS,WAAW;AACjD,QAAM,cAAc,YAAY,SAAS,cAAc;AACvD,QAAM,gBAAgB,YAAY,SAAS,gBAAgB;AAC3D,QAAM,QAAQ,YAAY,SAAS,OAAO;AAC1C,QAAM,MAAM,YAAY,SAAS,KAAK;AAEtC,MAAI,CAAC,OAAO,CAAC,SAAS,CAAC,YAAY,CAAC,eAAe,CAAC,iBAAiB,UAAU,QAAQ,CAAC,KAAK;AAC3F,UAAM,IAAI,uBAAuB,uCAAuC;AAAA,EAC1E;AAEA,SAAO,EAAE,KAAK,OAAO,UAAU,aAAa,eAAe,OAAO,IAAI;AACxE;AAdS;AAuBT,eAAsB,SACpB,gBACA,OACA,SACwB;AACxB,QAAM,MAAM,MAAM,eAAe;AACjC,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,MAAM,WAAW,SAAS,GAAG;AACnC,QAAM,MAAM,MAAM;AAElB,QAAM,SAAkC;AAAA,IACtC,OAAO,MAAM;AAAA,IACb,WAAW,MAAM;AAAA,IACjB,cAAc,MAAM;AAAA,IACpB,gBAAgB,MAAM;AAAA,IACtB,OAAO,MAAM;AAAA,EACf;AAEA,SAAO,IAAI,QAAQ,MAAM,EACtB,mBAAmB,EAAE,KAAK,aAAa,KAAK,IAAI,IAAI,CAAC,EACrD,UAAU,OAAO,MAAM,MAAM,CAAC,EAC9B,YAAY,2BAA2B,EACvC,WAAW,MAAM,GAAG,EACpB,YAAY,GAAG,EACf,kBAAkB,GAAG,EACrB,OAAO,OAAO,WAAW,CAAC,EAC1B,KAAK,IAAI,UAAU;AACxB;AA5BsB;AA+CtB,eAAsB,WACpB,gBACA,MACA,SACoC;AACpC,QAAM,MAAM,MAAM,eAAe;AACjC,MAAI,CAAC,KAAK;AAER,UAAM,IAAI,uBAAuB,2BAA2B;AAAA,EAC9D;AAEA,QAAM,YAAY,MAAM,UAAU,IAAI,WAAW,WAAW;AAE5D,MAAI;AACJ,MAAI;AACF,UAAM,SAAS,MAAM,UAAU,MAAM,WAAW;AAAA,MAC9C,YAAY,CAAC,WAAW;AAAA,MACxB,QAAQ,OAAO,QAAQ,MAAM;AAAA,MAC7B,UAAU;AAAA,MACV,gBAAgB;AAAA,MAChB,aAAa,QAAQ,QAAQ,SAAY,SAAY,IAAI,KAAK,QAAQ,GAAG;AAAA,IAC3E,CAAC;AACD,cAAU,OAAO;AAAA,EACnB,QAAQ;AAGN,UAAM,IAAI,uBAAuB,0BAA0B;AAAA,EAC7D;AAEA,SAAO,mBAAmB,OAAO;AACnC;AA9BsB;;;ACxLtB,SAAS,YAAY,aAAa,kBAAkB;AAoBpD,IAAM,sBAAsB,CAAC,sBAAsB,eAAe;AAGlE,IAAM,sBAAsB;AAgCrB,SAAS,WAAW,QAAwB;AACjD,SAAO,WAAW,QAAQ,EAAE,OAAO,MAAM,EAAE,OAAO,KAAK;AACzD;AAFgB;AAShB,eAAsB,eACpB,OACA,OAC2B;AAC3B,QAAM,WAAW,WAAW;AAC5B,QAAM,aAAsC,MAAM,2BAA2B;AAC7E,QAAM,aAAa,MAAM,cAAc,CAAC,GAAG,mBAAmB;AAG9D,QAAM,eACJ,eAAe,wBACX,YAAY,mBAAmB,EAAE,SAAS,KAAK,IAC/C;AAEN,QAAM,MAAM,MAAM,MAAM,OAAO;AAAA,IAC7B;AAAA,IACA,kBAAkB,eAAe,WAAW,YAAY,IAAI;AAAA,IAC5D,cAAc,MAAM;AAAA,IACpB,YAAY,MAAM,cAAc;AAAA,IAChC,yBAAyB;AAAA,IACzB;AAAA,IACA,QAAQ,MAAM;AAAA,EAChB,CAAC;AAED,SAAO;AAAA,IACL,UAAU,IAAI;AAAA,IACd,GAAI,eAAe,EAAE,aAAa,IAAI,CAAC;AAAA,IACvC,cAAc,IAAI;AAAA,IAClB,YAAY,IAAI;AAAA,IAChB,yBAAyB,IAAI;AAAA,IAC7B,YAAY,IAAI;AAAA,IAChB,QAAQ,IAAI;AAAA,EACd;AACF;AAjCsB;AAyCf,SAAS,mBACd,QACA,aACS;AACT,MAAI,CAAC,YAAa,QAAO;AACzB,SAAO,OAAO,aAAa,SAAS,WAAW;AACjD;AANgB;AAoBT,IAAM,yBAA6D;AAAA,EACxE,EAAE,OAAO,CAAC,aAAa,eAAe,GAAG,UAAU,SAAS;AAAA,EAC5D,EAAE,OAAO,CAAC,eAAe,YAAY,GAAG,UAAU,UAAU;AAC9D;AAGA,SAAS,gBAAgB,MAAc,MAAuB;AAC5D,SAAO,SAAS,QAAQ,KAAK,SAAS,IAAI,IAAI,EAAE;AAClD;AAFS;AAWF,SAAS,yBACd,cACA,QAA4C,wBAC7B;AACf,aAAW,OAAO,cAAc;AAC9B,QAAI;AACJ,QAAI;AACF,aAAO,IAAI,IAAI,GAAG,EAAE,KAAK,YAAY;AAAA,IACvC,QAAQ;AACN;AAAA,IACF;AACA,UAAM,QAAQ,MAAM,KAAK,CAAC,SAAS,KAAK,MAAM,KAAK,CAAC,SAAS,gBAAgB,MAAM,IAAI,CAAC,CAAC;AACzF,QAAI,MAAO,QAAO,MAAM;AAAA,EAC1B;AACA,SAAO;AACT;AAfgB;;;AC7HT,IAAM,6BAA6B;AAWnC,IAAM,kCAAN,cAA8C,MAAM;AAAA,EA5B3D,OA4B2D;AAAA;AAAA;AAAA,EAChD;AAAA,EAET,YAAY,QAAgB;AAC1B;AAAA,MACE,sCAAsC,MAAM,iBAAY,0BAA0B;AAAA,IACpF;AACA,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAGA,SAAS,gBAAgB,OAA2B;AAElD,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,WAAW;AAChD;AAHS;AAST,eAAsB,iBAAiB,UAAmC;AACxE,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,QAAQ;AAC9C,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AACzD,SAAO,gBAAgB,IAAI,WAAW,MAAM,CAAC;AAC/C;AAJsB;AAatB,SAAS,mBAAmB,GAAW,GAAoB;AACzD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,MAAI,WAAW;AACf,WAAS,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAAG;AACpC,gBAAY,EAAE,WAAW,CAAC,IAAI,EAAE,WAAW,CAAC;AAAA,EAC9C;AACA,SAAO,aAAa;AACtB;AAPS;AAiBT,eAAsB,gBACpB,UACA,iBACA,SAAuC,4BACrB;AAClB,MAAI,WAAW,4BAA4B;AACzC,UAAM,IAAI,gCAAgC,MAAM;AAAA,EAClD;AACA,MAAI,CAAC,gBAAiB,QAAO;AAE7B,QAAM,WAAW,MAAM,iBAAiB,QAAQ;AAChD,SAAO,mBAAmB,UAAU,eAAe;AACrD;AAZsB;;;AClDtB,IAAM,eAAe;AAQd,SAAS,2BAA4C;AAC1D,QAAM,WAAW,oBAAI,IAAoB;AACzC,SAAO;AAAA,IACL,QAAQ,KAAa,OAAwB;AAC3C,iBAAW,CAAC,MAAM,SAAS,KAAK,UAAU;AACxC,YAAI,aAAa,MAAO,UAAS,OAAO,IAAI;AAAA,MAC9C;AACA,UAAI,SAAS,IAAI,GAAG,EAAG,QAAO;AAC9B,eAAS,IAAI,KAAK,QAAQ,YAAY;AACtC,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAZgB;;;ACtChB,SAAS,WAAAA,UAAS,aAAAC,YAAW,aAAAC,kBAAkC;AAsBxD,IAAM,2BAA2B,KAAK;AAG7C,IAAMC,2BAA0B;AAazB,IAAM,mBAAN,cAA+B,MAAM;AAAA,EAtC5C,OAsC4C;AAAA;AAAA;AAAA,EACjC;AAAA,EAET,YAAY,MAA4B,SAAkB;AACxD,UAAM,WAAW,IAAI;AACrB,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AA0BA,SAASC,YAAW,KAAsB;AACxC,SAAO,KAAK,OAAO,OAAO,KAAK,IAAI,KAAK,GAAI;AAC9C;AAFS,OAAAA,aAAA;AAYT,eAAsB,gBACpB,gBACA,OACA,SACwB;AACxB,QAAM,MAAM,MAAM,eAAe;AACjC,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,MAAMA,YAAW,SAAS,GAAG;AACnC,QAAM,MAAM,OAAO,MAAM,cAAc;AACvC,QAAM,QAAQ,MAAM,OAAO,KAAK,GAAG;AAEnC,SAAO,IAAIC,SAAQ,EAAE,OAAO,MAAM,OAAO,MAAM,CAA6B,EACzE,mBAAmB,EAAE,KAAK,aAAa,KAAK,IAAI,IAAI,CAAC,EACrD,UAAU,OAAO,MAAM,MAAM,CAAC,EAC9B,YAAY,iBAAiB,MAAM,QAAQ,MAAM,gBAAgB,yBAAyB,CAAC,EAC3F,WAAW,MAAM,OAAO,EACxB,YAAY,GAAG,EACf,kBAAkB,GAAG,EACrB,OAAO,OAAO,WAAW,CAAC,EAC1B,KAAK,IAAI,UAAU;AACxB;AArBsB;AAkCtB,SAAS,YAAY,OAA0B;AAC7C,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,MAAM,GAAI,QAAO,CAAC;AAC9D,SAAO,CAAC,GAAG,IAAI,IAAI,MAAM,KAAK,EAAE,MAAM,KAAK,CAAC,CAAC;AAC/C;AAHS;AAsBT,eAAe,gBACb,gBACA,OACA,SACqB;AACrB,QAAM,MAAM,MAAM,eAAe;AAEjC,MAAI,CAAC,IAAK,OAAM,IAAI,iBAAiB,iBAAiB,2BAA2B;AAEjF,MAAI;AACF,UAAM,EAAE,QAAQ,IAAI,MAAMC,WAAU,OAAO,MAAMC,WAAU,IAAI,WAAW,WAAW,GAAG;AAAA,MACtF,YAAY,CAAC,WAAW;AAAA,MACxB,QAAQ,OAAO,QAAQ,MAAM;AAAA,MAC7B,UAAU,iBAAiB,QAAQ,QAAQ,QAAQ,gBAAgB,yBAAyB;AAAA,MAC5F,gBAAgBJ;AAAA,MAChB,aAAa,QAAQ,QAAQ,SAAY,SAAY,IAAI,KAAK,QAAQ,GAAG;AAAA,IAC3E,CAAC;AACD,WAAO;AAAA,EACT,QAAQ;AACN,UAAM,IAAI,iBAAiB,iBAAiB,2BAA2B;AAAA,EACzE;AACF;AArBe;AAuBf,eAAsB,kBACpB,gBACA,OACA,SAC8B;AAC9B,QAAM,UAAU,MAAM,gBAAgB,gBAAgB,OAAO,OAAO;AAEpE,QAAM,QAAQ,OAAO,QAAQ,UAAU,WAAW,QAAQ,QAAQ;AAClE,QAAM,UAAU,OAAO,QAAQ,QAAQ,WAAW,QAAQ,MAAM;AAChE,MAAI,CAAC,SAAS,CAAC,SAAS;AACtB,UAAM,IAAI,iBAAiB,iBAAiB,gCAAgC;AAAA,EAC9E;AAEA,QAAM,SAAS,YAAY,QAAQ,KAAK;AACxC,MAAI,QAAQ,iBAAiB,CAAC,OAAO,SAAS,QAAQ,aAAa,GAAG;AACpE,UAAM,IAAI;AAAA,MACR;AAAA,MACA,+BAA+B,QAAQ,aAAa;AAAA,IACtD;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,SAAS,OAAO;AAClC;AAtBsB;;;ACnKtB,SAAS,cAAAK,aAAY,eAAAC,oBAAmB;AA+BxC,IAAM,sBAAsB;AAGrB,IAAM,uBAAuB,KAAK,KAAK,KAAK,KAAK;AAWjD,IAAM,oBAAN,cAAgC,MAAM;AAAA,EA7C7C,OA6C6C;AAAA;AAAA;AAAA,EAClC;AAAA,EAET,YAAY,MAA6B,SAAkB;AACzD,UAAM,WAAW,IAAI;AACrB,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAUO,SAAS,UAAU,OAAuB;AAC/C,SAAOC,YAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;AAFgB;AAKhB,SAAS,gBAAwB;AAC/B,SAAOC,aAAY,mBAAmB,EAAE,SAAS,KAAK;AACxD;AAFS;AAUT,SAAS,SAAS,SAAoC;AACpD,SAAO,IAAI,KAAK,KAAK,IAAI,KAAK,QAAQ,SAAS,qBAAqB;AACtE;AAFS;AAQT,eAAsB,kBACpB,SACA,SAC6B;AAC7B,QAAM,eAAe,cAAc;AACnC,QAAM,MAAuB;AAAA,IAC3B,WAAW,UAAU,YAAY;AAAA,IACjC,WAAW,QAAQ;AAAA,IACnB,SAAS,QAAQ;AAAA,IACjB,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB,WAAW,SAAS,OAAO;AAAA,IAC3B,aAAa;AAAA,EACf;AACA,QAAM,QAAQ,MAAM,OAAO,GAAG;AAC9B,SAAO,EAAE,cAAc,QAAQ,QAAQ,OAAO;AAChD;AAhBsB;AA8BtB,SAAS,kBAAkB,QAA4C;AACrE,QAAM,SAAS,oBAAI,IAAgC;AACnD,QAAM,aAAa,oBAAI,IAAsB;AAC7C,aAAW,OAAO,QAAQ;AACxB,WAAO,IAAI,IAAI,WAAW,GAAG;AAC7B,QAAI,CAAC,IAAI,YAAa;AACtB,UAAM,WAAW,WAAW,IAAI,IAAI,WAAW,KAAK,CAAC;AACrD,aAAS,KAAK,IAAI,SAAS;AAC3B,eAAW,IAAI,IAAI,aAAa,QAAQ;AAAA,EAC1C;AACA,SAAO,EAAE,QAAQ,WAAW;AAC9B;AAXS;AAmBT,SAAS,eAAe,OAAqB,UAA+B;AAC1E,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAQ,CAAC,QAAQ;AACvB,SAAO,MAAM,SAAS,GAAG;AACvB,UAAM,OAAO,MAAM,MAAM;AACzB,QAAI,CAAC,QAAQ,QAAQ,IAAI,IAAI,EAAG;AAChC,YAAQ,IAAI,IAAI;AAEhB,UAAM,SAAS,MAAM,OAAO,IAAI,IAAI,GAAG,eAAe;AACtD,QAAI,UAAU,CAAC,QAAQ,IAAI,MAAM,EAAG,OAAM,KAAK,MAAM;AAErD,UAAM,YAAY,MAAM,WAAW,IAAI,IAAI,KAAK,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,QAAQ,IAAI,KAAK,CAAC;AACzF,UAAM,KAAK,GAAG,QAAQ;AAAA,EACxB;AACA,SAAO;AACT;AAfS;AAsBT,eAAe,cACb,SACA,UACA,UACe;AAGf,QAAM,SAAS,MAAM,QAAQ,MAAM,WAAW,SAAS,WAAW,SAAS,QAAQ;AACnF,QAAM,UAAU,eAAe,kBAAkB,MAAM,GAAG,QAAQ;AAClE,QAAM,QAAQ,MAAM,aAAa,CAAC,GAAG,OAAO,GAAG,oBAAI,KAAK,CAAC;AAC3D;AAVe;AAaf,SAAS,eAAe,SAA6B,WAAgC;AACnF,QAAM,SAAS,aAAa,QAAQ;AACpC,QAAM,WAAW,IAAI,IAAI,QAAQ,MAAM;AACvC,aAAW,SAAS,QAAQ;AAC1B,QAAI,CAAC,SAAS,IAAI,KAAK,GAAG;AACxB,YAAM,IAAI;AAAA,QACR;AAAA,QACA,UAAU,KAAK;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAZS;AAyBT,eAAsB,mBACpB,SACA,WACA,kBACA,WAC6B;AAC7B,QAAM,YAAY,UAAU,SAAS;AACrC,QAAM,UAAU,MAAM,QAAQ,MAAM,WAAW,SAAS;AAExD,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,kBAAkB,iBAAiB,uBAAuB;AAAA,EACtE;AACA,MAAI,QAAQ,aAAa,kBAAkB;AACzC,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAGA,MAAI,QAAQ,UAAU,QAAQ,KAAK,KAAK,IAAI,GAAG;AAC7C,UAAM,IAAI,kBAAkB,iBAAiB,uBAAuB;AAAA,EACtE;AAGA,MAAI,QAAQ,aAAc,MAAM,QAAQ,MAAM,aAAa,SAAS,GAAI;AACtE,UAAM,OAAO,SAAS,SAAS,SAAS;AAAA,EAC1C;AAEA,QAAM,SAAS,eAAe,SAAS,SAAS;AAChD,QAAM,qBAAqB,cAAc;AACzC,QAAM,UAAU,MAAM,QAAQ,MAAM;AAAA,IAClC;AAAA,MACE,WAAW,UAAU,kBAAkB;AAAA,MACvC,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,MACjB,UAAU,QAAQ;AAAA,MAClB;AAAA,MACA,WAAW,SAAS,OAAO;AAAA,MAC3B,aAAa;AAAA,IACf;AAAA,IACA;AAAA,IACA,oBAAI,KAAK;AAAA,EACX;AASA,MAAI,CAAC,QAAS,OAAM,OAAO,SAAS,SAAS,SAAS;AAEtD,SAAO,EAAE,cAAc,oBAAoB,OAAO;AACpD;AAvDsB;AA0DtB,eAAe,OACb,SACA,SACA,WACgB;AAChB,QAAM,cAAc,SAAS,SAAS,SAAS;AAC/C,QAAM,IAAI;AAAA,IACR;AAAA,IACA;AAAA,EACF;AACF;AAVe;AA0Bf,eAAsB,wBACpB,SACA,WACsC;AACtC,QAAM,MAAM,MAAM,QAAQ,MAAM,WAAW,UAAU,SAAS,CAAC;AAC/D,SAAO,MAAM,EAAE,WAAW,IAAI,WAAW,SAAS,IAAI,QAAQ,IAAI;AACpE;AANsB;;;ACzOf,IAAM,sBAAqC;AAAA;AAAA,EAEhD,WAAW;AAAA,EACX,OAAO;AAAA,EACP,UAAU;AAAA,EACV,MAAM;AAAA,EACN,6BAA6B;AAAA,EAC7B,2BAA2B;AAC7B;AAsIA,SAAS,eACP,QAUA;AACA,SAAO;AAAA,IACL,QAAQ,OAAO,UAAU,CAAC,GAAG,oBAAoB;AAAA,IACjD,cAAc,OAAO,gBAAgB;AAAA,IACrC,OAAO,EAAE,GAAG,qBAAqB,GAAG,OAAO,MAAM;AAAA,IACjD,WAAW,OAAO,aAAa;AAAA,IAC/B,oBAAoB,OAAO,sBAAsB;AAAA,IACjD,uBAAuB,OAAO,yBAAyB;AAAA,IACvD,mBAAmB,OAAO,qBAAqB;AAAA,EACjD;AACF;AArBS;AAuBF,SAAS,sBAAsB,QAAyC;AAC7E,QAAM,UAAU,OAAO,WAAW;AAClC,QAAM,iBAAiB,OAAO,kBAAkB,CAAC;AACjD,SAAO;AAAA,IACL,QAAQ,OAAO;AAAA,IACf,gBAAgB,OAAO;AAAA;AAAA;AAAA,IAGvB,SAAS,OAAO,YAAY,aAAa,UAAU,MAAM;AAAA;AAAA;AAAA,IAGzD,YAAY,OAAO,cAAc,sBAAsB;AAAA,IACvD;AAAA,IACA,GAAG,eAAe,MAAM;AAAA;AAAA,IAExB,YACE,OAAO,eAAe,eAAe,yBAAyB,IAAI,OAAO;AAAA,IAC3E,SAAS,gBAAgB,MAAM;AAAA,IAC/B,GAAI,OAAO,cAAc,EAAE,aAAa,OAAO,YAAY,IAAI,CAAC;AAAA,IAChE,UAAU,wBAAC,YAAY,kBAAkB,SAAS,cAAc,GAAtD;AAAA,EACZ;AACF;AArBgB;AA4BhB,SAAS,gBAAgB,QAAoD;AAC3E,QAAM,cAAc,IAAI,IAAI,OAAO,wBAAwB,CAAC,CAAC;AAC7D,QAAM,EAAE,gBAAgB,IAAI;AAC5B,SAAO,OAAO,SAAS,QAAQ,WAAW;AACxC,QAAI,YAAY,IAAI,OAAO,QAAQ,EAAG,QAAO;AAC7C,QAAI,CAAC,gBAAiB,QAAO;AAC7B,WAAO,gBAAgB,SAAS,QAAQ,MAAM;AAAA,EAChD;AACF;AARS;AAWF,SAAS,WAAqB;AACnC,SAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AAClD;AAFgB;;;ACvMhB,SAAS,YAAY,KAA2B;AAC9C,QAAM,IAAI,IAAI;AACd,SAAO;AAAA,IACL,cAAc,EAAE,IAAI,eAAe;AAAA,IACnC,UAAU,EAAE,IAAI,WAAW;AAAA,IAC3B,aAAa,EAAE,IAAI,cAAc;AAAA,IACjC,eAAe,EAAE,IAAI,gBAAgB;AAAA,IACrC,qBAAqB,EAAE,IAAI,uBAAuB;AAAA,IAClD,OAAO,EAAE,IAAI,OAAO;AAAA,IACpB,OAAO,EAAE,IAAI,OAAO;AAAA,EACtB;AACF;AAXS;AAcT,SAAS,WAAW,UAA4B;AAC9C,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,SAAS,EAAE,CAAC;AAClE;AAFS;AAST,SAAS,WAAW,SAA2B;AAC7C,SAAO,IAAI,SAAS,SAAS;AAAA,IAC3B,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,4BAA4B;AAAA,EACzD,CAAC;AACH;AALS;AAWT,SAAS,cACP,aACA,OACA,OACU;AACV,QAAM,SAAS,IAAI,IAAI,WAAW;AAClC,SAAO,aAAa,IAAI,SAAS,KAAK;AACtC,MAAI,UAAU,KAAM,QAAO,aAAa,IAAI,SAAS,KAAK;AAC1D,SAAO,WAAW,OAAO,SAAS,CAAC;AACrC;AATS;AAmBT,SAAS,iBAAiB,OAAsB,SAAqC;AACnF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,YAAY,MAAM,MAAM,KAAK,EAAE,OAAO,OAAO;AACnD,QAAM,aAAa,IAAI,IAAY,OAAO;AAC1C,SAAO,UAAU,MAAM,CAAC,cAAc,WAAW,IAAI,SAAS,CAAC;AACjE;AALS;AAaT,eAAe,0BACb,SACA,QACwE;AACxE,MAAI,CAAC,OAAO,SAAU,QAAO,WAAW,oCAAoC;AAC5E,MAAI,CAAC,OAAO,YAAa,QAAO,WAAW,uCAAuC;AAElF,QAAM,SAAS,MAAM,QAAQ,OAAO,QAAQ,eAAe,OAAO,QAAQ;AAC1E,MAAI,CAAC,OAAQ,QAAO,WAAW,mCAAmC;AAClE,MAAI,CAAC,mBAAmB,QAAQ,OAAO,WAAW,GAAG;AACnD,WAAO,WAAW,iDAAiD;AAAA,EACrE;AAEA,SAAO,EAAE,QAAQ,aAAa,OAAO,YAAY;AACnD;AAde;AAqBf,SAAS,yBACP,QACA,aACA,cACiB;AACjB,QAAM,EAAE,MAAM,IAAI;AAElB,MAAI,OAAO,iBAAiB,QAAQ;AAClC,WAAO,cAAc,aAAa,6BAA6B,KAAK;AAAA,EACtE;AAEA,MAAI,CAAC,OAAO,iBAAiB,OAAO,wBAAwB,4BAA4B;AACtF,WAAO,cAAc,aAAa,mBAAmB,KAAK;AAAA,EAC5D;AACA,MAAI,CAAC,iBAAiB,OAAO,OAAO,YAAY,GAAG;AACjD,WAAO,cAAc,aAAa,iBAAiB,KAAK;AAAA,EAC1D;AACA,SAAO;AACT;AAlBS;AAoCT,eAAe,oBACb,SACA,SACA,KACA,QACA,WACmB;AACnB,QAAM,EAAE,aAAa,MAAM,IAAI;AAE/B,QAAM,UAAU,MAAM,QAAQ,eAAe,OAAO;AACpD,MAAI,CAAC,SAAS,OAAO;AAInB,UAAM,WAAW,IAAI,IAAI,QAAQ,WAAW,MAAM;AAClD,aAAS,aAAa,IAAI,QAAQ,oBAAoB,IAAI,WAAW,IAAI,MAAM;AAC/E,WAAO,WAAW,SAAS,SAAS,CAAC;AAAA,EACvC;AAUA,QAAM,SAAS,UAAU,MAAM,MAAM,KAAK,EAAE,OAAO,OAAO;AAC1D,MAAI,CAAE,MAAM,QAAQ,QAAQ,SAAS,UAAU,QAAQ,MAAM,GAAI;AAE/D,WAAO,cAAc,aAAa,iBAAiB,KAAK;AAAA,EAC1D;AAEA,QAAM,OAAO,MAAM,SAAS,QAAQ,YAAY;AAAA;AAAA;AAAA;AAAA,IAI9C,KAAK,QAAQ,WAAW,QAAQ;AAAA,IAChC,OAAO,QAAQ;AAAA,IACf,UAAU,UAAU;AAAA,IACpB;AAAA,IACA,eAAe,UAAU;AAAA,IACzB,OAAO,UAAU;AAAA,IACjB;AAAA,EACF,CAAC;AAED,MAAI,CAAC,MAAM;AAGT,WAAO,cAAc,aAAa,gBAAgB,KAAK;AAAA,EACzD;AAEA,QAAM,UAAU,IAAI,IAAI,WAAW;AACnC,UAAQ,aAAa,IAAI,QAAQ,IAAI;AACrC,MAAI,UAAU,KAAM,SAAQ,aAAa,IAAI,SAAS,KAAK;AAC3D,SAAO,WAAW,QAAQ,SAAS,CAAC;AACtC;AAxDe;AA2Df,eAAsB,kBACpB,SACA,SACmB;AACnB,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,QAAM,SAAS,QAAQ,SAAS,OAAO;AACvC,QAAM,SAAS,YAAY,GAAG;AAG9B,QAAM,eAAe,MAAM,0BAA0B,SAAS,MAAM;AACpE,MAAI,wBAAwB,SAAU,QAAO;AAC7C,QAAM,EAAE,QAAQ,YAAY,IAAI;AAGhC,QAAM,eAAe,yBAAyB,QAAQ,aAAa,OAAO,MAAM;AAChF,MAAI,aAAc,QAAO;AAGzB,SAAO,oBAAoB,SAAS,SAAS,KAAK,QAAQ;AAAA,IACxD;AAAA,IACA,UAAU,OAAO;AAAA,IACjB;AAAA,IACA,eAAe,OAAO;AAAA,IACtB,OAAO,OAAO,SAAS;AAAA,IACvB,OAAO,OAAO;AAAA,EAChB,CAAC;AACH;AA1BsB;;;AC9LtB,IAAM,yBAA6D;AAAA,EACjE;AAAA,EACA;AACF;AAGA,IAAM,wBAA2C,CAAC,sBAAsB,eAAe;AAEvF,IAAMC,uBAAsB,CAAC,sBAAsB,eAAe;AAClE,IAAM,sBAA+C;AAErD,IAAM,eAAe;AAAA,EACnB,gBAAgB;AAAA,EAChB,iBAAiB;AACnB;AAGA,SAAS,kBACP,OACA,QACA,aACU;AACV,QAAM,OAAqE,EAAE,MAAM;AACnF,MAAI,YAAa,MAAK,oBAAoB;AAC1C,SAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG,EAAE,QAAQ,SAAS,EAAE,GAAG,aAAa,EAAE,CAAC;AACpF;AARS;AAWT,SAAS,cAAc,OAAwB;AAC7C,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AAGzB,WAAO,QAAQ,IAAI,QAAQ,KAAK,QAAQ,IAAI,IAAI;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AATS;AA4BT,SAAS,OAAU,OAA0B;AAC3C,SAAO,EAAE,IAAI,MAAM,MAAM;AAC3B;AAFS;AAIT,SAAS,OAAU,UAAoC;AACrD,SAAO,EAAE,IAAI,OAAO,SAAS;AAC/B;AAFS;AAQT,SAAS,qBAAqB,KAAqC;AACjE,MACE,CAAC,MAAM,QAAQ,GAAG,KAClB,IAAI,WAAW,KACf,CAAC,IAAI,MAAM,CAAC,QAAuB,OAAO,QAAQ,YAAY,cAAc,GAAG,CAAC,GAChF;AACA,WAAO;AAAA,MACL;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO,OAAO,CAAC,GAAG,GAAG,CAAC;AACxB;AAfS;AAkBT,SAAS,mBAAmB,KAAoD;AAC9E,MAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO,OAAO,mBAAmB;AACxE,MACE,OAAO,QAAQ,YACf,CAAC,uBAAuB,SAAS,GAA8B,GAC/D;AACA,WAAO;AAAA,MACL;AAAA,QACE;AAAA,QACA;AAAA,QACA,sDAAsD,uBAAuB,KAAK,IAAI,CAAC;AAAA,MACzF;AAAA,IACF;AAAA,EACF;AACA,SAAO,OAAO,GAA8B;AAC9C;AAfS;AAkBT,SAAS,mBAAmB,KAAqC;AAC/D,MAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO,OAAO,CAAC,GAAGA,oBAAmB,CAAC;AAC7E,MACE,CAAC,MAAM,QAAQ,GAAG,KAClB,IAAI,WAAW,KACf,CAAC,IAAI;AAAA,IACH,CAAC,UACC,OAAO,UAAU,YAAY,sBAAsB,SAAS,KAAK;AAAA,EACrE,GACA;AACA,WAAO;AAAA,MACL;AAAA,QACE;AAAA,QACA;AAAA,QACA,uCAAuC,sBAAsB,KAAK,IAAI,CAAC;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AACA,SAAO,OAAO,CAAC,GAAG,GAAG,CAAC;AACxB;AAnBS;AAyBT,SAAS,eAAe,KAAc,iBAA2D;AAC/F,MAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO,OAAO,CAAC,GAAG,eAAe,CAAC;AACzE,MAAI,OAAO,QAAQ,UAAU;AAC3B,WAAO;AAAA,MACL,kBAAkB,2BAA2B,KAAK,wCAAwC;AAAA,IAC5F;AAAA,EACF;AACA,QAAM,YAAY,IAAI,MAAM,KAAK,EAAE,OAAO,OAAO;AACjD,QAAM,YAAY,IAAI,IAAY,eAAe;AACjD,MAAI,CAAC,UAAU,MAAM,CAAC,UAAU,UAAU,IAAI,KAAK,CAAC,GAAG;AACrD,WAAO;AAAA,MACL;AAAA,QACE;AAAA,QACA;AAAA,QACA,8BAA8B,gBAAgB,KAAK,GAAG,CAAC;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AACA,SAAO,OAAO,UAAU,SAAS,IAAI,YAAY,CAAC,GAAG,eAAe,CAAC;AACvE;AAnBS;AA0BT,SAAS,iBACP,UACA,iBACkB;AAClB,QAAM,eAAe,qBAAqB,SAAS,aAAa;AAChE,MAAI,CAAC,aAAa,GAAI,QAAO;AAE7B,QAAM,aAAa,mBAAmB,SAAS,0BAA0B;AACzE,MAAI,CAAC,WAAW,GAAI,QAAO;AAE3B,QAAM,aAAa,mBAAmB,SAAS,WAAW;AAC1D,MAAI,CAAC,WAAW,GAAI,QAAO;AAE3B,QAAM,SAAS,eAAe,SAAS,OAAO,eAAe;AAC7D,MAAI,CAAC,OAAO,GAAI,QAAO;AAEvB,QAAM,gBAAgB,SAAS;AAC/B,QAAM,aAAa,OAAO,kBAAkB,WAAW,gBAAgB;AAEvE,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,OAAO;AAAA,MACL,cAAc,aAAa;AAAA,MAC3B;AAAA,MACA,yBAAyB,WAAW;AAAA,MACpC,YAAY,WAAW;AAAA,MACvB,QAAQ,OAAO;AAAA,IACjB;AAAA,EACF;AACF;AA7BS;AAgCF,SAAS,uBAAiC;AAC/C,SAAO,IAAI;AAAA,IACT,KAAK,UAAU;AAAA,MACb,OAAO;AAAA,MACP,mBAAmB;AAAA,IACrB,CAAC;AAAA,IACD,EAAE,QAAQ,KAAK,SAAS,EAAE,GAAG,aAAa,EAAE;AAAA,EAC9C;AACF;AARgB;AAWhB,eAAsB,iBACpB,SACA,SACmB;AAEnB,MAAI;AACJ,MAAI;AACF,UAAM,SAAkB,MAAM,QAAQ,KAAK;AAC3C,QAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG;AAC1E,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,eAAW;AAAA,EACb,QAAQ;AACN,WAAO,kBAAkB,2BAA2B,KAAK,iCAAiC;AAAA,EAC5F;AAEA,QAAM,YAAY,iBAAiB,UAAU,QAAQ,MAAM;AAC3D,MAAI,CAAC,UAAU,GAAI,QAAO,UAAU;AAEpC,QAAM,aAAa,MAAM,eAAe,QAAQ,OAAO,SAAS,UAAU,KAAK;AAE/E,QAAM,eAA4C;AAAA,IAChD,WAAW,WAAW;AAAA,IACtB,GAAI,WAAW,eAAe,EAAE,eAAe,WAAW,aAAa,IAAI,CAAC;AAAA,IAC5E,qBAAqB,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,IACjD,4BAA4B,WAAW;AAAA,IACvC,eAAe,WAAW;AAAA,IAC1B,aAAa,WAAW;AAAA,IACxB,OAAO,WAAW,OAAO,KAAK,GAAG;AAAA,IACjC,GAAI,WAAW,aAAa,EAAE,aAAa,WAAW,WAAW,IAAI,CAAC;AAAA,EACxE;AAEA,SAAO,IAAI,SAAS,KAAK,UAAU,YAAY,GAAG;AAAA,IAChD,QAAQ;AAAA,IACR,SAAS,EAAE,GAAG,aAAa;AAAA,EAC7B,CAAC;AACH;AAxCsB;;;ACjPtB,SAAS,cAAAC,aAAY,uBAAuB;AA+B5C,IAAMC,gBAAe;AAAA,EACnB,gBAAgB;AAAA,EAChB,iBAAiB;AACnB;AAGO,SAAS,WACd,OACA,QACA,aACA,UAAkC,CAAC,GACzB;AACV,QAAM,OAA8D,EAAE,MAAM;AAC5E,MAAI,YAAa,MAAK,oBAAoB;AAC1C,SAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,IACxC;AAAA,IACA,SAAS,EAAE,GAAGA,eAAc,GAAG,QAAQ;AAAA,EACzC,CAAC;AACH;AAZgB;AAeT,SAAS,aAAa,SAAyC;AACpE,SAAO,IAAI,SAAS,KAAK,UAAU,OAAO,GAAG,EAAE,QAAQ,KAAK,SAAS,EAAE,GAAGA,cAAa,EAAE,CAAC;AAC5F;AAFgB;AAKhB,SAAS,YAAY,GAAW,GAAoB;AAClD,QAAM,OAAO,OAAO,KAAK,GAAG,KAAK;AACjC,QAAM,OAAO,OAAO,KAAK,GAAG,KAAK;AACjC,MAAI,KAAK,WAAW,KAAK,UAAU,KAAK,WAAW,EAAG,QAAO;AAC7D,SAAO,gBAAgB,MAAM,IAAI;AACnC;AALS;AAQT,SAAS,UAAU,OAAuB;AACxC,SAAOC,YAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;AAFS;AAeF,SAAS,sBACd,SACA,MACmB;AACnB,QAAM,gBAAgB,QAAQ,QAAQ,IAAI,eAAe;AACzD,MAAI,iBAAiB,cAAc,WAAW,QAAQ,GAAG;AACvD,UAAM,UAAU,OAAO,KAAK,cAAc,MAAM,CAAC,GAAG,QAAQ,EAAE,SAAS,MAAM;AAC7E,UAAM,YAAY,QAAQ,QAAQ,GAAG;AACrC,QAAI,cAAc,IAAI;AACpB,aAAO;AAAA,QACL,UAAU,QAAQ,MAAM,GAAG,SAAS;AAAA,QACpC,cAAc,QAAQ,MAAM,YAAY,CAAC;AAAA,MAC3C;AAAA,IACF;AAAA,EACF;AACA,SAAO,EAAE,UAAU,KAAK,IAAI,WAAW,GAAG,cAAc,KAAK,IAAI,eAAe,EAAE;AACpF;AAhBgB;AAmBhB,IAAM,wBAAwB,EAAE,oBAAoB,4BAA4B;AAShF,eAAsB,mBACpB,SACA,aACA,kBAC0B;AAC1B,QAAM,WAAW,YAAY;AAC7B,MAAI,CAAC,UAAU;AACb,WAAO,WAAW,kBAAkB,KAAK,qBAAqB,qBAAqB;AAAA,EACrF;AACA,MAAI,oBAAoB,aAAa,kBAAkB;AAErD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,QAAQ,eAAe,QAAQ;AACpD,MAAI,CAAC,QAAQ;AACX,WAAO,WAAW,kBAAkB,KAAK,kBAAkB,qBAAqB;AAAA,EAClF;AAEA,MAAI,OAAO,4BAA4B,uBAAuB;AAC5D,UAAM,SAAS,YAAY;AAC3B,QAAI,CAAC,UAAU,CAAC,OAAO,kBAAkB;AACvC,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,YAAY,UAAU,MAAM,GAAG,OAAO,gBAAgB,GAAG;AAC5D,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AA7CsB;;;ACtEtB,IAAM,+BAA+B;AAarC,eAAe,qBACb,SACA,OACA,UACe;AACf,QAAM,YAAY,QAAQ;AAC1B,QAAM,QAAQ,QAAQ,OAAO;AAC7B,MAAI,CAAC,aAAa,CAAC,MAAO;AAC1B,MAAI;AACF,UAAM,wBAAwB,SAAS,EAAE,WAAW,MAAM,GAAG,OAAO,QAAQ;AAAA,EAC9E,QAAQ;AAAA,EAGR;AACF;AAde;AAiBf,eAAe,wBACb,SACA,OACA,OACA,UACe;AACf,QAAM,EAAE,WAAW,MAAM,IAAI;AAC7B,QAAM,CAAC,QAAQ,MAAM,IAAI,MAAM,QAAQ,IAAI;AAAA,IACzC,UAAU,cAAc,KAAK;AAAA,IAC7B,QAAQ,OAAO,QAAQ,eAAe,QAAQ;AAAA,EAChD,CAAC;AAGD,MAAI,CAAC,OAAQ;AAEb,QAAM,aAAa,UAAU,sBAAsB;AACnD,QAAM,eAAe,MAAM,MAAM,aAAa,QAAQ,QAAQ;AAC9D,QAAM,MAAM,oBAAI,KAAK;AACrB,MAAI,gBAAgB,IAAI,QAAQ,IAAI,aAAa,QAAQ,IAAI,WAAY;AAEzE,QAAM,MAAM,eAAe;AAAA,IACzB;AAAA,IACA,eAAe;AAAA,IACf,YAAY,QAAQ,cAAc;AAAA;AAAA;AAAA,IAGlC,MAAM,SAAS,yBAAyB,OAAO,cAAc,UAAU,aAAa,IAAI;AAAA,IACxF,IAAI;AAAA,EACN,CAAC;AACH;AA7Be;AA0Cf,SAAS,4BAA4B,MAA2D;AAC9F,QAAM,OAAO,KAAK,IAAI,MAAM;AAC5B,QAAM,cAAc,KAAK,IAAI,cAAc;AAC3C,QAAM,eAAe,KAAK,IAAI,eAAe;AAE7C,MAAI,CAAC,KAAM,QAAO,WAAW,mBAAmB,KAAK,cAAc;AACnE,MAAI,CAAC,YAAa,QAAO,WAAW,mBAAmB,KAAK,sBAAsB;AAClF,MAAI,CAAC,aAAc,QAAO,WAAW,mBAAmB,KAAK,uBAAuB;AAEpF,SAAO,EAAE,MAAM,aAAa,aAAa;AAC3C;AAVS;AAqBT,eAAe,WACb,SACA,QACA,aACA,QAC4D;AAC5D,QAAM,EAAE,MAAM,aAAa,aAAa,IAAI;AAG5C,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,WAAW,QAAQ,YAAY,MAAM,EAAE,OAAO,CAAC;AAAA,EAClE,SAAS,OAAO;AACd,QAAI,iBAAiB,wBAAwB;AAC3C,aAAO,WAAW,iBAAiB,KAAK,uCAAuC;AAAA,IACjF;AACA,UAAM;AAAA,EACR;AAEA,QAAM,YAAY,MAAM;AAAA,IACtB,QAAQ,OAAO;AAAA,IACf;AAAA,IACA,SAAS;AAAA,EACX;AACA,MAAI,UAAW,QAAO;AAGtB,MAAI,gBAAgB,SAAS,aAAa;AACxC,WAAO,WAAW,iBAAiB,KAAK,uBAAuB;AAAA,EACjE;AAGA,MAAI,CAAE,MAAM,gBAAgB,cAAc,SAAS,aAAa,GAAI;AAClE,WAAO,WAAW,iBAAiB,KAAK,0BAA0B;AAAA,EACpE;AAGA,MAAI,CAAE,MAAM,QAAQ,WAAW,QAAQ,SAAS,KAAK,KAAK,IAAI,CAAC,GAAI;AACjE,WAAO,WAAW,iBAAiB,KAAK,iCAAiC;AAAA,EAC3E;AAEA,SAAO;AACT;AA1Ce;AA6Cf,eAAe,wBACb,SACA,MACA,aACA,QACmB;AACnB,QAAM,SAAS,4BAA4B,IAAI;AAC/C,MAAI,kBAAkB,SAAU,QAAO;AAEvC,QAAM,WAAW,MAAM,WAAW,SAAS,QAAQ,aAAa,MAAM;AACtE,MAAI,oBAAoB,SAAU,QAAO;AAEzC,QAAM,SAAS,SAAS,MAAM,MAAM,KAAK,EAAE,OAAO,OAAO;AAEzD,QAAM,cAAc,MAAM,gBAAgB,QAAQ,YAAY;AAAA,IAC5D,OAAO,SAAS;AAAA,IAChB,SAAS,SAAS;AAAA,IAClB;AAAA,IACA;AAAA,IACA,cAAc,QAAQ;AAAA,IACtB,YAAY,QAAQ;AAAA,EACtB,CAAC;AACD,MAAI,CAAC,aAAa;AAGhB,WAAO,WAAW,mBAAmB,KAAK,4BAA4B;AAAA,EACxE;AAEA,QAAM,UAAU,MAAM;AAAA,IACpB,EAAE,OAAO,QAAQ,OAAO,eAAe,OAAO,QAAQ,kBAAkB;AAAA,IACxE;AAAA,MACE,WAAW,SAAS;AAAA,MACpB,SAAS,SAAS;AAAA,MAClB,UAAU,SAAS;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,qBAAqB,SAAS,SAAS,OAAO,SAAS,QAAQ;AAErE,SAAO,aAAa;AAAA,IAClB,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,YAAY,QAAQ,yBAAyB;AAAA,IAC7C,eAAe,QAAQ;AAAA,IACvB,OAAO,QAAQ,OAAO,KAAK,GAAG;AAAA,EAChC,CAAC;AACH;AA/Ce;AAkDf,eAAe,mBACb,SACA,MACA,aACA,QACmB;AACnB,QAAM,eAAe,KAAK,IAAI,eAAe;AAC7C,QAAM,iBAAiB,KAAK,IAAI,OAAO;AAEvC,MAAI,CAAC,aAAc,QAAO,WAAW,mBAAmB,KAAK,uBAAuB;AAMpF,QAAM,YAAY,MAAM,mBAAmB,QAAQ,OAAO,SAAS,WAAW;AAC9E,MAAI,UAAW,QAAO;AACtB,QAAM,WAAW,YAAY;AAE7B,QAAM,YAAY,iBAAiB,eAAe,MAAM,KAAK,EAAE,OAAO,OAAO,IAAI;AACjF,QAAM,iBAAiB;AAAA,IACrB,OAAO,QAAQ,OAAO;AAAA,IACtB,OAAO,QAAQ;AAAA,EACjB;AAKA,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,mBAAmB,gBAAgB,cAAc,UAAU,SAAS;AAAA,EACtF,SAAS,OAAO;AACd,QAAI,iBAAiB,mBAAmB;AACtC,aAAO,WAAW,MAAM,MAAM,KAAK,MAAM,OAAO;AAAA,IAClD;AACA,UAAM;AAAA,EACR;AAKA,QAAM,WAAW,MAAM,wBAAwB,gBAAgB,QAAQ,YAAY;AACnF,MAAI,CAAC,SAAU,QAAO,WAAW,iBAAiB,KAAK,iCAAiC;AAExF,QAAM,cAAc,MAAM,gBAAgB,QAAQ,YAAY;AAAA,IAC5D,OAAO,SAAS;AAAA,IAChB,SAAS,SAAS;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB;AAAA,IACA,cAAc,QAAQ;AAAA,IACtB,YAAY,QAAQ;AAAA,EACtB,CAAC;AACD,MAAI,CAAC,YAAa,QAAO,WAAW,mBAAmB,KAAK,4BAA4B;AAExF,QAAM,qBAAqB,SAAS,SAAS,WAAW,QAAQ;AAEhE,SAAO,aAAa;AAAA,IAClB,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,YAAY,QAAQ,yBAAyB;AAAA,IAC7C,eAAe,QAAQ;AAAA,IACvB,OAAO,QAAQ,OAAO,KAAK,GAAG;AAAA,EAChC,CAAC;AACH;AA/De;AAqEf,eAAsB,cACpB,SACA,SACmB;AACnB,QAAM,SAAS,QAAQ,SAAS,OAAO;AAEvC,MAAI;AACJ,MAAI;AACF,WAAO,IAAI,gBAAgB,MAAM,QAAQ,KAAK,CAAC;AAAA,EACjD,QAAQ;AACN,WAAO,WAAW,mBAAmB,KAAK,wBAAwB;AAAA,EACpE;AAEA,QAAM,YAAY,KAAK,IAAI,YAAY;AACvC,MAAI,CAAC,UAAW,QAAO,WAAW,mBAAmB,KAAK,oBAAoB;AAE9E,QAAM,cAAc,sBAAsB,SAAS,IAAI;AAEvD,UAAQ,WAAW;AAAA,IACjB,KAAK;AACH,aAAO,wBAAwB,SAAS,MAAM,aAAa,MAAM;AAAA,IACnE,KAAK;AACH,aAAO,mBAAmB,SAAS,MAAM,aAAa,MAAM;AAAA,IAC9D;AACE,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,eAAe,SAAS;AAAA,MAC1B;AAAA,EACJ;AACF;AA9BsB;;;AChNtB,SAAS,aACP,MACA,SAAS,KACT,UAAkC,CAAC,GACzB;AACV,SAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,IACxC;AAAA,IACA,SAAS,EAAE,gBAAgB,mCAAmC,GAAG,QAAQ;AAAA,EAC3E,CAAC;AACH;AATS;AAiBT,eAAe,aAAa,SAA6C;AACvE,QAAM,MAAM,MAAM,QAAQ,WAAW;AACrC,MAAI,CAAC,IAAK,QAAO,aAAa,EAAE,OAAO,0BAA0B,GAAG,GAAG;AACvE,SAAO,aAAa,EAAE,MAAM,CAAC,IAAI,SAAS,EAAE,GAAG,KAAK;AAAA;AAAA;AAAA,IAGlD,iBAAiB;AAAA,EACnB,CAAC;AACH;AARe;AAWf,SAAS,kBACP,SACqF;AACrF,SAAO;AAAA,IACL,6BAA6B,8BAAO,YAClC;AAAA,MACE,iCAAiC;AAAA,QAC/B,QAAQ,OAAO,QAAQ,SAAS,OAAO,CAAC;AAAA,QACxC,iBAAiB,CAAC,GAAG,QAAQ,MAAM;AAAA;AAAA;AAAA,QAGnC,OAAO,QAAQ;AAAA,MACjB,CAAC;AAAA,IACH,GAT2B;AAAA,IAU7B,2BAA2B,8BAAO,YAAY;AAC5C,YAAM,SAAS,QAAQ,SAAS,OAAO;AACvC,aAAO;AAAA,QACL,+BAA+B;AAAA,UAC7B,UAAU,iBAAiB,QAAQ,QAAQ,YAAY;AAAA,UACvD,sBAAsB,CAAC,OAAO,MAAM,CAAC;AAAA,UACrC,iBAAiB,CAAC,GAAG,QAAQ,MAAM;AAAA,QACrC,CAAC;AAAA,MACH;AAAA,IACF,GAT2B;AAAA,EAU7B;AACF;AAzBS;AAmCT,SAAS,cAAc,SAA4C;AACjE,QAAM,QACJ,wBAAC,YACD,OAAO,YACL,QAAQ,QAAQ,IAAI,QAAQ,OAAO,IAAI,SAAS,GAFlD;AAGF,QAAM,YAAY,kBAAkB,OAAO;AAE3C,SAAO;AAAA,IACL,WAAW,MAAM,CAAC,YAAY,kBAAkB,SAAS,OAAO,CAAC;AAAA,IACjE,OAAO,MAAM,CAAC,YAAY,cAAc,SAAS,OAAO,CAAC;AAAA,IACzD,UAAU,8BAAO,YACf,QAAQ,QAAQ,IAAI,iBAAiB,SAAS,OAAO,IAAI,qBAAqB,GADtE;AAAA,IAEV,MAAM,MAAM,MAAM,aAAa,OAAO,CAAC;AAAA,IACvC,6BAA6B,MAAM,UAAU,2BAA2B;AAAA,IACxE,2BAA2B,MAAM,UAAU,yBAAyB;AAAA,EACtE;AACF;AAhBS;AAmBT,SAAS,YAAY,SAA0B,UAA6C;AAC1F,QAAM,EAAE,MAAM,IAAI;AAClB,SAAO;AAAA,IACL;AAAA,MACE,QAAQ;AAAA,MACR,MAAM,MAAM;AAAA,MACZ,QAAQ,SAAS;AAAA,IACnB;AAAA,IACA;AAAA,MACE,QAAQ;AAAA,MACR,MAAM,MAAM;AAAA,MACZ,QAAQ,SAAS;AAAA,IACnB;AAAA,IACA,EAAE,QAAQ,OAAO,MAAM,MAAM,MAAM,QAAQ,SAAS,KAAK;AAAA,IACzD,EAAE,QAAQ,OAAO,MAAM,MAAM,WAAW,QAAQ,SAAS,UAAU;AAAA,IACnE,EAAE,QAAQ,QAAQ,MAAM,MAAM,OAAO,QAAQ,SAAS,MAAM;AAAA,IAC5D,EAAE,QAAQ,QAAQ,MAAM,MAAM,UAAU,QAAQ,SAAS,SAAS;AAAA,EACpE;AACF;AAlBS;AAoBF,SAAS,kBAAkB,QAAqC;AACrE,QAAM,UAAU,sBAAsB,MAAM;AAC5C,QAAM,WAAW,cAAc,OAAO;AAEtC,SAAO;AAAA,IACL,QAAQ,YAAY,SAAS,QAAQ;AAAA,IACrC;AAAA,IACA,cAAc,wBAAC,OAAO,SAAS,YAC7B,kBAAkB,QAAQ,YAAY,OAAO;AAAA,MAC3C,GAAG;AAAA,MACH,QAAQ,QAAQ,SAAS,OAAO;AAAA,MAChC,cAAc,QAAQ;AAAA,IACxB,CAAC,GALW;AAAA,IAMd;AAAA,EACF;AACF;AAfgB;","names":["SignJWT","jwtVerify","importJWK","CLOCK_TOLERANCE_SECONDS","nowSeconds","SignJWT","jwtVerify","importJWK","createHash","randomBytes","createHash","randomBytes","DEFAULT_GRANT_TYPES","createHash","JSON_HEADERS","createHash"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/oauth/prisma-stores.ts","../src/oauth/connections.ts"],"sourcesContent":["import type {\n McpConnectionStore,\n McpOauthStores,\n NewOAuthClient,\n NewRefreshToken,\n OAuthClientStore,\n RefreshTokenStore,\n StoredMcpConnection,\n StoredOAuthClient,\n StoredRefreshToken,\n} from \"./stores\";\n\n/**\n * The ports of `./stores.ts`, filled by Prisma (12-23).\n *\n * The package owns the three models (`prisma/mcp.prisma`), so their delegate\n * shapes are known and this adapter can be exact. A host with Prisma therefore\n * writes ONE line —\n *\n * stores: createPrismaMcpStores(async () => getPrismaClient() as unknown as McpOauthPrisma)\n *\n * — and no host code at all beyond it. The client is duck-typed (only the\n * delegates used, only the arguments used) so this file never imports a project's\n * generated client, and a non-Prisma host fills the ports directly instead.\n */\n\n/** A `where` on the composite unique of `mcp_connections`. */\ninterface ConnectionKey {\n userId_oauthClientId: { userId: string; oauthClientId: string };\n}\n\n/** The minimal Prisma surface the AS needs. Every field is one the surface writes. */\nexport interface McpOauthPrisma {\n oAuthClient: {\n create(args: { data: NewOAuthClient }): Promise<StoredOAuthClient>;\n findUnique(args: { where: { clientId: string } }): Promise<StoredOAuthClient | null>;\n };\n oAuthRefreshToken: {\n create(args: { data: NewRefreshToken }): Promise<unknown>;\n findUnique(args: { where: { tokenHash: string } }): Promise<StoredRefreshToken | null>;\n findFirst(args: { where: { rotatedFrom: string } }): Promise<{ tokenHash: string } | null>;\n findMany(args: {\n where: { userEmail: string; clientId: string };\n }): Promise<StoredRefreshToken[]>;\n // No single-row `update`: the rotation used to revoke its parent with one and\n // that was the bug (unconditional, so two concurrent rotations both won). Every\n // revoke here is now an `updateMany` with a predicate that says WHICH rows may\n // move, which is also why this delegate list stays honest about what is written.\n updateMany(args: {\n where:\n | { tokenHash: { in: string[] } }\n | { userEmail: string; clientId: string; revokedAt: null };\n data: { revokedAt: Date };\n }): Promise<{ count: number }>;\n };\n mcpConnection: {\n findUnique(args: {\n where: ConnectionKey;\n select: { lastActiveAt: true };\n }): Promise<{ lastActiveAt: Date } | null>;\n findFirst(args: {\n where: { userId: string; revokedAt: null; host: null };\n orderBy: { lastActiveAt: \"desc\" };\n select: { id: true };\n }): Promise<{ id: string } | null>;\n findMany(args: {\n where: { userId: string; revokedAt: null; host?: string | null };\n orderBy?: { lastActiveAt: \"desc\" };\n select: Record<string, true>;\n }): Promise<Record<string, unknown>[]>;\n upsert(args: {\n where: ConnectionKey;\n create: Record<string, unknown>;\n update: Record<string, unknown>;\n }): Promise<unknown>;\n update(args: { where: { id: string }; data: Record<string, unknown> }): Promise<unknown>;\n updateMany(args: {\n where: { id: { in: string[] } } | { userId: string; revokedAt: null; host: string };\n data: Record<string, unknown>;\n }): Promise<{ count: number }>;\n };\n /**\n * Prisma's INTERACTIVE transaction, used for the rotation's claim + write. The\n * callback form (not the array form) is required: the successor may only be\n * created once the conditional revoke has reported that it, and not a concurrent\n * sibling, claimed the parent — see `RefreshTokenStore.rotate`.\n */\n $transaction<T>(fn: (tx: McpOauthTx) => Promise<T>): Promise<T>;\n}\n\n/**\n * The delegate subset used INSIDE the rotation transaction. Not exported: it is\n * reachable structurally through `McpOauthPrisma.$transaction`, so no host ever\n * needs to name it, and exporting a type nobody imports is what knip flags.\n */\ninterface McpOauthTx {\n oAuthRefreshToken: {\n create(args: { data: NewRefreshToken }): Promise<unknown>;\n updateMany(args: {\n where: { tokenHash: string; revokedAt: null };\n data: { revokedAt: Date };\n }): Promise<{ count: number }>;\n };\n}\n\n/** A lazily-resolved client, so a host's singleton is awaited per call. */\nexport type McpOauthPrismaProvider = () => Promise<McpOauthPrisma>;\n\nfunction clientStore(getPrisma: McpOauthPrismaProvider): OAuthClientStore {\n return {\n async create(client: NewOAuthClient) {\n const prisma = await getPrisma();\n return prisma.oAuthClient.create({ data: client });\n },\n async findByClientId(clientId: string) {\n const prisma = await getPrisma();\n return prisma.oAuthClient.findUnique({ where: { clientId } });\n },\n };\n}\n\nfunction refreshTokenStore(getPrisma: McpOauthPrismaProvider): RefreshTokenStore {\n return {\n async create(token) {\n const prisma = await getPrisma();\n await prisma.oAuthRefreshToken.create({ data: token });\n },\n async findByHash(tokenHash) {\n const prisma = await getPrisma();\n return prisma.oAuthRefreshToken.findUnique({ where: { tokenHash } });\n },\n async hasSuccessor(tokenHash) {\n const prisma = await getPrisma();\n const successor = await prisma.oAuthRefreshToken.findFirst({\n where: { rotatedFrom: tokenHash },\n });\n return successor !== null;\n },\n async listFamily(userEmail, clientId) {\n const prisma = await getPrisma();\n return prisma.oAuthRefreshToken.findMany({ where: { userEmail, clientId } });\n },\n async revokeHashes(tokenHashes, at) {\n if (tokenHashes.length === 0) return;\n const prisma = await getPrisma();\n await prisma.oAuthRefreshToken.updateMany({\n where: { tokenHash: { in: [...tokenHashes] } },\n data: { revokedAt: at },\n });\n },\n async rotate(successor, parentHash, at) {\n const prisma = await getPrisma();\n return prisma.$transaction(async (tx) => {\n // CLAIM-ONCE. The `revokedAt: null` predicate is what makes this safe under\n // concurrency, and it is load-bearing rather than defensive: on Postgres's\n // default READ COMMITTED, a second transaction's `updateMany` blocks on the\n // row lock, then re-evaluates this WHERE against the COMMITTED row — which\n // now has a `revokedAt` — and reports 0 rows. So exactly one caller can ever\n // see count 1, and it is the only one that goes on to create a successor.\n // An unconditional `update` would let both through: two live successors of\n // one parent, and replay detection silently defeated (it waits for a third\n // use of the parent that now never comes).\n const { count } = await tx.oAuthRefreshToken.updateMany({\n where: { tokenHash: parentHash, revokedAt: null },\n data: { revokedAt: at },\n });\n // Lost the claim: write NOTHING. The zero-row update commits as the no-op\n // it is, so there is nothing to roll back.\n if (count !== 1) return false;\n // Same transaction as the claim, so a crash cannot leave a live parent AND\n // a live child either.\n await tx.oAuthRefreshToken.create({ data: successor });\n return true;\n });\n },\n async revokeLiveForClient(userEmail, clientId) {\n const prisma = await getPrisma();\n const { count } = await prisma.oAuthRefreshToken.updateMany({\n where: { userEmail, clientId, revokedAt: null },\n data: { revokedAt: new Date() },\n });\n return count;\n },\n };\n}\n\n/** The connection columns the account surface reads. */\nconst CONNECTION_SELECT = {\n oauthClientId: true,\n clientName: true,\n host: true,\n connectedAt: true,\n lastActiveAt: true,\n} as const;\n\nfunction connectionStore(getPrisma: McpOauthPrismaProvider): McpConnectionStore {\n return {\n async lastActiveAt(userId, oauthClientId) {\n const prisma = await getPrisma();\n const row = await prisma.mcpConnection.findUnique({\n where: { userId_oauthClientId: { userId, oauthClientId } },\n select: { lastActiveAt: true },\n });\n return row?.lastActiveAt ?? null;\n },\n async recordActivity({ userId, oauthClientId, clientName, host, at }) {\n const prisma = await getPrisma();\n await prisma.mcpConnection.upsert({\n where: { userId_oauthClientId: { userId, oauthClientId } },\n create: { userId, oauthClientId, clientName, host, connectedAt: at, lastActiveAt: at },\n // Never blank a known host on refresh — keep the existing attribution\n // when this grant cannot derive one.\n update: {\n clientName,\n lastActiveAt: at,\n revokedAt: null,\n ...(host ? { host } : {}),\n },\n });\n },\n async listActive(userId) {\n const prisma = await getPrisma();\n const rows = await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null },\n orderBy: { lastActiveAt: \"desc\" },\n select: { ...CONNECTION_SELECT },\n });\n return rows as unknown as StoredMcpConnection[];\n },\n revokeByHost: (userId, host) => revokeByHost(getPrisma, userId, host),\n announce: (userId, host) => announce(getPrisma, userId, host),\n };\n}\n\n/**\n * Disconnect one provider's connections — and ONLY that provider's.\n *\n * Every read and write is scoped by `userId`: a connection is per-user (an MCP\n * bearer is not tenant-scoped), so the user id IS the isolation here, and the\n * `id` list passed to the update comes from a query that already applied it.\n */\nasync function revokeByHost(\n getPrisma: McpOauthPrismaProvider,\n userId: string,\n host: string,\n): Promise<string[]> {\n const prisma = await getPrisma();\n const attributed = await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null, host },\n select: { id: true, oauthClientId: true },\n });\n // A legacy `host = null` row is claimed only when the provider has no row of\n // its own: pre-attribution connections must stay disconnectable, but a provider\n // that DID attribute can never revoke another assistant's row.\n const targets =\n attributed.length > 0\n ? attributed\n : await prisma.mcpConnection.findMany({\n where: { userId, revokedAt: null, host: null },\n select: { id: true, oauthClientId: true },\n });\n if (targets.length === 0) return [];\n await prisma.mcpConnection.updateMany({\n where: { id: { in: targets.map((row) => String(row.id)) } },\n data: { revokedAt: new Date() },\n });\n return targets.map((row) => String(row.oauthClientId));\n}\n\n/** A provider's self-report: refresh its own row, or claim the unattributed one. */\nasync function announce(\n getPrisma: McpOauthPrismaProvider,\n userId: string,\n host: string,\n): Promise<number> {\n const prisma = await getPrisma();\n const now = new Date();\n const refreshed = await prisma.mcpConnection.updateMany({\n where: { userId, revokedAt: null, host },\n data: { lastActiveAt: now, revokedAt: null },\n });\n if (refreshed.count > 0) return refreshed.count;\n\n // No row for this provider yet — attribute the just-connected one. Scoped by\n // user, so a self-report can never reach another account's connection.\n const candidate = await prisma.mcpConnection.findFirst({\n where: { userId, revokedAt: null, host: null },\n orderBy: { lastActiveAt: \"desc\" },\n select: { id: true },\n });\n if (!candidate) return 0;\n await prisma.mcpConnection.update({\n where: { id: candidate.id },\n data: { host, lastActiveAt: now, revokedAt: null },\n });\n return 1;\n}\n\n/** Every port, over one lazily-resolved Prisma client. */\nexport function createPrismaMcpStores(getPrisma: McpOauthPrismaProvider): McpOauthStores {\n return {\n clients: clientStore(getPrisma),\n refreshTokens: refreshTokenStore(getPrisma),\n connections: connectionStore(getPrisma),\n };\n}\n","import { providerForHostId, type AiProvider } from \"../guide\";\nimport type {\n McpConnectionStore,\n RefreshTokenStore,\n StoredMcpConnection,\n} from \"./stores\";\n\n/**\n * The account surface's connection OPERATIONS (12-48) — the half of the\n * `GET/DELETE /api/account/mcp-connections` endpoints that is contract rather\n * than host vocabulary.\n *\n * The ROUTE stays in the host on purpose: it mixes the host's session\n * resolution, its response envelope, its published plugin URLs and its logger,\n * and injecting all four here would make the config surface bigger than the\n * handler it replaces. What must NOT stay in each host is the disconnect's\n * both-halves rule, because getting it half right LOOKS right:\n *\n * `connections.revokeByHost` ends the connection rows and returns the OAuth\n * client ids behind them — and a host that stops there has revoked nothing that\n * matters. The assistant still holds a live refresh token for each of those\n * clients, rotates it on schedule, and the very next grant records fresh\n * activity: the card the user just disconnected lights green again on its own.\n * So the rule is one function: revoke the rows AND end every live refresh token\n * of each returned client, in the same call, with no way to import one half\n * without the other.\n *\n * Deliberately NOT invalidated here: the assistant's current ACCESS token.\n * Those are self-contained JWTs the server does not track; a just-disconnected\n * host keeps working for at most their TTL (15 minutes by default) and can then\n * obtain nothing further.\n */\n\n/** An active AI connection, narrowed for display. */\nexport interface AiConnectionSnapshot {\n oauthClientId: string;\n clientName: string | null;\n /** The provider this connection is attributed to (`null` = pre-attribution). */\n host: AiProvider | null;\n connectedAt: Date;\n lastActiveAt: Date;\n}\n\n/** The caller the operations act for — always the session's own user. */\nexport interface AiConnectionCaller {\n /** The host's user id — what `mcp_connections` rows are keyed by. */\n userId: string;\n /** The identity refresh tokens are bound to (the AS binds by email). */\n email: string;\n}\n\n/** What one disconnect actually ended, for the host's log and response. */\nexport interface AiDisconnectResult {\n /** OAuth client ids whose connection rows were revoked. */\n disconnectedClientIds: string[];\n /** Live refresh tokens ended across those clients — the half that cuts access. */\n revokedRefreshTokens: number;\n}\n\n/** Narrow a stored `host` string to a known provider, or `null`. */\nfunction asProvider(host: string | null): AiProvider | null {\n return host === null ? null : providerForHostId(host);\n}\n\n/**\n * A user's active connections, most-recently-active first, with the stored open\n * `host` string narrowed to the package's closed {@link AiProvider} union — the\n * store cannot know which assistants have screens, but the union is this\n * package's own vocabulary (`guide.ts`), so the narrowing lives beside it\n * rather than being re-derived in every host.\n */\nexport async function listAiConnections(\n connections: McpConnectionStore,\n userId: string,\n): Promise<AiConnectionSnapshot[]> {\n const rows: StoredMcpConnection[] = await connections.listActive(userId);\n return rows.map((row) => ({ ...row, host: asProvider(row.host) }));\n}\n\n/**\n * Disconnect one provider for this user — BOTH halves, atomically from the\n * caller's point of view (see the module doc for why one half alone is a\n * disconnect that undoes itself).\n *\n * Idempotent: disconnecting a provider that was never connected returns zero\n * counts rather than failing, so a double-click is harmless. Repeat calls also\n * report zero — `revokeLiveForClient` skips already-revoked tokens by contract.\n */\nexport async function disconnectAiHost(\n stores: { connections: McpConnectionStore; refreshTokens: RefreshTokenStore },\n caller: AiConnectionCaller,\n host: AiProvider,\n): Promise<AiDisconnectResult> {\n const disconnectedClientIds = await stores.connections.revokeByHost(caller.userId, host);\n const revoked = await Promise.all(\n disconnectedClientIds.map((clientId) =>\n stores.refreshTokens.revokeLiveForClient(caller.email, clientId),\n ),\n );\n return {\n disconnectedClientIds,\n revokedRefreshTokens: revoked.reduce((total, count) => total + count, 0),\n };\n}\n"],"mappings":"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","names":[]}
|