fastmcp 4.16.13 → 4.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -72,6 +72,7 @@ var proxyDcrClientStorageSchema = _zod.z.object({
72
72
  callbackUrl: _zod.z.string(),
73
73
  clientId: _zod.z.string(),
74
74
  clientSecret: _zod.z.string().optional(),
75
+ expiresAt: storedDateSchema.optional(),
75
76
  metadata: dcrClientMetadataSchema.optional(),
76
77
  redirectUris: _zod.z.array(_zod.z.string()),
77
78
  registeredAt: storedDateSchema
@@ -172,10 +173,18 @@ var OAuthProxyStateStore = class {
172
173
  `${STORAGE_KEY_PREFIX.transaction}${transactionId}`
173
174
  );
174
175
  }
176
+ /**
177
+ * A client carrying `expiresAt` is a cached resolution rather than a
178
+ * permanent registration (CIMD), so a lapsed copy is dropped and reported as
179
+ * a miss — the caller re-resolves it from the source of truth.
180
+ */
175
181
  async getRegisteredClientByClientId(clientId) {
176
182
  const cached = this.registeredClientsByClientId.get(clientId);
177
183
  if (cached) {
178
- return cached;
184
+ if (!cached.expiresAt || !this.isExpired(cached.expiresAt)) {
185
+ return cached;
186
+ }
187
+ this.registeredClientsByClientId.delete(clientId);
179
188
  }
180
189
  const stored = await this.tokenStorage.get(
181
190
  `${STORAGE_KEY_PREFIX.client}${clientId}`
@@ -184,6 +193,10 @@ var OAuthProxyStateStore = class {
184
193
  if (!parsed.success) {
185
194
  return null;
186
195
  }
196
+ if (parsed.data.expiresAt && this.isExpired(parsed.data.expiresAt)) {
197
+ await this.tokenStorage.delete(`${STORAGE_KEY_PREFIX.client}${clientId}`);
198
+ return null;
199
+ }
187
200
  this.cacheRegisteredClient(parsed.data);
188
201
  return parsed.data;
189
202
  }
@@ -205,12 +218,6 @@ var OAuthProxyStateStore = class {
205
218
  }
206
219
  return parsed.data;
207
220
  }
208
- async isTransactionCallbackRegistered(transaction) {
209
- const registeredClient = await this.getRegisteredClientByClientId(
210
- transaction.clientId
211
- );
212
- return _nullishCoalesce(_optionalChain([registeredClient, 'optionalAccess', _ => _.redirectUris, 'access', _2 => _2.includes, 'call', _3 => _3(transaction.clientCallbackUrl)]), () => ( false));
213
- }
214
221
  /**
215
222
  * Record that an authorization code has been redeemed, so a later attempt
216
223
  * gets "already used" rather than looking like an unknown code. Keyed and
@@ -233,7 +240,8 @@ var OAuthProxyStateStore = class {
233
240
  async saveRegisteredClient(client) {
234
241
  await this.tokenStorage.save(
235
242
  `${STORAGE_KEY_PREFIX.client}${client.clientId}`,
236
- client
243
+ client,
244
+ client.expiresAt ? this.getTtlSeconds(client.expiresAt) : void 0
237
245
  );
238
246
  }
239
247
  async saveTransaction(transaction) {
@@ -276,6 +284,172 @@ var DEFAULT_AUTHORIZATION_CODE_TTL = 300;
276
284
  var DEFAULT_TRANSACTION_TTL = 600;
277
285
  var DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS = 1e4;
278
286
 
287
+ // src/auth/utils/cimd.ts
288
+ var CIMD_FETCH_TIMEOUT_MS = 5e3;
289
+ var CIMD_CLIENT_TTL_MS = 15 * 60 * 1e3;
290
+ var CIMD_MAX_RESPONSE_BYTES = 65536;
291
+ async function resolveCimdClient(clientId, requestedRedirectUri, validateRedirectUri) {
292
+ let url;
293
+ try {
294
+ url = new URL(clientId);
295
+ } catch (e) {
296
+ return null;
297
+ }
298
+ if (url.protocol !== "https:" || url.search) {
299
+ return null;
300
+ }
301
+ if (isPrivateOrLoopbackHost(url.hostname)) {
302
+ return null;
303
+ }
304
+ let response;
305
+ try {
306
+ response = await fetch(clientId, {
307
+ redirect: "error",
308
+ signal: AbortSignal.timeout(CIMD_FETCH_TIMEOUT_MS)
309
+ });
310
+ } catch (e2) {
311
+ return null;
312
+ }
313
+ if (!response.ok) {
314
+ return null;
315
+ }
316
+ const text = await readBoundedText(response);
317
+ if (text === null) {
318
+ return null;
319
+ }
320
+ let parsed;
321
+ try {
322
+ parsed = JSON.parse(text);
323
+ } catch (e3) {
324
+ return null;
325
+ }
326
+ if (typeof parsed !== "object" || parsed === null) {
327
+ return null;
328
+ }
329
+ const doc = parsed;
330
+ if (doc.client_id !== clientId) {
331
+ return null;
332
+ }
333
+ if (!isStringArray(doc.redirect_uris) || doc.redirect_uris.length === 0) {
334
+ return null;
335
+ }
336
+ const redirectUris = doc.redirect_uris;
337
+ if (requestedRedirectUri && !redirectUris.includes(requestedRedirectUri)) {
338
+ return null;
339
+ }
340
+ if (!redirectUris.every((uri) => validateRedirectUri(uri))) {
341
+ return null;
342
+ }
343
+ const metadata = {
344
+ client_name: optionalString(doc.client_name),
345
+ client_uri: optionalString(doc.client_uri),
346
+ logo_uri: optionalString(doc.logo_uri),
347
+ policy_uri: optionalString(doc.policy_uri),
348
+ scope: optionalString(doc.scope),
349
+ tos_uri: optionalString(doc.tos_uri)
350
+ };
351
+ return {
352
+ callbackUrl: redirectUris[0],
353
+ clientId,
354
+ // CIMD is a public-client mechanism secured by PKCE; there is no secret.
355
+ clientSecret: void 0,
356
+ expiresAt: new Date(Date.now() + CIMD_CLIENT_TTL_MS),
357
+ metadata,
358
+ redirectUris,
359
+ registeredAt: /* @__PURE__ */ new Date()
360
+ };
361
+ }
362
+ function expandIPv6(host) {
363
+ const sides = host.split("::");
364
+ if (sides.length > 2) {
365
+ return null;
366
+ }
367
+ const head = sides[0] ? sides[0].split(":") : [];
368
+ const tail = sides.length === 2 && sides[1] ? sides[1].split(":") : [];
369
+ if (sides.length === 1 && head.length !== 8) {
370
+ return null;
371
+ }
372
+ const missing = 8 - head.length - tail.length;
373
+ if (missing < 0) {
374
+ return null;
375
+ }
376
+ const parts = [...head, ...Array(missing).fill("0"), ...tail];
377
+ if (parts.length !== 8) {
378
+ return null;
379
+ }
380
+ const groups = parts.map((g) => Number.parseInt(g || "0", 16));
381
+ return groups.every((g) => !Number.isNaN(g)) ? groups : null;
382
+ }
383
+ function isPrivateOrLoopbackHost(hostname) {
384
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
385
+ if (host === "localhost" || host === "0.0.0.0") {
386
+ return true;
387
+ }
388
+ if (host.includes(":")) {
389
+ return isPrivateOrLoopbackIPv6(host);
390
+ }
391
+ return host === "127.0.0.1" || /^169\.254\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
392
+ }
393
+ function isPrivateOrLoopbackIPv6(host) {
394
+ const groups = expandIPv6(host);
395
+ if (!groups) {
396
+ return false;
397
+ }
398
+ if (groups.every((g) => g === 0)) {
399
+ return true;
400
+ }
401
+ if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) {
402
+ return true;
403
+ }
404
+ const [first] = groups;
405
+ if ((first & 65472) === 65152) {
406
+ return true;
407
+ }
408
+ if ((first & 65024) === 64512) {
409
+ return true;
410
+ }
411
+ if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && (groups[5] === 0 || groups[5] === 65535)) {
412
+ const ipv4 = `${groups[6] >> 8}.${groups[6] & 255}.${groups[7] >> 8}.${groups[7] & 255}`;
413
+ return isPrivateOrLoopbackHost(ipv4);
414
+ }
415
+ return false;
416
+ }
417
+ function isStringArray(value) {
418
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
419
+ }
420
+ function optionalString(value) {
421
+ return typeof value === "string" ? value : void 0;
422
+ }
423
+ async function readBoundedText(response) {
424
+ const contentLength = response.headers.get("content-length");
425
+ if (contentLength && Number(contentLength) > CIMD_MAX_RESPONSE_BYTES) {
426
+ return null;
427
+ }
428
+ const reader = _optionalChain([response, 'access', _ => _.body, 'optionalAccess', _2 => _2.getReader, 'call', _3 => _3()]);
429
+ if (!reader) {
430
+ return null;
431
+ }
432
+ const chunks = [];
433
+ let totalBytes = 0;
434
+ try {
435
+ for (; ; ) {
436
+ const { done, value } = await reader.read();
437
+ if (done) {
438
+ break;
439
+ }
440
+ totalBytes += value.byteLength;
441
+ if (totalBytes > CIMD_MAX_RESPONSE_BYTES) {
442
+ await reader.cancel();
443
+ return null;
444
+ }
445
+ chunks.push(value);
446
+ }
447
+ } catch (e4) {
448
+ return null;
449
+ }
450
+ return Buffer.concat(chunks).toString("utf-8");
451
+ }
452
+
279
453
  // src/auth/utils/claimsExtractor.ts
