@cat-factory/integrations 0.130.2 → 0.131.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.
@@ -0,0 +1,489 @@
1
+ import { MCP_OAUTH_DEFAULT_HEADER, MCP_OAUTH_DEFAULT_HEADER_TEMPLATE, ValidationError, noopLogger, redactSecrets, } from '@cat-factory/kernel';
2
+ import { McpOAuthError, assertAllowedOAuthUrl, buildAuthorizationUrl, discoverMcpOAuthEndpoints, exchangeAuthorizationCode, refreshAccessToken, requestClientCredentialsToken, } from './mcpOAuthClient.js';
3
+ /**
4
+ * HKDF domain tag separating this store's ciphertexts from every other cipher in the platform.
5
+ * ONE tag covers both things sealed here (the stored token set and the in-flight authorization
6
+ * request), which is why each carries an explicit `kind` claim that the opener pins — the same
7
+ * discipline the token signer applies with its `aud`, so a value minted for one purpose can never
8
+ * be opened as the other.
9
+ */
10
+ export const MCP_OAUTH_CIPHER_INFO = 'cat-factory:mcp-oauth';
11
+ /**
12
+ * How long an in-flight authorization request stays valid. Long enough to sign in to a vendor and
13
+ * approve a consent screen, short enough that an abandoned one is not a lasting artefact.
14
+ */
15
+ const AUTHORIZATION_REQUEST_TTL_MS = 10 * 60 * 1000;
16
+ /**
17
+ * How far before its stated expiry an access token is treated as spent.
18
+ *
19
+ * A dispatched token is used by an agent in a container for the length of a run, not at the
20
+ * instant it is handed over, so "expires in 20 seconds" is functionally expired. Refreshing early
21
+ * costs one round trip; handing over a token that dies mid-run costs the agent its tool with no
22
+ * unavailability reason to state, because the platform believed it was wired.
23
+ */
24
+ const EXPIRY_SKEW_MS = 60_000;
25
+ /** How many times a lost refresh race re-reads before giving up. */
26
+ const MAX_REFRESH_ATTEMPTS = 3;
27
+ /**
28
+ * Owns a workspace's OAuth grants against remote (`http`) MCP tool servers: starting a grant,
29
+ * completing one, minting and refreshing access tokens for a dispatch, and reporting what a board
30
+ * is connected to.
31
+ *
32
+ * The split from `CapabilityCredentialsService` beside it is the split between a credential a
33
+ * human TYPES and one a human GRANTS. A typed credential is inert until a dispatch reads it; a
34
+ * granted one expires, refreshes, gets revoked at the vendor, and belongs to a named person's
35
+ * account. That is why this is a service of its own rather than another key in the checklist: the
36
+ * checklist's whole shape (a key, a write-only value, a last-written date) cannot express any of it.
37
+ *
38
+ * STRICTLY the store plus the protocol. What a deployment DECLARES is registry state, and the
39
+ * caller passes the declaration in — this package has no business reaching into the agent-kind
40
+ * registry, exactly as the credential service has none.
41
+ *
42
+ * CONCURRENCY: the refresh path is the contended one, and it is contended by two dispatches rather
43
+ * than two humans. It rides a rev-guarded `compareAndSwap`, and the loser does NOT re-apply its own
44
+ * write (the rule every other rev-guarded path in this repo follows) — it re-reads and adopts the
45
+ * winner's tokens. Re-applying would be actively wrong here: an authorization server that rotates
46
+ * refresh tokens has already invalidated the loser's, so re-applying replaces a working grant with
47
+ * a dead one.
48
+ */
49
+ export class McpOAuthService {
50
+ deps;
51
+ log;
52
+ constructor(deps) {
53
+ this.deps = deps;
54
+ this.log = deps.logger ?? noopLogger;
55
+ }
56
+ get fetchDeps() {
57
+ return this.deps.fetchImpl ? { fetch: this.deps.fetchImpl } : {};
58
+ }
59
+ /**
60
+ * Begin an `authorization_code` grant: resolve the endpoints, mint a PKCE pair, and return the
61
+ * vendor URL to send the operator to.
62
+ *
63
+ * Throws rather than returning a status, because every failure here is a REFUSAL an operator
64
+ * asked for directly (a declaration that cannot be granted, a deployment with no redirect URL,
65
+ * an authorization server that publishes nothing) and the surface that called it is a button
66
+ * press, not a run.
67
+ */
68
+ async startAuthorization(input) {
69
+ if (input.oauth.grant !== 'authorization_code') {
70
+ throw new ValidationError(`Tool server '${input.serverId}' authenticates with the client-credentials grant, which ` +
71
+ `needs no authorization: its token is minted from the deployment's own client on the ` +
72
+ `first dispatch that needs one.`, { reason: 'oauth_grant_not_interactive' });
73
+ }
74
+ const endpoints = await this.resolveEndpoints(input.serverUrl, input.oauth);
75
+ const codeVerifier = randomCodeVerifier();
76
+ const request = {
77
+ kind: 'authorization-request',
78
+ workspaceId: input.workspaceId,
79
+ serverId: input.serverId,
80
+ userId: input.userId,
81
+ codeVerifier,
82
+ redirectUri: input.redirectUri,
83
+ tokenUrl: endpoints.tokenUrl,
84
+ useBasicAuth: endpoints.useBasicAuth,
85
+ resource: input.oauth.resource ?? input.serverUrl,
86
+ exp: this.deps.clock.now() + AUTHORIZATION_REQUEST_TTL_MS,
87
+ };
88
+ return {
89
+ url: buildAuthorizationUrl({
90
+ authorizationUrl: endpoints.authorizationUrl,
91
+ clientId: input.oauth.clientId,
92
+ redirectUri: input.redirectUri,
93
+ state: await this.deps.secretCipher.encrypt(JSON.stringify(request)),
94
+ codeChallenge: await codeChallengeFor(codeVerifier),
95
+ ...(input.oauth.scopes?.length ? { scopes: input.oauth.scopes } : {}),
96
+ resource: request.resource,
97
+ }),
98
+ };
99
+ }
100
+ /**
101
+ * Open the sealed `state` a callback carried, or null when it is absent, forged, expired or not
102
+ * an authorization request at all.
103
+ *
104
+ * Null rather than a thrown cause on purpose: the four are indistinguishable to the caller and
105
+ * every one of them is a 401, so telling them apart would only tell an attacker which of their
106
+ * guesses was closer.
107
+ */
108
+ async readAuthorizationRequest(state) {
109
+ if (!state)
110
+ return null;
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(await this.deps.secretCipher.decrypt(state));
114
+ }
115
+ catch {
116
+ // silent-catch-ok: a state that will not open is a 401 whatever the reason, and the reason is
117
+ // attacker-supplied. The caller logs the refusal with the request's own correlation id.
118
+ return null;
119
+ }
120
+ const request = parsed;
121
+ if (!request || request.kind !== 'authorization-request')
122
+ return null;
123
+ if (typeof request.exp !== 'number' || request.exp < this.deps.clock.now())
124
+ return null;
125
+ return request;
126
+ }
127
+ /**
128
+ * Complete a grant: exchange the code for tokens and seal them for the workspace.
129
+ *
130
+ * A BLIND upsert, unlike the refresh path: a human just authorised, and what they authorised
131
+ * supersedes whatever the row held — including a grant a colleague made a minute earlier, which
132
+ * is exactly the "reconnect with a different account" case a rev guard would turn into a
133
+ * spurious conflict.
134
+ */
135
+ async completeAuthorization(request, input) {
136
+ const tokens = await exchangeAuthorizationCode({
137
+ tokenUrl: request.tokenUrl,
138
+ clientId: input.clientId,
139
+ ...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
140
+ useBasicAuth: request.useBasicAuth,
141
+ resource: request.resource,
142
+ code: input.code,
143
+ redirectUri: request.redirectUri,
144
+ codeVerifier: request.codeVerifier,
145
+ }, this.fetchDeps).catch((error) => {
146
+ throw asRefusal(error);
147
+ });
148
+ const now = this.deps.clock.now();
149
+ await this.deps.mcpOAuthGrantRepository.upsert(await this.buildRecord({
150
+ workspaceId: request.workspaceId,
151
+ serverId: request.serverId,
152
+ tokens,
153
+ now,
154
+ rev: 0,
155
+ createdAt: now,
156
+ summary: {
157
+ connectedAt: now,
158
+ ...(request.userId ? { connectedBy: request.userId } : {}),
159
+ },
160
+ }));
161
+ this.log.info('mcp tool server oauth grant stored', {
162
+ workspaceId: request.workspaceId,
163
+ toolServerId: request.serverId,
164
+ refreshable: Boolean(tokens.refreshToken),
165
+ });
166
+ }
167
+ /** Drop a workspace's grant for one server. Idempotent: a missing row is already disconnected. */
168
+ async disconnect(workspaceId, serverId) {
169
+ await this.deps.mcpOAuthGrantRepository.delete(workspaceId, serverId);
170
+ }
171
+ /**
172
+ * The non-secret connection state of every grant a workspace holds, keyed by server id.
173
+ *
174
+ * ONE read for the whole inventory rather than a lookup per declared server: the operator surface
175
+ * renders a row per DECLARATION, and the declarations are the deployment's whole registry.
176
+ */
177
+ async listStatuses(workspaceId) {
178
+ const records = await this.deps.mcpOAuthGrantRepository.listByWorkspace(workspaceId);
179
+ return new Map(records.map((record) => [record.serverId, parseSummary(record)]));
180
+ }
181
+ /**
182
+ * The access token one dispatch needs, refreshing or minting it when what is stored will not do.
183
+ *
184
+ * Never throws: a dispatch asking for a tool is not a place to fail a run, so every failure comes
185
+ * back as a `token_failed` result the caller states to the agent. That is the same disposition
186
+ * `resolveToolServers` gives an unresolved static credential, and for the same reason.
187
+ */
188
+ async accessToken(input) {
189
+ try {
190
+ const token = await this.resolveToken(input);
191
+ return token
192
+ ? {
193
+ status: 'ok',
194
+ header: input.oauth.header ?? MCP_OAUTH_DEFAULT_HEADER,
195
+ value: (input.oauth.headerTemplate ?? MCP_OAUTH_DEFAULT_HEADER_TEMPLATE).replaceAll('{value}', token),
196
+ }
197
+ : { status: 'not_connected' };
198
+ }
199
+ catch (error) {
200
+ const message = describeOAuthError(error);
201
+ this.log.warn('mcp tool server oauth token could not be obtained', {
202
+ workspaceId: input.workspaceId,
203
+ toolServerId: input.serverId,
204
+ detail: message,
205
+ });
206
+ await this.recordFailure(input.workspaceId, input.serverId, message);
207
+ return { status: 'token_failed', error: message };
208
+ }
209
+ }
210
+ /** The raw access token, or null when nothing is granted. Throws on a real failure. */
211
+ async resolveToken(input) {
212
+ for (let attempt = 0; attempt < MAX_REFRESH_ATTEMPTS; attempt++) {
213
+ const record = await this.deps.mcpOAuthGrantRepository.get(input.workspaceId, input.serverId);
214
+ const stored = record ? await this.openTokens(record) : null;
215
+ if (stored && !this.isSpent(stored)) {
216
+ // A live token also settles any failure the summary still claims: `lastError` is written by
217
+ // whichever dispatch could not mint one, and nothing else would ever take it back off a
218
+ // connection that started working again. Only touches the row when there is something to
219
+ // clear, so the ordinary dispatch stays a single read.
220
+ if (record && parseSummary(record).lastError !== undefined)
221
+ await this.clearFailure(record);
222
+ return stored.accessToken;
223
+ }
224
+ if (input.oauth.grant === 'authorization_code' && !stored)
225
+ return null;
226
+ if (input.oauth.grant === 'authorization_code' && !stored?.refreshToken) {
227
+ throw new McpOAuthError(`The stored access token has expired and the authorization server issued no refresh ` +
228
+ `token, so the connection has to be granted again.`, true);
229
+ }
230
+ let tokens;
231
+ try {
232
+ tokens = await this.mintTokens(input, stored?.refreshToken);
233
+ }
234
+ catch (error) {
235
+ // The refresh race the rev guard below CANNOT settle, and the likelier half of it against a
236
+ // rotating authorization server. Two dispatches find the same token spent and POST the same
237
+ // refresh token; the winner's exchange rotates it, which INVALIDATES the loser's copy, so
238
+ // the loser fails right here with `invalid_grant` and never reaches the compareAndSwap that
239
+ // would have told it it lost. Re-reading is what tells the two apart: a row that has moved
240
+ // on to a live token means a peer succeeded, and this dispatch wants a token rather than a
241
+ // diagnosis. With nothing new stored, the failure is real and propagates untouched.
242
+ const adopted = await this.adoptConcurrentToken(input.workspaceId, input.serverId, record);
243
+ if (adopted) {
244
+ this.log.info('adopted a concurrently refreshed mcp oauth token after a lost race', {
245
+ workspaceId: input.workspaceId,
246
+ toolServerId: input.serverId,
247
+ });
248
+ return adopted;
249
+ }
250
+ throw error;
251
+ }
252
+ const now = this.deps.clock.now();
253
+ const swapped = await this.deps.mcpOAuthGrantRepository.compareAndSwap(await this.buildRecord({
254
+ workspaceId: input.workspaceId,
255
+ serverId: input.serverId,
256
+ tokens: {
257
+ ...tokens,
258
+ // An authorization server that rotates refresh tokens returns a new one; one that does
259
+ // not returns none, and DROPPING the old one there would turn a working grant into a
260
+ // single-use one. Carrying it forward is what makes both behaviours refreshable.
261
+ ...(tokens.refreshToken ? {} : { refreshToken: stored?.refreshToken }),
262
+ },
263
+ now,
264
+ rev: (record?.rev ?? -1) + 1,
265
+ createdAt: record?.createdAt ?? now,
266
+ summary: {
267
+ ...(record ? dropError(parseSummary(record)) : {}),
268
+ ...(record ? {} : { connectedAt: now }),
269
+ },
270
+ }), record?.rev ?? null);
271
+ // Lost the race: another dispatch refreshed first. Loop back and read THEIR tokens rather
272
+ // than re-applying ours — a rotated refresh token makes ours the stale set, so re-applying
273
+ // would replace a working grant with a dead one.
274
+ if (swapped)
275
+ return tokens.accessToken;
276
+ }
277
+ throw new McpOAuthError(`The stored grant is being refreshed by several runs at once and this dispatch could not ` +
278
+ `settle on a token; retry.`, false);
279
+ }
280
+ /**
281
+ * One exchange at the token endpoint: the machine grant mints from the deployment's own client,
282
+ * the interactive one spends the stored refresh token.
283
+ *
284
+ * Split out of {@link resolveToken} so that method reads as what it is (the read, the spend
285
+ * check, the race handling and the swap) rather than carrying the wire shape of two requests
286
+ * through the middle of it.
287
+ */
288
+ async mintTokens(input, refreshToken) {
289
+ const endpoints = await this.resolveEndpoints(input.serverUrl, input.oauth);
290
+ const common = {
291
+ tokenUrl: endpoints.tokenUrl,
292
+ clientId: input.oauth.clientId,
293
+ ...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
294
+ useBasicAuth: endpoints.useBasicAuth,
295
+ resource: input.oauth.resource ?? input.serverUrl,
296
+ };
297
+ const scopes = input.oauth.scopes?.length ? { scopes: input.oauth.scopes } : {};
298
+ return input.oauth.grant === 'client_credentials'
299
+ ? requestClientCredentialsToken({ ...common, ...scopes }, this.fetchDeps)
300
+ : // Reached only past the guards above, which return or throw when there is no refresh token.
301
+ refreshAccessToken({ ...common, refreshToken: refreshToken }, this.fetchDeps);
302
+ }
303
+ /**
304
+ * The token a CONCURRENT dispatch stored while this one was failing to mint its own, or null.
305
+ *
306
+ * Best effort by construction: it runs on a path that already has a real error to report, so
307
+ * anything that goes wrong here (an unreadable row, a rotated key) means only that there is
308
+ * nothing to adopt, and the caller rethrows the failure that brought it here. Requires the row to
309
+ * have MOVED: an unchanged `rev` is the same grant this attempt already read and failed with,
310
+ * and returning its expired token would hand the run a credential the vendor has finished with.
311
+ */
312
+ async adoptConcurrentToken(workspaceId, serverId, seen) {
313
+ try {
314
+ const record = await this.deps.mcpOAuthGrantRepository.get(workspaceId, serverId);
315
+ if (!record || record.rev === seen?.rev)
316
+ return null;
317
+ const stored = await this.openTokens(record);
318
+ return stored && !this.isSpent(stored) ? stored.accessToken : null;
319
+ }
320
+ catch {
321
+ // silent-catch-ok: this is a recovery read behind an error that is about to propagate, and
322
+ // its own failure is not a second thing to report: it only means nothing could be adopted.
323
+ return null;
324
+ }
325
+ }
326
+ /** Endpoints: the declaration's, when it pinned them, else discovered from the server url. */
327
+ async resolveEndpoints(serverUrl, oauth) {
328
+ if (oauth.authorizationUrl && oauth.tokenUrl) {
329
+ // The SAME floor a discovered endpoint and every redirect hop is held to, from the one
330
+ // implementation: a rule enforced on two of three paths is not a rule.
331
+ assertAllowedOAuthUrl(oauth.authorizationUrl, 'declared OAuth authorizationUrl');
332
+ assertAllowedOAuthUrl(oauth.tokenUrl, 'declared OAuth tokenUrl');
333
+ return {
334
+ authorizationUrl: oauth.authorizationUrl,
335
+ tokenUrl: oauth.tokenUrl,
336
+ useBasicAuth: false,
337
+ };
338
+ }
339
+ const discovered = await discoverMcpOAuthEndpoints(serverUrl, this.fetchDeps);
340
+ // A HALF-declared pair still overrides its half: pinning one endpoint and discovering the other
341
+ // is a legitimate declaration (a vendor whose metadata is right about one and stale about the
342
+ // other), and silently ignoring the pin would send tokens somewhere the operator refused.
343
+ return {
344
+ authorizationUrl: oauth.authorizationUrl ?? discovered.authorizationUrl,
345
+ tokenUrl: oauth.tokenUrl ?? discovered.tokenUrl,
346
+ useBasicAuth: discovered.useBasicAuth,
347
+ };
348
+ }
349
+ /** Whether a stored access token is too close to its expiry to hand to a run. */
350
+ isSpent(tokens) {
351
+ return (tokens.expiresAt !== undefined && tokens.expiresAt - EXPIRY_SKEW_MS <= this.deps.clock.now());
352
+ }
353
+ async openTokens(record) {
354
+ try {
355
+ const parsed = JSON.parse(await this.deps.secretCipher.decrypt(record.tokens));
356
+ return parsed?.kind === 'tokens' && typeof parsed.accessToken === 'string' ? parsed : null;
357
+ }
358
+ catch (error) {
359
+ // A row that will not open is a rotated ENCRYPTION_KEY or a corrupt blob, and it is
360
+ // permanent: nothing this process does will decrypt it, so it must read as a broken
361
+ // connection to be re-granted rather than as an absent one that a dispatch quietly retries.
362
+ throw new McpOAuthError(`The stored grant could not be opened (${describeOAuthError(error)}). It was sealed with a ` +
363
+ `different encryption key; disconnect and grant it again.`, true);
364
+ }
365
+ }
366
+ async buildRecord(input) {
367
+ const expiresAt = input.tokens.expiresIn !== undefined ? input.now + input.tokens.expiresIn * 1000 : undefined;
368
+ const stored = {
369
+ kind: 'tokens',
370
+ accessToken: input.tokens.accessToken,
371
+ ...(input.tokens.refreshToken ? { refreshToken: input.tokens.refreshToken } : {}),
372
+ ...(expiresAt !== undefined ? { expiresAt } : {}),
373
+ ...(input.tokens.scope ? { scope: input.tokens.scope } : {}),
374
+ };
375
+ const summary = {
376
+ ...input.summary,
377
+ ...(input.tokens.scope ? { scopes: input.tokens.scope.split(/\s+/).filter(Boolean) } : {}),
378
+ ...(expiresAt !== undefined ? { expiresAt } : {}),
379
+ refreshable: Boolean(stored.refreshToken),
380
+ };
381
+ return {
382
+ workspaceId: input.workspaceId,
383
+ serverId: input.serverId,
384
+ tokens: await this.deps.secretCipher.encrypt(JSON.stringify(stored)),
385
+ summary: JSON.stringify(summary),
386
+ rev: input.rev,
387
+ createdAt: input.createdAt,
388
+ updatedAt: input.now,
389
+ };
390
+ }
391
+ /**
392
+ * Record on the SUMMARY that the last token exchange failed, so the operator surface can say a
393
+ * connection stopped working without anyone reading a run's prompt.
394
+ *
395
+ * Best effort by construction, and it only ever touches a row that already exists: a failure with
396
+ * nothing stored is `not_connected`, which the surface already states. The rev guard is NOT
397
+ * retried on a lost swap: the note is advisory, and losing it to a concurrent refresh that
398
+ * SUCCEEDED is the correct outcome rather than a lost write.
399
+ */
400
+ async recordFailure(workspaceId, serverId, message) {
401
+ const record = await this.deps.mcpOAuthGrantRepository.get(workspaceId, serverId);
402
+ if (!record)
403
+ return;
404
+ await this.writeSummary(record, { ...parseSummary(record), lastError: message });
405
+ }
406
+ /**
407
+ * Take a recorded failure back off a connection that has started working again.
408
+ *
409
+ * The other half of {@link recordFailure}, and the surface reads wrong without it: `lastError`
410
+ * describes the last exchange that failed, so on a token that is merely CACHED (the common
411
+ * dispatch, which mints nothing) nothing would ever clear it, and one transient vendor outage
412
+ * would leave a red "the last token renewal failed" banner on a working grant until the access
413
+ * token happened to expire. Same advisory, same unretried rev guard.
414
+ */
415
+ async clearFailure(record) {
416
+ await this.writeSummary(record, dropError(parseSummary(record)));
417
+ }
418
+ /** One rev-guarded, unretried write of the non-secret half. */
419
+ async writeSummary(record, summary) {
420
+ await this.deps.mcpOAuthGrantRepository.compareAndSwap({
421
+ ...record,
422
+ summary: JSON.stringify(summary),
423
+ rev: record.rev + 1,
424
+ updatedAt: this.deps.clock.now(),
425
+ }, record.rev);
426
+ }
427
+ }
428
+ /** Parse the persisted non-secret summary, tolerating a corrupt row (the view still loads). */
429
+ function parseSummary(record) {
430
+ try {
431
+ const parsed = JSON.parse(record.summary);
432
+ return parsed && typeof parsed === 'object' ? parsed : {};
433
+ }
434
+ catch {
435
+ // silent-catch-ok: a summary is a display projection of a row whose SEALED half is the truth,
436
+ // so a drifted one must never keep an operator from seeing that the grant exists.
437
+ return {};
438
+ }
439
+ }
440
+ /** The summary with any recorded failure cleared — what a successful exchange leaves behind. */
441
+ function dropError(summary) {
442
+ const { lastError: _dropped, ...rest } = summary;
443
+ return rest;
444
+ }
445
+ /**
446
+ * An OAuth failure as an operator-facing sentence, scrubbed.
447
+ *
448
+ * `describeError` in kernel would do the scrubbing, and this exists beside it because an
449
+ * `McpOAuthError`'s message IS the operator-facing sentence: wrapping it in a generic description
450
+ * would bury the one part of it that names the fix.
451
+ */
452
+ function describeOAuthError(error) {
453
+ const raw = error instanceof Error ? error.message : String(error);
454
+ return redactSecrets(raw) ?? 'unknown error';
455
+ }
456
+ /**
457
+ * An OAuth failure as an HTTP refusal, for the INTERACTIVE paths (starting and completing a grant),
458
+ * where the caller is a person and a thrown cause is the answer they asked for.
459
+ *
460
+ * A PERMANENT failure becomes a 422 carrying `details.reason`, because what must change is the
461
+ * declaration or the grant and the operator is the one who can change it. A TRANSIENT one is
462
+ * rethrown untouched and surfaces as a 500, which is the honest status for "the vendor's
463
+ * authorization server did not answer": mapping it to a 4xx would tell the operator to fix
464
+ * something that is not theirs.
465
+ */
466
+ function asRefusal(error) {
467
+ if (error instanceof McpOAuthError && error.permanent) {
468
+ return new ValidationError(describeOAuthError(error), { reason: 'oauth_exchange_refused' });
469
+ }
470
+ return error;
471
+ }
472
+ /** A PKCE code verifier: 32 random bytes, base64url — the RFC 7636 recommended shape. */
473
+ function randomCodeVerifier() {
474
+ const bytes = new Uint8Array(32);
475
+ crypto.getRandomValues(bytes);
476
+ return base64url(bytes);
477
+ }
478
+ /** The S256 challenge for a verifier. */
479
+ async function codeChallengeFor(verifier) {
480
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
481
+ return base64url(new Uint8Array(digest));
482
+ }
483
+ function base64url(bytes) {
484
+ let binary = '';
485
+ for (const byte of bytes)
486
+ binary += String.fromCharCode(byte);
487
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
488
+ }
489
+ //# sourceMappingURL=McpOAuthService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"McpOAuthService.js","sourceRoot":"","sources":["../../../src/modules/mcpOAuth/McpOAuthService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ /** How long the whole discovery walk, or one token call, may take. */
2
+ export declare const MCP_OAUTH_TIMEOUT_MS = 10000;
3
+ /**
4
+ * The one URL floor every request in this module is held to, applied to DECLARED endpoints,
5
+ * DISCOVERED ones, and every redirect hop alike.
6
+ *
7
+ * Two rules, and the second is here because discovery reads a THIRD PARTY's document. A remote
8
+ * server's protected-resource metadata names its authorization servers, so a compromised or hostile
9
+ * server chooses URLs this deployment then fetches; `isAllowedMcpHttpUrl` alone would accept
10
+ * `https://169.254.169.254/…` and turn the walk into a probe of the instance-metadata service.
11
+ * Blocking metadata hosts while still ALLOWING private and loopback ones is the same trade the
12
+ * Kubernetes apiserver guard makes, and it keeps a sidecar MCP server on a private host connectable.
13
+ *
14
+ * One floor for every URL rather than a stricter rule for discovered ones: a declaration pointing an
15
+ * OAuth endpoint at a metadata host is nonsense too, so there is nothing to gain by admitting it.
16
+ */
17
+ export declare function assertAllowedOAuthUrl(raw: string, what: string): void;
18
+ /** Injected so tests drive the whole flow without a network. */
19
+ export interface McpOAuthFetch {
20
+ fetch?: typeof fetch;
21
+ }
22
+ /** Where a server's authorization server is, and how it wants the client to authenticate. */
23
+ export interface McpOAuthEndpoints {
24
+ authorizationUrl: string;
25
+ tokenUrl: string;
26
+ /**
27
+ * Whether the token endpoint is asked for HTTP Basic client authentication instead of the
28
+ * credentials-in-the-body form.
29
+ *
30
+ * Defaults to the body form (`client_secret_post`), which every authorization server this
31
+ * platform has met accepts, and flips only when the AS metadata advertises Basic and NOT post.
32
+ * Deciding it from the metadata rather than trying both matters because a retry-on-401 would
33
+ * send the client secret twice, to a server that has already refused it once.
34
+ */
35
+ useBasicAuth: boolean;
36
+ }
37
+ /** A token response, normalised. `expiresIn` is seconds, as the wire states it. */
38
+ export interface McpOAuthTokens {
39
+ accessToken: string;
40
+ refreshToken?: string;
41
+ expiresIn?: number;
42
+ scope?: string;
43
+ tokenType?: string;
44
+ }
45
+ /** A failure with a cause an operator can act on. Never carries a credential (see `describe`). */
46
+ export declare class McpOAuthError extends Error {
47
+ /** True when retrying cannot help: the grant or the client registration must change. */
48
+ readonly permanent: boolean;
49
+ constructor(message: string,
50
+ /** True when retrying cannot help: the grant or the client registration must change. */
51
+ permanent: boolean);
52
+ }
53
+ /**
54
+ * Discover a remote MCP server's authorization endpoints, per the MCP authorization spec: the
55
+ * server's own protected-resource metadata (RFC 9728) names its authorization server, and that
56
+ * server's metadata (RFC 8414, or OpenID Connect discovery) names the endpoints.
57
+ *
58
+ * The walk is what makes a vendor server connectable from a declaration that names only its url.
59
+ * Without it a deployment has to find two endpoint URLs in a vendor's docs, and they are exactly
60
+ * the strings a vendor changes when it re-platforms.
61
+ *
62
+ * EVERY url the walk touches goes through {@link assertAllowedOAuthUrl}: each candidate, each
63
+ * redirect hop, and both endpoints that come out of it. A metadata document is a third party
64
+ * telling this deployment where to send its client secret and receive its tokens, which is the one
65
+ * place in this flow where an outsider chooses a URL this side then fetches, so it is also the one
66
+ * place where checking the first URL and trusting the rest would not be checking at all.
67
+ */
68
+ export declare function discoverMcpOAuthEndpoints(serverUrl: string, deps?: McpOAuthFetch): Promise<McpOAuthEndpoints>;
69
+ /** The authorization URL an operator's browser is sent to. */
70
+ export declare function buildAuthorizationUrl(input: {
71
+ authorizationUrl: string;
72
+ clientId: string;
73
+ redirectUri: string;
74
+ state: string;
75
+ codeChallenge: string;
76
+ scopes?: string[];
77
+ resource: string;
78
+ }): string;
79
+ export interface TokenRequestInput {
80
+ tokenUrl: string;
81
+ clientId: string;
82
+ clientSecret?: string;
83
+ useBasicAuth?: boolean;
84
+ resource: string;
85
+ }
86
+ /** Exchange an authorization code (plus its PKCE verifier) for a token set. */
87
+ export declare function exchangeAuthorizationCode(input: TokenRequestInput & {
88
+ code: string;
89
+ redirectUri: string;
90
+ codeVerifier: string;
91
+ }, deps?: McpOAuthFetch): Promise<McpOAuthTokens>;
92
+ /** Exchange a refresh token for a fresh token set. */
93
+ export declare function refreshAccessToken(input: TokenRequestInput & {
94
+ refreshToken: string;
95
+ scopes?: string[];
96
+ }, deps?: McpOAuthFetch): Promise<McpOAuthTokens>;
97
+ /** Mint a token from the deployment's own client credentials — the no-human grant. */
98
+ export declare function requestClientCredentialsToken(input: TokenRequestInput & {
99
+ scopes?: string[];
100
+ }, deps?: McpOAuthFetch): Promise<McpOAuthTokens>;
101
+ //# sourceMappingURL=mcpOAuthClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpOAuthClient.d.ts","sourceRoot":"","sources":["../../../src/modules/mcpOAuth/mcpOAuthClient.ts"],"names":[],"mappings":"AAkBA,sEAAsE;AACtE,eAAO,MAAM,oBAAoB,QAAS,CAAA;AAa1C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAwBrE;AAED,gEAAgE;AAChE,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CACrB;AAED,6FAA6F;AAC7F,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;;;OAQG;IACH,YAAY,EAAE,OAAO,CAAA;CACtB;AAED,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,kGAAkG;AAClG,qBAAa,aAAc,SAAQ,KAAK;IAGpC,wFAAwF;IACxF,QAAQ,CAAC,SAAS,EAAE,OAAO;IAH7B,YACE,OAAO,EAAE,MAAM;IACf,wFAAwF;IAC/E,SAAS,EAAE,OAAO,EAI5B;CACF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,yBAAyB,CAC7C,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,iBAAiB,CAAC,CA2D5B;AA6ED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE;IAC3C,gBAAgB,EAAE,MAAM,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB,GAAG,MAAM,CAcT;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,+EAA+E;AAC/E,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,iBAAiB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,EACtF,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,cAAc,CAAC,CAWzB;AAED,sDAAsD;AACtD,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,iBAAiB,GAAG;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EACtE,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,cAAc,CAAC,CAUzB;AAED,sFAAsF;AACtF,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,iBAAiB,GAAG;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EAChD,IAAI,GAAE,aAAkB,GACvB,OAAO,CAAC,cAAc,CAAC,CASzB"}