@12-apps/mcp 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,446 @@
1
+ import { a as McpSigningKeyProvider, R as RefreshTokenStore, N as NewOAuthClient, S as StoredOAuthClient, b as NewRefreshToken, c as StoredRefreshToken, d as McpOauthStores, e as McpConnectionStore } from '../create-api-mcp-oauth-CwVXKK-A.js';
2
+ export { f as ACCESS_TOKEN_TTL_SECONDS, g as AccessTokenError, h as AccessTokenErrorCode, A as ApiMcpOauth, C as CodeReplayStore, D as DEFAULT_MCP_RESOURCE_PATH, i as DEFAULT_OAUTH_PATHS, j as DEFAULT_PROVIDER_ROOTS, k as DEFAULT_SIGNING_KEY_ENV, l as DEFAULT_SIGNING_KEY_ID_ENV, m as MCP_SUPPORTED_SCOPES, n as McpConnectionRecording, M as McpOauthConfig, o as McpOauthContext, p as McpOauthHandlers, q as McpOauthPaths, r as McpOauthRoute, s as McpOauthSession, t as McpScope, u as McpSigningKey, O as OAuthClientStore, P as ProviderAttributionRule, v as PublicSigningJwk, w as RegisterClientInput, x as RegisteredClient, y as SIGNING_ALG, z as SignAccessTokenInput, B as StoredMcpConnection, T as TokenEndpointAuthMethod, V as VerifiedAccessToken, E as VerifyAccessTokenOptions, F as createApiMcpOauth, G as hashSecret, H as inProcessCodeReplayStore, I as issuer, J as loadSigningKeyFromEnv, K as matchesRedirectUri, L as originFromRequest, Q as providerFromRedirectUris, U as registerClient, W as resolveMcpOauthConfig, X as resolveTrustedOrigin, Y as resourceAudience, Z as signAccessToken, _ as signingKeyProvider, $ as trustedOriginsFromEnv, a0 as verifyAccessToken } from '../create-api-mcp-oauth-CwVXKK-A.js';
3
+ import { h as AiProvider } from '../guide-DV5MQbCg.js';
4
+ import 'jose';
5
+
6
+ /**
7
+ * Stateless authorization-code mint/verify (12-23, ported from the origin host's
8
+ * `lib/mcp/oauth/authorization-code.ts` — behaviour unchanged; the signing key
9
+ * arrives through a provider instead of an env read).
10
+ *
11
+ * The authorization code is a short-lived (<=60s) ES256-signed JWT — no DB table,
12
+ * no cleanup job. It binds the signed-in user (`sub`/`email`), the `client_id`,
13
+ * the `redirect_uri`, the PKCE `code_challenge`, and the requested `scope`, plus a
14
+ * unique `jti` the token endpoint records once to enforce single-use (replay)
15
+ * semantics on top of the short expiry.
16
+ *
17
+ * The code carries a DISTINCT audience (`oauth:code`) from the access token
18
+ * (`${origin}/api/mcp`), so a code can never be presented to the resource server
19
+ * as a bearer access token (and vice versa): {@link verifyCode} pins
20
+ * `audience: "oauth:code"`, and the access-token verifier pins the resource
21
+ * audience — each rejects the other's blobs.
22
+ */
23
+ /**
24
+ * Audience pinning the code to the OAuth code-exchange step only. Distinct from
25
+ * the access-token audience so a code cannot be replayed as an access token.
26
+ */
27
+ declare const AUTHORIZATION_CODE_AUDIENCE = "oauth:code";
28
+ /** Authorization-code lifetime — single-use and short-lived (<=60s per spec). */
29
+ declare const AUTHORIZATION_CODE_TTL_SECONDS = 60;
30
+ /** Fields bound into a minted authorization code. */
31
+ interface MintCodeInput {
32
+ /** The OAuth subject bound to the code (identity from the cookie session). */
33
+ sub: string;
34
+ /** The signed-in user's email (the identity all downstream tokens bind to). */
35
+ email: string;
36
+ /** The OAuth client the code is issued to. */
37
+ clientId: string;
38
+ /** The exact registered redirect URI the flow started with. */
39
+ redirectUri: string;
40
+ /** The PKCE S256 `code_challenge` the token endpoint verifies against. */
41
+ codeChallenge: string;
42
+ /** The requested scope (space-delimited), carried through to the token. */
43
+ scope: string;
44
+ /** The deployment origin — derives the code's `iss`. */
45
+ origin: string;
46
+ }
47
+ /** The bound fields a verified authorization code resolves to. */
48
+ interface VerifiedAuthorizationCode {
49
+ sub: string;
50
+ email: string;
51
+ clientId: string;
52
+ redirectUri: string;
53
+ codeChallenge: string;
54
+ scope: string;
55
+ /** The one-time identifier the token endpoint records to enforce single-use. */
56
+ jti: string;
57
+ }
58
+ /** The single failure discriminator for the OAuth token endpoint. */
59
+ type AuthorizationCodeErrorCode = "invalid_grant";
60
+ /**
61
+ * A typed authorization-code failure. Every rejection (expired, wrong-audience,
62
+ * wrong-issuer, tampered, bad-signature, unconfigured key) surfaces as
63
+ * `invalid_grant` per RFC 6749 §5.2 for the token endpoint.
64
+ */
65
+ declare class AuthorizationCodeError extends Error {
66
+ readonly code: AuthorizationCodeErrorCode;
67
+ constructor(message?: string);
68
+ }
69
+ /** Deterministic-clock option shared by mint + verify. */
70
+ interface ClockOption {
71
+ /** Epoch milliseconds; defaults to `Date.now()`. Injected for deterministic tests. */
72
+ now?: number;
73
+ }
74
+ /**
75
+ * Mint a single-use, stateless authorization code bound to the flow inputs.
76
+ *
77
+ * Returns `null` when no signing key is configured (safe-by-default: the AS
78
+ * refuses to issue rather than falling back to a weaker mode). Sets the `kid`
79
+ * header so the same key resolves the code at verify time.
80
+ */
81
+ declare function mintCode(loadSigningKey: McpSigningKeyProvider, input: MintCodeInput, options?: ClockOption): Promise<string | null>;
82
+ /** Options for {@link verifyCode}. */
83
+ interface VerifyCodeOptions extends ClockOption {
84
+ /** The deployment origin — derives the expected `iss`. */
85
+ origin: string;
86
+ }
87
+ /**
88
+ * Verify a stateless authorization code and return its bound fields.
89
+ *
90
+ * Validates signature (via the public JWK selected by `kid`), `iss`, the
91
+ * `oauth:code` audience, and `exp`. Every failure — expired, wrong-audience (e.g.
92
+ * an access token), wrong-issuer, tampered, bad-signature, or no configured key —
93
+ * throws an {@link AuthorizationCodeError} (`invalid_grant`).
94
+ *
95
+ * The returned `jti` is the one-time identifier the token endpoint records to
96
+ * enforce single-use on top of the short expiry (replay guard).
97
+ */
98
+ declare function verifyCode(loadSigningKey: McpSigningKeyProvider, code: string, options: VerifyCodeOptions): Promise<VerifiedAuthorizationCode>;
99
+
100
+ /**
101
+ * PKCE (RFC 7636) S256 challenge helpers for the OAuth authorization server
102
+ * (12-23, ported verbatim from the origin host's `lib/mcp/oauth/pkce.ts`).
103
+ *
104
+ * OAuth 2.1 mandates the `S256` code-challenge method and forbids `plain`, so
105
+ * this module computes `BASE64URL(SHA-256(code_verifier))` and compares it to
106
+ * the stored `code_challenge` in constant time. The authorization endpoint
107
+ * binds a `code_challenge` into the stateless authorization code; the token
108
+ * endpoint calls {@link verifyChallenge} with the presented `code_verifier` to
109
+ * prove the redeeming client is the one that started the flow.
110
+ *
111
+ * `plain` is refused (throws {@link UnsupportedChallengeMethodError}) rather
112
+ * than silently accepted: `plain` offers no protection against an intercepted
113
+ * authorization code, which is the exact threat PKCE exists to close.
114
+ */
115
+ /** The only PKCE method this server accepts (OAuth 2.1 requires S256). */
116
+ declare const SUPPORTED_CHALLENGE_METHOD = "S256";
117
+ /**
118
+ * PKCE code-challenge methods, including the rejected legacy `plain`.
119
+ *
120
+ * @public exported because it is a parameter type of the exported
121
+ * {@link verifyChallenge}.
122
+ */
123
+ type CodeChallengeMethod = "S256" | "plain";
124
+ /** Thrown when a caller supplies a challenge method other than `S256`. */
125
+ declare class UnsupportedChallengeMethodError extends Error {
126
+ readonly method: string;
127
+ constructor(method: string);
128
+ }
129
+ /**
130
+ * Compute the RFC 7636 S256 challenge for a `code_verifier`:
131
+ * `BASE64URL(SHA-256(ASCII(verifier)))`.
132
+ */
133
+ declare function computeChallenge(verifier: string): Promise<string>;
134
+ /**
135
+ * Verify a presented `code_verifier` against a stored `code_challenge`.
136
+ *
137
+ * Recomputes the S256 challenge from `verifier` and constant-time-compares it
138
+ * to `storedChallenge`. Returns `true` on a match, `false` on a mismatch (or an
139
+ * empty stored challenge). Any method other than `S256` throws
140
+ * {@link UnsupportedChallengeMethodError} — `plain` is never accepted.
141
+ */
142
+ declare function verifyChallenge(verifier: string, storedChallenge: string, method?: CodeChallengeMethod | string): Promise<boolean>;
143
+
144
+ /** Refresh-token lifetime — long-lived relative to the 15-min access token. */
145
+ declare const REFRESH_TOKEN_TTL_MS: number;
146
+ /** The single failure discriminator surfaced to the token endpoint. */
147
+ type RefreshTokenErrorCode = "invalid_grant" | "invalid_scope";
148
+ /**
149
+ * A typed refresh-token failure. Every rejection — unknown, expired, revoked,
150
+ * already-rotated (replay), wrong client, or a scope-broadening request —
151
+ * surfaces as a discriminated error the token endpoint maps to the RFC 6749
152
+ * error JSON.
153
+ */
154
+ declare class RefreshTokenError extends Error {
155
+ readonly code: RefreshTokenErrorCode;
156
+ constructor(code: RefreshTokenErrorCode, message?: string);
157
+ }
158
+ /** The result of issuing/rotating: the plaintext token (once) + bound scopes. */
159
+ interface IssuedRefreshToken {
160
+ /** The opaque plaintext refresh token — returned once, never persisted. */
161
+ refreshToken: string;
162
+ scopes: string[];
163
+ }
164
+ /** SHA-256 hex digest — the at-rest form of an opaque refresh token. */
165
+ declare function hashToken(token: string): string;
166
+ interface RefreshTokenContext {
167
+ store: RefreshTokenStore;
168
+ /** Lifetime of a newly stored token. Default 30 days. */
169
+ ttlMs?: number;
170
+ }
171
+ /**
172
+ * Issue a fresh (root) refresh token bound to a user (email + OAuth `sub`) +
173
+ * client + scopes. The plaintext is returned once; only its hash is stored.
174
+ */
175
+ declare function issueRefreshToken(context: RefreshTokenContext, binding: {
176
+ userEmail: string;
177
+ userSub: string;
178
+ clientId: string;
179
+ scopes: string[];
180
+ }): Promise<IssuedRefreshToken>;
181
+ /**
182
+ * Rotate a refresh token on use: validate it (must exist, be BOUND to the
183
+ * presenting client, be unexpired, unrevoked and un-rotated), then issue a NEW
184
+ * token chained via `rotatedFrom` and revoke the consumed one. Optionally NARROW
185
+ * scope; a broadening request is `invalid_scope`.
186
+ *
187
+ * Client binding (OAuth 2.1 §4.3 / RFC 6749 §10.4) is checked BEFORE any rotation
188
+ * or revocation, so client A can never redeem client B's refresh token — nor
189
+ * silently consume B's token by trying: the token stays live for its rightful
190
+ * owner.
191
+ */
192
+ declare function rotateRefreshToken(context: RefreshTokenContext, plaintext: string, expectedClientId: string, newScopes?: string[]): Promise<IssuedRefreshToken>;
193
+ /** The stable identity a refresh token is bound to. */
194
+ interface RefreshTokenIdentity {
195
+ /** The user's email — the identity the AS binds to and route guards resolve by. */
196
+ userEmail: string;
197
+ /** The original OAuth subject, kept stable across every rotation. */
198
+ userSub: string;
199
+ }
200
+ /**
201
+ * Resolve the identity (`email` + original OAuth `sub`) a refresh token is bound
202
+ * to. The token endpoint uses this after rotation to mint the successor access
203
+ * token with the correct email AND the SAME stable `sub` as the initial token (no
204
+ * re-consent, no `sub` drift). `null` if the row is unexpectedly absent.
205
+ */
206
+ declare function getRefreshTokenIdentity(context: RefreshTokenContext, plaintext: string): Promise<RefreshTokenIdentity | null>;
207
+
208
+ /**
209
+ * The ports of `./stores.ts`, filled by Prisma (12-23).
210
+ *
211
+ * The package owns the three models (`prisma/mcp.prisma`), so their delegate
212
+ * shapes are known and this adapter can be exact. A host with Prisma therefore
213
+ * writes ONE line —
214
+ *
215
+ * stores: createPrismaMcpStores(async () => getPrismaClient() as unknown as McpOauthPrisma)
216
+ *
217
+ * — and no host code at all beyond it. The client is duck-typed (only the
218
+ * delegates used, only the arguments used) so this file never imports a project's
219
+ * generated client, and a non-Prisma host fills the ports directly instead.
220
+ */
221
+ /** A `where` on the composite unique of `mcp_connections`. */
222
+ interface ConnectionKey {
223
+ userId_oauthClientId: {
224
+ userId: string;
225
+ oauthClientId: string;
226
+ };
227
+ }
228
+ /** The minimal Prisma surface the AS needs. Every field is one the surface writes. */
229
+ interface McpOauthPrisma {
230
+ oAuthClient: {
231
+ create(args: {
232
+ data: NewOAuthClient;
233
+ }): Promise<StoredOAuthClient>;
234
+ findUnique(args: {
235
+ where: {
236
+ clientId: string;
237
+ };
238
+ }): Promise<StoredOAuthClient | null>;
239
+ };
240
+ oAuthRefreshToken: {
241
+ create(args: {
242
+ data: NewRefreshToken;
243
+ }): Promise<unknown>;
244
+ findUnique(args: {
245
+ where: {
246
+ tokenHash: string;
247
+ };
248
+ }): Promise<StoredRefreshToken | null>;
249
+ findFirst(args: {
250
+ where: {
251
+ rotatedFrom: string;
252
+ };
253
+ }): Promise<{
254
+ tokenHash: string;
255
+ } | null>;
256
+ findMany(args: {
257
+ where: {
258
+ userEmail: string;
259
+ clientId: string;
260
+ };
261
+ }): Promise<StoredRefreshToken[]>;
262
+ updateMany(args: {
263
+ where: {
264
+ tokenHash: {
265
+ in: string[];
266
+ };
267
+ } | {
268
+ userEmail: string;
269
+ clientId: string;
270
+ revokedAt: null;
271
+ };
272
+ data: {
273
+ revokedAt: Date;
274
+ };
275
+ }): Promise<{
276
+ count: number;
277
+ }>;
278
+ };
279
+ mcpConnection: {
280
+ findUnique(args: {
281
+ where: ConnectionKey;
282
+ select: {
283
+ lastActiveAt: true;
284
+ };
285
+ }): Promise<{
286
+ lastActiveAt: Date;
287
+ } | null>;
288
+ findFirst(args: {
289
+ where: {
290
+ userId: string;
291
+ revokedAt: null;
292
+ host: null;
293
+ };
294
+ orderBy: {
295
+ lastActiveAt: "desc";
296
+ };
297
+ select: {
298
+ id: true;
299
+ };
300
+ }): Promise<{
301
+ id: string;
302
+ } | null>;
303
+ findMany(args: {
304
+ where: {
305
+ userId: string;
306
+ revokedAt: null;
307
+ host?: string | null;
308
+ };
309
+ orderBy?: {
310
+ lastActiveAt: "desc";
311
+ };
312
+ select: Record<string, true>;
313
+ }): Promise<Record<string, unknown>[]>;
314
+ upsert(args: {
315
+ where: ConnectionKey;
316
+ create: Record<string, unknown>;
317
+ update: Record<string, unknown>;
318
+ }): Promise<unknown>;
319
+ update(args: {
320
+ where: {
321
+ id: string;
322
+ };
323
+ data: Record<string, unknown>;
324
+ }): Promise<unknown>;
325
+ updateMany(args: {
326
+ where: {
327
+ id: {
328
+ in: string[];
329
+ };
330
+ } | {
331
+ userId: string;
332
+ revokedAt: null;
333
+ host: string;
334
+ };
335
+ data: Record<string, unknown>;
336
+ }): Promise<{
337
+ count: number;
338
+ }>;
339
+ };
340
+ /**
341
+ * Prisma's INTERACTIVE transaction, used for the rotation's claim + write. The
342
+ * callback form (not the array form) is required: the successor may only be
343
+ * created once the conditional revoke has reported that it, and not a concurrent
344
+ * sibling, claimed the parent — see `RefreshTokenStore.rotate`.
345
+ */
346
+ $transaction<T>(fn: (tx: McpOauthTx) => Promise<T>): Promise<T>;
347
+ }
348
+ /**
349
+ * The delegate subset used INSIDE the rotation transaction. Not exported: it is
350
+ * reachable structurally through `McpOauthPrisma.$transaction`, so no host ever
351
+ * needs to name it, and exporting a type nobody imports is what knip flags.
352
+ */
353
+ interface McpOauthTx {
354
+ oAuthRefreshToken: {
355
+ create(args: {
356
+ data: NewRefreshToken;
357
+ }): Promise<unknown>;
358
+ updateMany(args: {
359
+ where: {
360
+ tokenHash: string;
361
+ revokedAt: null;
362
+ };
363
+ data: {
364
+ revokedAt: Date;
365
+ };
366
+ }): Promise<{
367
+ count: number;
368
+ }>;
369
+ };
370
+ }
371
+ /** A lazily-resolved client, so a host's singleton is awaited per call. */
372
+ type McpOauthPrismaProvider = () => Promise<McpOauthPrisma>;
373
+ /** Every port, over one lazily-resolved Prisma client. */
374
+ declare function createPrismaMcpStores(getPrisma: McpOauthPrismaProvider): McpOauthStores;
375
+
376
+ /**
377
+ * The account surface's connection OPERATIONS (12-48) — the half of the
378
+ * `GET/DELETE /api/account/mcp-connections` endpoints that is contract rather
379
+ * than host vocabulary.
380
+ *
381
+ * The ROUTE stays in the host on purpose: it mixes the host's session
382
+ * resolution, its response envelope, its published plugin URLs and its logger,
383
+ * and injecting all four here would make the config surface bigger than the
384
+ * handler it replaces. What must NOT stay in each host is the disconnect's
385
+ * both-halves rule, because getting it half right LOOKS right:
386
+ *
387
+ * `connections.revokeByHost` ends the connection rows and returns the OAuth
388
+ * client ids behind them — and a host that stops there has revoked nothing that
389
+ * matters. The assistant still holds a live refresh token for each of those
390
+ * clients, rotates it on schedule, and the very next grant records fresh
391
+ * activity: the card the user just disconnected lights green again on its own.
392
+ * So the rule is one function: revoke the rows AND end every live refresh token
393
+ * of each returned client, in the same call, with no way to import one half
394
+ * without the other.
395
+ *
396
+ * Deliberately NOT invalidated here: the assistant's current ACCESS token.
397
+ * Those are self-contained JWTs the server does not track; a just-disconnected
398
+ * host keeps working for at most their TTL (15 minutes by default) and can then
399
+ * obtain nothing further.
400
+ */
401
+ /** An active AI connection, narrowed for display. */
402
+ interface AiConnectionSnapshot {
403
+ oauthClientId: string;
404
+ clientName: string | null;
405
+ /** The provider this connection is attributed to (`null` = pre-attribution). */
406
+ host: AiProvider | null;
407
+ connectedAt: Date;
408
+ lastActiveAt: Date;
409
+ }
410
+ /** The caller the operations act for — always the session's own user. */
411
+ interface AiConnectionCaller {
412
+ /** The host's user id — what `mcp_connections` rows are keyed by. */
413
+ userId: string;
414
+ /** The identity refresh tokens are bound to (the AS binds by email). */
415
+ email: string;
416
+ }
417
+ /** What one disconnect actually ended, for the host's log and response. */
418
+ interface AiDisconnectResult {
419
+ /** OAuth client ids whose connection rows were revoked. */
420
+ disconnectedClientIds: string[];
421
+ /** Live refresh tokens ended across those clients — the half that cuts access. */
422
+ revokedRefreshTokens: number;
423
+ }
424
+ /**
425
+ * A user's active connections, most-recently-active first, with the stored open
426
+ * `host` string narrowed to the package's closed {@link AiProvider} union — the
427
+ * store cannot know which assistants have screens, but the union is this
428
+ * package's own vocabulary (`guide.ts`), so the narrowing lives beside it
429
+ * rather than being re-derived in every host.
430
+ */
431
+ declare function listAiConnections(connections: McpConnectionStore, userId: string): Promise<AiConnectionSnapshot[]>;
432
+ /**
433
+ * Disconnect one provider for this user — BOTH halves, atomically from the
434
+ * caller's point of view (see the module doc for why one half alone is a
435
+ * disconnect that undoes itself).
436
+ *
437
+ * Idempotent: disconnecting a provider that was never connected returns zero
438
+ * counts rather than failing, so a double-click is harmless. Repeat calls also
439
+ * report zero — `revokeLiveForClient` skips already-revoked tokens by contract.
440
+ */
441
+ declare function disconnectAiHost(stores: {
442
+ connections: McpConnectionStore;
443
+ refreshTokens: RefreshTokenStore;
444
+ }, caller: AiConnectionCaller, host: AiProvider): Promise<AiDisconnectResult>;
445
+
446
+ export { AUTHORIZATION_CODE_AUDIENCE, AUTHORIZATION_CODE_TTL_SECONDS, type AiConnectionCaller, type AiConnectionSnapshot, type AiDisconnectResult, AuthorizationCodeError, type AuthorizationCodeErrorCode, type CodeChallengeMethod, type IssuedRefreshToken, McpConnectionStore, type McpOauthPrisma, type McpOauthPrismaProvider, McpOauthStores, McpSigningKeyProvider, type MintCodeInput, NewOAuthClient, NewRefreshToken, REFRESH_TOKEN_TTL_MS, type RefreshTokenContext, RefreshTokenError, type RefreshTokenErrorCode, type RefreshTokenIdentity, RefreshTokenStore, SUPPORTED_CHALLENGE_METHOD, StoredOAuthClient, StoredRefreshToken, UnsupportedChallengeMethodError, type VerifiedAuthorizationCode, type VerifyCodeOptions, computeChallenge, createPrismaMcpStores, disconnectAiHost, getRefreshTokenIdentity, hashToken, issueRefreshToken, listAiConnections, mintCode, rotateRefreshToken, verifyChallenge, verifyCode };