280
454
  var ClaimsExtractor = (_class = class {
281
455
 
@@ -413,7 +587,7 @@ var ClaimsExtractor = (_class = class {
413
587
  const stringified = JSON.stringify(value);
414
588
  const maxSize = _nullishCoalesce(this.config.maxClaimValueSize, () => ( 2e3));
415
589
  return stringified.length <= maxSize;
416
- } catch (e) {
590
+ } catch (e5) {
417
591
  return false;
418
592
  }
419
593
  }
@@ -694,7 +868,7 @@ var ConsentManager = class {
694
868
  return null;
695
869
  }
696
870
  return data;
697
- } catch (e2) {
871
+ } catch (e6) {
698
872
  return null;
699
873
  }
700
874
  }
@@ -1155,9 +1329,13 @@ var OAuthProxy = (_class4 = class {
1155
1329
  __init7() {this.ownedTokenStorage = null}
1156
1330
  /**
1157
1331
  * Keyed by proxy-issued client_id for authorize/token-exchange lookups and
1158
- * for the defence-in-depth callback checks. A registration never changes
1332
+ * for the defence-in-depth callback checks. A DCR registration never changes
1159
1333
  * after it is written, so caching it locally cannot go stale; it is also
1160
1334
  * persisted, so another instance can hydrate it.
1335
+ *
1336
+ * A CIMD client is not a registration but a snapshot of a document that can
1337
+ * change under us, so it is cached with an `expiresAt` and re-resolved once
1338
+ * that passes.
1161
1339
  */
1162
1340
  __init8() {this.registeredClientsByClientId = /* @__PURE__ */ new Map()}
1163
1341
 
@@ -1167,6 +1345,7 @@ var OAuthProxy = (_class4 = class {
1167
1345
  allowPlainPkce: true,
1168
1346
  authorizationCodeTtl: DEFAULT_AUTHORIZATION_CODE_TTL,
1169
1347
  consentRequired: true,
1348
+ enableCimd: false,
1170
1349
  enableTokenSwap: true,
1171
1350
  // Enabled by default for security
1172
1351
  redirectPath: "/oauth/callback",
@@ -1223,7 +1402,10 @@ var OAuthProxy = (_class4 = class {
1223
1402
  "Only 'code' response type is supported"
1224
1403
  );
1225
1404
  }
1226
- const registeredClient = await this.stateStore.getRegisteredClientByClientId(params.client_id);
1405
+ const registeredClient = await this.resolveClient(
1406
+ params.client_id,
1407
+ params.redirect_uri
1408
+ );
1227
1409
  if (!registeredClient) {
1228
1410
  throw new OAuthProxyError("invalid_client", "Unknown client_id");
1229
1411
  }
@@ -1275,7 +1457,7 @@ var OAuthProxy = (_class4 = class {
1275
1457
  "Only authorization_code grant type is supported"
1276
1458
  );
1277
1459
  }
1278
- const registeredClient = await this.stateStore.getRegisteredClientByClientId(request.client_id);
1460
+ const registeredClient = await this.resolveClient(request.client_id);
1279
1461
  if (!registeredClient) {
1280
1462
  throw new OAuthProxyError("invalid_client", "Unknown client_id");
1281
1463
  }
@@ -1363,6 +1545,7 @@ var OAuthProxy = (_class4 = class {
1363
1545
  getAuthorizationServerMetadata() {
1364
1546
  return {
1365
1547
  authorizationEndpoint: `${this.config.baseUrl}/oauth/authorize`,
1548
+ ...this.config.enableCimd ? { clientIdMetadataDocumentSupported: true } : {},
1366
1549
  codeChallengeMethodsSupported: this.config.allowPlainPkce ? ["S256", "plain"] : ["S256"],
1367
1550
  grantTypesSupported: ["authorization_code", "refresh_token"],
1368
1551
  issuer: this.config.baseUrl,
@@ -1398,7 +1581,7 @@ var OAuthProxy = (_class4 = class {
1398
1581
  if (!transaction) {
1399
1582
  throw new OAuthProxyError("invalid_request", "Invalid or expired state");
1400
1583
  }
1401
- if (!await this.stateStore.isTransactionCallbackRegistered(transaction)) {
1584
+ if (!await this.isTransactionCallbackRegistered(transaction)) {
1402
1585
  throw new OAuthProxyError(
1403
1586
  "invalid_request",
1404
1587
  "Transaction callback URL is not registered"
@@ -1444,7 +1627,7 @@ var OAuthProxy = (_class4 = class {
1444
1627
  }
1445
1628
  if (action === "deny") {
1446
1629
  await this.stateStore.deleteTransaction(transactionId);
1447
- if (!await this.stateStore.isTransactionCallbackRegistered(transaction)) {
1630
+ if (!await this.isTransactionCallbackRegistered(transaction)) {
1448
1631
  throw new OAuthProxyError(
1449
1632
  "invalid_request",
1450
1633
  "Transaction callback URL is not registered"
@@ -1641,7 +1824,7 @@ var OAuthProxy = (_class4 = class {
1641
1824
  const error = await tokenResponse.json();
1642
1825
  errorCode = error.error || "server_error";
1643
1826
  errorDescription = error.error_description;
1644
- } catch (e3) {
1827
+ } catch (e7) {
1645
1828
  errorDescription = `Upstream returned HTTP ${tokenResponse.status} ${tokenResponse.statusText}`;
1646
1829
  }
1647
1830
  throw new OAuthProxyError(errorCode, errorDescription);
@@ -1805,7 +1988,7 @@ var OAuthProxy = (_class4 = class {
1805
1988
  const error = await tokenResponse.json();
1806
1989
  errorCode = error.error || "invalid_grant";
1807
1990
  errorDescription = error.error_description;
1808
- } catch (e4) {
1991
+ } catch (e8) {
1809
1992
  errorDescription = `Upstream returned HTTP ${tokenResponse.status} ${tokenResponse.statusText}`;
1810
1993
  }
1811
1994
  throw new OAuthProxyError(errorCode, errorDescription);
@@ -2030,6 +2213,21 @@ var OAuthProxy = (_class4 = class {
2030
2213
  }
2031
2214
  return response;
2032
2215
  }
2216
+ /**
2217
+ * Defence in depth for the callback and consent-denial redirects: the
2218
+ * transaction's stored callback URL must still be registered for its client.
2219
+ *
2220
+ * Resolution goes through {@link resolveClient}, so a CIMD registration whose
2221
+ * cached copy lapsed mid-flow is re-fetched rather than mistaken for one that
2222
+ * was revoked.
2223
+ */
2224
+ async isTransactionCallbackRegistered(transaction) {
2225
+ const registeredClient = await this.resolveClient(
2226
+ transaction.clientId,
2227
+ transaction.clientCallbackUrl
2228
+ );
2229
+ return _nullishCoalesce(_optionalChain([registeredClient, 'optionalAccess', _17 => _17.redirectUris, 'access', _18 => _18.includes, 'call', _19 => _19(transaction.clientCallbackUrl)]), () => ( false));
2230
+ }
2033
2231
  /**
2034
2232
  * Match URI against pattern (supports wildcards)
2035
2233
  */
@@ -2140,7 +2338,7 @@ var OAuthProxy = (_class4 = class {
2140
2338
  const error = await tokenResponse.json();
2141
2339
  errorCode = error.error || "invalid_grant";
2142
2340
  errorDescription = error.error_description || "Upstream refresh failed";
2143
- } catch (e5) {
2341
+ } catch (e9) {
2144
2342
  errorDescription = `Upstream returned HTTP ${tokenResponse.status} ${tokenResponse.statusText}`;
2145
2343
  }
2146
2344
  throw new OAuthProxyError(errorCode, errorDescription);
@@ -2157,6 +2355,32 @@ var OAuthProxy = (_class4 = class {
2157
2355
  tokenType: tokens.token_type || "Bearer"
2158
2356
  };
2159
2357
  }
2358
+ /**
2359
+ * Resolve the client behind a `client_id`: a stored registration if there is
2360
+ * one, otherwise — when CIMD is enabled — the client's metadata document.
2361
+ *
2362
+ * A resolved CIMD client is cached like a DCR registration but carries the
2363
+ * `expiresAt` the resolver stamped on it, so the store drops it once it
2364
+ * lapses and the next call re-fetches the document. Every lookup goes
2365
+ * through here, so a lapse always means "re-resolve", never "unknown
2366
+ * client" mid-flow.
2367
+ */
2368
+ async resolveClient(clientId, requestedRedirectUri) {
2369
+ const registeredClient = await this.stateStore.getRegisteredClientByClientId(clientId);
2370
+ if (registeredClient || !this.config.enableCimd) {
2371
+ return registeredClient;
2372
+ }
2373
+ const resolved = await resolveCimdClient(
2374
+ clientId,
2375
+ requestedRedirectUri,
2376
+ (uri) => this.validateRedirectUri(uri)
2377
+ );
2378
+ if (resolved) {
2379
+ this.stateStore.cacheRegisteredClient(resolved);
2380
+ await this.stateStore.saveRegisteredClient(resolved);
2381
+ }
2382
+ return resolved;
2383
+ }
2160
2384
  /**
2161
2385
  * Put a claimed refresh-token mapping back after a rotation failed, keeping
2162
2386
  * whatever lifetime it had left.
@@ -2222,9 +2446,13 @@ var OAuthProxy = (_class4 = class {
2222
2446
  * theft. Do not loosen the default beyond loopback addresses.
2223
2447
  */
2224
2448
  validateRedirectUri(uri) {
2449
+ let parsed;
2225
2450
  try {
2226
- new URL(uri);
2227
- } catch (e6) {
2451
+ parsed = new URL(uri);
2452
+ } catch (e10) {
2453
+ return false;
2454
+ }
2455
+ if (parsed.username !== "" || parsed.password !== "") {
2228
2456
  return false;
2229
2457
  }
2230
2458
  const patterns = this.config.allowedRedirectUriPatterns;
@@ -2286,7 +2514,7 @@ var AuthProvider = class {
2286
2514
  if (!request) {
2287
2515
  return void 0;
2288
2516
  }
2289
- const authHeader = _optionalChain([request, 'access', _17 => _17.headers, 'optionalAccess', _18 => _18.authorization]);
2517
+ const authHeader = _optionalChain([request, 'access', _20 => _20.headers, 'optionalAccess', _21 => _21.authorization]);
2290
2518
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
2291
2519
  return void 0;
2292
2520
  }
@@ -2522,7 +2750,7 @@ var DiskStore = (_class5 = class {
2522
2750
  console.warn(`Failed to read/parse file ${file}, deleting:`, error);
2523
2751
  try {
2524
2752
  await _promises.rm.call(void 0, _path.join.call(void 0, this.directory, file));
2525
- } catch (e7) {
2753
+ } catch (e11) {
2526
2754
  }
2527
2755
  }
2528
2756
  }
@@ -2602,7 +2830,7 @@ var DiskStore = (_class5 = class {
2602
2830
  await this.ensureDirectory();
2603
2831
  const files = await _promises.readdir.call(void 0, this.directory);
2604
2832
  return files.filter((f) => f.endsWith(this.fileExtension)).length;
2605
- } catch (e8) {
2833
+ } catch (e12) {
2606
2834
  return 0;
2607
2835
  }
2608
2836
  }
@@ -2826,4 +3054,4 @@ Original error: ${error.message}`
2826
3054
 
2827
3055
 
2828
3056
  exports.getAuthSession = getAuthSession; exports.requireAll = requireAll; exports.requireAny = requireAny; exports.requireAuth = requireAuth; exports.requireRole = requireRole; exports.requireScopes = requireScopes; exports.DEFAULT_ACCESS_TOKEN_TTL = DEFAULT_ACCESS_TOKEN_TTL; exports.DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH = DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH; exports.DEFAULT_REFRESH_TOKEN_TTL = DEFAULT_REFRESH_TOKEN_TTL; exports.DEFAULT_AUTHORIZATION_CODE_TTL = DEFAULT_AUTHORIZATION_CODE_TTL; exports.DEFAULT_TRANSACTION_TTL = DEFAULT_TRANSACTION_TTL; exports.ConsentManager = ConsentManager; exports.JWTIssuer = JWTIssuer; exports.PKCEUtils = PKCEUtils; exports.EncryptedTokenStorage = EncryptedTokenStorage; exports.MemoryTokenStorage = MemoryTokenStorage; exports.OAuthProxy = OAuthProxy; exports.OAuthProxyError = OAuthProxyError; exports.AuthProvider = AuthProvider; exports.AzureProvider = AzureProvider; exports.GitHubProvider = GitHubProvider; exports.GoogleProvider = GoogleProvider; exports.OAuthProvider = OAuthProvider; exports.DiskStore = DiskStore; exports.JWKSVerifier = JWKSVerifier;
2829
- //# sourceMappingURL=chunk-QKDEME2F.cjs.map
3057
+ //# sourceMappingURL=chunk-BL2SWQWP.cjs.map