fastmcp 3.25.4 → 3.26.3

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,1875 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6;// src/auth/OAuthProxy.ts
2
+ var _crypto = require('crypto');
3
+ var _zod = require('zod');
4
+
5
+ // src/auth/utils/claimsExtractor.ts
6
+ var ClaimsExtractor = (_class = class {
7
+
8
+ // Claims that MUST NOT be copied from upstream (protect proxy's JWT integrity)
9
+ __init() {this.PROTECTED_CLAIMS = /* @__PURE__ */ new Set([
10
+ "aud",
11
+ "client_id",
12
+ "exp",
13
+ "iat",
14
+ "iss",
15
+ "jti",
16
+ "nbf"
17
+ ])}
18
+ constructor(config) {;_class.prototype.__init.call(this);
19
+ if (typeof config === "boolean") {
20
+ config = config ? {} : { fromAccessToken: false, fromIdToken: false };
21
+ }
22
+ this.config = {
23
+ allowComplexClaims: config.allowComplexClaims || false,
24
+ allowedClaims: config.allowedClaims,
25
+ blockedClaims: config.blockedClaims || [],
26
+ claimPrefix: config.claimPrefix !== void 0 ? config.claimPrefix : false,
27
+ // Default: no prefix
28
+ fromAccessToken: config.fromAccessToken !== false,
29
+ // Default: true
30
+ fromIdToken: config.fromIdToken !== false,
31
+ // Default: true
32
+ maxClaimValueSize: config.maxClaimValueSize || 2e3
33
+ };
34
+ }
35
+ /**
36
+ * Extract claims from a token (access token or ID token)
37
+ */
38
+ async extract(token, tokenType) {
39
+ if (tokenType === "access" && !this.config.fromAccessToken) {
40
+ return null;
41
+ }
42
+ if (tokenType === "id" && !this.config.fromIdToken) {
43
+ return null;
44
+ }
45
+ if (!this.isJWT(token)) {
46
+ return null;
47
+ }
48
+ const payload = this.decodeJWTPayload(token);
49
+ if (!payload) {
50
+ return null;
51
+ }
52
+ const filtered = this.filterClaims(payload);
53
+ return this.applyPrefix(filtered);
54
+ }
55
+ /**
56
+ * Apply prefix to claim names (if configured)
57
+ */
58
+ applyPrefix(claims) {
59
+ const prefix = this.config.claimPrefix;
60
+ if (prefix === false || prefix === "" || prefix === void 0) {
61
+ return claims;
62
+ }
63
+ const result = {};
64
+ for (const [key, value] of Object.entries(claims)) {
65
+ result[`${prefix}${key}`] = value;
66
+ }
67
+ return result;
68
+ }
69
+ /**
70
+ * Decode JWT payload without signature verification
71
+ * Safe because token came from trusted upstream via server-to-server exchange
72
+ */
73
+ decodeJWTPayload(token) {
74
+ try {
75
+ const parts = token.split(".");
76
+ if (parts.length !== 3) {
77
+ return null;
78
+ }
79
+ const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
80
+ return JSON.parse(payload);
81
+ } catch (error) {
82
+ console.warn(`Failed to decode JWT payload: ${error}`);
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Filter claims based on security rules
88
+ */
89
+ filterClaims(claims) {
90
+ const result = {};
91
+ for (const [key, value] of Object.entries(claims)) {
92
+ if (this.PROTECTED_CLAIMS.has(key)) {
93
+ continue;
94
+ }
95
+ if (_optionalChain([this, 'access', _ => _.config, 'access', _2 => _2.blockedClaims, 'optionalAccess', _3 => _3.includes, 'call', _4 => _4(key)])) {
96
+ continue;
97
+ }
98
+ if (this.config.allowedClaims && !this.config.allowedClaims.includes(key)) {
99
+ continue;
100
+ }
101
+ if (!this.isValidClaimValue(value)) {
102
+ console.warn(`Skipping claim '${key}' due to invalid value`);
103
+ continue;
104
+ }
105
+ result[key] = value;
106
+ }
107
+ return result;
108
+ }
109
+ /**
110
+ * Check if a token is in JWT format
111
+ */
112
+ isJWT(token) {
113
+ return token.split(".").length === 3;
114
+ }
115
+ /**
116
+ * Validate a claim value (type and size checks)
117
+ */
118
+ isValidClaimValue(value) {
119
+ if (value === null || value === void 0) {
120
+ return false;
121
+ }
122
+ const type = typeof value;
123
+ if (type === "string") {
124
+ const maxSize = _nullishCoalesce(this.config.maxClaimValueSize, () => ( 2e3));
125
+ return value.length <= maxSize;
126
+ }
127
+ if (type === "number" || type === "boolean") {
128
+ return true;
129
+ }
130
+ if (Array.isArray(value) || type === "object") {
131
+ if (!this.config.allowComplexClaims) {
132
+ return false;
133
+ }
134
+ try {
135
+ const stringified = JSON.stringify(value);
136
+ const maxSize = _nullishCoalesce(this.config.maxClaimValueSize, () => ( 2e3));
137
+ return stringified.length <= maxSize;
138
+ } catch (e) {
139
+ return false;
140
+ }
141
+ }
142
+ return false;
143
+ }
144
+ }, _class);
145
+
146
+ // src/auth/utils/consent.ts
147
+
148
+ var ConsentManager = class {
149
+
150
+ constructor(signingKey) {
151
+ this.signingKey = signingKey || this.generateDefaultKey();
152
+ }
153
+ /**
154
+ * Create HTTP response with consent screen
155
+ */
156
+ createConsentResponse(transaction, provider) {
157
+ const consentData = {
158
+ clientName: "MCP Client",
159
+ provider,
160
+ scope: transaction.scope,
161
+ timestamp: Date.now(),
162
+ transactionId: transaction.id
163
+ };
164
+ const html = this.generateConsentScreen(consentData);
165
+ return new Response(html, {
166
+ headers: {
167
+ "Content-Type": "text/html; charset=utf-8"
168
+ },
169
+ status: 200
170
+ });
171
+ }
172
+ /**
173
+ * Generate HTML for consent screen
174
+ */
175
+ generateConsentScreen(data) {
176
+ const { clientName, provider, scope, transactionId } = data;
177
+ return `
178
+ <!DOCTYPE html>
179
+ <html lang="en">
180
+ <head>
181
+ <meta charset="UTF-8">
182
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
183
+ <title>Authorization Request</title>
184
+ <style>
185
+ * {
186
+ margin: 0;
187
+ padding: 0;
188
+ box-sizing: border-box;
189
+ }
190
+
191
+ body {
192
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
193
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
194
+ min-height: 100vh;
195
+ display: flex;
196
+ justify-content: center;
197
+ align-items: center;
198
+ padding: 20px;
199
+ }
200
+
201
+ .consent-container {
202
+ background: white;
203
+ border-radius: 12px;
204
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
205
+ max-width: 480px;
206
+ width: 100%;
207
+ padding: 40px;
208
+ }
209
+
210
+ .header {
211
+ text-align: center;
212
+ margin-bottom: 30px;
213
+ }
214
+
215
+ .header h1 {
216
+ color: #1a202c;
217
+ font-size: 24px;
218
+ margin-bottom: 8px;
219
+ }
220
+
221
+ .header p {
222
+ color: #718096;
223
+ font-size: 14px;
224
+ }
225
+
226
+ .app-info {
227
+ background: #f7fafc;
228
+ border-radius: 8px;
229
+ padding: 20px;
230
+ margin-bottom: 24px;
231
+ }
232
+
233
+ .app-info h2 {
234
+ color: #2d3748;
235
+ font-size: 18px;
236
+ margin-bottom: 12px;
237
+ }
238
+
239
+ .app-name {
240
+ color: #667eea;
241
+ font-weight: 600;
242
+ }
243
+
244
+ .permissions {
245
+ margin-top: 16px;
246
+ }
247
+
248
+ .permissions h3 {
249
+ color: #4a5568;
250
+ font-size: 14px;
251
+ margin-bottom: 8px;
252
+ font-weight: 600;
253
+ }
254
+
255
+ .permissions ul {
256
+ list-style: none;
257
+ }
258
+
259
+ .permissions li {
260
+ color: #718096;
261
+ font-size: 14px;
262
+ padding: 6px 0;
263
+ padding-left: 24px;
264
+ position: relative;
265
+ }
266
+
267
+ .permissions li:before {
268
+ content: "\u2713";
269
+ position: absolute;
270
+ left: 0;
271
+ color: #48bb78;
272
+ font-weight: bold;
273
+ }
274
+
275
+ .warning {
276
+ background: #fffaf0;
277
+ border-left: 4px solid #ed8936;
278
+ padding: 12px 16px;
279
+ margin-bottom: 24px;
280
+ border-radius: 4px;
281
+ }
282
+
283
+ .warning p {
284
+ color: #744210;
285
+ font-size: 13px;
286
+ line-height: 1.5;
287
+ }
288
+
289
+ .actions {
290
+ display: flex;
291
+ gap: 12px;
292
+ }
293
+
294
+ button {
295
+ flex: 1;
296
+ padding: 14px 24px;
297
+ border: none;
298
+ border-radius: 6px;
299
+ font-size: 16px;
300
+ font-weight: 600;
301
+ cursor: pointer;
302
+ transition: all 0.2s;
303
+ }
304
+
305
+ .approve {
306
+ background: #667eea;
307
+ color: white;
308
+ }
309
+
310
+ .approve:hover {
311
+ background: #5a67d8;
312
+ transform: translateY(-1px);
313
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
314
+ }
315
+
316
+ .deny {
317
+ background: #e2e8f0;
318
+ color: #4a5568;
319
+ }
320
+
321
+ .deny:hover {
322
+ background: #cbd5e0;
323
+ }
324
+
325
+ .footer {
326
+ margin-top: 24px;
327
+ text-align: center;
328
+ color: #a0aec0;
329
+ font-size: 12px;
330
+ }
331
+ </style>
332
+ </head>
333
+ <body>
334
+ <div class="consent-container">
335
+ <div class="header">
336
+ <h1>\u{1F510} Authorization Request</h1>
337
+ <p>via ${this.escapeHtml(provider)}</p>
338
+ </div>
339
+
340
+ <div class="app-info">
341
+ <h2>
342
+ <span class="app-name">${this.escapeHtml(clientName || "An application")}</span>
343
+ requests access
344
+ </h2>
345
+
346
+ <div class="permissions">
347
+ <h3>This will allow the app to:</h3>
348
+ <ul>
349
+ ${scope.map((s) => `<li>${this.escapeHtml(this.formatScope(s))}</li>`).join("")}
350
+ </ul>
351
+ </div>
352
+ </div>
353
+
354
+ <div class="warning">
355
+ <p>
356
+ <strong>\u26A0\uFE0F Important:</strong> Only approve if you trust this application.
357
+ By approving, you authorize it to access your account information.
358
+ </p>
359
+ </div>
360
+
361
+ <form method="POST" action="/oauth/consent">
362
+ <input type="hidden" name="transaction_id" value="${this.escapeHtml(transactionId)}">
363
+ <div class="actions">
364
+ <button type="submit" name="action" value="deny" class="deny">
365
+ Deny
366
+ </button>
367
+ <button type="submit" name="action" value="approve" class="approve">
368
+ Approve
369
+ </button>
370
+ </div>
371
+ </form>
372
+
373
+ <div class="footer">
374
+ <p>This consent is required to prevent unauthorized access.</p>
375
+ </div>
376
+ </div>
377
+ </body>
378
+ </html>
379
+ `.trim();
380
+ }
381
+ /**
382
+ * Sign consent data for cookie
383
+ */
384
+ signConsentCookie(data) {
385
+ const payload = JSON.stringify(data);
386
+ const signature = this.sign(payload);
387
+ return `${Buffer.from(payload).toString("base64")}.${signature}`;
388
+ }
389
+ /**
390
+ * Validate and parse consent cookie
391
+ */
392
+ validateConsentCookie(cookie) {
393
+ try {
394
+ const [payloadB64, signature] = cookie.split(".");
395
+ if (!payloadB64 || !signature) {
396
+ return null;
397
+ }
398
+ const payload = Buffer.from(payloadB64, "base64").toString("utf8");
399
+ const expectedSignature = this.sign(payload);
400
+ if (signature !== expectedSignature) {
401
+ return null;
402
+ }
403
+ const data = JSON.parse(payload);
404
+ const age = Date.now() - data.timestamp;
405
+ if (age > 5 * 60 * 1e3) {
406
+ return null;
407
+ }
408
+ return data;
409
+ } catch (e2) {
410
+ return null;
411
+ }
412
+ }
413
+ /**
414
+ * Escape HTML to prevent XSS
415
+ */
416
+ escapeHtml(text) {
417
+ const map = {
418
+ "'": "&#x27;",
419
+ '"': "&quot;",
420
+ "/": "&#x2F;",
421
+ "&": "&amp;",
422
+ "<": "&lt;",
423
+ ">": "&gt;"
424
+ };
425
+ return text.replace(/[&<>"'/]/g, (char) => map[char] || char);
426
+ }
427
+ /**
428
+ * Format scope for display
429
+ */
430
+ formatScope(scope) {
431
+ const scopeMap = {
432
+ email: "Access your email address",
433
+ openid: "Verify your identity",
434
+ profile: "View your basic profile information",
435
+ "read:user": "Read your user information",
436
+ "write:user": "Modify your user information"
437
+ };
438
+ return scopeMap[scope] || scope.replace(/_/g, " ").replace(/:/g, " - ");
439
+ }
440
+ /**
441
+ * Generate default signing key if none provided
442
+ */
443
+ generateDefaultKey() {
444
+ return `fastmcp-consent-${Date.now()}-${Math.random()}`;
445
+ }
446
+ /**
447
+ * Sign a payload using HMAC-SHA256
448
+ */
449
+ sign(payload) {
450
+ return _crypto.createHmac.call(void 0, "sha256", this.signingKey).update(payload).digest("hex");
451
+ }
452
+ };
453
+
454
+ // src/auth/utils/jwtIssuer.ts
455
+
456
+ var _util = require('util');
457
+ var pbkdf2Async = _util.promisify.call(void 0, _crypto.pbkdf2);
458
+ var JWTIssuer = class {
459
+
460
+
461
+
462
+
463
+
464
+ constructor(config) {
465
+ this.issuer = config.issuer;
466
+ this.audience = config.audience;
467
+ this.accessTokenTtl = config.accessTokenTtl || 3600;
468
+ this.refreshTokenTtl = config.refreshTokenTtl || 2592e3;
469
+ this.signingKey = Buffer.from(config.signingKey);
470
+ }
471
+ /**
472
+ * Derive a signing key from a secret
473
+ * Uses PBKDF2 for key derivation
474
+ */
475
+ static async deriveKey(secret, iterations = 1e5) {
476
+ const salt = Buffer.from("fastmcp-oauth-proxy");
477
+ const key = await pbkdf2Async(secret, salt, iterations, 32, "sha256");
478
+ return key.toString("base64");
479
+ }
480
+ /**
481
+ * Issue an access token
482
+ */
483
+ issueAccessToken(clientId, scope, additionalClaims) {
484
+ const now = Math.floor(Date.now() / 1e3);
485
+ const jti = this.generateJti();
486
+ const claims = {
487
+ aud: this.audience,
488
+ client_id: clientId,
489
+ exp: now + this.accessTokenTtl,
490
+ iat: now,
491
+ iss: this.issuer,
492
+ jti,
493
+ scope,
494
+ // Merge additional claims (custom claims from upstream)
495
+ ...additionalClaims || {}
496
+ };
497
+ return this.signToken(claims);
498
+ }
499
+ /**
500
+ * Issue a refresh token
501
+ */
502
+ issueRefreshToken(clientId, scope, additionalClaims) {
503
+ const now = Math.floor(Date.now() / 1e3);
504
+ const jti = this.generateJti();
505
+ const claims = {
506
+ aud: this.audience,
507
+ client_id: clientId,
508
+ exp: now + this.refreshTokenTtl,
509
+ iat: now,
510
+ iss: this.issuer,
511
+ jti,
512
+ scope,
513
+ // Merge additional claims (custom claims from upstream)
514
+ ...additionalClaims || {}
515
+ };
516
+ return this.signToken(claims);
517
+ }
518
+ /**
519
+ * Validate a JWT token
520
+ */
521
+ async verify(token) {
522
+ try {
523
+ const parts = token.split(".");
524
+ if (parts.length !== 3) {
525
+ return {
526
+ error: "Invalid token format",
527
+ valid: false
528
+ };
529
+ }
530
+ const [headerB64, payloadB64, signatureB64] = parts;
531
+ const expectedSignature = this.sign(`${headerB64}.${payloadB64}`);
532
+ if (signatureB64 !== expectedSignature) {
533
+ return {
534
+ error: "Invalid signature",
535
+ valid: false
536
+ };
537
+ }
538
+ const claims = JSON.parse(
539
+ Buffer.from(payloadB64, "base64url").toString("utf-8")
540
+ );
541
+ const now = Math.floor(Date.now() / 1e3);
542
+ if (claims.exp <= now) {
543
+ return {
544
+ claims,
545
+ error: "Token expired",
546
+ valid: false
547
+ };
548
+ }
549
+ if (claims.iss !== this.issuer) {
550
+ return {
551
+ claims,
552
+ error: "Invalid issuer",
553
+ valid: false
554
+ };
555
+ }
556
+ if (claims.aud !== this.audience) {
557
+ return {
558
+ claims,
559
+ error: "Invalid audience",
560
+ valid: false
561
+ };
562
+ }
563
+ return {
564
+ claims,
565
+ valid: true
566
+ };
567
+ } catch (error) {
568
+ return {
569
+ error: error instanceof Error ? error.message : "Validation failed",
570
+ valid: false
571
+ };
572
+ }
573
+ }
574
+ /**
575
+ * Generate unique JWT ID
576
+ */
577
+ generateJti() {
578
+ return _crypto.randomBytes.call(void 0, 16).toString("base64url");
579
+ }
580
+ /**
581
+ * Sign data with HMAC-SHA256
582
+ */
583
+ sign(data) {
584
+ const hmac = _crypto.createHmac.call(void 0, "sha256", this.signingKey);
585
+ hmac.update(data);
586
+ return hmac.digest("base64url");
587
+ }
588
+ /**
589
+ * Sign a JWT token
590
+ */
591
+ signToken(claims) {
592
+ const header = {
593
+ alg: "HS256",
594
+ typ: "JWT"
595
+ };
596
+ const headerB64 = Buffer.from(JSON.stringify(header)).toString("base64url");
597
+ const payloadB64 = Buffer.from(JSON.stringify(claims)).toString(
598
+ "base64url"
599
+ );
600
+ const signature = this.sign(`${headerB64}.${payloadB64}`);
601
+ return `${headerB64}.${payloadB64}.${signature}`;
602
+ }
603
+ };
604
+
605
+ // src/auth/utils/pkce.ts
606
+
607
+ var PKCEUtils = class _PKCEUtils {
608
+ /**
609
+ * Generate a code challenge from a verifier
610
+ * @param verifier The code verifier
611
+ * @param method Challenge method: 'S256' or 'plain' (default: 'S256')
612
+ * @returns Base64URL-encoded challenge string
613
+ */
614
+ static generateChallenge(verifier, method = "S256") {
615
+ if (method === "plain") {
616
+ return verifier;
617
+ }
618
+ if (method === "S256") {
619
+ const hash = _crypto.createHash.call(void 0, "sha256");
620
+ hash.update(verifier);
621
+ return _PKCEUtils.base64URLEncode(hash.digest());
622
+ }
623
+ throw new Error(`Unsupported challenge method: ${method}`);
624
+ }
625
+ /**
626
+ * Generate a complete PKCE pair (verifier + challenge)
627
+ * @param method Challenge method: 'S256' or 'plain' (default: 'S256')
628
+ * @returns Object containing verifier and challenge
629
+ */
630
+ static generatePair(method = "S256") {
631
+ const verifier = _PKCEUtils.generateVerifier();
632
+ const challenge = _PKCEUtils.generateChallenge(verifier, method);
633
+ return {
634
+ challenge,
635
+ verifier
636
+ };
637
+ }
638
+ /**
639
+ * Generate a cryptographically secure code verifier
640
+ * @param length Length of verifier (43-128 characters, default: 128)
641
+ * @returns Base64URL-encoded verifier string
642
+ */
643
+ static generateVerifier(length = 128) {
644
+ if (length < 43 || length > 128) {
645
+ throw new Error("PKCE verifier length must be between 43 and 128");
646
+ }
647
+ const byteLength = Math.ceil(length * 3 / 4);
648
+ const randomBytesBuffer = _crypto.randomBytes.call(void 0, byteLength);
649
+ return _PKCEUtils.base64URLEncode(randomBytesBuffer).slice(0, length);
650
+ }
651
+ /**
652
+ * Validate a code verifier against a challenge
653
+ * @param verifier The code verifier to validate
654
+ * @param challenge The expected challenge
655
+ * @param method The challenge method used
656
+ * @returns True if verifier matches challenge
657
+ */
658
+ static validateChallenge(verifier, challenge, method) {
659
+ if (!verifier || !challenge) {
660
+ return false;
661
+ }
662
+ if (method === "plain") {
663
+ return verifier === challenge;
664
+ }
665
+ if (method === "S256") {
666
+ const computedChallenge = _PKCEUtils.generateChallenge(verifier, "S256");
667
+ return computedChallenge === challenge;
668
+ }
669
+ return false;
670
+ }
671
+ /**
672
+ * Encode a buffer as base64url (RFC 4648)
673
+ * @param buffer Buffer to encode
674
+ * @returns Base64URL-encoded string
675
+ */
676
+ static base64URLEncode(buffer) {
677
+ return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
678
+ }
679
+ };
680
+
681
+ // src/auth/utils/tokenStore.ts
682
+
683
+
684
+
685
+
686
+
687
+
688
+ var EncryptedTokenStorage = (_class2 = class {
689
+ __init2() {this.algorithm = "aes-256-gcm"}
690
+
691
+
692
+ constructor(backend, encryptionKey) {;_class2.prototype.__init2.call(this);
693
+ this.backend = backend;
694
+ const salt = Buffer.from("fastmcp-oauth-proxy-salt");
695
+ this.encryptionKey = _crypto.scryptSync.call(void 0, encryptionKey, salt, 32);
696
+ }
697
+ async cleanup() {
698
+ await this.backend.cleanup();
699
+ }
700
+ async delete(key) {
701
+ await this.backend.delete(key);
702
+ }
703
+ async get(key) {
704
+ const encrypted = await this.backend.get(key);
705
+ if (!encrypted) {
706
+ return null;
707
+ }
708
+ try {
709
+ const decrypted = await this.decrypt(
710
+ encrypted,
711
+ this.encryptionKey
712
+ );
713
+ return JSON.parse(decrypted);
714
+ } catch (error) {
715
+ console.error("Failed to decrypt value:", error);
716
+ return null;
717
+ }
718
+ }
719
+ async save(key, value, ttl) {
720
+ const encrypted = await this.encrypt(
721
+ JSON.stringify(value),
722
+ this.encryptionKey
723
+ );
724
+ await this.backend.save(key, encrypted, ttl);
725
+ }
726
+ async decrypt(ciphertext, key) {
727
+ const parts = ciphertext.split(":");
728
+ if (parts.length !== 3) {
729
+ throw new Error("Invalid encrypted data format");
730
+ }
731
+ const [ivHex, authTagHex, encrypted] = parts;
732
+ const iv = Buffer.from(ivHex, "hex");
733
+ const authTag = Buffer.from(authTagHex, "hex");
734
+ const decipher = _crypto.createDecipheriv.call(void 0, this.algorithm, key, iv);
735
+ decipher.setAuthTag(
736
+ authTag
737
+ );
738
+ let decrypted = decipher.update(encrypted, "hex", "utf8");
739
+ decrypted += decipher.final("utf8");
740
+ return decrypted;
741
+ }
742
+ async encrypt(plaintext, key) {
743
+ const iv = _crypto.randomBytes.call(void 0, 16);
744
+ const cipher = _crypto.createCipheriv.call(void 0, this.algorithm, key, iv);
745
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
746
+ encrypted += cipher.final("hex");
747
+ const authTag = cipher.getAuthTag();
748
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
749
+ }
750
+ }, _class2);
751
+ var MemoryTokenStorage = (_class3 = class {
752
+ __init3() {this.cleanupInterval = null}
753
+ __init4() {this.store = /* @__PURE__ */ new Map()}
754
+ constructor(cleanupIntervalMs = 6e4) {;_class3.prototype.__init3.call(this);_class3.prototype.__init4.call(this);
755
+ this.cleanupInterval = setInterval(
756
+ () => void this.cleanup(),
757
+ cleanupIntervalMs
758
+ );
759
+ }
760
+ async cleanup() {
761
+ const now = Date.now();
762
+ const keysToDelete = [];
763
+ for (const [key, entry] of this.store.entries()) {
764
+ if (entry.expiresAt < now) {
765
+ keysToDelete.push(key);
766
+ }
767
+ }
768
+ for (const key of keysToDelete) {
769
+ this.store.delete(key);
770
+ }
771
+ }
772
+ async delete(key) {
773
+ this.store.delete(key);
774
+ }
775
+ /**
776
+ * Destroy the storage and clear cleanup interval
777
+ */
778
+ destroy() {
779
+ if (this.cleanupInterval) {
780
+ clearInterval(this.cleanupInterval);
781
+ this.cleanupInterval = null;
782
+ }
783
+ this.store.clear();
784
+ }
785
+ async get(key) {
786
+ const entry = this.store.get(key);
787
+ if (!entry) {
788
+ return null;
789
+ }
790
+ if (entry.expiresAt < Date.now()) {
791
+ this.store.delete(key);
792
+ return null;
793
+ }
794
+ return entry.value;
795
+ }
796
+ async save(key, value, ttl) {
797
+ const expiresAt = ttl ? Date.now() + ttl * 1e3 : Number.MAX_SAFE_INTEGER;
798
+ this.store.set(key, {
799
+ expiresAt,
800
+ value
801
+ });
802
+ }
803
+ /**
804
+ * Get the number of stored items
805
+ */
806
+ size() {
807
+ return this.store.size;
808
+ }
809
+ }, _class3);
810
+
811
+ // src/auth/OAuthProxy.ts
812
+ var OAuthProxy = (_class4 = class {
813
+ __init5() {this.claimsExtractor = null}
814
+ __init6() {this.cleanupInterval = null}
815
+ __init7() {this.clientCodes = /* @__PURE__ */ new Map()}
816
+
817
+
818
+
819
+ __init8() {this.registeredClients = /* @__PURE__ */ new Map()}
820
+
821
+ __init9() {this.transactions = /* @__PURE__ */ new Map()}
822
+ constructor(config) {;_class4.prototype.__init5.call(this);_class4.prototype.__init6.call(this);_class4.prototype.__init7.call(this);_class4.prototype.__init8.call(this);_class4.prototype.__init9.call(this);
823
+ this.config = {
824
+ allowedRedirectUriPatterns: ["https://*", "http://localhost:*"],
825
+ authorizationCodeTtl: 300,
826
+ // 5 minutes
827
+ consentRequired: true,
828
+ enableTokenSwap: true,
829
+ // Enabled by default for security
830
+ redirectPath: "/oauth/callback",
831
+ transactionTtl: 600,
832
+ // 10 minutes
833
+ ...config
834
+ };
835
+ let storage = config.tokenStorage || new MemoryTokenStorage();
836
+ const isAlreadyEncrypted = storage.constructor.name === "EncryptedTokenStorage";
837
+ if (!isAlreadyEncrypted && config.encryptionKey !== false) {
838
+ const encryptionKey = typeof config.encryptionKey === "string" ? config.encryptionKey : this.generateSigningKey();
839
+ storage = new EncryptedTokenStorage(storage, encryptionKey);
840
+ }
841
+ this.tokenStorage = storage;
842
+ this.consentManager = new ConsentManager(
843
+ config.consentSigningKey || this.generateSigningKey()
844
+ );
845
+ if (this.config.enableTokenSwap) {
846
+ const signingKey = this.config.jwtSigningKey || this.generateSigningKey();
847
+ this.jwtIssuer = new JWTIssuer({
848
+ audience: this.config.baseUrl,
849
+ issuer: this.config.baseUrl,
850
+ signingKey
851
+ });
852
+ }
853
+ const claimsConfig = config.customClaimsPassthrough !== void 0 ? config.customClaimsPassthrough : true;
854
+ if (claimsConfig !== false) {
855
+ this.claimsExtractor = new ClaimsExtractor(claimsConfig);
856
+ }
857
+ this.startCleanup();
858
+ }
859
+ /**
860
+ * OAuth authorization endpoint
861
+ */
862
+ async authorize(params) {
863
+ if (!params.client_id || !params.redirect_uri || !params.response_type) {
864
+ throw new OAuthProxyError(
865
+ "invalid_request",
866
+ "Missing required parameters"
867
+ );
868
+ }
869
+ if (params.response_type !== "code") {
870
+ throw new OAuthProxyError(
871
+ "unsupported_response_type",
872
+ "Only 'code' response type is supported"
873
+ );
874
+ }
875
+ if (params.code_challenge && !params.code_challenge_method) {
876
+ throw new OAuthProxyError(
877
+ "invalid_request",
878
+ "code_challenge_method required when code_challenge is present"
879
+ );
880
+ }
881
+ const transaction = await this.createTransaction(params);
882
+ if (this.config.consentRequired && !transaction.consentGiven) {
883
+ return this.consentManager.createConsentResponse(
884
+ transaction,
885
+ this.getProviderName()
886
+ );
887
+ }
888
+ return this.redirectToUpstream(transaction);
889
+ }
890
+ /**
891
+ * Stop cleanup interval and destroy resources
892
+ */
893
+ destroy() {
894
+ if (this.cleanupInterval) {
895
+ clearInterval(this.cleanupInterval);
896
+ this.cleanupInterval = null;
897
+ }
898
+ this.transactions.clear();
899
+ this.clientCodes.clear();
900
+ this.registeredClients.clear();
901
+ }
902
+ /**
903
+ * Token endpoint - exchange authorization code for tokens
904
+ */
905
+ async exchangeAuthorizationCode(request) {
906
+ if (request.grant_type !== "authorization_code") {
907
+ throw new OAuthProxyError(
908
+ "unsupported_grant_type",
909
+ "Only authorization_code grant type is supported"
910
+ );
911
+ }
912
+ const clientCode = this.clientCodes.get(request.code);
913
+ if (!clientCode) {
914
+ throw new OAuthProxyError(
915
+ "invalid_grant",
916
+ "Invalid or expired authorization code"
917
+ );
918
+ }
919
+ if (clientCode.clientId !== request.client_id) {
920
+ throw new OAuthProxyError("invalid_client", "Client ID mismatch");
921
+ }
922
+ if (clientCode.codeChallenge) {
923
+ if (!request.code_verifier) {
924
+ throw new OAuthProxyError(
925
+ "invalid_request",
926
+ "code_verifier required for PKCE"
927
+ );
928
+ }
929
+ const valid = PKCEUtils.validateChallenge(
930
+ request.code_verifier,
931
+ clientCode.codeChallenge,
932
+ clientCode.codeChallengeMethod
933
+ );
934
+ if (!valid) {
935
+ throw new OAuthProxyError("invalid_grant", "Invalid PKCE verifier");
936
+ }
937
+ }
938
+ if (clientCode.used) {
939
+ throw new OAuthProxyError(
940
+ "invalid_grant",
941
+ "Authorization code already used"
942
+ );
943
+ }
944
+ clientCode.used = true;
945
+ this.clientCodes.set(request.code, clientCode);
946
+ if (this.config.enableTokenSwap && this.jwtIssuer) {
947
+ return await this.issueSwappedTokens(
948
+ clientCode.clientId,
949
+ clientCode.upstreamTokens
950
+ );
951
+ } else {
952
+ const response = {
953
+ access_token: clientCode.upstreamTokens.accessToken,
954
+ expires_in: clientCode.upstreamTokens.expiresIn,
955
+ token_type: clientCode.upstreamTokens.tokenType
956
+ };
957
+ if (clientCode.upstreamTokens.refreshToken) {
958
+ response.refresh_token = clientCode.upstreamTokens.refreshToken;
959
+ }
960
+ if (clientCode.upstreamTokens.idToken) {
961
+ response.id_token = clientCode.upstreamTokens.idToken;
962
+ }
963
+ if (clientCode.upstreamTokens.scope.length > 0) {
964
+ response.scope = clientCode.upstreamTokens.scope.join(" ");
965
+ }
966
+ return response;
967
+ }
968
+ }
969
+ /**
970
+ * Token endpoint - refresh access token
971
+ */
972
+ async exchangeRefreshToken(request) {
973
+ if (request.grant_type !== "refresh_token") {
974
+ throw new OAuthProxyError(
975
+ "unsupported_grant_type",
976
+ "Only refresh_token grant type is supported"
977
+ );
978
+ }
979
+ const tokenResponse = await fetch(this.config.upstreamTokenEndpoint, {
980
+ body: new URLSearchParams({
981
+ client_id: this.config.upstreamClientId,
982
+ client_secret: this.config.upstreamClientSecret,
983
+ grant_type: "refresh_token",
984
+ refresh_token: request.refresh_token,
985
+ ...request.scope && { scope: request.scope }
986
+ }),
987
+ headers: {
988
+ "Content-Type": "application/x-www-form-urlencoded"
989
+ },
990
+ method: "POST"
991
+ });
992
+ if (!tokenResponse.ok) {
993
+ const error = await tokenResponse.json();
994
+ throw new OAuthProxyError(
995
+ error.error || "invalid_grant",
996
+ error.error_description
997
+ );
998
+ }
999
+ const tokens = await this.parseTokenResponse(tokenResponse);
1000
+ return {
1001
+ access_token: tokens.access_token,
1002
+ expires_in: tokens.expires_in || 3600,
1003
+ id_token: tokens.id_token,
1004
+ refresh_token: tokens.refresh_token,
1005
+ scope: tokens.scope,
1006
+ token_type: tokens.token_type || "Bearer"
1007
+ };
1008
+ }
1009
+ /**
1010
+ * Get OAuth discovery metadata
1011
+ */
1012
+ getAuthorizationServerMetadata() {
1013
+ return {
1014
+ authorizationEndpoint: `${this.config.baseUrl}/oauth/authorize`,
1015
+ codeChallengeMethodsSupported: ["S256", "plain"],
1016
+ grantTypesSupported: ["authorization_code", "refresh_token"],
1017
+ issuer: this.config.baseUrl,
1018
+ registrationEndpoint: `${this.config.baseUrl}/oauth/register`,
1019
+ responseTypesSupported: ["code"],
1020
+ scopesSupported: this.config.scopes || [],
1021
+ tokenEndpoint: `${this.config.baseUrl}/oauth/token`,
1022
+ tokenEndpointAuthMethodsSupported: [
1023
+ "client_secret_basic",
1024
+ "client_secret_post"
1025
+ ]
1026
+ };
1027
+ }
1028
+ /**
1029
+ * Handle OAuth callback from upstream provider
1030
+ */
1031
+ async handleCallback(request) {
1032
+ const url = new URL(request.url);
1033
+ const code = url.searchParams.get("code");
1034
+ const state = url.searchParams.get("state");
1035
+ const error = url.searchParams.get("error");
1036
+ if (error) {
1037
+ const errorDescription = url.searchParams.get("error_description");
1038
+ throw new OAuthProxyError(error, errorDescription || void 0);
1039
+ }
1040
+ if (!code || !state) {
1041
+ throw new OAuthProxyError(
1042
+ "invalid_request",
1043
+ "Missing code or state parameter"
1044
+ );
1045
+ }
1046
+ const transaction = this.transactions.get(state);
1047
+ if (!transaction) {
1048
+ throw new OAuthProxyError("invalid_request", "Invalid or expired state");
1049
+ }
1050
+ const upstreamTokens = await this.exchangeUpstreamCode(code, transaction);
1051
+ const clientCode = this.generateAuthorizationCode(
1052
+ transaction,
1053
+ upstreamTokens
1054
+ );
1055
+ this.transactions.delete(state);
1056
+ const redirectUrl = new URL(transaction.clientCallbackUrl);
1057
+ redirectUrl.searchParams.set("code", clientCode);
1058
+ redirectUrl.searchParams.set("state", transaction.state);
1059
+ return new Response(null, {
1060
+ headers: {
1061
+ Location: redirectUrl.toString()
1062
+ },
1063
+ status: 302
1064
+ });
1065
+ }
1066
+ /**
1067
+ * Handle consent form submission
1068
+ */
1069
+ async handleConsent(request) {
1070
+ const formData = await request.formData();
1071
+ const transactionId = formData.get("transaction_id");
1072
+ const action = formData.get("action");
1073
+ if (!transactionId) {
1074
+ throw new OAuthProxyError("invalid_request", "Missing transaction_id");
1075
+ }
1076
+ const transaction = this.transactions.get(transactionId);
1077
+ if (!transaction) {
1078
+ throw new OAuthProxyError(
1079
+ "invalid_request",
1080
+ "Invalid or expired transaction"
1081
+ );
1082
+ }
1083
+ if (action === "deny") {
1084
+ this.transactions.delete(transactionId);
1085
+ const redirectUrl = new URL(transaction.clientCallbackUrl);
1086
+ redirectUrl.searchParams.set("error", "access_denied");
1087
+ redirectUrl.searchParams.set(
1088
+ "error_description",
1089
+ "User denied authorization"
1090
+ );
1091
+ redirectUrl.searchParams.set("state", transaction.state);
1092
+ return new Response(null, {
1093
+ headers: {
1094
+ Location: redirectUrl.toString()
1095
+ },
1096
+ status: 302
1097
+ });
1098
+ }
1099
+ transaction.consentGiven = true;
1100
+ this.transactions.set(transactionId, transaction);
1101
+ return this.redirectToUpstream(transaction);
1102
+ }
1103
+ /**
1104
+ * Load upstream tokens from a FastMCP JWT
1105
+ */
1106
+ async loadUpstreamTokens(fastmcpToken) {
1107
+ if (!this.jwtIssuer) {
1108
+ return null;
1109
+ }
1110
+ const result = await this.jwtIssuer.verify(fastmcpToken);
1111
+ if (!result.valid || !_optionalChain([result, 'access', _5 => _5.claims, 'optionalAccess', _6 => _6.jti])) {
1112
+ return null;
1113
+ }
1114
+ const mapping = await this.tokenStorage.get(
1115
+ `mapping:${result.claims.jti}`
1116
+ );
1117
+ if (!mapping) {
1118
+ return null;
1119
+ }
1120
+ const upstreamTokens = await this.tokenStorage.get(
1121
+ `upstream:${mapping.upstreamTokenKey}`
1122
+ );
1123
+ return upstreamTokens;
1124
+ }
1125
+ /**
1126
+ * RFC 7591 Dynamic Client Registration
1127
+ */
1128
+ async registerClient(request) {
1129
+ if (!request.redirect_uris || request.redirect_uris.length === 0) {
1130
+ throw new OAuthProxyError(
1131
+ "invalid_client_metadata",
1132
+ "redirect_uris is required"
1133
+ );
1134
+ }
1135
+ for (const uri of request.redirect_uris) {
1136
+ if (!this.validateRedirectUri(uri)) {
1137
+ throw new OAuthProxyError(
1138
+ "invalid_redirect_uri",
1139
+ `Invalid redirect URI: ${uri}`
1140
+ );
1141
+ }
1142
+ }
1143
+ const clientId = this.config.upstreamClientId;
1144
+ const client = {
1145
+ callbackUrl: request.redirect_uris[0],
1146
+ clientId,
1147
+ clientSecret: this.config.upstreamClientSecret,
1148
+ metadata: {
1149
+ client_name: request.client_name,
1150
+ client_uri: request.client_uri,
1151
+ contacts: request.contacts,
1152
+ jwks: request.jwks,
1153
+ jwks_uri: request.jwks_uri,
1154
+ logo_uri: request.logo_uri,
1155
+ policy_uri: request.policy_uri,
1156
+ scope: request.scope,
1157
+ software_id: request.software_id,
1158
+ software_version: request.software_version,
1159
+ tos_uri: request.tos_uri
1160
+ },
1161
+ registeredAt: /* @__PURE__ */ new Date()
1162
+ };
1163
+ this.registeredClients.set(request.redirect_uris[0], client);
1164
+ const response = {
1165
+ client_id: clientId,
1166
+ client_id_issued_at: Math.floor(Date.now() / 1e3),
1167
+ // Echo back optional metadata
1168
+ client_name: request.client_name,
1169
+ client_secret: this.config.upstreamClientSecret,
1170
+ client_secret_expires_at: 0,
1171
+ // Never expires
1172
+ client_uri: request.client_uri,
1173
+ contacts: request.contacts,
1174
+ grant_types: request.grant_types || [
1175
+ "authorization_code",
1176
+ "refresh_token"
1177
+ ],
1178
+ jwks: request.jwks,
1179
+ jwks_uri: request.jwks_uri,
1180
+ logo_uri: request.logo_uri,
1181
+ policy_uri: request.policy_uri,
1182
+ redirect_uris: request.redirect_uris,
1183
+ response_types: request.response_types || ["code"],
1184
+ scope: request.scope,
1185
+ software_id: request.software_id,
1186
+ software_version: request.software_version,
1187
+ token_endpoint_auth_method: request.token_endpoint_auth_method || "client_secret_basic",
1188
+ tos_uri: request.tos_uri
1189
+ };
1190
+ return response;
1191
+ }
1192
+ /**
1193
+ * Clean up expired transactions and codes
1194
+ */
1195
+ cleanup() {
1196
+ const now = Date.now();
1197
+ for (const [id, transaction] of this.transactions.entries()) {
1198
+ if (transaction.expiresAt.getTime() < now) {
1199
+ this.transactions.delete(id);
1200
+ }
1201
+ }
1202
+ for (const [code, clientCode] of this.clientCodes.entries()) {
1203
+ if (clientCode.expiresAt.getTime() < now) {
1204
+ this.clientCodes.delete(code);
1205
+ }
1206
+ }
1207
+ void this.tokenStorage.cleanup();
1208
+ }
1209
+ /**
1210
+ * Create a new OAuth transaction
1211
+ */
1212
+ async createTransaction(params) {
1213
+ const transactionId = this.generateId();
1214
+ const proxyPkce = PKCEUtils.generatePair("S256");
1215
+ const transaction = {
1216
+ clientCallbackUrl: params.redirect_uri,
1217
+ clientCodeChallenge: params.code_challenge || "",
1218
+ clientCodeChallengeMethod: params.code_challenge_method || "plain",
1219
+ clientId: params.client_id,
1220
+ createdAt: /* @__PURE__ */ new Date(),
1221
+ expiresAt: new Date(
1222
+ Date.now() + (this.config.transactionTtl || 600) * 1e3
1223
+ ),
1224
+ id: transactionId,
1225
+ proxyCodeChallenge: proxyPkce.challenge,
1226
+ proxyCodeVerifier: proxyPkce.verifier,
1227
+ scope: params.scope ? params.scope.split(" ") : this.config.scopes || [],
1228
+ state: params.state || this.generateId()
1229
+ };
1230
+ this.transactions.set(transactionId, transaction);
1231
+ return transaction;
1232
+ }
1233
+ /**
1234
+ * Exchange authorization code with upstream provider
1235
+ */
1236
+ async exchangeUpstreamCode(code, transaction) {
1237
+ const tokenResponse = await fetch(this.config.upstreamTokenEndpoint, {
1238
+ body: new URLSearchParams({
1239
+ client_id: this.config.upstreamClientId,
1240
+ client_secret: this.config.upstreamClientSecret,
1241
+ code,
1242
+ code_verifier: transaction.proxyCodeVerifier,
1243
+ grant_type: "authorization_code",
1244
+ redirect_uri: `${this.config.baseUrl}${this.config.redirectPath}`
1245
+ }),
1246
+ headers: {
1247
+ "Content-Type": "application/x-www-form-urlencoded"
1248
+ },
1249
+ method: "POST"
1250
+ });
1251
+ if (!tokenResponse.ok) {
1252
+ const error = await tokenResponse.json();
1253
+ throw new OAuthProxyError(
1254
+ error.error || "server_error",
1255
+ error.error_description
1256
+ );
1257
+ }
1258
+ const tokens = await this.parseTokenResponse(tokenResponse);
1259
+ return {
1260
+ accessToken: tokens.access_token,
1261
+ expiresIn: tokens.expires_in || 3600,
1262
+ idToken: tokens.id_token,
1263
+ issuedAt: /* @__PURE__ */ new Date(),
1264
+ refreshToken: tokens.refresh_token,
1265
+ scope: tokens.scope ? tokens.scope.split(" ") : transaction.scope,
1266
+ tokenType: tokens.token_type || "Bearer"
1267
+ };
1268
+ }
1269
+ /**
1270
+ * Extract JTI from a JWT token
1271
+ */
1272
+ async extractJti(token) {
1273
+ if (!this.jwtIssuer) {
1274
+ throw new Error("JWT issuer not initialized");
1275
+ }
1276
+ const result = await this.jwtIssuer.verify(token);
1277
+ if (!result.valid || !_optionalChain([result, 'access', _7 => _7.claims, 'optionalAccess', _8 => _8.jti])) {
1278
+ throw new Error("Failed to extract JTI from token");
1279
+ }
1280
+ return result.claims.jti;
1281
+ }
1282
+ /**
1283
+ * Extract custom claims from upstream tokens
1284
+ * Combines claims from access token and ID token (if present)
1285
+ */
1286
+ async extractUpstreamClaims(upstreamTokens) {
1287
+ if (!this.claimsExtractor) {
1288
+ return null;
1289
+ }
1290
+ const allClaims = {};
1291
+ const accessClaims = await this.claimsExtractor.extract(
1292
+ upstreamTokens.accessToken,
1293
+ "access"
1294
+ );
1295
+ if (accessClaims) {
1296
+ Object.assign(allClaims, accessClaims);
1297
+ }
1298
+ if (upstreamTokens.idToken) {
1299
+ const idClaims = await this.claimsExtractor.extract(
1300
+ upstreamTokens.idToken,
1301
+ "id"
1302
+ );
1303
+ if (idClaims) {
1304
+ for (const [key, value] of Object.entries(idClaims)) {
1305
+ if (!(key in allClaims)) {
1306
+ allClaims[key] = value;
1307
+ }
1308
+ }
1309
+ }
1310
+ }
1311
+ return Object.keys(allClaims).length > 0 ? allClaims : null;
1312
+ }
1313
+ /**
1314
+ * Generate authorization code for client
1315
+ */
1316
+ generateAuthorizationCode(transaction, upstreamTokens) {
1317
+ const code = this.generateId();
1318
+ const clientCode = {
1319
+ clientId: transaction.clientId,
1320
+ code,
1321
+ codeChallenge: transaction.clientCodeChallenge,
1322
+ codeChallengeMethod: transaction.clientCodeChallengeMethod,
1323
+ createdAt: /* @__PURE__ */ new Date(),
1324
+ expiresAt: new Date(
1325
+ Date.now() + (this.config.authorizationCodeTtl || 300) * 1e3
1326
+ ),
1327
+ transactionId: transaction.id,
1328
+ upstreamTokens
1329
+ };
1330
+ this.clientCodes.set(code, clientCode);
1331
+ return code;
1332
+ }
1333
+ /**
1334
+ * Generate secure random ID
1335
+ */
1336
+ generateId() {
1337
+ return _crypto.randomBytes.call(void 0, 32).toString("base64url");
1338
+ }
1339
+ /**
1340
+ * Generate signing key for consent cookies
1341
+ */
1342
+ generateSigningKey() {
1343
+ return _crypto.randomBytes.call(void 0, 32).toString("hex");
1344
+ }
1345
+ /**
1346
+ * Get provider name for display
1347
+ */
1348
+ getProviderName() {
1349
+ const url = new URL(this.config.upstreamAuthorizationEndpoint);
1350
+ return url.hostname;
1351
+ }
1352
+ /**
1353
+ * Issue swapped tokens (JWT pattern)
1354
+ * Issues short-lived FastMCP JWTs and stores upstream tokens securely
1355
+ */
1356
+ async issueSwappedTokens(clientId, upstreamTokens) {
1357
+ if (!this.jwtIssuer) {
1358
+ throw new Error("JWT issuer not initialized");
1359
+ }
1360
+ const customClaims = await this.extractUpstreamClaims(upstreamTokens);
1361
+ const upstreamTokenKey = this.generateId();
1362
+ await this.tokenStorage.save(
1363
+ `upstream:${upstreamTokenKey}`,
1364
+ upstreamTokens,
1365
+ upstreamTokens.expiresIn
1366
+ );
1367
+ const accessToken = this.jwtIssuer.issueAccessToken(
1368
+ clientId,
1369
+ upstreamTokens.scope,
1370
+ customClaims || void 0
1371
+ );
1372
+ const accessJti = await this.extractJti(accessToken);
1373
+ await this.tokenStorage.save(
1374
+ `mapping:${accessJti}`,
1375
+ {
1376
+ clientId,
1377
+ createdAt: /* @__PURE__ */ new Date(),
1378
+ expiresAt: new Date(Date.now() + upstreamTokens.expiresIn * 1e3),
1379
+ jti: accessJti,
1380
+ scope: upstreamTokens.scope,
1381
+ upstreamTokenKey
1382
+ },
1383
+ upstreamTokens.expiresIn
1384
+ );
1385
+ const response = {
1386
+ access_token: accessToken,
1387
+ expires_in: 3600,
1388
+ // FastMCP JWT expiration (1 hour)
1389
+ scope: upstreamTokens.scope.join(" "),
1390
+ token_type: "Bearer"
1391
+ };
1392
+ if (upstreamTokens.refreshToken) {
1393
+ const refreshToken = this.jwtIssuer.issueRefreshToken(
1394
+ clientId,
1395
+ upstreamTokens.scope,
1396
+ customClaims || void 0
1397
+ );
1398
+ const refreshJti = await this.extractJti(refreshToken);
1399
+ await this.tokenStorage.save(
1400
+ `mapping:${refreshJti}`,
1401
+ {
1402
+ clientId,
1403
+ createdAt: /* @__PURE__ */ new Date(),
1404
+ expiresAt: new Date(Date.now() + 2592e3 * 1e3),
1405
+ // 30 days
1406
+ jti: refreshJti,
1407
+ scope: upstreamTokens.scope,
1408
+ upstreamTokenKey
1409
+ },
1410
+ 2592e3
1411
+ // 30 days
1412
+ );
1413
+ response.refresh_token = refreshToken;
1414
+ }
1415
+ return response;
1416
+ }
1417
+ /**
1418
+ * Match URI against pattern (supports wildcards)
1419
+ */
1420
+ matchesPattern(uri, pattern) {
1421
+ const regex = new RegExp(
1422
+ "^" + pattern.replace(/\*/g, ".*").replace(/\?/g, ".") + "$"
1423
+ );
1424
+ return regex.test(uri);
1425
+ }
1426
+ /**
1427
+ * Parse token response that can be either JSON or URL-encoded
1428
+ * GitHub Apps return URL-encoded format, most providers return JSON
1429
+ */
1430
+ async parseTokenResponse(response) {
1431
+ const contentType = (response.headers.get("content-type") || "").toLowerCase();
1432
+ const tokenResponseSchema = _zod.z.object({
1433
+ access_token: _zod.z.string().min(1, "access_token cannot be empty"),
1434
+ expires_in: _zod.z.number().int().positive().optional(),
1435
+ id_token: _zod.z.string().optional(),
1436
+ refresh_token: _zod.z.string().optional(),
1437
+ scope: _zod.z.string().optional(),
1438
+ token_type: _zod.z.string().optional()
1439
+ });
1440
+ if (contentType.includes("application/x-www-form-urlencoded")) {
1441
+ const text = await response.text();
1442
+ const params = new URLSearchParams(text);
1443
+ const rawData = {
1444
+ access_token: params.get("access_token") || "",
1445
+ expires_in: params.get("expires_in") ? parseInt(params.get("expires_in")) : void 0,
1446
+ id_token: params.get("id_token") || void 0,
1447
+ refresh_token: params.get("refresh_token") || void 0,
1448
+ scope: params.get("scope") || void 0,
1449
+ token_type: params.get("token_type") || void 0
1450
+ };
1451
+ return tokenResponseSchema.parse(rawData);
1452
+ }
1453
+ const rawJson = await response.json();
1454
+ return tokenResponseSchema.parse(rawJson);
1455
+ }
1456
+ /**
1457
+ * Redirect to upstream OAuth provider
1458
+ */
1459
+ redirectToUpstream(transaction) {
1460
+ const authUrl = new URL(this.config.upstreamAuthorizationEndpoint);
1461
+ authUrl.searchParams.set("client_id", this.config.upstreamClientId);
1462
+ authUrl.searchParams.set(
1463
+ "redirect_uri",
1464
+ `${this.config.baseUrl}${this.config.redirectPath}`
1465
+ );
1466
+ authUrl.searchParams.set("response_type", "code");
1467
+ authUrl.searchParams.set("state", transaction.id);
1468
+ if (transaction.scope.length > 0) {
1469
+ authUrl.searchParams.set("scope", transaction.scope.join(" "));
1470
+ }
1471
+ if (!this.config.forwardPkce) {
1472
+ authUrl.searchParams.set(
1473
+ "code_challenge",
1474
+ transaction.proxyCodeChallenge
1475
+ );
1476
+ authUrl.searchParams.set("code_challenge_method", "S256");
1477
+ }
1478
+ return new Response(null, {
1479
+ headers: {
1480
+ Location: authUrl.toString()
1481
+ },
1482
+ status: 302
1483
+ });
1484
+ }
1485
+ /**
1486
+ * Start periodic cleanup of expired transactions and codes
1487
+ */
1488
+ startCleanup() {
1489
+ this.cleanupInterval = setInterval(() => {
1490
+ this.cleanup();
1491
+ }, 6e4);
1492
+ }
1493
+ /**
1494
+ * Validate redirect URI against allowed patterns
1495
+ */
1496
+ validateRedirectUri(uri) {
1497
+ try {
1498
+ const url = new URL(uri);
1499
+ const patterns = this.config.allowedRedirectUriPatterns || [];
1500
+ for (const pattern of patterns) {
1501
+ if (this.matchesPattern(uri, pattern)) {
1502
+ return true;
1503
+ }
1504
+ }
1505
+ return url.protocol === "https:" || url.hostname === "localhost" || url.hostname === "127.0.0.1";
1506
+ } catch (e3) {
1507
+ return false;
1508
+ }
1509
+ }
1510
+ }, _class4);
1511
+ var OAuthProxyError = class extends Error {
1512
+ constructor(code, description, statusCode = 400) {
1513
+ super(code);
1514
+ this.code = code;
1515
+ this.description = description;
1516
+ this.statusCode = statusCode;
1517
+ this.name = "OAuthProxyError";
1518
+ }
1519
+ toJSON() {
1520
+ return {
1521
+ error: this.code,
1522
+ error_description: this.description
1523
+ };
1524
+ }
1525
+ toResponse() {
1526
+ return new Response(JSON.stringify(this.toJSON()), {
1527
+ headers: { "Content-Type": "application/json" },
1528
+ status: this.statusCode
1529
+ });
1530
+ }
1531
+ };
1532
+
1533
+ // src/auth/providers/AzureProvider.ts
1534
+ var AzureProvider = class extends OAuthProxy {
1535
+ constructor(config) {
1536
+ const tenantId = config.tenantId || "common";
1537
+ super({
1538
+ baseUrl: config.baseUrl,
1539
+ consentRequired: config.consentRequired,
1540
+ scopes: config.scopes || ["openid", "profile", "email"],
1541
+ upstreamAuthorizationEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`,
1542
+ upstreamClientId: config.clientId,
1543
+ upstreamClientSecret: config.clientSecret,
1544
+ upstreamTokenEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`
1545
+ });
1546
+ }
1547
+ };
1548
+
1549
+ // src/auth/providers/GitHubProvider.ts
1550
+ var GitHubProvider = class extends OAuthProxy {
1551
+ constructor(config) {
1552
+ super({
1553
+ baseUrl: config.baseUrl,
1554
+ consentRequired: config.consentRequired,
1555
+ scopes: config.scopes || ["read:user", "user:email"],
1556
+ upstreamAuthorizationEndpoint: "https://github.com/login/oauth/authorize",
1557
+ upstreamClientId: config.clientId,
1558
+ upstreamClientSecret: config.clientSecret,
1559
+ upstreamTokenEndpoint: "https://github.com/login/oauth/access_token"
1560
+ });
1561
+ }
1562
+ };
1563
+
1564
+ // src/auth/providers/GoogleProvider.ts
1565
+ var GoogleProvider = class extends OAuthProxy {
1566
+ constructor(config) {
1567
+ super({
1568
+ baseUrl: config.baseUrl,
1569
+ consentRequired: config.consentRequired,
1570
+ scopes: config.scopes || ["openid", "profile", "email"],
1571
+ upstreamAuthorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
1572
+ upstreamClientId: config.clientId,
1573
+ upstreamClientSecret: config.clientSecret,
1574
+ upstreamTokenEndpoint: "https://oauth2.googleapis.com/token"
1575
+ });
1576
+ }
1577
+ };
1578
+
1579
+ // src/auth/utils/diskStore.ts
1580
+ var _promises = require('fs/promises');
1581
+ var _path = require('path');
1582
+ var DiskStore = (_class5 = class {
1583
+ __init10() {this.cleanupInterval = null}
1584
+
1585
+
1586
+ constructor(options) {;_class5.prototype.__init10.call(this);
1587
+ this.directory = options.directory;
1588
+ this.fileExtension = options.fileExtension || ".json";
1589
+ void this.ensureDirectory();
1590
+ const cleanupIntervalMs = options.cleanupIntervalMs || 6e4;
1591
+ this.cleanupInterval = setInterval(() => {
1592
+ void this.cleanup();
1593
+ }, cleanupIntervalMs);
1594
+ }
1595
+ /**
1596
+ * Clean up expired entries
1597
+ */
1598
+ async cleanup() {
1599
+ try {
1600
+ await this.ensureDirectory();
1601
+ const files = await _promises.readdir.call(void 0, this.directory);
1602
+ const now = Date.now();
1603
+ for (const file of files) {
1604
+ if (!file.endsWith(this.fileExtension)) {
1605
+ continue;
1606
+ }
1607
+ try {
1608
+ const filePath = _path.join.call(void 0, this.directory, file);
1609
+ const content = await _promises.readFile.call(void 0, filePath, "utf-8");
1610
+ const entry = JSON.parse(content);
1611
+ if (entry.expiresAt < now) {
1612
+ await _promises.rm.call(void 0, filePath);
1613
+ }
1614
+ } catch (error) {
1615
+ console.warn(`Failed to read/parse file ${file}, deleting:`, error);
1616
+ try {
1617
+ await _promises.rm.call(void 0, _path.join.call(void 0, this.directory, file));
1618
+ } catch (e4) {
1619
+ }
1620
+ }
1621
+ }
1622
+ } catch (error) {
1623
+ console.error("Cleanup failed:", error);
1624
+ }
1625
+ }
1626
+ /**
1627
+ * Delete a value
1628
+ */
1629
+ async delete(key) {
1630
+ const filePath = this.getFilePath(key);
1631
+ try {
1632
+ await _promises.rm.call(void 0, filePath);
1633
+ } catch (error) {
1634
+ if (error.code !== "ENOENT") {
1635
+ console.error(`Failed to delete key ${key}:`, error);
1636
+ }
1637
+ }
1638
+ }
1639
+ /**
1640
+ * Destroy the storage and clear cleanup interval
1641
+ */
1642
+ destroy() {
1643
+ if (this.cleanupInterval) {
1644
+ clearInterval(this.cleanupInterval);
1645
+ this.cleanupInterval = null;
1646
+ }
1647
+ }
1648
+ /**
1649
+ * Retrieve a value
1650
+ */
1651
+ async get(key) {
1652
+ const filePath = this.getFilePath(key);
1653
+ try {
1654
+ const content = await _promises.readFile.call(void 0, filePath, "utf-8");
1655
+ const entry = JSON.parse(content);
1656
+ if (entry.expiresAt < Date.now()) {
1657
+ await _promises.rm.call(void 0, filePath);
1658
+ return null;
1659
+ }
1660
+ return entry.value;
1661
+ } catch (error) {
1662
+ if (error.code === "ENOENT") {
1663
+ return null;
1664
+ }
1665
+ console.error(`Failed to read key ${key}:`, error);
1666
+ return null;
1667
+ }
1668
+ }
1669
+ /**
1670
+ * Save a value with optional TTL
1671
+ */
1672
+ async save(key, value, ttl) {
1673
+ await this.ensureDirectory();
1674
+ const filePath = this.getFilePath(key);
1675
+ const expiresAt = ttl ? Date.now() + ttl * 1e3 : Number.MAX_SAFE_INTEGER;
1676
+ const entry = {
1677
+ expiresAt,
1678
+ value
1679
+ };
1680
+ try {
1681
+ await _promises.writeFile.call(void 0, filePath, JSON.stringify(entry, null, 2), "utf-8");
1682
+ } catch (error) {
1683
+ console.error(`Failed to save key ${key}:`, error);
1684
+ throw error;
1685
+ }
1686
+ }
1687
+ /**
1688
+ * Get the number of stored items
1689
+ */
1690
+ async size() {
1691
+ try {
1692
+ await this.ensureDirectory();
1693
+ const files = await _promises.readdir.call(void 0, this.directory);
1694
+ return files.filter((f) => f.endsWith(this.fileExtension)).length;
1695
+ } catch (e5) {
1696
+ return 0;
1697
+ }
1698
+ }
1699
+ /**
1700
+ * Ensure storage directory exists
1701
+ */
1702
+ async ensureDirectory() {
1703
+ try {
1704
+ const stats = await _promises.stat.call(void 0, this.directory);
1705
+ if (!stats.isDirectory()) {
1706
+ throw new Error(`Path ${this.directory} exists but is not a directory`);
1707
+ }
1708
+ } catch (error) {
1709
+ if (error.code === "ENOENT") {
1710
+ await _promises.mkdir.call(void 0, this.directory, { recursive: true });
1711
+ } else {
1712
+ throw error;
1713
+ }
1714
+ }
1715
+ }
1716
+ /**
1717
+ * Get file path for a key
1718
+ */
1719
+ getFilePath(key) {
1720
+ const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, "_");
1721
+ return _path.join.call(void 0, this.directory, `${sanitizedKey}${this.fileExtension}`);
1722
+ }
1723
+ }, _class5);
1724
+
1725
+ // src/auth/utils/jwks.ts
1726
+ var JWKSVerifier = (_class6 = class {
1727
+
1728
+
1729
+ __init11() {this.joseLoaded = false}
1730
+
1731
+ constructor(config) {;_class6.prototype.__init11.call(this);
1732
+ this.config = {
1733
+ cacheDuration: 36e5,
1734
+ // 1 hour
1735
+ cooldownDuration: 3e4,
1736
+ // 30 seconds
1737
+ ...config,
1738
+ audience: config.audience || "",
1739
+ issuer: config.issuer || ""
1740
+ };
1741
+ }
1742
+ /**
1743
+ * Get the JWKS URI being used
1744
+ */
1745
+ getJwksUri() {
1746
+ return this.config.jwksUri;
1747
+ }
1748
+ /**
1749
+ * Refresh the JWKS cache
1750
+ * Useful if you need to force a key refresh
1751
+ */
1752
+ async refreshKeys() {
1753
+ await this.loadJose();
1754
+ this.jwksCache = this.jose.createRemoteJWKSet(
1755
+ new URL(this.config.jwksUri),
1756
+ {
1757
+ cacheMaxAge: this.config.cacheDuration,
1758
+ cooldownDuration: this.config.cooldownDuration
1759
+ }
1760
+ );
1761
+ }
1762
+ /**
1763
+ * Verify a JWT token using JWKS
1764
+ *
1765
+ * @param token - The JWT token to verify
1766
+ * @returns Verification result with claims if valid
1767
+ *
1768
+ * @example
1769
+ * ```typescript
1770
+ * const result = await verifier.verify(token);
1771
+ * if (result.valid) {
1772
+ * console.log('User:', result.claims?.client_id);
1773
+ * } else {
1774
+ * console.error('Invalid token:', result.error);
1775
+ * }
1776
+ * ```
1777
+ */
1778
+ async verify(token) {
1779
+ try {
1780
+ await this.loadJose();
1781
+ const verifyOptions = {};
1782
+ if (this.config.audience) {
1783
+ verifyOptions.audience = this.config.audience;
1784
+ }
1785
+ if (this.config.issuer) {
1786
+ verifyOptions.issuer = this.config.issuer;
1787
+ }
1788
+ const { payload } = await this.jose.jwtVerify(
1789
+ token,
1790
+ this.jwksCache,
1791
+ verifyOptions
1792
+ );
1793
+ const claims = {
1794
+ aud: payload.aud,
1795
+ client_id: payload.client_id || payload.sub,
1796
+ exp: payload.exp,
1797
+ iat: payload.iat,
1798
+ iss: payload.iss,
1799
+ jti: payload.jti || "",
1800
+ scope: this.parseScope(payload.scope),
1801
+ ...payload
1802
+ // Include all other claims
1803
+ };
1804
+ return {
1805
+ claims,
1806
+ valid: true
1807
+ };
1808
+ } catch (error) {
1809
+ return {
1810
+ error: error.message || "Token verification failed",
1811
+ valid: false
1812
+ };
1813
+ }
1814
+ }
1815
+ /**
1816
+ * Lazy load the jose library
1817
+ * Only loads when verification is first attempted
1818
+ */
1819
+ async loadJose() {
1820
+ if (this.joseLoaded) {
1821
+ return;
1822
+ }
1823
+ try {
1824
+ this.jose = await Promise.resolve().then(() => _interopRequireWildcard(require("jose")));
1825
+ this.joseLoaded = true;
1826
+ this.jwksCache = this.jose.createRemoteJWKSet(
1827
+ new URL(this.config.jwksUri),
1828
+ {
1829
+ cacheMaxAge: this.config.cacheDuration,
1830
+ cooldownDuration: this.config.cooldownDuration
1831
+ }
1832
+ );
1833
+ } catch (error) {
1834
+ throw new Error(
1835
+ `JWKS verification requires the 'jose' package.
1836
+ Install it with: npm install jose
1837
+
1838
+ If you don't need JWKS support, use HS256 signing instead (default).
1839
+
1840
+ Original error: ${error.message}`
1841
+ );
1842
+ }
1843
+ }
1844
+ /**
1845
+ * Parse scope from token payload
1846
+ * Handles both string (space-separated) and array formats
1847
+ */
1848
+ parseScope(scope) {
1849
+ if (!scope) {
1850
+ return [];
1851
+ }
1852
+ if (typeof scope === "string") {
1853
+ return scope.split(" ").filter(Boolean);
1854
+ }
1855
+ if (Array.isArray(scope)) {
1856
+ return scope;
1857
+ }
1858
+ return [];
1859
+ }
1860
+ }, _class6);
1861
+
1862
+
1863
+
1864
+
1865
+
1866
+
1867
+
1868
+
1869
+
1870
+
1871
+
1872
+
1873
+
1874
+ exports.AzureProvider = AzureProvider; exports.ConsentManager = ConsentManager; exports.DiskStore = DiskStore; exports.EncryptedTokenStorage = EncryptedTokenStorage; exports.GitHubProvider = GitHubProvider; exports.GoogleProvider = GoogleProvider; exports.JWKSVerifier = JWKSVerifier; exports.JWTIssuer = JWTIssuer; exports.MemoryTokenStorage = MemoryTokenStorage; exports.OAuthProxy = OAuthProxy; exports.OAuthProxyError = OAuthProxyError; exports.PKCEUtils = PKCEUtils;
1875
+ //# sourceMappingURL=index.cjs.map