fastmcp 4.16.14 → 4.17.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.
@@ -72,9 +72,11 @@ 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
- registeredAt: storedDateSchema
78
+ registeredAt: storedDateSchema,
79
+ source: _zod.z.literal("cimd").optional()
78
80
  });
79
81
  var upstreamTokenSetStorageSchema = _zod.z.object({
80
82
  accessToken: _zod.z.string(),
@@ -172,10 +174,18 @@ var OAuthProxyStateStore = class {
172
174
  `${STORAGE_KEY_PREFIX.transaction}${transactionId}`
173
175
  );
174
176
  }
177
+ /**
178
+ * A client carrying `expiresAt` is a cached resolution rather than a
179
+ * permanent registration (CIMD), so a lapsed copy is dropped and reported as
180
+ * a miss — the caller re-resolves it from the source of truth.
181
+ */
175
182
  async getRegisteredClientByClientId(clientId) {
176
183
  const cached = this.registeredClientsByClientId.get(clientId);
177
184
  if (cached) {
178
- return cached;
185
+ if (!cached.expiresAt || !this.isExpired(cached.expiresAt)) {
186
+ return cached;
187
+ }
188
+ this.registeredClientsByClientId.delete(clientId);
179
189
  }
180
190
  const stored = await this.tokenStorage.get(
181
191
  `${STORAGE_KEY_PREFIX.client}${clientId}`
@@ -184,6 +194,10 @@ var OAuthProxyStateStore = class {
184
194
  if (!parsed.success) {
185
195
  return null;
186
196
  }
197
+ if (parsed.data.expiresAt && this.isExpired(parsed.data.expiresAt)) {
198
+ await this.tokenStorage.delete(`${STORAGE_KEY_PREFIX.client}${clientId}`);
199
+ return null;
200
+ }
187
201
  this.cacheRegisteredClient(parsed.data);
188
202
  return parsed.data;
189
203
  }
@@ -205,12 +219,6 @@ var OAuthProxyStateStore = class {
205
219
  }
206
220
  return parsed.data;
207
221
  }
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
222
  /**
215
223
  * Record that an authorization code has been redeemed, so a later attempt
216
224
  * gets "already used" rather than looking like an unknown code. Keyed and
@@ -233,7 +241,8 @@ var OAuthProxyStateStore = class {
233
241
  async saveRegisteredClient(client) {
234
242
  await this.tokenStorage.save(
235
243
  `${STORAGE_KEY_PREFIX.client}${client.clientId}`,
236
- client
244
+ client,
245
+ client.expiresAt ? this.getTtlSeconds(client.expiresAt) : void 0
237
246
  );
238
247
  }
239
248
  async saveTransaction(transaction) {
@@ -276,6 +285,173 @@ var DEFAULT_AUTHORIZATION_CODE_TTL = 300;
276
285
  var DEFAULT_TRANSACTION_TTL = 600;
277
286
  var DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS = 1e4;
278
287
 
288
+ // src/auth/utils/cimd.ts
289
+ var CIMD_FETCH_TIMEOUT_MS = 5e3;
290
+ var CIMD_CLIENT_TTL_MS = 15 * 60 * 1e3;
291
+ var CIMD_MAX_RESPONSE_BYTES = 65536;
292
+ async function resolveCimdClient(clientId, requestedRedirectUri, validateRedirectUri) {
293
+ let url;
294
+ try {
295
+ url = new URL(clientId);
296
+ } catch (e) {
297
+ return null;
298
+ }
299
+ if (url.protocol !== "https:" || url.search) {
300
+ return null;
301
+ }
302
+ if (isPrivateOrLoopbackHost(url.hostname)) {
303
+ return null;
304
+ }
305
+ let response;
306
+ try {
307
+ response = await fetch(clientId, {
308
+ redirect: "error",
309
+ signal: AbortSignal.timeout(CIMD_FETCH_TIMEOUT_MS)
310
+ });
311
+ } catch (e2) {
312
+ return null;
313
+ }
314
+ if (!response.ok) {
315
+ return null;
316
+ }
317
+ const text = await readBoundedText(response);
318
+ if (text === null) {
319
+ return null;
320
+ }
321
+ let parsed;
322
+ try {
323
+ parsed = JSON.parse(text);
324
+ } catch (e3) {
325
+ return null;
326
+ }
327
+ if (typeof parsed !== "object" || parsed === null) {
328
+ return null;
329
+ }
330
+ const doc = parsed;
331
+ if (doc.client_id !== clientId) {
332
+ return null;
333
+ }
334
+ if (!isStringArray(doc.redirect_uris) || doc.redirect_uris.length === 0) {
335
+ return null;
336
+ }
337
+ const redirectUris = doc.redirect_uris;
338
+ if (requestedRedirectUri && !redirectUris.includes(requestedRedirectUri)) {
339
+ return null;
340
+ }
341
+ if (!redirectUris.every((uri) => validateRedirectUri(uri))) {
342
+ return null;
343
+ }
344
+ const metadata = {
345
+ client_name: optionalString(doc.client_name),
346
+ client_uri: optionalString(doc.client_uri),
347
+ logo_uri: optionalString(doc.logo_uri),
348
+ policy_uri: optionalString(doc.policy_uri),
349
+ scope: optionalString(doc.scope),
350
+ tos_uri: optionalString(doc.tos_uri)
351
+ };
352
+ return {
353
+ callbackUrl: redirectUris[0],
354
+ clientId,
355
+ // CIMD is a public-client mechanism secured by PKCE; there is no secret.
356
+ clientSecret: void 0,
357
+ expiresAt: new Date(Date.now() + CIMD_CLIENT_TTL_MS),
358
+ metadata,
359
+ redirectUris,
360
+ registeredAt: /* @__PURE__ */ new Date(),
361
+ source: "cimd"
362
+ };
363
+ }
364
+ function expandIPv6(host) {
365
+ const sides = host.split("::");
366
+ if (sides.length > 2) {
367
+ return null;
368
+ }
369
+ const head = sides[0] ? sides[0].split(":") : [];
370
+ const tail = sides.length === 2 && sides[1] ? sides[1].split(":") : [];
371
+ if (sides.length === 1 && head.length !== 8) {
372
+ return null;
373
+ }
374
+ const missing = 8 - head.length - tail.length;
375
+ if (missing < 0) {
376
+ return null;
377
+ }
378
+ const parts = [...head, ...Array(missing).fill("0"), ...tail];
379
+ if (parts.length !== 8) {
380
+ return null;
381
+ }
382
+ const groups = parts.map((g) => Number.parseInt(g || "0", 16));
383
+ return groups.every((g) => !Number.isNaN(g)) ? groups : null;
384
+ }
385
+ function isPrivateOrLoopbackHost(hostname) {
386
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
387
+ if (host === "localhost" || host === "0.0.0.0") {
388
+ return true;
389
+ }
390
+ if (host.includes(":")) {
391
+ return isPrivateOrLoopbackIPv6(host);
392
+ }
393
+ 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);
394
+ }
395
+ function isPrivateOrLoopbackIPv6(host) {
396
+ const groups = expandIPv6(host);
397
+ if (!groups) {
398
+ return false;
399
+ }
400
+ if (groups.every((g) => g === 0)) {
401
+ return true;
402
+ }
403
+ if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1) {
404
+ return true;
405
+ }
406
+ const [first] = groups;
407
+ if ((first & 65472) === 65152) {
408
+ return true;
409
+ }
410
+ if ((first & 65024) === 64512) {
411
+ return true;
412
+ }
413
+ if (groups[0] === 0 && groups[1] === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && (groups[5] === 0 || groups[5] === 65535)) {
414
+ const ipv4 = `${groups[6] >> 8}.${groups[6] & 255}.${groups[7] >> 8}.${groups[7] & 255}`;
415
+ return isPrivateOrLoopbackHost(ipv4);
416
+ }
417
+ return false;
418
+ }
419
+ function isStringArray(value) {
420
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
421
+ }
422
+ function optionalString(value) {
423
+ return typeof value === "string" ? value : void 0;
424
+ }
425
+ async function readBoundedText(response) {
426
+ const contentLength = response.headers.get("content-length");
427
+ if (contentLength && Number(contentLength) > CIMD_MAX_RESPONSE_BYTES) {
428
+ return null;
429
+ }
430
+ const reader = _optionalChain([response, 'access', _ => _.body, 'optionalAccess', _2 => _2.getReader, 'call', _3 => _3()]);
431
+ if (!reader) {
432
+ return null;
433
+ }
434
+ const chunks = [];
435
+ let totalBytes = 0;
436
+ try {
437
+ for (; ; ) {
438
+ const { done, value } = await reader.read();
439
+ if (done) {
440
+ break;
441
+ }
442
+ totalBytes += value.byteLength;
443
+ if (totalBytes > CIMD_MAX_RESPONSE_BYTES) {
444
+ await reader.cancel();
445
+ return null;
446
+ }
447
+ chunks.push(value);
448
+ }
449
+ } catch (e4) {
450
+ return null;
451
+ }
452
+ return Buffer.concat(chunks).toString("utf-8");
453
+ }
454
+
279
455
  // src/auth/utils/claimsExtractor.ts
