@12-apps/mcp 3.1.0 → 3.2.1
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 +32 -0
- package/dist/chunk-7QVYU63E.js +7 -0
- package/dist/chunk-7QVYU63E.js.map +1 -0
- package/dist/chunk-FYEVBTDU.js +162 -0
- package/dist/chunk-FYEVBTDU.js.map +1 -0
- package/dist/chunk-HAZOPC6U.js +229 -0
- package/dist/chunk-HAZOPC6U.js.map +1 -0
- package/dist/chunk-UIILEGAC.js +1247 -0
- package/dist/chunk-UIILEGAC.js.map +1 -0
- package/dist/chunk-WJJNKKNS.js +63 -0
- package/dist/chunk-WJJNKKNS.js.map +1 -0
- package/dist/coverage-gate/index.d.ts +129 -0
- package/dist/coverage-gate/index.js +163 -0
- package/dist/coverage-gate/index.js.map +1 -0
- package/dist/create-api-mcp-oauth-CwVXKK-A.d.ts +647 -0
- package/dist/generate/index.d.ts +91 -0
- package/dist/generate/index.js +101 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/generate-Dx3cK8th.d.ts +184 -0
- package/dist/guide-DV5MQbCg.d.ts +135 -0
- package/dist/hono/index.d.ts +28 -0
- package/dist/hono/index.js +25 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/index.d.ts +382 -0
- package/dist/index.js +331 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/index.d.ts +446 -0
- package/dist/oauth/index.js +279 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/react/index.d.ts +239 -0
- package/dist/react/index.js +1183 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +31 -10
- package/prisma/migrations/20260812150000_add_mcp_oauth_tables/migration.sql +6 -6
- package/src/index.ts +13 -0
- package/src/server/jsonrpc.ts +167 -0
|
@@ -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 };
|