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.
- package/dist/FastMCP.cjs +3 -3
- package/dist/FastMCP.d.cts +3 -2
- package/dist/FastMCP.d.ts +3 -2
- package/dist/FastMCP.js +2 -2
- package/dist/{OAuthProvider-Bvt5a7yr.d.cts → OAuthProvider-BWezdrAA.d.cts} +43 -1
- package/dist/{OAuthProvider-Bvt5a7yr.d.ts → OAuthProvider-BWezdrAA.d.ts} +43 -1
- package/dist/auth/index.cjs +2 -2
- package/dist/auth/index.d.cts +2 -2
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +1 -1
- package/dist/{chunk-QKDEME2F.cjs → chunk-BL2SWQWP.cjs} +252 -24
- package/dist/chunk-BL2SWQWP.cjs.map +1 -0
- package/dist/{chunk-QHGRXJLW.cjs → chunk-SYZRGVQX.cjs} +8 -1
- package/dist/chunk-SYZRGVQX.cjs.map +1 -0
- package/dist/{chunk-V5SKPW6E.js → chunk-UKKIJDLP.js} +243 -15
- package/dist/chunk-UKKIJDLP.js.map +1 -0
- package/dist/{chunk-CFPNOSVW.js → chunk-UODI5E6U.js} +8 -1
- package/dist/chunk-UODI5E6U.js.map +1 -0
- package/dist/examples/custom-routes.cjs +3 -3
- package/dist/examples/custom-routes.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-CFPNOSVW.js.map +0 -1
- package/dist/chunk-QHGRXJLW.cjs.map +0 -1
- package/dist/chunk-QKDEME2F.cjs.map +0 -1
- package/dist/chunk-V5SKPW6E.js.map +0 -1
|
@@ -72,6 +72,7 @@ var proxyDcrClientStorageSchema = z.object({
|
|
|
72
72
|
callbackUrl: z.string(),
|
|
73
73
|
clientId: z.string(),
|
|
74
74
|
clientSecret: z.string().optional(),
|
|
75
|
+
expiresAt: storedDateSchema.optional(),
|
|
75
76
|
metadata: dcrClientMetadataSchema.optional(),
|
|
76
77
|
redirectUris: z.array(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
|
-
|
|
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 registeredClient?.redirectUris.includes(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 {
|
|
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 {
|
|
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 {
|
|
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 = response.body?.getReader();
|
|
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 {
|
|
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 {
|
|
281
455
|
config;
|
|
@@ -1155,9 +1329,13 @@ var OAuthProxy = class {
|
|
|
1155
1329
|
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
|
registeredClientsByClientId = /* @__PURE__ */ new Map();
|
|
1163
1341
|
stateStore;
|
|
@@ -1167,6 +1345,7 @@ var OAuthProxy = 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 = class {
|
|
|
1223
1402
|
"Only 'code' response type is supported"
|
|
1224
1403
|
);
|
|
1225
1404
|
}
|
|
1226
|
-
const registeredClient = await this.
|
|
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 = class {
|
|
|
1275
1457
|
"Only authorization_code grant type is supported"
|
|
1276
1458
|
);
|
|
1277
1459
|
}
|
|
1278
|
-
const registeredClient = await this.
|
|
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 = 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 = class {
|
|
|
1398
1581
|
if (!transaction) {
|
|
1399
1582
|
throw new OAuthProxyError("invalid_request", "Invalid or expired state");
|
|
1400
1583
|
}
|
|
1401
|
-
if (!await this.
|
|
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 = class {
|
|
|
1444
1627
|
}
|
|
1445
1628
|
if (action === "deny") {
|
|
1446
1629
|
await this.stateStore.deleteTransaction(transactionId);
|
|
1447
|
-
if (!await this.
|
|
1630
|
+
if (!await this.isTransactionCallbackRegistered(transaction)) {
|
|
1448
1631
|
throw new OAuthProxyError(
|
|
1449
1632
|
"invalid_request",
|
|
1450
1633
|
"Transaction callback URL is not registered"
|
|
@@ -2030,6 +2213,21 @@ var OAuthProxy = 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 registeredClient?.redirectUris.includes(transaction.clientCallbackUrl) ?? false;
|
|
2230
|
+
}
|
|
2033
2231
|
/**
|
|
2034
2232
|
* Match URI against pattern (supports wildcards)
|
|
2035
2233
|
*/
|
|
@@ -2157,6 +2355,32 @@ var OAuthProxy = 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,11 +2446,15 @@ var OAuthProxy = 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);
|
|
2451
|
+
parsed = new URL(uri);
|
|
2227
2452
|
} catch {
|
|
2228
2453
|
return false;
|
|
2229
2454
|
}
|
|
2455
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2230
2458
|
const patterns = this.config.allowedRedirectUriPatterns;
|
|
2231
2459
|
if (Array.isArray(patterns) && patterns.length === 0) {
|
|
2232
2460
|
return false;
|
|
@@ -2826,4 +3054,4 @@ export {
|
|
|
2826
3054
|
DiskStore,
|
|
2827
3055
|
JWKSVerifier
|
|
2828
3056
|
};
|
|
2829
|
-
//# sourceMappingURL=chunk-
|
|
3057
|
+
//# sourceMappingURL=chunk-UKKIJDLP.js.map
|