280
456
  var ClaimsExtractor = (_class = class {
281
457
 
@@ -413,7 +589,7 @@ var ClaimsExtractor = (_class = class {
413
589
  const stringified = JSON.stringify(value);
414
590
  const maxSize = _nullishCoalesce(this.config.maxClaimValueSize, () => ( 2e3));
415
591
  return stringified.length <= maxSize;
416
- } catch (e) {
592
+ } catch (e5) {
417
593
  return false;
418
594
  }
419
595
  }
@@ -694,7 +870,7 @@ var ConsentManager = class {
694
870
  return null;
695
871
  }
696
872
  return data;
697
- } catch (e2) {
873
+ } catch (e6) {
698
874
  return null;
699
875
  }
700
876
  }
@@ -1155,9 +1331,13 @@ var OAuthProxy = (_class4 = class {
1155
1331
  __init7() {this.ownedTokenStorage = null}
1156
1332
  /**
1157
1333
  * Keyed by proxy-issued client_id for authorize/token-exchange lookups and
1158
- * for the defence-in-depth callback checks. A registration never changes
1334
+ * for the defence-in-depth callback checks. A DCR registration never changes
1159
1335
  * after it is written, so caching it locally cannot go stale; it is also
1160
1336
  * persisted, so another instance can hydrate it.
1337
+ *
1338
+ * A CIMD client is not a registration but a snapshot of a document that can
1339
+ * change under us, so it is cached with an `expiresAt` and re-resolved once
1340
+ * that passes.
1161
1341
  */
1162
1342
  __init8() {this.registeredClientsByClientId = /* @__PURE__ */ new Map()}
1163
1343
 
@@ -1167,6 +1347,7 @@ var OAuthProxy = (_class4 = class {
1167
1347
  allowPlainPkce: true,
1168
1348
  authorizationCodeTtl: DEFAULT_AUTHORIZATION_CODE_TTL,
1169
1349
  consentRequired: true,
1350
+ enableCimd: false,
1170
1351
  enableTokenSwap: true,
1171
1352
  // Enabled by default for security
1172
1353
  redirectPath: "/oauth/callback",
@@ -1223,7 +1404,10 @@ var OAuthProxy = (_class4 = class {
1223
1404
  "Only 'code' response type is supported"
1224
1405
  );
1225
1406
  }
1226
- const registeredClient = await this.stateStore.getRegisteredClientByClientId(params.client_id);
1407
+ const registeredClient = await this.resolveClient(
1408
+ params.client_id,
1409
+ params.redirect_uri
1410
+ );
1227
1411
  if (!registeredClient) {
1228
1412
  throw new OAuthProxyError("invalid_client", "Unknown client_id");
1229
1413
  }
@@ -1245,6 +1429,20 @@ var OAuthProxy = (_class4 = class {
1245
1429
  "code_challenge_method 'plain' is not supported; use S256"
1246
1430
  );
1247
1431
  }
1432
+ if (registeredClient.source === "cimd") {
1433
+ if (!params.code_challenge) {
1434
+ throw new OAuthProxyError(
1435
+ "invalid_request",
1436
+ "code_challenge is required for Client ID Metadata Document clients"
1437
+ );
1438
+ }
1439
+ if (params.code_challenge_method !== "S256") {
1440
+ throw new OAuthProxyError(
1441
+ "invalid_request",
1442
+ "code_challenge_method must be S256 for Client ID Metadata Document clients"
1443
+ );
1444
+ }
1445
+ }
1248
1446
  const transaction = await this.createTransaction(params);
1249
1447
  if (this.config.consentRequired && !transaction.consentGiven) {
1250
1448
  return this.consentManager.createConsentResponse(
@@ -1275,7 +1473,7 @@ var OAuthProxy = (_class4 = class {
1275
1473
  "Only authorization_code grant type is supported"
1276
1474
  );
1277
1475
  }
1278
- const registeredClient = await this.stateStore.getRegisteredClientByClientId(request.client_id);
1476
+ const registeredClient = await this.resolveClient(request.client_id);
1279
1477
  if (!registeredClient) {
1280
1478
  throw new OAuthProxyError("invalid_client", "Unknown client_id");
1281
1479
  }
@@ -1297,6 +1495,12 @@ var OAuthProxy = (_class4 = class {
1297
1495
  if (clientCode.clientId !== request.client_id) {
1298
1496
  throw new OAuthProxyError("invalid_client", "Client ID mismatch");
1299
1497
  }
1498
+ if (registeredClient.source === "cimd" && !clientCode.codeChallenge) {
1499
+ throw new OAuthProxyError(
1500
+ "invalid_grant",
1501
+ "PKCE is required for Client ID Metadata Document clients"
1502
+ );
1503
+ }
1300
1504
  if (clientCode.codeChallenge) {
1301
1505
  if (!request.code_verifier) {
1302
1506
  throw new OAuthProxyError(
@@ -1363,6 +1567,7 @@ var OAuthProxy = (_class4 = class {
1363
1567
  getAuthorizationServerMetadata() {
1364
1568
  return {
1365
1569
  authorizationEndpoint: `${this.config.baseUrl}/oauth/authorize`,
1570
+ ...this.config.enableCimd ? { clientIdMetadataDocumentSupported: true } : {},
1366
1571
  codeChallengeMethodsSupported: this.config.allowPlainPkce ? ["S256", "plain"] : ["S256"],
1367
1572
  grantTypesSupported: ["authorization_code", "refresh_token"],
1368
1573
  issuer: this.config.baseUrl,
@@ -1398,7 +1603,7 @@ var OAuthProxy = (_class4 = class {
1398
1603
  if (!transaction) {
1399
1604
  throw new OAuthProxyError("invalid_request", "Invalid or expired state");
1400
1605
  }
1401
- if (!await this.stateStore.isTransactionCallbackRegistered(transaction)) {
1606
+ if (!await this.isTransactionCallbackRegistered(transaction)) {
1402
1607
  throw new OAuthProxyError(
1403
1608
  "invalid_request",
1404
1609
  "Transaction callback URL is not registered"
@@ -1444,7 +1649,7 @@ var OAuthProxy = (_class4 = class {
1444
1649
  }
1445
1650
  if (action === "deny") {
1446
1651
  await this.stateStore.deleteTransaction(transactionId);
1447
- if (!await this.stateStore.isTransactionCallbackRegistered(transaction)) {
1652
+ if (!await this.isTransactionCallbackRegistered(transaction)) {
1448
1653
  throw new OAuthProxyError(
1449
1654
  "invalid_request",
1450
1655
  "Transaction callback URL is not registered"
@@ -1641,7 +1846,7 @@ var OAuthProxy = (_class4 = class {
1641
1846
  const error = await tokenResponse.json();
1642
1847
  errorCode = error.error || "server_error";
1643
1848
  errorDescription = error.error_description;
1644
- } catch (e3) {
1849
+ } catch (e7) {
1645
1850
  errorDescription = `Upstream returned HTTP ${tokenResponse.status} ${tokenResponse.statusText}`;
1646
1851
  }
1647
1852
  throw new OAuthProxyError(errorCode, errorDescription);
@@ -1805,7 +2010,7 @@ var OAuthProxy = (_class4 = class {
1805
2010
  const error = await tokenResponse.json();
1806
2011
  errorCode = error.error || "invalid_grant";
1807
2012
  errorDescription = error.error_description;
1808
- } catch (e4) {
2013
+ } catch (e8) {
1809
2014
  errorDescription = `Upstream returned HTTP ${tokenResponse.status} ${tokenResponse.statusText}`;
1810
2015
  }
1811
2016
  throw new OAuthProxyError(errorCode, errorDescription);
@@ -2030,6 +2235,21 @@ var OAuthProxy = (_class4 = class {
2030
2235
  }
2031
2236
  return response;
2032
2237
  }
2238
+ /**
2239
+ * Defence in depth for the callback and consent-denial redirects: the
2240
+ * transaction's stored callback URL must still be registered for its client.
2241
+ *
2242
+ * Resolution goes through {@link resolveClient}, so a CIMD registration whose
2243
+ * cached copy lapsed mid-flow is re-fetched rather than mistaken for one that
2244
+ * was revoked.
2245
+ */
2246
+ async isTransactionCallbackRegistered(transaction) {
2247
+ const registeredClient = await this.resolveClient(
2248
+ transaction.clientId,
2249
+ transaction.clientCallbackUrl
2250
+ );
2251
+ return _nullishCoalesce(_optionalChain([registeredClient, 'optionalAccess', _17 => _17.redirectUris, 'access', _18 => _18.includes, 'call', _19 => _19(transaction.clientCallbackUrl)]), () => ( false));
2252
+ }
2033
2253
  /**
2034
2254
  * Match URI against pattern (supports wildcards)
2035
2255
  */
@@ -2140,7 +2360,7 @@ var OAuthProxy = (_class4 = class {
2140
2360
  const error = await tokenResponse.json();
2141
2361
  errorCode = error.error || "invalid_grant";
2142
2362
  errorDescription = error.error_description || "Upstream refresh failed";
2143
- } catch (e5) {
2363
+ } catch (e9) {
2144
2364
  errorDescription = `Upstream returned HTTP ${tokenResponse.status} ${tokenResponse.statusText}`;
2145
2365
  }
2146
2366
  throw new OAuthProxyError(errorCode, errorDescription);
@@ -2157,6 +2377,32 @@ var OAuthProxy = (_class4 = class {
2157
2377
  tokenType: tokens.token_type || "Bearer"
2158
2378
  };
2159
2379
  }
2380
+ /**
2381
+ * Resolve the client behind a `client_id`: a stored registration if there is
2382
+ * one, otherwise — when CIMD is enabled — the client's metadata document.
2383
+ *
2384
+ * A resolved CIMD client is cached like a DCR registration but carries the
2385
+ * `expiresAt` the resolver stamped on it, so the store drops it once it
2386
+ * lapses and the next call re-fetches the document. Every lookup goes
2387
+ * through here, so a lapse always means "re-resolve", never "unknown
2388
+ * client" mid-flow.
2389
+ */
2390
+ async resolveClient(clientId, requestedRedirectUri) {
2391
+ const registeredClient = await this.stateStore.getRegisteredClientByClientId(clientId);
2392
+ if (registeredClient || !this.config.enableCimd) {
2393
+ return registeredClient;
2394
+ }
2395
+ const resolved = await resolveCimdClient(
2396
+ clientId,
2397
+ requestedRedirectUri,
2398
+ (uri) => this.validateRedirectUri(uri)
2399
+ );
2400
+ if (resolved) {
2401
+ this.stateStore.cacheRegisteredClient(resolved);
2402
+ await this.stateStore.saveRegisteredClient(resolved);
2403
+ }
2404
+ return resolved;
2405
+ }
2160
2406
  /**
2161
2407
  * Put a claimed refresh-token mapping back after a rotation failed, keeping
2162
2408
  * whatever lifetime it had left.
@@ -2225,7 +2471,7 @@ var OAuthProxy = (_class4 = class {
2225
2471
  let parsed;
2226
2472
  try {
2227
2473
  parsed = new URL(uri);
2228
- } catch (e6) {
2474
+ } catch (e10) {
2229
2475
  return false;
2230
2476
  }
2231
2477
  if (parsed.username !== "" || parsed.password !== "") {
@@ -2290,7 +2536,7 @@ var AuthProvider = class {
2290
2536
  if (!request) {
2291
2537
  return void 0;
2292
2538
  }
2293
- const authHeader = _optionalChain([request, 'access', _17 => _17.headers, 'optionalAccess', _18 => _18.authorization]);
2539
+ const authHeader = _optionalChain([request, 'access', _20 => _20.headers, 'optionalAccess', _21 => _21.authorization]);
2294
2540
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
2295
2541
  return void 0;
2296
2542
  }
@@ -2526,7 +2772,7 @@ var DiskStore = (_class5 = class {
2526
2772
  console.warn(`Failed to read/parse file ${file}, deleting:`, error);
2527
2773
  try {
2528
2774
  await _promises.rm.call(void 0, _path.join.call(void 0, this.directory, file));
2529
- } catch (e7) {
2775
+ } catch (e11) {
2530
2776
  }
2531
2777
  }
2532
2778
  }
@@ -2606,7 +2852,7 @@ var DiskStore = (_class5 = class {
2606
2852
  await this.ensureDirectory();
2607
2853
  const files = await _promises.readdir.call(void 0, this.directory);
2608
2854
  return files.filter((f) => f.endsWith(this.fileExtension)).length;
2609
- } catch (e8) {
2855
+ } catch (e12) {
2610
2856
  return 0;
2611
2857
  }
2612
2858
  }
@@ -2830,4 +3076,4 @@ Original error: ${error.message}`
2830
3076
 
2831
3077
 
2832
3078
  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;
2833
- //# sourceMappingURL=chunk-YZWUEW3A.cjs.map
3079
+ //# sourceMappingURL=chunk-E3HXGE2O.cjs.map