@cat-factory/integrations 0.153.12 → 0.155.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,585 @@
1
+ import { ConflictError, noopLogger } from '@cat-factory/kernel';
2
+ import { PUBLIC_API_SCOPES } from '@cat-factory/contracts';
3
+ import { scopeSatisfies } from '../publicApi/PublicApiKeyService.js';
4
+ // ---------------------------------------------------------------------------
5
+ // The SERVING half of MCP authorization: this deployment acting as the authorization server for
6
+ // its own hosted MCP endpoint, so a host (claude.ai, an IDE, a hosted agent) connects by pressing
7
+ // a button and approving a consent screen instead of being handed a key to paste into a config
8
+ // file that then sits on disk in plaintext forever.
9
+ //
10
+ // The counterpart of `McpOAuthService` next door, pointed the other way: that one is this platform
11
+ // as an OAuth CLIENT of a vendor's MCP server, this one is a vendor's host as an OAuth client of
12
+ // OURS. They share no code because they share no problem: a client discovers endpoints, keeps a
13
+ // refresh token and mints for a dispatch, while a server registers clients, renders consent and
14
+ // issues.
15
+ // What they do share is the file's central trick, and its justification is the same as the one
16
+ // recorded for the in-flight authorization request there: EVERY piece of state this flow needs
17
+ // between two requests is SEALED into the value the other party carries, so the whole authorization
18
+ // server costs no table, no migration on either runtime, and no sweeper for the rows behind every
19
+ // consent screen anyone ever abandoned.
20
+ //
21
+ // There are three such values, each sealed under the deployment's own key with an explicit `kind`
22
+ // the opener pins (one HKDF tag covers all three, so the claim is what keeps a value minted for one
23
+ // purpose from being opened as another):
24
+ //
25
+ // - the CLIENT ID a dynamic registration answers with, carrying the client's name and its
26
+ // registered redirect URIs;
27
+ // - the AUTHORIZATION REQUEST the consent screen is opened on, carrying the client, the redirect
28
+ // URI already validated against that registration, and the PKCE challenge;
29
+ // - the CODE the browser carries back to the host, carrying everything above plus what the human
30
+ // actually approved (which board, which scope) and who approved it.
31
+ //
32
+ // What the flow finally ISSUES is an ordinary public-API key. That is the whole reason this fits in
33
+ // one file: the credential a host ends up with is the same credential the surface already
34
+ // authenticates, so nothing downstream of the token endpoint learns a second token format, the key
35
+ // panel lists an OAuth-connected host beside every other integration, and REVOKING one is the
36
+ // button that is already there. What it costs is an access token that does not expire, which is
37
+ // why no refresh grant is advertised: `metadataDocuments.ts` states that where a client reads it.
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * HKDF domain tag separating this server's ciphertexts from every other cipher in the platform,
41
+ * the consuming side's included: a `state` minted for a vendor grant must not open as a client
42
+ * registration here, and it cannot, because the two derive different keys from the same master.
43
+ */
44
+ export const MCP_AUTH_SERVER_CIPHER_INFO = 'cat-factory:mcp-authorization-server';
45
+ /**
46
+ * How long an in-flight authorization request stays valid: the window between a host opening the
47
+ * authorization URL and a human finishing with the consent screen. Long enough to sign in first,
48
+ * short enough that an abandoned tab is not a lasting artefact.
49
+ */
50
+ const AUTHORIZATION_REQUEST_TTL_MS = 15 * 60 * 1000;
51
+ /**
52
+ * How long an issued authorization code stays redeemable. Deliberately far shorter than the
53
+ * request above: the code is redeemed by a machine, in one round trip, immediately.
54
+ *
55
+ * It is also the whole of the code's replay defence, and the trade is worth stating rather than
56
+ * implying. With no row there is no single-use enforcement, so a code presented twice inside this
57
+ * window is exchanged twice. What stops that being a way IN is PKCE: redeeming needs the verifier,
58
+ * which never left the host, so a code captured from browser history, a referrer or a proxy log is
59
+ * unredeemable by whoever captured it. What is left is a legitimate host redeeming its own code
60
+ * twice and getting two keys of the scope its user already approved, which is untidy rather than
61
+ * dangerous, and visible in the key panel either way.
62
+ */
63
+ const AUTHORIZATION_CODE_TTL_MS = 60 * 1000;
64
+ /** The PKCE method the MCP authorization spec requires. Plain is refused, not tolerated. */
65
+ const REQUIRED_CODE_CHALLENGE_METHOD = 'S256';
66
+ /** Registered redirect URIs one client may declare, and the longest name it may register. */
67
+ const MAX_REDIRECT_URIS = 8;
68
+ const MAX_CLIENT_NAME_LENGTH = 120;
69
+ /**
70
+ * The scope the consent screen offers before anyone has chosen: the middle of the ladder, which is
71
+ * what an MCP host is actually for (read the board, author and start work on it) and stops short of
72
+ * both rungs that answer a parked run or merge a pull request.
73
+ */
74
+ export const CONSENT_DEFAULT_SCOPE = 'write';
75
+ export class McpOAuthProtocolError extends Error {
76
+ oauthError;
77
+ constructor(oauthError, message) {
78
+ super(message);
79
+ this.oauthError = oauthError;
80
+ this.name = 'McpOAuthProtocolError';
81
+ }
82
+ }
83
+ /**
84
+ * A refusal at the authorization endpoint that the CLIENT must be told about, on the redirect URI
85
+ * it registered (RFC 6749 §4.1.2.1).
86
+ *
87
+ * The distinction this type carries is the whole of that section's rule, and it is a distinction
88
+ * only this service can make. Where the request cannot be ATTRIBUTED to a registered redirect
89
+ * target (an unknown `client_id`, a `redirect_uri` the client never registered), the refusal has
90
+ * nowhere safe to go: bouncing it back would turn this deployment's own origin into a forwarder to
91
+ * any address an attacker names, which is the open redirect the registration check exists to
92
+ * prevent. Those stay a plain {@link McpOAuthProtocolError} and are rendered as a page.
93
+ *
94
+ * Once the redirect URI HAS been matched against the registration, the opposite is true: a bad
95
+ * `response_type`, a missing PKCE challenge or a `resource` naming someone else are faults in the
96
+ * host's own request, and it is the host that must hear about them. Rendered as a page instead,
97
+ * they leave a conforming client waiting on a callback that never arrives until it times out, and
98
+ * the fault it then reports is "this deployment did not answer" rather than the one line it got
99
+ * wrong.
100
+ */
101
+ export class McpOAuthRedirectableError extends McpOAuthProtocolError {
102
+ target;
103
+ constructor(oauthError, message,
104
+ /** The already-validated address to report this on, and the host's `state` to echo. */
105
+ target) {
106
+ super(oauthError, message);
107
+ this.target = target;
108
+ this.name = 'McpOAuthRedirectableError';
109
+ }
110
+ }
111
+ /**
112
+ * Where a refusal the client must hear about goes.
113
+ *
114
+ * Exported as a function over the error rather than a method, because the target rides the error
115
+ * itself: the controller catching it needs no access to the server that threw it, and there is no
116
+ * way to spell "report this somewhere the request was not validated against".
117
+ */
118
+ export function mcpOAuthErrorRedirect(error) {
119
+ return redirectWithError(error.target, error.oauthError, error.message);
120
+ }
121
+ /**
122
+ * This deployment as the authorization server for its own hosted MCP endpoint.
123
+ *
124
+ * Stateless by construction (see the file header): every method turns one carried value into the
125
+ * next, and the only thing that outlives the flow is the key the token endpoint mints, a row that
126
+ * already existed as a concept, listed and revocable where an operator already looks.
127
+ */
128
+ export class McpAuthorizationServer {
129
+ deps;
130
+ log;
131
+ constructor(deps) {
132
+ this.deps = deps;
133
+ this.log = deps.logger ?? noopLogger;
134
+ }
135
+ /**
136
+ * Register a client dynamically (RFC 7591), answering with a `client_id` that IS its own
137
+ * registration record.
138
+ *
139
+ * Dynamic registration is what makes this reachable from a host nobody configured: claude.ai and
140
+ * the IDE clients register themselves on first connect and have no console at this deployment to
141
+ * be registered in by hand. The consuming half of this initiative deliberately does NOT perform
142
+ * registration against a vendor, and the asymmetry is not an inconsistency: there, a client
143
+ * minted at runtime would be deployment state with no operator-visible identity at the vendor,
144
+ * nobody able to find, rotate or revoke it. Here the registration confers NOTHING on its own
145
+ * (no scope, no board, no token, not even the ability to ask again) until a signed-in human with
146
+ * `secrets.manage` approves a specific board and scope on the consent screen. What that human
147
+ * approves is a key they can see and revoke, so the registration is a name and a redirect list,
148
+ * and sealing it into the identifier makes it exactly as revocable as it is powerful.
149
+ *
150
+ * Only PUBLIC clients are registered (no secret is issued), which is what the MCP hosts are:
151
+ * a native app or a browser-side host cannot keep one, and PKCE is what actually binds the
152
+ * exchange to the party that started it.
153
+ */
154
+ async registerClient(input) {
155
+ const redirectUris = normaliseRedirectUris(input.redirectUris);
156
+ const clientName = normaliseClientName(input.clientName);
157
+ const issuedAt = this.deps.clock.now();
158
+ const client = { kind: 'mcp-client', name: clientName, redirectUris, issuedAt };
159
+ const clientId = await this.deps.secretCipher.encrypt(JSON.stringify(client));
160
+ this.log.info('mcp authorization server registered a client', {
161
+ clientName,
162
+ redirectOrigins: redirectUris.map(originOf),
163
+ });
164
+ return { clientId, clientName, redirectUris, issuedAt };
165
+ }
166
+ /**
167
+ * Validate an authorization request and seal it for the consent screen.
168
+ *
169
+ * Everything a later step must not re-derive from attacker-supplied input is pinned here: the
170
+ * redirect URI is checked against the registration NOW, so nothing downstream has to trust the
171
+ * one the browser carries, and the PKCE challenge is captured before any human sees the screen.
172
+ *
173
+ * The refusals are split by WHO can be told. A request whose `redirect_uri` is not registered
174
+ * cannot be reported by redirecting to it (that is the open-redirect this check exists to
175
+ * prevent), so it throws and the controller renders it as a page. Everything else is a fault the
176
+ * client should hear about on its own redirect, and the controller sends it there.
177
+ */
178
+ async beginAuthorization(input) {
179
+ const client = await this.openClient(input.clientId);
180
+ if (!client.redirectUris.includes(input.redirectUri)) {
181
+ throw new McpOAuthProtocolError('invalid_request', 'redirect_uri does not match any URI this client registered.');
182
+ }
183
+ // Past this line the address is one the client itself registered, so every further refusal is
184
+ // REPORTABLE to it rather than only to the human standing in front of the browser.
185
+ const target = {
186
+ redirectUri: input.redirectUri,
187
+ ...(input.state ? { state: input.state } : {}),
188
+ };
189
+ if (input.responseType !== 'code') {
190
+ throw new McpOAuthRedirectableError('unsupported_response_type', `Only the authorization-code response type is served; got '${input.responseType}'.`, target);
191
+ }
192
+ if (input.codeChallengeMethod !== REQUIRED_CODE_CHALLENGE_METHOD || !input.codeChallenge) {
193
+ throw new McpOAuthRedirectableError('invalid_request', `This authorization server requires PKCE with code_challenge_method=${REQUIRED_CODE_CHALLENGE_METHOD}.`, target);
194
+ }
195
+ // RFC 8707. Checked rather than ignored because the whole point of the parameter is that a
196
+ // token is minted FOR one resource: accepting a request that names someone else's would let a
197
+ // host believe it holds a token this deployment vouched for against a surface it does not
198
+ // serve. Absent is tolerated (a client one revision behind), a MISMATCH never is.
199
+ if (input.resource && !resourceMatches(input.resource, input.expectedResource)) {
200
+ throw new McpOAuthRedirectableError('invalid_request', `This authorization server issues tokens for ${input.expectedResource}, not ${input.resource}.`, target);
201
+ }
202
+ const requestedScope = readScope(input.scope);
203
+ const request = {
204
+ kind: 'mcp-authorization-request',
205
+ clientId: input.clientId,
206
+ clientName: client.name,
207
+ redirectUri: input.redirectUri,
208
+ codeChallenge: input.codeChallenge,
209
+ ...(input.state ? { state: input.state } : {}),
210
+ ...(requestedScope ? { requestedScope } : {}),
211
+ exp: this.deps.clock.now() + AUTHORIZATION_REQUEST_TTL_MS,
212
+ };
213
+ return {
214
+ sealedRequest: await this.deps.secretCipher.encrypt(JSON.stringify(request)),
215
+ summary: summarise(request),
216
+ };
217
+ }
218
+ /**
219
+ * Open a sealed authorization request, or `null` when it is absent, forged, expired or not an
220
+ * authorization request at all.
221
+ *
222
+ * Null rather than a cause, for the reason the consuming side records about its own state: the
223
+ * four are indistinguishable to the caller, every one of them refuses the same way, and telling
224
+ * them apart tells whoever is guessing which guess was closer.
225
+ */
226
+ async readAuthorizationRequest(sealed) {
227
+ if (!sealed)
228
+ return null;
229
+ const request = await this.open(sealed, 'mcp-authorization-request');
230
+ if (!request)
231
+ return null;
232
+ return request.exp >= this.deps.clock.now() ? request : null;
233
+ }
234
+ /** What the consent screen renders about the party asking. */
235
+ describeRequest(request) {
236
+ return summarise(request);
237
+ }
238
+ /**
239
+ * Turn an approved request into the redirect the browser follows back to the host.
240
+ *
241
+ * The APPROVAL is the caller's to establish, not this service's: who is signed in, and whether
242
+ * they still hold `secrets.manage` on the board they picked, is resolved at the controller
243
+ * through the one shared workspace-access resolution, the same division the consuming side's
244
+ * completion makes, and for the same reason (the board is a value inside the flow rather than a
245
+ * segment in the path, so no gate can bind it).
246
+ */
247
+ async approve(request, approval) {
248
+ const code = {
249
+ kind: 'mcp-authorization-code',
250
+ clientId: request.clientId,
251
+ clientName: request.clientName,
252
+ redirectUri: request.redirectUri,
253
+ codeChallenge: request.codeChallenge,
254
+ accountId: approval.accountId,
255
+ workspaceId: approval.workspaceId,
256
+ scope: approval.scope,
257
+ approvedByUserId: approval.approvedByUserId,
258
+ exp: this.deps.clock.now() + AUTHORIZATION_CODE_TTL_MS,
259
+ };
260
+ const url = new URL(request.redirectUri);
261
+ url.searchParams.set('code', await this.deps.secretCipher.encrypt(JSON.stringify(code)));
262
+ if (request.state)
263
+ url.searchParams.set('state', request.state);
264
+ return { redirectTo: url.toString() };
265
+ }
266
+ /**
267
+ * The redirect that reports a REFUSAL to the host (RFC 6749 §4.1.2.1).
268
+ *
269
+ * A denial is an answer, not a dead end: a host left waiting on a tab the human closed reports a
270
+ * timeout, which sends its user looking for a fault in the deployment instead of at the choice
271
+ * they just made.
272
+ */
273
+ denial(request) {
274
+ return {
275
+ redirectTo: redirectWithError({ redirectUri: request.redirectUri, ...(request.state ? { state: request.state } : {}) }, 'access_denied', 'The request was declined.'),
276
+ };
277
+ }
278
+ /**
279
+ * Redeem a code for an access token: the one call a host makes on its own, with no browser.
280
+ *
281
+ * PKCE is verified here and it is the only thing that binds this exchange to the host that
282
+ * started the flow, since a public client presents no secret. The redirect URI and client id are
283
+ * checked against the code as well: belt and braces on the same binding, and the check RFC 6749
284
+ * §4.1.3 asks for.
285
+ */
286
+ async redeemCode(input) {
287
+ const code = await this.open(input.code, 'mcp-authorization-code');
288
+ if (!code) {
289
+ throw new McpOAuthProtocolError('invalid_grant', 'This authorization code is invalid or has expired.');
290
+ }
291
+ if (code.exp < this.deps.clock.now()) {
292
+ throw new McpOAuthProtocolError('invalid_grant', 'This authorization code has expired.');
293
+ }
294
+ if (code.clientId !== input.clientId) {
295
+ throw new McpOAuthProtocolError('invalid_grant', 'This authorization code was issued to a different client.');
296
+ }
297
+ if (input.redirectUri && input.redirectUri !== code.redirectUri) {
298
+ throw new McpOAuthProtocolError('invalid_grant', 'redirect_uri does not match the one this code was issued for.');
299
+ }
300
+ if (!(await pkceMatches(input.codeVerifier, code.codeChallenge))) {
301
+ throw new McpOAuthProtocolError('invalid_grant', 'The PKCE code verifier does not match.');
302
+ }
303
+ const issued = await this.issueKeyFor(code);
304
+ this.log.info('mcp authorization server issued an access token', {
305
+ workspaceId: code.workspaceId,
306
+ keyId: issued.record.id,
307
+ scope: code.scope,
308
+ clientName: code.clientName,
309
+ });
310
+ return { accessToken: issued.secret, scope: code.scope, keyId: issued.record.id };
311
+ }
312
+ /**
313
+ * Mint the key an approved code redeems for, answering a refusal in the protocol's vocabulary.
314
+ *
315
+ * The one refusal `issue` can raise that a host will actually meet is the per-board key cap, and
316
+ * it arrives at the WORST moment in the flow: the human has already approved, the browser has
317
+ * already gone back to the host, and what is left is a machine-to-machine call. Left to reach
318
+ * `handleError`, that answers the deployment's own `{ error: { code, message } }` envelope with
319
+ * a 409, which is not a shape any OAuth client parses: the host reports a broken connection and
320
+ * names nothing a person could act on. Translated here, the same fact arrives as
321
+ * `error_description`, which every host renders, naming the board's key panel and what to do
322
+ * there. This is the one place a `DomainError` is deliberately re-mapped rather than rethrown,
323
+ * for the reason the file header gives: this endpoint answers in OAuth's vocabulary, so an
324
+ * envelope is the foreign shape here and the protocol code is the native one.
325
+ */
326
+ async issueKeyFor(code) {
327
+ try {
328
+ return await this.deps.publicApiKeys.issue({
329
+ accountId: code.accountId,
330
+ workspaceId: code.workspaceId,
331
+ createdByUserId: code.approvedByUserId,
332
+ // The connected host, on its own side. Opaque to the platform like every other external
333
+ // identity, and what makes a run this host starts attributable to it rather than to the
334
+ // person who happened to approve the connection months earlier.
335
+ externalIdentity: externalIdentityFor(code.clientName),
336
+ }, keyLabelFor(code.clientName), code.scope);
337
+ }
338
+ catch (error) {
339
+ if (!(error instanceof ConflictError))
340
+ throw error;
341
+ throw new McpOAuthProtocolError('access_denied', `${error.message}. The connection was approved, but no key could be issued on that board: ` +
342
+ 'revoke one in the board settings and connect again.');
343
+ }
344
+ }
345
+ /** Open a sealed value and pin its `kind`, or null if it will not open as that kind. */
346
+ async open(sealed, kind) {
347
+ let parsed;
348
+ try {
349
+ parsed = JSON.parse(await this.deps.secretCipher.decrypt(sealed));
350
+ }
351
+ catch {
352
+ // silent-catch-ok: a value that will not open is refused whatever the reason, and the reason
353
+ // is supplied by whoever presented it. The controller logs the refusal with its request id.
354
+ return null;
355
+ }
356
+ const value = parsed;
357
+ return value && value.kind === kind ? value : null;
358
+ }
359
+ /** Open a `client_id`, refusing in the protocol's own vocabulary when it is not one of ours. */
360
+ async openClient(clientId) {
361
+ const client = await this.open(clientId, 'mcp-client');
362
+ if (!client) {
363
+ throw new McpOAuthProtocolError('invalid_client', 'This client_id was not issued by this deployment. Register the client first.');
364
+ }
365
+ return client;
366
+ }
367
+ }
368
+ /** A refusal delivered where the protocol puts one: the client's own registered redirect URI. */
369
+ function redirectWithError(target, error, description) {
370
+ const url = new URL(target.redirectUri);
371
+ url.searchParams.set('error', error);
372
+ url.searchParams.set('error_description', description);
373
+ if (target.state)
374
+ url.searchParams.set('state', target.state);
375
+ return url.toString();
376
+ }
377
+ /** The label an issued key carries in the panel, so a person can tell what to revoke. */
378
+ function keyLabelFor(clientName) {
379
+ return `MCP: ${clientName}`;
380
+ }
381
+ /**
382
+ * Who the issued key acts for, on the host's own side.
383
+ *
384
+ * Built here rather than at the call site because it is the ONE value in this flow that leaves it:
385
+ * `externalIdentity` is echoed on the key resource, on run projections and on `GET /api/v1/me`, and
386
+ * the name inside it is a stranger's free text. {@link normaliseClientName} is what holds it to the
387
+ * same rule `publicApiExternalIdentitySchema` states on the wire, which this path does not cross.
388
+ */
389
+ function externalIdentityFor(clientName) {
390
+ return `mcp-client:${clientName}`;
391
+ }
392
+ /** The non-secret half of a request, for the consent screen. */
393
+ function summarise(request) {
394
+ return {
395
+ clientName: request.clientName,
396
+ redirectOrigin: originOf(request.redirectUri),
397
+ defaultScope: consentDefaultScope(request.requestedScope),
398
+ ...(request.requestedScope ? { requestedScope: request.requestedScope } : {}),
399
+ };
400
+ }
401
+ /**
402
+ * The scope the consent screen PRESELECTS, which is never a scope a stranger asked for above the
403
+ * platform's own default.
404
+ *
405
+ * A registration is unauthenticated: anyone who can reach `/oauth/register` can name themselves and
406
+ * then ask for `admin`. Letting that ASK become the preselected radio button would make the top of
407
+ * the ladder the default on a screen whose whole subject is a grant, decided by the party asking
408
+ * for it rather than the one granting it, and a person clicking Connect on what looks like the
409
+ * shipped default would hand over the rung that can delete tasks and merge pull requests.
410
+ *
411
+ * So the ask is honoured only DOWNWARD. A host that wants less than the default gets less
412
+ * preselected (nothing is protected by talking someone into granting `read`); a host that wants
413
+ * more gets the default, and {@link McpAuthorizationRequestSummary.requestedScope} still carries
414
+ * what it asked for, so the screen can say so and a human can raise it deliberately.
415
+ */
416
+ function consentDefaultScope(requested) {
417
+ if (!requested)
418
+ return CONSENT_DEFAULT_SCOPE;
419
+ return scopeSatisfies(requested, CONSENT_DEFAULT_SCOPE) ? CONSENT_DEFAULT_SCOPE : requested;
420
+ }
421
+ /**
422
+ * The redirect URIs a registration may carry.
423
+ *
424
+ * `https` anywhere, plus loopback `http` (RFC 8252 §7.3: a native host listens on 127.0.0.1 on a
425
+ * port it picked at start-up, which is the shape every desktop MCP host uses) and the custom
426
+ * schemes the same RFC gives native apps. A bare `http://` on any other host is refused: the
427
+ * authorization code travels on that URL.
428
+ */
429
+ function normaliseRedirectUris(raw) {
430
+ const list = Array.isArray(raw) ? raw : [];
431
+ const uris = list.filter((entry) => typeof entry === 'string' && entry.length > 0);
432
+ if (!uris.length) {
433
+ throw new McpOAuthProtocolError('invalid_client_metadata', 'redirect_uris must name at least one URI this client will be redirected to.');
434
+ }
435
+ if (uris.length > MAX_REDIRECT_URIS) {
436
+ throw new McpOAuthProtocolError('invalid_client_metadata', `A client may register at most ${MAX_REDIRECT_URIS} redirect URIs.`);
437
+ }
438
+ for (const uri of uris) {
439
+ if (!isAllowedRedirectUri(uri)) {
440
+ throw new McpOAuthProtocolError('invalid_redirect_uri', `${uri} is not a redirect URI this authorization server will send a code to. Use https, ` +
441
+ 'a loopback http address, or a private-use scheme.');
442
+ }
443
+ }
444
+ return uris;
445
+ }
446
+ /** Whether a registered redirect URI is one this server will send an authorization code to. */
447
+ export function isAllowedRedirectUri(raw) {
448
+ let url;
449
+ try {
450
+ url = new URL(raw);
451
+ }
452
+ catch {
453
+ // silent-catch-ok: an unparseable value is refused by the caller with the message above, which
454
+ // says more than a parse failure would.
455
+ return false;
456
+ }
457
+ // A fragment is where a redirect URI cannot carry one (RFC 6749 §3.1.2), and appending our query
458
+ // to a URL that has one would put the code after the fragment, where it never reaches the host.
459
+ if (url.hash)
460
+ return false;
461
+ if (url.protocol === 'https:')
462
+ return true;
463
+ if (url.protocol === 'http:')
464
+ return isLoopbackHost(url.hostname);
465
+ // A private-use scheme (`com.example.app:/callback`, `vscode://…`) is how a native host is
466
+ // reached without a listening socket. There is no transport to hold to https, because there is
467
+ // no transport: the OS hands the URL to whichever application claimed the scheme.
468
+ //
469
+ // Which is exactly why the test is what a scheme MEANS rather than a list of bad names. A scheme
470
+ // the BROWSER interprets is not routed to an application at all, and this URL is navigated to
471
+ // (`window.location.assign`) from the consent screen with an authorization code appended, so
472
+ // `javascript:` there executes in the SPA's own document and `data:`/`blob:`/`file:` render
473
+ // attacker-chosen content on it. Naming only `javascript:` reads as a fix and admits every
474
+ // sibling; excluding the schemes with browser-defined meaning is the rule the RFC states
475
+ // (a private-use scheme is by definition NOT a standard one) and covers the ones nobody
476
+ // remembered.
477
+ return (/^[a-z][a-z0-9+.-]*:$/i.test(url.protocol) &&
478
+ !BROWSER_INTERPRETED_SCHEMES.has(url.protocol.toLowerCase()));
479
+ }
480
+ /**
481
+ * Schemes a browser gives its own meaning to, which therefore cannot be a private-use scheme.
482
+ *
483
+ * The URL standard's "special" schemes minus the two handled above, plus the pseudo-schemes that
484
+ * resolve inside the current document rather than dispatching to an application. Every member is
485
+ * defined by a standard, so this set is closed by something other than the imagination of whoever
486
+ * edits it next.
487
+ */
488
+ const BROWSER_INTERPRETED_SCHEMES = new Set([
489
+ // URL-standard special schemes (http/https answered above on their own terms).
490
+ 'ftp:',
491
+ 'file:',
492
+ 'ws:',
493
+ 'wss:',
494
+ // Resolved by the browser in the current document's context, never dispatched to an app.
495
+ 'javascript:',
496
+ 'vbscript:',
497
+ 'data:',
498
+ 'blob:',
499
+ 'about:',
500
+ 'filesystem:',
501
+ 'view-source:',
502
+ ]);
503
+ /** Loopback, in both families plus the name, which is what a native host actually registers. */
504
+ function isLoopbackHost(hostname) {
505
+ const host = hostname.replace(/^\[|\]$/g, '');
506
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1';
507
+ }
508
+ /**
509
+ * A client name that is safe to render on a consent screen, in a key label, and in the
510
+ * `externalIdentity` the name is spliced into.
511
+ *
512
+ * That last one is why the class here is not a matter of taste. `externalIdentity` normally arrives
513
+ * over the wire, where `publicApiExternalIdentitySchema` refuses control characters AND the two
514
+ * Unicode separators; a name registered here reaches `PublicApiKeyService.issue` directly, so this
515
+ * function is the only thing standing where that schema stands. The separators matter for the same
516
+ * reason the controls do: U+2028 and U+2029 are LINE BREAKS to every renderer that matters, so a
517
+ * name carrying one breaks a line-oriented log or a table row on whichever surface echoes the value
518
+ * next, and the schema's own doc records that they are refused rather than tolerated.
519
+ */
520
+ function normaliseClientName(raw) {
521
+ // Stripped rather than refused: the name is a third party's free text on a screen whose whole job
522
+ // is to say WHO is asking, and refusing the registration over a stray character would take that
523
+ // screen away instead of cleaning it up.
524
+ const cleaned = (raw ?? '').replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, ' ').trim();
525
+ if (!cleaned)
526
+ return 'An unnamed MCP client';
527
+ return cleaned.length > MAX_CLIENT_NAME_LENGTH
528
+ ? `${cleaned.slice(0, MAX_CLIENT_NAME_LENGTH - 1)}…`
529
+ : cleaned;
530
+ }
531
+ /** The origin of a redirect URI, for a human to judge. Falls back to the raw value. */
532
+ function originOf(raw) {
533
+ try {
534
+ const url = new URL(raw);
535
+ return url.origin === 'null' ? `${url.protocol}//` : url.origin;
536
+ }
537
+ catch {
538
+ // silent-catch-ok: only reachable for a value that already passed registration, and showing it
539
+ // verbatim is more use on a consent screen than showing nothing.
540
+ return raw;
541
+ }
542
+ }
543
+ /**
544
+ * The requested scope, when the host named one of ours.
545
+ *
546
+ * An unknown scope is DROPPED rather than refused, and the consent screen then offers its own
547
+ * default. The scope a token carries is what a human picked on that screen, so a host asking for
548
+ * something this deployment does not have is asking for a default it will be told about, not
549
+ * making a claim that has to be adjudicated. A space-separated list takes the first member the
550
+ * ladder knows, which is what a client sending `mcp read` means.
551
+ */
552
+ function readScope(raw) {
553
+ for (const candidate of (raw ?? '').split(/\s+/)) {
554
+ const match = PUBLIC_API_SCOPES.find((scope) => scope === candidate);
555
+ if (match)
556
+ return match;
557
+ }
558
+ return undefined;
559
+ }
560
+ /**
561
+ * Whether an RFC 8707 `resource` names this deployment's MCP endpoint.
562
+ *
563
+ * Compared with a trailing slash tolerated on either side, because that is the one difference a
564
+ * conforming client and a conforming server routinely disagree about, and refusing over it would
565
+ * be this deployment failing a host for being right.
566
+ */
567
+ function resourceMatches(presented, expected) {
568
+ const trim = (value) => value.replace(/\/+$/, '');
569
+ return trim(presented) === trim(expected);
570
+ }
571
+ /** Whether a presented verifier hashes to the challenge the request was sealed with. */
572
+ async function pkceMatches(verifier, challenge) {
573
+ if (!verifier)
574
+ return false;
575
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
576
+ return base64url(new Uint8Array(digest)) === challenge;
577
+ }
578
+ /** Base64url, unpadded, the encoding PKCE's S256 challenge is stated in. */
579
+ function base64url(bytes) {
580
+ let binary = '';
581
+ for (const byte of bytes)
582
+ binary += String.fromCharCode(byte);
583
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
584
+ }
585
+ //# sourceMappingURL=McpAuthorizationServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpAuthorizationServer.js","sourceRoot":"","sources":["../../../src/modules/mcpAuthServer/McpAuthorizationServer.ts"],"names":[],"mappings":"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"}