fastmcp 3.31.0 → 3.33.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/README.md CHANGED
@@ -19,6 +19,7 @@ A TypeScript framework for building [MCP](https://glama.ai/mcp) servers capable
19
19
  - [Logging](#logging)
20
20
  - [Error handling](#errors)
21
21
  - [HTTP Streaming](#http-streaming) (with SSE compatibility)
22
+ - [HTTPS Support](#https-support) for secure connections
22
23
  - [Custom HTTP routes](#custom-http-routes) for REST APIs, webhooks, and admin interfaces
23
24
  - [Edge Runtime Support](#edge-runtime-support) for Cloudflare Workers, Deno Deploy, and more
24
25
  - [Stateless mode](#stateless-mode) for serverless deployments
@@ -184,6 +185,40 @@ const transport = new SSEClientTransport(new URL(`http://localhost:8080/sse`));
184
185
  await client.connect(transport);
185
186
  ```
186
187
 
188
+ ##### HTTPS Support
189
+
190
+ FastMCP supports HTTPS for secure connections by providing SSL certificate options:
191
+
192
+ ```ts
193
+ server.start({
194
+ transportType: "httpStream",
195
+ httpStream: {
196
+ port: 8443,
197
+ sslCert: "./path/to/cert.pem",
198
+ sslKey: "./path/to/key.pem",
199
+ sslCa: "./path/to/ca.pem", // Optional: for client certificate authentication
200
+ },
201
+ });
202
+ ```
203
+
204
+ This will start the server with HTTPS on `https://localhost:8443/mcp`.
205
+
206
+ **SSL Options:**
207
+
208
+ - `sslCert` - Path to SSL certificate file
209
+ - `sslKey` - Path to SSL private key file
210
+ - `sslCa` - (Optional) Path to CA certificate for mutual TLS authentication
211
+
212
+ **For testing**, you can generate self-signed certificates:
213
+
214
+ ```bash
215
+ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"
216
+ ```
217
+
218
+ **For production**, obtain certificates from a trusted CA like Let's Encrypt.
219
+
220
+ See the [https-server example](src/examples/https-server.ts) for a complete demonstration.
221
+
187
222
  #### Custom HTTP Routes
188
223
 
189
224
  FastMCP allows you to add custom HTTP routes alongside MCP endpoints, enabling you to build comprehensive HTTP services that include REST APIs, webhooks, admin interfaces, and more - all within the same server process.
package/dist/FastMCP.cjs CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
 
9
9
 
10
- var _chunkW6DQCBJ3cjs = require('./chunk-W6DQCBJ3.cjs');
10
+ var _chunkEVM4KQGRcjs = require('./chunk-EVM4KQGR.cjs');
11
11
 
12
12
 
13
13
 
@@ -20,7 +20,7 @@ var _chunkW6DQCBJ3cjs = require('./chunk-W6DQCBJ3.cjs');
20
20
 
21
21
 
22
22
 
23
- var _chunk3MN6Z6V4cjs = require('./chunk-3MN6Z6V4.cjs');
23
+ var _chunk7UDY4VFQcjs = require('./chunk-7UDY4VFQ.cjs');
24
24
 
25
25
 
26
26
 
@@ -41,5 +41,5 @@ var _chunk3MN6Z6V4cjs = require('./chunk-3MN6Z6V4.cjs');
41
41
 
42
42
 
43
43
 
44
- exports.AuthProvider = _chunk3MN6Z6V4cjs.AuthProvider; exports.AzureProvider = _chunk3MN6Z6V4cjs.AzureProvider; exports.DiscoveryDocumentCache = _chunkW6DQCBJ3cjs.DiscoveryDocumentCache; exports.FastMCP = _chunkW6DQCBJ3cjs.FastMCP; exports.FastMCPSession = _chunkW6DQCBJ3cjs.FastMCPSession; exports.GitHubProvider = _chunk3MN6Z6V4cjs.GitHubProvider; exports.GoogleProvider = _chunk3MN6Z6V4cjs.GoogleProvider; exports.OAuthProvider = _chunk3MN6Z6V4cjs.OAuthProvider; exports.ServerState = _chunkW6DQCBJ3cjs.ServerState; exports.UnexpectedStateError = _chunkW6DQCBJ3cjs.UnexpectedStateError; exports.UserError = _chunkW6DQCBJ3cjs.UserError; exports.audioContent = _chunkW6DQCBJ3cjs.audioContent; exports.getAuthSession = _chunk3MN6Z6V4cjs.getAuthSession; exports.imageContent = _chunkW6DQCBJ3cjs.imageContent; exports.requireAll = _chunk3MN6Z6V4cjs.requireAll; exports.requireAny = _chunk3MN6Z6V4cjs.requireAny; exports.requireAuth = _chunk3MN6Z6V4cjs.requireAuth; exports.requireRole = _chunk3MN6Z6V4cjs.requireRole; exports.requireScopes = _chunk3MN6Z6V4cjs.requireScopes;
44
+ exports.AuthProvider = _chunk7UDY4VFQcjs.AuthProvider; exports.AzureProvider = _chunk7UDY4VFQcjs.AzureProvider; exports.DiscoveryDocumentCache = _chunkEVM4KQGRcjs.DiscoveryDocumentCache; exports.FastMCP = _chunkEVM4KQGRcjs.FastMCP; exports.FastMCPSession = _chunkEVM4KQGRcjs.FastMCPSession; exports.GitHubProvider = _chunk7UDY4VFQcjs.GitHubProvider; exports.GoogleProvider = _chunk7UDY4VFQcjs.GoogleProvider; exports.OAuthProvider = _chunk7UDY4VFQcjs.OAuthProvider; exports.ServerState = _chunkEVM4KQGRcjs.ServerState; exports.UnexpectedStateError = _chunkEVM4KQGRcjs.UnexpectedStateError; exports.UserError = _chunkEVM4KQGRcjs.UserError; exports.audioContent = _chunkEVM4KQGRcjs.audioContent; exports.getAuthSession = _chunk7UDY4VFQcjs.getAuthSession; exports.imageContent = _chunkEVM4KQGRcjs.imageContent; exports.requireAll = _chunk7UDY4VFQcjs.requireAll; exports.requireAny = _chunk7UDY4VFQcjs.requireAny; exports.requireAuth = _chunk7UDY4VFQcjs.requireAuth; exports.requireRole = _chunk7UDY4VFQcjs.requireRole; exports.requireScopes = _chunk7UDY4VFQcjs.requireScopes;
45
45
  //# sourceMappingURL=FastMCP.cjs.map
@@ -572,6 +572,20 @@ type ServerOptions<T extends FastMCPSessionAuth> = {
572
572
  version: `${number}.${number}.${number}`;
573
573
  };
574
574
  type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolParameters> = {
575
+ /**
576
+ * MCP ext-apps metadata for linking interactive UI components.
577
+ * This field is passed through to the tool listing response.
578
+ * @see https://modelcontextprotocol.github.io/ext-apps/
579
+ */
580
+ _meta?: {
581
+ /** Additional metadata fields */
582
+ [key: string]: unknown;
583
+ /** UI component configuration */
584
+ ui?: {
585
+ /** URI of the resource serving the UI (e.g., "ui://my-tool/app.html") */
586
+ resourceUri?: string;
587
+ };
588
+ };
575
589
  annotations?: {
576
590
  /**
577
591
  * When true, the tool leverages incremental content streaming
@@ -839,6 +853,9 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
839
853
  eventStore?: EventStore;
840
854
  host?: string;
841
855
  port: number;
856
+ sslCa?: string;
857
+ sslCert?: string;
858
+ sslKey?: string;
842
859
  stateless?: boolean;
843
860
  };
844
861
  transportType: "httpStream" | "stdio";
package/dist/FastMCP.d.ts CHANGED
@@ -572,6 +572,20 @@ type ServerOptions<T extends FastMCPSessionAuth> = {
572
572
  version: `${number}.${number}.${number}`;
573
573
  };
574
574
  type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolParameters> = {
575
+ /**
576
+ * MCP ext-apps metadata for linking interactive UI components.
577
+ * This field is passed through to the tool listing response.
578
+ * @see https://modelcontextprotocol.github.io/ext-apps/
579
+ */
580
+ _meta?: {
581
+ /** Additional metadata fields */
582
+ [key: string]: unknown;
583
+ /** UI component configuration */
584
+ ui?: {
585
+ /** URI of the resource serving the UI (e.g., "ui://my-tool/app.html") */
586
+ resourceUri?: string;
587
+ };
588
+ };
575
589
  annotations?: {
576
590
  /**
577
591
  * When true, the tool leverages incremental content streaming
@@ -839,6 +853,9 @@ declare class FastMCP<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends
839
853
  eventStore?: EventStore;
840
854
  host?: string;
841
855
  port: number;
856
+ sslCa?: string;
857
+ sslCert?: string;
858
+ sslKey?: string;
842
859
  stateless?: boolean;
843
860
  };
844
861
  transportType: "httpStream" | "stdio";
package/dist/FastMCP.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  UserError,
8
8
  audioContent,
9
9
  imageContent
10
- } from "./chunk-77V5MOHV.js";
10
+ } from "./chunk-J3GVXEY5.js";
11
11
  import {
12
12
  AuthProvider,
13
13
  AzureProvider,
@@ -20,7 +20,7 @@ import {
20
20
  requireAuth,
21
21
  requireRole,
22
22
  requireScopes
23
- } from "./chunk-KLQDFMQV.js";
23
+ } from "./chunk-H4VC4YTC.js";
24
24
  export {
25
25
  AuthProvider,
26
26
  AzureProvider,
@@ -24,7 +24,7 @@
24
24
 
25
25
 
26
26
 
27
- var _chunk3MN6Z6V4cjs = require('../chunk-3MN6Z6V4.cjs');
27
+ var _chunk7UDY4VFQcjs = require('../chunk-7UDY4VFQ.cjs');
28
28
 
29
29
 
30
30
 
@@ -51,5 +51,5 @@ var _chunk3MN6Z6V4cjs = require('../chunk-3MN6Z6V4.cjs');
51
51
 
52
52
 
53
53
 
54
- exports.AuthProvider = _chunk3MN6Z6V4cjs.AuthProvider; exports.AzureProvider = _chunk3MN6Z6V4cjs.AzureProvider; exports.ConsentManager = _chunk3MN6Z6V4cjs.ConsentManager; exports.DEFAULT_ACCESS_TOKEN_TTL = _chunk3MN6Z6V4cjs.DEFAULT_ACCESS_TOKEN_TTL; exports.DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH = _chunk3MN6Z6V4cjs.DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH; exports.DEFAULT_AUTHORIZATION_CODE_TTL = _chunk3MN6Z6V4cjs.DEFAULT_AUTHORIZATION_CODE_TTL; exports.DEFAULT_REFRESH_TOKEN_TTL = _chunk3MN6Z6V4cjs.DEFAULT_REFRESH_TOKEN_TTL; exports.DEFAULT_TRANSACTION_TTL = _chunk3MN6Z6V4cjs.DEFAULT_TRANSACTION_TTL; exports.DiskStore = _chunk3MN6Z6V4cjs.DiskStore; exports.EncryptedTokenStorage = _chunk3MN6Z6V4cjs.EncryptedTokenStorage; exports.GitHubProvider = _chunk3MN6Z6V4cjs.GitHubProvider; exports.GoogleProvider = _chunk3MN6Z6V4cjs.GoogleProvider; exports.JWKSVerifier = _chunk3MN6Z6V4cjs.JWKSVerifier; exports.JWTIssuer = _chunk3MN6Z6V4cjs.JWTIssuer; exports.MemoryTokenStorage = _chunk3MN6Z6V4cjs.MemoryTokenStorage; exports.OAuthProvider = _chunk3MN6Z6V4cjs.OAuthProvider; exports.OAuthProxy = _chunk3MN6Z6V4cjs.OAuthProxy; exports.OAuthProxyError = _chunk3MN6Z6V4cjs.OAuthProxyError; exports.PKCEUtils = _chunk3MN6Z6V4cjs.PKCEUtils; exports.getAuthSession = _chunk3MN6Z6V4cjs.getAuthSession; exports.requireAll = _chunk3MN6Z6V4cjs.requireAll; exports.requireAny = _chunk3MN6Z6V4cjs.requireAny; exports.requireAuth = _chunk3MN6Z6V4cjs.requireAuth; exports.requireRole = _chunk3MN6Z6V4cjs.requireRole; exports.requireScopes = _chunk3MN6Z6V4cjs.requireScopes;
54
+ exports.AuthProvider = _chunk7UDY4VFQcjs.AuthProvider; exports.AzureProvider = _chunk7UDY4VFQcjs.AzureProvider; exports.ConsentManager = _chunk7UDY4VFQcjs.ConsentManager; exports.DEFAULT_ACCESS_TOKEN_TTL = _chunk7UDY4VFQcjs.DEFAULT_ACCESS_TOKEN_TTL; exports.DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH = _chunk7UDY4VFQcjs.DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH; exports.DEFAULT_AUTHORIZATION_CODE_TTL = _chunk7UDY4VFQcjs.DEFAULT_AUTHORIZATION_CODE_TTL; exports.DEFAULT_REFRESH_TOKEN_TTL = _chunk7UDY4VFQcjs.DEFAULT_REFRESH_TOKEN_TTL; exports.DEFAULT_TRANSACTION_TTL = _chunk7UDY4VFQcjs.DEFAULT_TRANSACTION_TTL; exports.DiskStore = _chunk7UDY4VFQcjs.DiskStore; exports.EncryptedTokenStorage = _chunk7UDY4VFQcjs.EncryptedTokenStorage; exports.GitHubProvider = _chunk7UDY4VFQcjs.GitHubProvider; exports.GoogleProvider = _chunk7UDY4VFQcjs.GoogleProvider; exports.JWKSVerifier = _chunk7UDY4VFQcjs.JWKSVerifier; exports.JWTIssuer = _chunk7UDY4VFQcjs.JWTIssuer; exports.MemoryTokenStorage = _chunk7UDY4VFQcjs.MemoryTokenStorage; exports.OAuthProvider = _chunk7UDY4VFQcjs.OAuthProvider; exports.OAuthProxy = _chunk7UDY4VFQcjs.OAuthProxy; exports.OAuthProxyError = _chunk7UDY4VFQcjs.OAuthProxyError; exports.PKCEUtils = _chunk7UDY4VFQcjs.PKCEUtils; exports.getAuthSession = _chunk7UDY4VFQcjs.getAuthSession; exports.requireAll = _chunk7UDY4VFQcjs.requireAll; exports.requireAny = _chunk7UDY4VFQcjs.requireAny; exports.requireAuth = _chunk7UDY4VFQcjs.requireAuth; exports.requireRole = _chunk7UDY4VFQcjs.requireRole; exports.requireScopes = _chunk7UDY4VFQcjs.requireScopes;
55
55
  //# sourceMappingURL=index.cjs.map
@@ -24,7 +24,7 @@ import {
24
24
  requireAuth,
25
25
  requireRole,
26
26
  requireScopes
27
- } from "../chunk-KLQDFMQV.js";
27
+ } from "../chunk-H4VC4YTC.js";
28
28
  export {
29
29
  AuthProvider,
30
30
  AzureProvider,
@@ -1658,9 +1658,9 @@ var OAuthProxy = (_class4 = class {
1658
1658
  const contentType = (response.headers.get("content-type") || "").toLowerCase();
1659
1659
  const tokenResponseSchema = _zod.z.object({
1660
1660
  access_token: _zod.z.string().min(1, "access_token cannot be empty"),
1661
- expires_in: _zod.z.number().int().positive().optional(),
1661
+ expires_in: _zod.z.coerce.number().int().positive().optional(),
1662
1662
  id_token: _zod.z.string().optional(),
1663
- refresh_expires_in: _zod.z.number().int().positive().optional(),
1663
+ refresh_expires_in: _zod.z.coerce.number().int().positive().optional(),
1664
1664
  refresh_token: _zod.z.string().optional(),
1665
1665
  scope: _zod.z.string().optional(),
1666
1666
  token_type: _zod.z.string().optional()
@@ -2325,4 +2325,4 @@ Original error: ${error.message}`
2325
2325
 
2326
2326
 
2327
2327
  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;
2328
- //# sourceMappingURL=chunk-3MN6Z6V4.cjs.map
2328
+ //# sourceMappingURL=chunk-7UDY4VFQ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/fastmcp/fastmcp/dist/chunk-7UDY4VFQ.cjs","../src/auth/helpers.ts","../src/auth/OAuthProxy.ts","../src/auth/types.ts","../src/auth/utils/claimsExtractor.ts","../src/auth/utils/consent.ts","../src/auth/utils/jwtIssuer.ts","../src/auth/utils/pkce.ts","../src/auth/utils/tokenStore.ts","../src/auth/providers/AuthProvider.ts","../src/auth/providers/AzureProvider.ts","../src/auth/providers/GitHubProvider.ts","../src/auth/providers/GoogleProvider.ts","../src/auth/providers/OAuthProvider.ts","../src/auth/utils/diskStore.ts","../src/auth/utils/jwks.ts"],"names":["randomBytes"],"mappings":"AAAA;ACaO,SAAS,cAAA,CACd,OAAA,EACG;AACH,EAAA,GAAA,CAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,KAAA,CAAM,8BAA8B,CAAA;AAAA,EAChD;AACA,EAAA,OAAO,OAAA;AACT;AAKO,SAAS,UAAA,CAAA,GACX,MAAA,EACmB;AACtB,EAAA,OAAO,CAAC,IAAA,EAAA,GACN,MAAA,CAAO,KAAA;AAAA,IAAM,CAAC,KAAA,EAAA,GACZ,OAAO,MAAA,IAAU,WAAA,EAAa,KAAA,CAAM,IAAI,EAAA,EAAI;AAAA,EAC9C,CAAA;AACJ;AAKO,SAAS,UAAA,CAAA,GACX,MAAA,EACmB;AACtB,EAAA,OAAO,CAAC,IAAA,EAAA,GACN,MAAA,CAAO,IAAA,CAAK,CAAC,KAAA,EAAA,GAAW,OAAO,MAAA,IAAU,WAAA,EAAa,KAAA,CAAM,IAAI,EAAA,EAAI,KAAM,CAAA;AAC9E;AAKO,SAAS,WAAA,CAAmC,IAAA,EAAkB;AACnE,EAAA,OAAO,KAAA,IAAS,KAAA,EAAA,GAAa,KAAA,IAAS,IAAA;AACxC;AAKO,SAAS,WAAA,CAAA,GACX,YAAA,EACmB;AACtB,EAAA,OAAO,CAAC,IAAA,EAAA,GAAqB;AAC3B,IAAA,GAAA,CAAI,CAAC,IAAA,EAAM,OAAO,KAAA;AAClB,IAAA,MAAM,KAAA,EAAQ,IAAA,CAAiC,IAAA;AAC/C,IAAA,OAAO,OAAO,KAAA,IAAS,SAAA,GAAY,YAAA,CAAa,QAAA,CAAS,IAAI,CAAA;AAAA,EAC/D,CAAA;AACF;AAKO,SAAS,aAAA,CAAA,GACX,cAAA,EACmB;AACtB,EAAA,OAAO,CAAC,IAAA,EAAA,GAAqB;AAC3B,IAAA,GAAA,CAAI,CAAC,IAAA,EAAM,OAAO,KAAA;AAClB,IAAA,MAAM,WAAA,EAAc,IAAA,CAAiC,MAAA;AACrD,IAAA,GAAA,CAAI,CAAC,UAAA,EAAY,OAAO,KAAA;AAExB,IAAA,MAAM,SAAA,EAAW,KAAA,CAAM,OAAA,CAAQ,UAAU,EAAA,EACrC,IAAI,GAAA,CAAI,UAAU,EAAA,EAClB,WAAA,WAAsB,IAAA,EACpB,WAAA,kBACA,IAAI,GAAA,CAAI,CAAA;AAEd,IAAA,OAAO,cAAA,CAAe,KAAA,CAAM,CAAC,KAAA,EAAA,GAAU,QAAA,CAAS,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,EAC5D,CAAA;AACF;ADjDA;AACA;AE9BA,gCAA4B;AAC5B,0BAAkB;AFgClB;AACA;AG/BO,IAAM,yBAAA,EAA2B,IAAA;AACjC,IAAM,oCAAA,EAAsC,OAAA;AAC5C,IAAM,0BAAA,EAA4B,MAAA;AAClC,IAAM,+BAAA,EAAiC,GAAA;AACvC,IAAM,wBAAA,EAA0B,GAAA;AHiCvC;AACA;AIvCO,IAAM,gBAAA,YAAN,MAAsB;AAAA,EACnB;AAAA;AAAA,iBAGS,iBAAA,kBAAmB,IAAI,GAAA,CAAI;AAAA,IAC1C,KAAA;AAAA,IACA,WAAA;AAAA,IACA,KAAA;AAAA,IACA,KAAA;AAAA,IACA,KAAA;AAAA,IACA,KAAA;AAAA,IACA;AAAA,EACF,CAAC,EAAA;AAAA,EAED,WAAA,CAAY,MAAA,EAAiD;AAE3D,IAAA,GAAA,CAAI,OAAO,OAAA,IAAW,SAAA,EAAW;AAC/B,MAAA,OAAA,EAAS,OAAA,EAAS,CAAC,EAAA,EAAI,EAAE,eAAA,EAAiB,KAAA,EAAO,WAAA,EAAa,MAAM,CAAA;AAAA,IACtE;AAGA,IAAA,IAAA,CAAK,OAAA,EAAS;AAAA,MACZ,kBAAA,EAAoB,MAAA,CAAO,mBAAA,GAAsB,KAAA;AAAA,MACjD,aAAA,EAAe,MAAA,CAAO,aAAA;AAAA,MACtB,aAAA,EAAe,MAAA,CAAO,cAAA,GAAiB,CAAC,CAAA;AAAA,MACxC,WAAA,EACE,MAAA,CAAO,YAAA,IAAgB,KAAA,EAAA,EAAY,MAAA,CAAO,YAAA,EAAc,KAAA;AAAA;AAAA,MAC1D,eAAA,EAAiB,MAAA,CAAO,gBAAA,IAAoB,KAAA;AAAA;AAAA,MAC5C,WAAA,EAAa,MAAA,CAAO,YAAA,IAAgB,KAAA;AAAA;AAAA,MACpC,iBAAA,EAAmB,MAAA,CAAO,kBAAA,GAAqB;AAAA,IACjD,CAAA;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAA,CACJ,KAAA,EACA,SAAA,EACyC;AAEzC,IAAA,GAAA,CAAI,UAAA,IAAc,SAAA,GAAY,CAAC,IAAA,CAAK,MAAA,CAAO,eAAA,EAAiB;AAC1D,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,GAAA,CAAI,UAAA,IAAc,KAAA,GAAQ,CAAC,IAAA,CAAK,MAAA,CAAO,WAAA,EAAa;AAClD,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,GAAA,CAAI,CAAC,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA,EAAG;AAEtB,MAAA,OAAO,IAAA;AAAA,IACT;AAIA,IAAA,MAAM,QAAA,EAAU,IAAA,CAAK,gBAAA,CAAiB,KAAK,CAAA;AAC3C,IAAA,GAAA,CAAI,CAAC,OAAA,EAAS;AACZ,MAAA,OAAO,IAAA;AAAA,IACT;AAGA,IAAA,MAAM,SAAA,EAAW,IAAA,CAAK,YAAA,CAAa,OAAO,CAAA;AAG1C,IAAA,OAAO,IAAA,CAAK,WAAA,CAAY,QAAQ,CAAA;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA,EAKQ,WAAA,CACN,MAAA,EACyB;AACzB,IAAA,MAAM,OAAA,EAAS,IAAA,CAAK,MAAA,CAAO,WAAA;AAG3B,IAAA,GAAA,CAAI,OAAA,IAAW,MAAA,GAAS,OAAA,IAAW,GAAA,GAAM,OAAA,IAAW,KAAA,CAAA,EAAW;AAC7D,MAAA,OAAO,MAAA;AAAA,IACT;AAGA,IAAA,MAAM,OAAA,EAAkC,CAAC,CAAA;AACzC,IAAA,IAAA,CAAA,MAAW,CAAC,GAAA,EAAK,KAAK,EAAA,GAAK,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,EAAG;AACjD,MAAA,MAAA,CAAO,CAAA,EAAA;AACT,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMQ,EAAA;AACF,IAAA;AACI,MAAA;AACF,MAAA;AACF,QAAA;AACF,MAAA;AAGM,MAAA;AACC,MAAA;AACA,IAAA;AAEC,MAAA;AACD,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AAGA,IAAA;AAEN,IAAA;AAEM,MAAA;AACF,QAAA;AACF,MAAA;AAGI,MAAA;AACF,QAAA;AACF,MAAA;AAIE,MAAA;AAGA,QAAA;AACF,MAAA;AAGK,MAAA;AACH,QAAA;AACA,QAAA;AACF,MAAA;AAEO,MAAA;AACT,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AACL,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACF,IAAA;AACK,MAAA;AACT,IAAA;AAEM,IAAA;AAGF,IAAA;AACI,MAAA;AACE,MAAA;AACV,IAAA;AAEI,IAAA;AACK,MAAA;AACT,IAAA;AAGU,IAAA;AAEH,MAAA;AACH,QAAA;AACF,MAAA;AAGI,MAAA;AACI,QAAA;AACA,QAAA;AACN,QAAA;AACM,MAAA;AAEN,QAAA;AACF,MAAA;AACF,IAAA;AAGO,IAAA;AACT,EAAA;AACF;AJnBc;AACA;AKtLL;AAOI;AACH,EAAA;AAEI,EAAA;AACL,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AAIQ,IAAA;AACJ,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AAEC,IAAA;AACL,MAAA;AACE,QAAA;AACF,MAAA;AACQ,MAAA;AACT,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AACU,IAAA;AAED,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAA;AAgKmC;AAAA;AAAA;AAAA;AAAA,uCAAA;AAK0C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAA;AAOW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8DAAA;AAaL;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBnF,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AACQ,IAAA;AACA,IAAA;AAEI,IAAA;AACZ,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AACM,IAAA;AACK,MAAA;AAEF,MAAA;AACH,QAAA;AACF,MAAA;AAEM,MAAA;AACA,MAAA;AAEF,MAAA;AACF,QAAA;AACF,MAAA;AAEM,MAAA;AAGA,MAAA;AACF,MAAA;AACF,QAAA;AACF,MAAA;AAEO,MAAA;AACD,IAAA;AACC,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKmB,EAAA;AACX,IAAA;AACC,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACP,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKoB,EAAA;AAEZ,IAAA;AACG,MAAA;AACC,MAAA;AACR,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACC,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKa,EAAA;AACJ,IAAA;AACT,EAAA;AACF;ALwJc;AACA;AM1eL;AACA;AAOH;AAgEO;AACH,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AAEI,EAAA;AACL,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAAA;AAMA,EAAA;AAIQ,IAAA;AACA,IAAA;AACC,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AAMQ,IAAA;AACA,IAAA;AAEA,IAAA;AACC,MAAA;AACL,MAAA;AACK,MAAA;AACA,MAAA;AACA,MAAA;AACL,MAAA;AACA,MAAA;AAAA;AAEI,MAAA;AACN,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AAMQ,IAAA;AACA,IAAA;AAEA,IAAA;AACC,MAAA;AACL,MAAA;AACK,MAAA;AACA,MAAA;AACA,MAAA;AACL,MAAA;AACA,MAAA;AAAA;AAEI,MAAA;AACN,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AACA,IAAA;AACI,MAAA;AACF,MAAA;AACF,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAEO,MAAA;AAGD,MAAA;AACF,MAAA;AACF,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAGM,MAAA;AACJ,QAAA;AACF,MAAA;AAGM,MAAA;AAEF,MAAA;AACF,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAEI,MAAA;AACF,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAEI,MAAA;AACF,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAEO,MAAA;AACL,QAAA;AACA,QAAA;AACF,MAAA;AACO,IAAA;AACA,MAAA;AACL,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACC,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKmC,EAAA;AAC3B,IAAA;AACD,IAAA;AACE,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKkB,EAAA;AACV,IAAA;AACC,MAAA;AACA,MAAA;AACP,IAAA;AAEM,IAAA;AACA,IAAA;AACJ,MAAA;AACF,IAAA;AAEM,IAAA;AAEI,IAAA;AACZ,EAAA;AACF;ANgYc;AACA;AOjoBL;AAOI;AAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAOd,EAAA;AAID,IAAA;AACK,MAAA;AACT,IAAA;AAEI,IAAA;AACI,MAAA;AACD,MAAA;AACE,MAAA;AACT,IAAA;AAEU,IAAA;AACZ,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,EAAA;AACC,IAAA;AACA,IAAA;AAEC,IAAA;AACL,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,EAAA;AACD,IAAA;AACI,MAAA;AACR,IAAA;AAIM,IAAA;AACA,IAAA;AAEC,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASO,EAAA;AAKA,IAAA;AACI,MAAA;AACT,IAAA;AAEI,IAAA;AACK,MAAA;AACT,IAAA;AAEI,IAAA;AACI,MAAA;AACC,MAAA;AACT,IAAA;AAGO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOe,EAAA;AACN,IAAA;AAKT,EAAA;AACF;APmmBc;AACA;AQ7sBd;AACE;AACA;AACA;AACA;AACK;AAaM;AACS,kBAAA;AACZ,EAAA;AACA,EAAA;AAEI,EAAA;AACL,IAAA;AAEC,IAAA;AACD,IAAA;AACP,EAAA;AAEM,EAAA;AACE,IAAA;AACR,EAAA;AAEM,EAAA;AACE,IAAA;AACR,EAAA;AAEU,EAAA;AACF,IAAA;AAED,IAAA;AACI,MAAA;AACT,IAAA;AAEI,IAAA;AACI,MAAA;AACJ,QAAA;AACK,QAAA;AACP,MAAA;AACO,MAAA;AACA,IAAA;AACC,MAAA;AACD,MAAA;AACT,IAAA;AACF,EAAA;AAEW,EAAA;AACH,IAAA;AACC,MAAA;AACA,MAAA;AACP,IAAA;AACM,IAAA;AACR,EAAA;AAEc,EAAA;AACN,IAAA;AACI,IAAA;AACF,MAAA;AACR,IAAA;AAEO,IAAA;AACD,IAAA;AACA,IAAA;AAEA,IAAA;AAEwD,IAAA;AAC5D,MAAA;AACF,IAAA;AAEI,IAAA;AACJ,IAAA;AAEO,IAAA;AACT,EAAA;AAEc,EAAA;AACN,IAAA;AACA,IAAA;AAEF,IAAA;AACJ,IAAA;AAGM,IAAA;AAKI,IAAA;AACZ,EAAA;AACF;AAKa;AACH,kBAAA;AACmC,kBAAA;AAE/B,EAAA;AAEL,IAAA;AACG,MAAA;AACN,MAAA;AACF,IAAA;AACF,EAAA;AAEM,EAAA;AACE,IAAA;AACA,IAAA;AAEN,IAAA;AACM,MAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAEA,IAAA;AACO,MAAA;AACP,IAAA;AACF,EAAA;AAEM,EAAA;AACC,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAKgB,EAAA;AACL,IAAA;AACP,MAAA;AACK,MAAA;AACP,IAAA;AACK,IAAA;AACP,EAAA;AAEU,EAAA;AACF,IAAA;AAED,IAAA;AACI,MAAA;AACT,IAAA;AAEU,IAAA;AACH,MAAA;AACE,MAAA;AACT,IAAA;AAEO,IAAA;AACT,EAAA;AAEW,EAAA;AACH,IAAA;AAED,IAAA;AACH,MAAA;AACA,MAAA;AACD,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAKe,EAAA;AACN,IAAA;AACT,EAAA;AACF;AR2pBc;AACA;AExyBD;AACH,kBAAA;AACA,kBAAA;AACA,kBAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,kBAAA;AACA,EAAA;AACA,kBAAA;AAEI,EAAA;AACL,IAAA;AACH,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAAiB;AACjB,MAAA;AACA,MAAA;AACA,MAAA;AACG,MAAA;AACL,IAAA;AAGI,IAAA;AAIE,IAAA;AAGD,IAAA;AAEG,MAAA;AAKN,MAAA;AACF,IAAA;AAEK,IAAA;AACA,IAAA;AACI,MAAA;AACT,IAAA;AAGS,IAAA;AAED,MAAA;AAED,MAAA;AACH,QAAA;AACA,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AAGM,IAAA;AAKF,IAAA;AACG,MAAA;AACP,IAAA;AAGK,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AAEC,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAEI,IAAA;AACI,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAGI,IAAA;AACI,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAGM,IAAA;AAGG,IAAA;AACA,MAAA;AACL,QAAA;AACK,QAAA;AACP,MAAA;AACF,IAAA;AAGO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKgB,EAAA;AACL,IAAA;AACP,MAAA;AACK,MAAA;AACP,IAAA;AAEK,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AAGA,IAAA;AACI,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAEM,IAAA;AACD,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAGI,IAAA;AACI,MAAA;AACR,IAAA;AAGI,IAAA;AACG,MAAA;AACG,QAAA;AACJ,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AAEM,MAAA;AACJ,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEK,MAAA;AACG,QAAA;AACR,MAAA;AACF,IAAA;AAGI,IAAA;AACI,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAGA,IAAA;AACK,IAAA;AAGI,IAAA;AAEA,MAAA;AACL,QAAA;AACA,QAAA;AACF,MAAA;AACK,IAAA;AAEC,MAAA;AACJ,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEI,MAAA;AACF,QAAA;AACF,MAAA;AAEI,MAAA;AACF,QAAA;AACF,MAAA;AAEI,MAAA;AACF,QAAA;AACF,MAAA;AAEO,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AACA,IAAA;AACI,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAGS,IAAA;AACA,MAAA;AACT,IAAA;AAGO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AAqBS,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACQ,MAAA;AACR,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AAGF,IAAA;AACI,MAAA;AACA,MAAA;AACR,IAAA;AAEK,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAGM,IAAA;AACD,IAAA;AACG,MAAA;AACR,IAAA;AAGM,IAAA;AAGA,IAAA;AACJ,MAAA;AACA,MAAA;AACF,IAAA;AAGK,IAAA;AAGC,IAAA;AACN,IAAA;AACA,IAAA;AAEO,IAAA;AACL,MAAA;AACE,QAAA;AACF,MAAA;AACQ,MAAA;AACT,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAED,IAAA;AACG,MAAA;AACR,IAAA;AAEM,IAAA;AACD,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAEI,IAAA;AAEG,MAAA;AACC,MAAA;AACN,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AAEO,MAAA;AACL,QAAA;AACE,UAAA;AACF,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AAGA,IAAA;AACK,IAAA;AAEE,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AAGM,IAAA;AACD,MAAA;AACT,IAAA;AAGM,IAAA;AACD,IAAA;AACI,MAAA;AACT,IAAA;AAGM,IAAA;AACJ,MAAA;AACF,IAAA;AAIK,IAAA;AACI,MAAA;AACT,IAAA;AAGM,IAAA;AACJ,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AAEC,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAGA,IAAA;AACO,MAAA;AACG,QAAA;AACJ,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAGM,IAAA;AACA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACM,QAAA;AACN,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAEK,IAAA;AAGC,IAAA;AACJ,MAAA;AACA,MAAA;AAAiD;AAEjD,MAAA;AACA,MAAA;AACA,MAAA;AAA0B;AAC1B,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACM,MAAA;AACN,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACA,MAAA;AACA,MAAA;AAEA,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACF,IAAA;AACK,MAAA;AACT,IAAA;AACS,MAAA;AACT,IAAA;AACS,MAAA;AACF,IAAA;AACE,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKwB,EAAA;AAChB,IAAA;AAGN,IAAA;AACM,MAAA;AACG,QAAA;AACP,MAAA;AACF,IAAA;AAGA,IAAA;AACM,MAAA;AACG,QAAA;AACP,MAAA;AACF,IAAA;AAGU,IAAA;AACZ,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AAGN,IAAA;AACA,IAAA;AAEA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACO,QAAA;AACP,MAAA;AACI,MAAA;AACJ,MAAA;AACA,MAAA;AACO,MAAA;AACA,MAAA;AACT,IAAA;AAEK,IAAA;AAEE,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AAIN,IAAA;AAGA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAGK,IAAA;AACH,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACJ,MAAA;AACF,IAAA;AAGI,IAAA;AACM,MAAA;AACV,IAAA;AAEM,IAAA;AACE,MAAA;AACN,MAAA;AACQ,MAAA;AACT,IAAA;AAEI,IAAA;AACG,MAAA;AAIA,MAAA;AACE,QAAA;AACA,QAAA;AACR,MAAA;AACF,IAAA;AAEM,IAAA;AAEC,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AACF,IAAA;AACF,MAAA;AACR,IAAA;AAEM,IAAA;AACD,IAAA;AACG,MAAA;AACR,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMc,EAAA;AAGF,IAAA;AACD,MAAA;AACT,IAAA;AAEM,IAAA;AAGA,IAAA;AACJ,MAAA;AACA,MAAA;AACF,IAAA;AACI,IAAA;AACK,MAAA;AACT,IAAA;AAGI,IAAA;AACI,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACI,MAAA;AAEF,QAAA;AACM,UAAA;AACF,YAAA;AACF,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AAIA,IAAA;AAEA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACO,QAAA;AACP,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAEK,IAAA;AAEE,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACCA,IAAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACCA,IAAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMQ,EAAA;AACA,IAAA;AACA,IAAA;AACC,MAAA;AACP,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACA,IAAA;AACC,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AAGN,IAAA;AAGA,IAAA;AACJ,MAAA;AACA,MAAA;AACI,MAAA;AACN,IAAA;AAGK,IAAA;AACH,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACJ,MAAA;AACF,IAAA;AAGI,IAAA;AACM,MAAA;AACV,IAAA;AAGM,IAAA;AACE,MAAA;AACN,MAAA;AACQ,MAAA;AACT,IAAA;AAEI,IAAA;AACG,MAAA;AAIA,MAAA;AACE,QAAA;AACA,QAAA;AACR,MAAA;AACF,IAAA;AAEM,IAAA;AAEC,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AAGF,IAAA;AACF,MAAA;AACR,IAAA;AAEM,IAAA;AACD,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAEM,IAAA;AACI,IAAA;AACF,MAAA;AACR,IAAA;AAEM,IAAA;AAMD,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAEM,IAAA;AACJ,MAAA;AACF,IAAA;AAEK,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAEK,IAAA;AACG,MAAA;AACJ,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AAEM,IAAA;AACJ,MAAA;AACQ,MAAA;AACV,IAAA;AAEI,IAAA;AACF,MAAA;AACF,IAAA;AAEM,IAAA;AAIA,IAAA;AACJ,MAAA;AACF,IAAA;AACM,IAAA;AAEA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAEO,IAAA;AACG,MAAA;AACR,MAAA;AACQ,MAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMc,EAAA;AAIF,IAAA;AACF,MAAA;AACR,IAAA;AAGM,IAAA;AAGF,IAAA;AACA,IAAA;AACF,MAAA;AACF,IAAA;AACE,MAAA;AACF,IAAA;AACE,MAAA;AACK,IAAA;AACL,MAAA;AACF,IAAA;AAIM,IAAA;AAOA,IAAA;AACA,IAAA;AACA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAGM,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAGM,IAAA;AAGA,IAAA;AACJ,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACK,QAAA;AACL,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACJ,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACF,IAAA;AAGI,IAAA;AACI,MAAA;AACJ,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACM,MAAA;AAGA,MAAA;AACJ,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AAEA,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AAMF,IAAA;AACF,MAAA;AACR,IAAA;AAEM,IAAA;AAEA,IAAA;AAEA,IAAA;AACA,IAAA;AAMA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACA,IAAA;AACJ,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACK,QAAA;AACL,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACJ,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACF,IAAA;AAEI,IAAA;AACI,MAAA;AACJ,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACM,MAAA;AAEA,MAAA;AACJ,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AAEA,MAAA;AACF,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACA,IAAA;AACE,MAAA;AACR,IAAA;AACO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMc,EAAA;AASN,IAAA;AAKA,IAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACD,IAAA;AAGG,IAAA;AACI,MAAA;AACA,MAAA;AAEA,MAAA;AACJ,QAAA;AACA,QAAA;AAGA,QAAA;AACA,QAAA;AAGA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AAEO,MAAA;AACT,IAAA;AAGM,IAAA;AACC,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACA,IAAA;AAEE,IAAA;AACA,IAAA;AACN,MAAA;AACQ,MAAA;AACV,IAAA;AACQ,IAAA;AACA,IAAA;AAEJ,IAAA;AACM,MAAA;AACV,IAAA;AAGU,IAAA;AACA,MAAA;AACN,QAAA;AACA,QAAA;AACF,MAAA;AACQ,MAAA;AACV,IAAA;AAEO,IAAA;AACL,MAAA;AACE,QAAA;AACF,MAAA;AACQ,MAAA;AACT,IAAA;AACH,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AAIN,IAAA;AAGA,IAAA;AACJ,MAAA;AACA,MAAA;AACI,MAAA;AACN,IAAA;AAGK,IAAA;AACH,MAAA;AACA,MAAA;AACF,IAAA;AAEM,IAAA;AACJ,MAAA;AACF,IAAA;AAGI,IAAA;AACM,MAAA;AACV,IAAA;AAGM,IAAA;AACE,MAAA;AACN,MAAA;AACQ,MAAA;AACT,IAAA;AAEI,IAAA;AACG,MAAA;AAIA,MAAA;AACE,QAAA;AACA,QAAA;AACR,MAAA;AACF,IAAA;AAEM,IAAA;AAIC,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACO,MAAA;AACP,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACD,IAAA;AACE,MAAA;AACC,IAAA;AACV,EAAA;AAAA;AAAA;AAAA;AAKQ,EAAA;AACF,IAAA;AACI,MAAA;AACA,MAAA;AAEN,MAAA;AACM,QAAA;AACF,UAAA;AACF,QAAA;AACF,MAAA;AAIE,MAAA;AAII,IAAA;AACC,MAAA;AACT,IAAA;AACF,EAAA;AACF;AAKa;AAEF,EAAA;AAIG,IAAA;AAJH,IAAA;AACA,IAAA;AACA,IAAA;AAGF,IAAA;AACP,EAAA;AAEqB,EAAA;AACZ,IAAA;AACE,MAAA;AACP,MAAA;AACF,IAAA;AACF,EAAA;AAEA,EAAA;AACS,IAAA;AACL,MAAA;AACQ,MAAA;AACT,IAAA;AACH,EAAA;AACF;AFgdc;AACA;ASrsDQ;AAGV,EAAA;AAAA;AAAA;AAAA;AAIwB,EAAA;AACtB,IAAA;AACH,MAAA;AACP,IAAA;AACO,IAAA;AACT,EAAA;AAEQ,EAAA;AAEI,EAAA;AACL,IAAA;AAEP,EAAA;AAAA;AAAA;AAAA;AAAA;AAMM,EAAA;AAGC,IAAA;AAEI,MAAA;AACT,IAAA;AAEM,IAAA;AACD,IAAA;AACI,MAAA;AACT,IAAA;AAEM,IAAA;AACA,IAAA;AAED,IAAA;AACI,MAAA;AACT,IAAA;AAEO,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AAYS,IAAA;AACL,MAAA;AACA,MAAA;AACA,MAAA;AACE,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACO,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKuB,EAAA;AACd,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AASU,EAAA;AACD,IAAA;AACL,MAAA;AACA,MAAA;AAGA,MAAA;AACA,MAAA;AACQ,MAAA;AACV,IAAA;AACF,EAAA;AAUF;AT8pDc;AACA;AUrzDD;AACH,EAAA;AAEI,EAAA;AACJ,IAAA;AACD,IAAA;AACP,EAAA;AAEU,EAAA;AACD,IAAA;AACL,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACQ,MAAA;AACR,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACH,EAAA;AAEU,EAAA;AACD,IAAA;AACT,EAAA;AAEU,EAAA;AACA,IAAA;AACV,EAAA;AAEU,EAAA;AACD,IAAA;AACT,EAAA;AACF;AVkzDc;AACA;AWj2DD;AACC,EAAA;AACJ,IAAA;AACR,EAAA;AAEU,EAAA;AACD,IAAA;AACL,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACQ,MAAA;AACR,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACH,EAAA;AAEU,EAAA;AACD,IAAA;AACT,EAAA;AAEU,EAAA;AACA,IAAA;AACV,EAAA;AAEU,EAAA;AACD,IAAA;AACT,EAAA;AACF;AX+1Dc;AACA;AYn4DD;AACC,EAAA;AACJ,IAAA;AACR,EAAA;AAEU,EAAA;AACD,IAAA;AACL,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACQ,MAAA;AACR,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACD,IAAA;AACH,EAAA;AAEU,EAAA;AACD,IAAA;AACT,EAAA;AAEU,EAAA;AACA,IAAA;AACV,EAAA;AAEU,EAAA;AACD,IAAA;AACT,EAAA;AACF;AZi4Dc;AACA;Aa56DD;AAGD,EAAA;AAEE,EAAA;AACJ,IAAA;AACD,IAAA;AACP,EAAA;AAEU,EAAA;AACD,IAAA;AACL,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACQ,MAAA;AACR,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AAED,IAAA;AACH,EAAA;AAEU,EAAA;AACD,IAAA;AACT,EAAA;AAEU,EAAA;AACA,IAAA;AACV,EAAA;AAEU,EAAA;AACD,IAAA;AACT,EAAA;AACF;Abs6Dc;AACA;Ac59DL;AACA;AAgCI;AACH,mBAAA;AACA,EAAA;AACA,EAAA;AAEI,EAAA;AACL,IAAA;AACA,IAAA;AAGK,IAAA;AAGJ,IAAA;AACD,IAAA;AACE,MAAA;AACJ,IAAA;AACL,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AACA,IAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AAEN,MAAA;AACO,QAAA;AACH,UAAA;AACF,QAAA;AAEI,QAAA;AACF,UAAA;AACA,UAAA;AACA,UAAA;AAEI,UAAA;AACF,YAAA;AACF,UAAA;AACF,QAAA;AAEE,UAAA;AACI,UAAA;AACF,YAAA;AACF,UAAA;AAEA,UAAA;AACF,QAAA;AACF,MAAA;AACO,IAAA;AACC,MAAA;AACV,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AACE,IAAA;AACF,IAAA;AACI,MAAA;AACC,IAAA;AAEF,MAAA;AACH,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKgB,EAAA;AACL,IAAA;AACP,MAAA;AACK,MAAA;AACP,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKU,EAAA;AACF,IAAA;AAEF,IAAA;AACI,MAAA;AACA,MAAA;AAGF,MAAA;AACI,QAAA;AACN,QAAA;AACF,MAAA;AAEO,MAAA;AACA,IAAA;AAEF,MAAA;AACH,QAAA;AACF,MAAA;AACQ,MAAA;AACD,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKW,EAAA;AACH,IAAA;AAEA,IAAA;AACA,IAAA;AAEA,IAAA;AACJ,MAAA;AACA,MAAA;AACF,IAAA;AAEI,IAAA;AACI,MAAA;AACC,IAAA;AACC,MAAA;AACF,MAAA;AACR,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKM,EAAA;AACA,IAAA;AACI,MAAA;AACA,MAAA;AACC,MAAA;AACD,IAAA;AACC,MAAA;AACT,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKc,EAAA;AACR,IAAA;AACI,MAAA;AACD,MAAA;AACG,QAAA;AACR,MAAA;AACO,IAAA;AACF,MAAA;AACG,QAAA;AACD,MAAA;AACC,QAAA;AACR,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKoB,EAAA;AAEZ,IAAA;AACC,IAAA;AACT,EAAA;AACF;Adm6Dc;AACA;AeviED;AACH,EAAA;AACA,EAAA;AACA,mBAAA;AACA,EAAA;AAEI,EAAA;AACL,IAAA;AACH,MAAA;AAAe;AACf,MAAA;AAAkB;AACf,MAAA;AACH,MAAA;AACQ,MAAA;AACV,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAKA,EAAA;AACS,IAAA;AACT,EAAA;AAAA;AAAA;AAAA;AAAA;AAMM,EAAA;AACE,IAAA;AAGD,IAAA;AACK,MAAA;AACR,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBM,EAAA;AACA,IAAA;AAEI,MAAA;AAGA,MAAA;AAEF,MAAA;AACF,QAAA;AACF,MAAA;AAEI,MAAA;AACF,QAAA;AACF,MAAA;AAEQ,MAAA;AACN,QAAA;AACK,QAAA;AACL,QAAA;AACF,MAAA;AAIM,MAAA;AACC,QAAA;AACL,QAAA;AACK,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACL,QAAA;AACG,QAAA;AAAA;AACL,MAAA;AAEO,MAAA;AACL,QAAA;AACA,QAAA;AACF,MAAA;AACO,IAAA;AACA,MAAA;AACL,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMc,EAAA;AACH,IAAA;AACP,MAAA;AACF,IAAA;AAEI,IAAA;AACG,MAAA;AACA,MAAA;AAGA,MAAA;AACC,QAAA;AACJ,QAAA;AACE,UAAA;AACA,UAAA;AACF,QAAA;AACF,MAAA;AACO,IAAA;AACD,MAAA;AACJ,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAAA;AAIF,MAAA;AACF,IAAA;AACF,EAAA;AAAA;AAAA;AAAA;AAAA;AAMmB,EAAA;AACZ,IAAA;AACK,MAAA;AACV,IAAA;AAEI,IAAA;AACK,MAAA;AACT,IAAA;AAEU,IAAA;AACD,MAAA;AACT,IAAA;AAEQ,IAAA;AACV,EAAA;AACF;AfshEc;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/fastmcp/fastmcp/dist/chunk-7UDY4VFQ.cjs","sourcesContent":[null,"/**\n * Authentication Helper Functions\n * Utility functions for use with canAccess on tools, resources, and prompts\n */\n\nimport type { OAuthSession } from \"./providers/AuthProvider.js\";\n\ntype SessionAuth = Record<string, unknown> | undefined;\n\n/**\n * Extract and type-cast OAuth session from canAccess context.\n * Throws if session is undefined (use with canAccess: requireAuth).\n */\nexport function getAuthSession<T extends OAuthSession = OAuthSession>(\n session: SessionAuth,\n): T {\n if (!session) {\n throw new Error(\"Session is not authenticated\");\n }\n return session as T;\n}\n\n/**\n * Combines multiple canAccess checks with AND logic.\n */\nexport function requireAll<T extends SessionAuth>(\n ...checks: Array<((auth: T) => boolean) | boolean>\n): (auth: T) => boolean {\n return (auth: T): boolean =>\n checks.every((check) =>\n typeof check === \"function\" ? check(auth) : check,\n );\n}\n\n/**\n * Combines multiple canAccess checks with OR logic.\n */\nexport function requireAny<T extends SessionAuth>(\n ...checks: Array<((auth: T) => boolean) | boolean>\n): (auth: T) => boolean {\n return (auth: T): boolean =>\n checks.some((check) => (typeof check === \"function\" ? check(auth) : check));\n}\n\n/**\n * Requires any authenticated session.\n */\nexport function requireAuth<T extends SessionAuth>(auth: T): boolean {\n return auth !== undefined && auth !== null;\n}\n\n/**\n * Requires session to have a specific role (OR logic for multiple roles).\n */\nexport function requireRole<T extends SessionAuth>(\n ...allowedRoles: string[]\n): (auth: T) => boolean {\n return (auth: T): boolean => {\n if (!auth) return false;\n const role = (auth as Record<string, unknown>).role;\n return typeof role === \"string\" && allowedRoles.includes(role);\n };\n}\n\n/**\n * Requires session to have specific scopes.\n */\nexport function requireScopes<T extends SessionAuth>(\n ...requiredScopes: string[]\n): (auth: T) => boolean {\n return (auth: T): boolean => {\n if (!auth) return false;\n const authScopes = (auth as Record<string, unknown>).scopes;\n if (!authScopes) return false;\n\n const scopeSet = Array.isArray(authScopes)\n ? new Set(authScopes)\n : authScopes instanceof Set\n ? authScopes\n : new Set();\n\n return requiredScopes.every((scope) => scopeSet.has(scope));\n };\n}\n","/**\n * OAuth 2.1 Proxy Implementation\n * Provides DCR-compatible interface for non-DCR OAuth providers\n */\n\nimport { randomBytes } from \"crypto\";\nimport { z } from \"zod\";\n\nimport type {\n AuthorizationParams,\n ClientCode,\n DCRRequest,\n DCRResponse,\n OAuthError,\n OAuthProxyConfig,\n OAuthTransaction,\n ProxyDCRClient,\n RefreshRequest,\n TokenRequest,\n TokenResponse,\n TokenStorage,\n UpstreamTokenSet,\n} from \"./types.js\";\n\nimport {\n DEFAULT_ACCESS_TOKEN_TTL,\n DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH,\n DEFAULT_AUTHORIZATION_CODE_TTL,\n DEFAULT_REFRESH_TOKEN_TTL,\n DEFAULT_TRANSACTION_TTL,\n} from \"./types.js\";\nimport { ClaimsExtractor } from \"./utils/claimsExtractor.js\";\nimport { ConsentManager } from \"./utils/consent.js\";\nimport { JWTIssuer } from \"./utils/jwtIssuer.js\";\nimport { PKCEUtils } from \"./utils/pkce.js\";\nimport {\n EncryptedTokenStorage,\n MemoryTokenStorage,\n} from \"./utils/tokenStore.js\";\n\n/**\n * OAuth 2.1 Proxy\n * Acts as transparent intermediary between MCP clients and upstream OAuth providers\n */\nexport class OAuthProxy {\n private claimsExtractor: ClaimsExtractor | null = null;\n private cleanupInterval: NodeJS.Timeout | null = null;\n private clientCodes: Map<string, ClientCode> = new Map();\n private config: OAuthProxyConfig;\n private consentManager: ConsentManager;\n private jwtIssuer?: JWTIssuer;\n private registeredClients: Map<string, ProxyDCRClient> = new Map();\n private tokenStorage: TokenStorage;\n private transactions: Map<string, OAuthTransaction> = new Map();\n\n constructor(config: OAuthProxyConfig) {\n this.config = {\n allowedRedirectUriPatterns: [\"https://*\", \"http://localhost:*\"],\n authorizationCodeTtl: DEFAULT_AUTHORIZATION_CODE_TTL,\n consentRequired: true,\n enableTokenSwap: true, // Enabled by default for security\n redirectPath: \"/oauth/callback\",\n transactionTtl: DEFAULT_TRANSACTION_TTL,\n upstreamTokenEndpointAuthMethod: \"client_secret_basic\",\n ...config,\n };\n\n // Set up token storage with encryption by default (matches Python's secure defaults)\n let storage = config.tokenStorage || new MemoryTokenStorage();\n\n // Wrap storage with encryption if not already encrypted\n // Check if it's already an EncryptedTokenStorage instance\n const isAlreadyEncrypted =\n storage.constructor.name === \"EncryptedTokenStorage\";\n\n if (!isAlreadyEncrypted && config.encryptionKey !== false) {\n // Auto-generate encryption key if not provided\n const encryptionKey =\n typeof config.encryptionKey === \"string\"\n ? config.encryptionKey\n : this.generateSigningKey();\n\n storage = new EncryptedTokenStorage(storage, encryptionKey);\n }\n\n this.tokenStorage = storage;\n this.consentManager = new ConsentManager(\n config.consentSigningKey || this.generateSigningKey(),\n );\n\n // Initialize JWT issuer if token swap is enabled\n if (this.config.enableTokenSwap) {\n // Auto-generate signing key if not provided\n const signingKey = this.config.jwtSigningKey || this.generateSigningKey();\n\n this.jwtIssuer = new JWTIssuer({\n audience: this.config.baseUrl,\n issuer: this.config.baseUrl,\n signingKey: signingKey,\n });\n }\n\n // Initialize claims extractor (enabled by default)\n const claimsConfig =\n config.customClaimsPassthrough !== undefined\n ? config.customClaimsPassthrough\n : true; // Default: enabled\n\n if (claimsConfig !== false) {\n this.claimsExtractor = new ClaimsExtractor(claimsConfig);\n }\n\n // Start periodic cleanup\n this.startCleanup();\n }\n\n /**\n * OAuth authorization endpoint\n */\n async authorize(params: AuthorizationParams): Promise<Response> {\n // Validate parameters\n if (!params.client_id || !params.redirect_uri || !params.response_type) {\n throw new OAuthProxyError(\n \"invalid_request\",\n \"Missing required parameters\",\n );\n }\n\n if (params.response_type !== \"code\") {\n throw new OAuthProxyError(\n \"unsupported_response_type\",\n \"Only 'code' response type is supported\",\n );\n }\n\n // Validate PKCE if provided\n if (params.code_challenge && !params.code_challenge_method) {\n throw new OAuthProxyError(\n \"invalid_request\",\n \"code_challenge_method required when code_challenge is present\",\n );\n }\n\n // Create transaction\n const transaction = await this.createTransaction(params);\n\n // If consent required, show consent screen\n if (this.config.consentRequired && !transaction.consentGiven) {\n return this.consentManager.createConsentResponse(\n transaction,\n this.getProviderName(),\n );\n }\n\n // Redirect to upstream provider\n return this.redirectToUpstream(transaction);\n }\n\n /**\n * Stop cleanup interval and destroy resources\n */\n destroy(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n\n this.transactions.clear();\n this.clientCodes.clear();\n this.registeredClients.clear();\n }\n\n /**\n * Token endpoint - exchange authorization code for tokens\n */\n async exchangeAuthorizationCode(\n request: TokenRequest,\n ): Promise<TokenResponse> {\n if (request.grant_type !== \"authorization_code\") {\n throw new OAuthProxyError(\n \"unsupported_grant_type\",\n \"Only authorization_code grant type is supported\",\n );\n }\n\n const clientCode = this.clientCodes.get(request.code);\n if (!clientCode) {\n throw new OAuthProxyError(\n \"invalid_grant\",\n \"Invalid or expired authorization code\",\n );\n }\n\n // Validate client\n if (clientCode.clientId !== request.client_id) {\n throw new OAuthProxyError(\"invalid_client\", \"Client ID mismatch\");\n }\n\n // Validate PKCE if used\n if (clientCode.codeChallenge) {\n if (!request.code_verifier) {\n throw new OAuthProxyError(\n \"invalid_request\",\n \"code_verifier required for PKCE\",\n );\n }\n\n const valid = PKCEUtils.validateChallenge(\n request.code_verifier,\n clientCode.codeChallenge,\n clientCode.codeChallengeMethod,\n );\n\n if (!valid) {\n throw new OAuthProxyError(\"invalid_grant\", \"Invalid PKCE verifier\");\n }\n }\n\n // Check if code was already used\n if (clientCode.used) {\n throw new OAuthProxyError(\n \"invalid_grant\",\n \"Authorization code already used\",\n );\n }\n\n // Mark code as used\n clientCode.used = true;\n this.clientCodes.set(request.code, clientCode);\n\n // Return tokens based on token swap setting\n if (this.config.enableTokenSwap && this.jwtIssuer) {\n // Token swap pattern: issue short-lived JWTs and store upstream tokens\n return await this.issueSwappedTokens(\n clientCode.clientId,\n clientCode.upstreamTokens,\n );\n } else {\n // Pass-through pattern: return upstream tokens directly\n const response: TokenResponse = {\n access_token: clientCode.upstreamTokens.accessToken,\n expires_in: clientCode.upstreamTokens.expiresIn,\n token_type: clientCode.upstreamTokens.tokenType,\n };\n\n if (clientCode.upstreamTokens.refreshToken) {\n response.refresh_token = clientCode.upstreamTokens.refreshToken;\n }\n\n if (clientCode.upstreamTokens.idToken) {\n response.id_token = clientCode.upstreamTokens.idToken;\n }\n\n if (clientCode.upstreamTokens.scope.length > 0) {\n response.scope = clientCode.upstreamTokens.scope.join(\" \");\n }\n\n return response;\n }\n }\n\n /**\n * Token endpoint - refresh access token\n */\n async exchangeRefreshToken(request: RefreshRequest): Promise<TokenResponse> {\n if (request.grant_type !== \"refresh_token\") {\n throw new OAuthProxyError(\n \"unsupported_grant_type\",\n \"Only refresh_token grant type is supported\",\n );\n }\n\n // Check for swap mode\n if (this.config.enableTokenSwap && this.jwtIssuer) {\n return await this.handleSwapModeRefresh(request);\n }\n\n // Passthrough mode: forward refresh token directly to upstream\n return await this.handlePassthroughRefresh(request);\n }\n\n /**\n * Get OAuth discovery metadata\n */\n getAuthorizationServerMetadata(): {\n authorizationEndpoint: string;\n codeChallengeMethodsSupported?: string[];\n dpopSigningAlgValuesSupported?: string[];\n grantTypesSupported?: string[];\n introspectionEndpoint?: string;\n issuer: string;\n jwksUri?: string;\n opPolicyUri?: string;\n opTosUri?: string;\n registrationEndpoint?: string;\n responseModesSupported?: string[];\n responseTypesSupported: string[];\n revocationEndpoint?: string;\n scopesSupported?: string[];\n serviceDocumentation?: string;\n tokenEndpoint: string;\n tokenEndpointAuthMethodsSupported?: string[];\n tokenEndpointAuthSigningAlgValuesSupported?: string[];\n uiLocalesSupported?: string[];\n } {\n return {\n authorizationEndpoint: `${this.config.baseUrl}/oauth/authorize`,\n codeChallengeMethodsSupported: [\"S256\", \"plain\"],\n grantTypesSupported: [\"authorization_code\", \"refresh_token\"],\n issuer: this.config.baseUrl,\n registrationEndpoint: `${this.config.baseUrl}/oauth/register`,\n responseTypesSupported: [\"code\"],\n scopesSupported: this.config.scopes || [],\n tokenEndpoint: `${this.config.baseUrl}/oauth/token`,\n tokenEndpointAuthMethodsSupported: [\n \"client_secret_basic\",\n \"client_secret_post\",\n ],\n };\n }\n\n /**\n * Handle OAuth callback from upstream provider\n */\n async handleCallback(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const code = url.searchParams.get(\"code\");\n const state = url.searchParams.get(\"state\");\n const error = url.searchParams.get(\"error\");\n\n // Check for errors from upstream\n if (error) {\n const errorDescription = url.searchParams.get(\"error_description\");\n throw new OAuthProxyError(error, errorDescription || undefined);\n }\n\n if (!code || !state) {\n throw new OAuthProxyError(\n \"invalid_request\",\n \"Missing code or state parameter\",\n );\n }\n\n // Retrieve transaction\n const transaction = this.transactions.get(state);\n if (!transaction) {\n throw new OAuthProxyError(\"invalid_request\", \"Invalid or expired state\");\n }\n\n // Exchange code with upstream provider\n const upstreamTokens = await this.exchangeUpstreamCode(code, transaction);\n\n // Generate authorization code for client\n const clientCode = this.generateAuthorizationCode(\n transaction,\n upstreamTokens,\n );\n\n // Clean up transaction\n this.transactions.delete(state);\n\n // Redirect to client callback with code\n const redirectUrl = new URL(transaction.clientCallbackUrl);\n redirectUrl.searchParams.set(\"code\", clientCode);\n redirectUrl.searchParams.set(\"state\", transaction.state);\n\n return new Response(null, {\n headers: {\n Location: redirectUrl.toString(),\n },\n status: 302,\n });\n }\n\n /**\n * Handle consent form submission\n */\n async handleConsent(request: Request): Promise<Response> {\n const formData = await request.formData();\n const transactionId = formData.get(\"transaction_id\") as string;\n const action = formData.get(\"action\") as string;\n\n if (!transactionId) {\n throw new OAuthProxyError(\"invalid_request\", \"Missing transaction_id\");\n }\n\n const transaction = this.transactions.get(transactionId);\n if (!transaction) {\n throw new OAuthProxyError(\n \"invalid_request\",\n \"Invalid or expired transaction\",\n );\n }\n\n if (action === \"deny\") {\n // User denied consent\n this.transactions.delete(transactionId);\n const redirectUrl = new URL(transaction.clientCallbackUrl);\n redirectUrl.searchParams.set(\"error\", \"access_denied\");\n redirectUrl.searchParams.set(\n \"error_description\",\n \"User denied authorization\",\n );\n redirectUrl.searchParams.set(\"state\", transaction.state);\n\n return new Response(null, {\n headers: {\n Location: redirectUrl.toString(),\n },\n status: 302,\n });\n }\n\n // User approved, mark consent and redirect to upstream\n transaction.consentGiven = true;\n this.transactions.set(transactionId, transaction);\n\n return this.redirectToUpstream(transaction);\n }\n\n /**\n * Load upstream tokens from a FastMCP JWT\n */\n async loadUpstreamTokens(\n fastmcpToken: string,\n ): Promise<null | UpstreamTokenSet> {\n if (!this.jwtIssuer) {\n return null;\n }\n\n // Verify FastMCP JWT\n const result = await this.jwtIssuer.verify(fastmcpToken);\n if (!result.valid || !result.claims?.jti) {\n return null;\n }\n\n // Look up token mapping\n const mapping = (await this.tokenStorage.get(\n `mapping:${result.claims.jti}`,\n )) as {\n upstreamTokenKey: string;\n } | null;\n\n if (!mapping) {\n return null;\n }\n\n // Retrieve upstream tokens\n const upstreamTokens = (await this.tokenStorage.get(\n `upstream:${mapping.upstreamTokenKey}`,\n )) as null | UpstreamTokenSet;\n\n return upstreamTokens;\n }\n\n /**\n * RFC 7591 Dynamic Client Registration\n */\n async registerClient(request: DCRRequest): Promise<DCRResponse> {\n // Validate required fields\n if (!request.redirect_uris || request.redirect_uris.length === 0) {\n throw new OAuthProxyError(\n \"invalid_client_metadata\",\n \"redirect_uris is required\",\n );\n }\n\n // Validate redirect URIs\n for (const uri of request.redirect_uris) {\n if (!this.validateRedirectUri(uri)) {\n throw new OAuthProxyError(\n \"invalid_redirect_uri\",\n `Invalid redirect URI: ${uri}`,\n );\n }\n }\n\n // Store client registration (indexed by primary redirect URI)\n const clientId = this.config.upstreamClientId;\n const client: ProxyDCRClient = {\n callbackUrl: request.redirect_uris[0],\n clientId,\n clientSecret: this.config.upstreamClientSecret,\n metadata: {\n client_name: request.client_name,\n client_uri: request.client_uri,\n contacts: request.contacts,\n jwks: request.jwks,\n jwks_uri: request.jwks_uri,\n logo_uri: request.logo_uri,\n policy_uri: request.policy_uri,\n scope: request.scope,\n software_id: request.software_id,\n software_version: request.software_version,\n tos_uri: request.tos_uri,\n },\n registeredAt: new Date(),\n };\n\n this.registeredClients.set(request.redirect_uris[0], client);\n\n // Return RFC 7591 compliant response\n const response: DCRResponse = {\n client_id: clientId,\n client_id_issued_at: Math.floor(Date.now() / 1000),\n // Echo back optional metadata\n client_name: request.client_name,\n client_secret: this.config.upstreamClientSecret,\n client_secret_expires_at: 0, // Never expires\n client_uri: request.client_uri,\n contacts: request.contacts,\n grant_types: request.grant_types || [\n \"authorization_code\",\n \"refresh_token\",\n ],\n jwks: request.jwks,\n jwks_uri: request.jwks_uri,\n logo_uri: request.logo_uri,\n policy_uri: request.policy_uri,\n redirect_uris: request.redirect_uris,\n response_types: request.response_types || [\"code\"],\n scope: request.scope,\n software_id: request.software_id,\n software_version: request.software_version,\n token_endpoint_auth_method:\n request.token_endpoint_auth_method || \"client_secret_basic\",\n tos_uri: request.tos_uri,\n };\n\n return response;\n }\n\n /**\n * Calculate access token TTL from upstream tokens\n */\n private calculateAccessTokenTtl(upstreamTokens: UpstreamTokenSet): number {\n if (upstreamTokens.expiresIn > 0) {\n return upstreamTokens.expiresIn;\n } else if (this.config.accessTokenTtl) {\n return this.config.accessTokenTtl;\n } else if (upstreamTokens.refreshToken) {\n return DEFAULT_ACCESS_TOKEN_TTL;\n } else {\n return DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH;\n }\n }\n\n /**\n * Clean up expired transactions and codes\n */\n private cleanup(): void {\n const now = Date.now();\n\n // Clean up expired transactions\n for (const [id, transaction] of this.transactions.entries()) {\n if (transaction.expiresAt.getTime() < now) {\n this.transactions.delete(id);\n }\n }\n\n // Clean up expired codes\n for (const [code, clientCode] of this.clientCodes.entries()) {\n if (clientCode.expiresAt.getTime() < now) {\n this.clientCodes.delete(code);\n }\n }\n\n // Clean up token storage\n void this.tokenStorage.cleanup();\n }\n\n /**\n * Create a new OAuth transaction\n */\n private async createTransaction(\n params: AuthorizationParams,\n ): Promise<OAuthTransaction> {\n const transactionId = this.generateId();\n const proxyPkce = PKCEUtils.generatePair(\"S256\");\n\n const transaction: OAuthTransaction = {\n clientCallbackUrl: params.redirect_uri,\n clientCodeChallenge: params.code_challenge || \"\",\n clientCodeChallengeMethod: params.code_challenge_method || \"plain\",\n clientId: params.client_id,\n createdAt: new Date(),\n expiresAt: new Date(\n Date.now() + (this.config.transactionTtl || 600) * 1000,\n ),\n id: transactionId,\n proxyCodeChallenge: proxyPkce.challenge,\n proxyCodeVerifier: proxyPkce.verifier,\n scope: params.scope ? params.scope.split(\" \") : this.config.scopes || [],\n state: params.state || this.generateId(),\n };\n\n this.transactions.set(transactionId, transaction);\n\n return transaction;\n }\n\n /**\n * Exchange authorization code with upstream provider\n */\n private async exchangeUpstreamCode(\n code: string,\n transaction: OAuthTransaction,\n ): Promise<UpstreamTokenSet> {\n const useBasicAuth =\n this.config.upstreamTokenEndpointAuthMethod === \"client_secret_basic\";\n\n const bodyParams: Record<string, string> = {\n code,\n code_verifier: transaction.proxyCodeVerifier,\n grant_type: \"authorization_code\",\n redirect_uri: `${this.config.baseUrl}${this.config.redirectPath}`,\n };\n\n // Include client credentials in body only for client_secret_post\n if (!useBasicAuth) {\n bodyParams.client_id = this.config.upstreamClientId;\n bodyParams.client_secret = this.config.upstreamClientSecret;\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n };\n\n // Add Basic Auth header for client_secret_basic\n if (useBasicAuth) {\n headers[\"Authorization\"] = this.getBasicAuthHeader();\n }\n\n const tokenResponse = await fetch(this.config.upstreamTokenEndpoint, {\n body: new URLSearchParams(bodyParams),\n headers,\n method: \"POST\",\n });\n\n if (!tokenResponse.ok) {\n const error = (await tokenResponse.json()) as {\n error?: string;\n error_description?: string;\n };\n throw new OAuthProxyError(\n error.error || \"server_error\",\n error.error_description,\n );\n }\n\n const tokens = await this.parseTokenResponse(tokenResponse);\n\n return {\n accessToken: tokens.access_token,\n expiresIn: tokens.expires_in || 3600,\n idToken: tokens.id_token,\n issuedAt: new Date(),\n refreshExpiresIn: tokens.refresh_expires_in,\n refreshToken: tokens.refresh_token,\n scope: tokens.scope ? tokens.scope.split(\" \") : transaction.scope,\n tokenType: tokens.token_type || \"Bearer\",\n };\n }\n\n /**\n * Extract JTI from a JWT token\n */\n private async extractJti(token: string): Promise<string> {\n if (!this.jwtIssuer) {\n throw new Error(\"JWT issuer not initialized\");\n }\n\n const result = await this.jwtIssuer.verify(token);\n if (!result.valid || !result.claims?.jti) {\n throw new Error(\"Failed to extract JTI from token\");\n }\n\n return result.claims.jti;\n }\n\n /**\n * Extract custom claims from upstream tokens\n * Combines claims from access token and ID token (if present)\n */\n private async extractUpstreamClaims(\n upstreamTokens: UpstreamTokenSet,\n ): Promise<null | Record<string, unknown>> {\n if (!this.claimsExtractor) {\n return null;\n }\n\n const allClaims: Record<string, unknown> = {};\n\n // Extract from access token (if JWT format)\n const accessClaims = await this.claimsExtractor.extract(\n upstreamTokens.accessToken,\n \"access\",\n );\n if (accessClaims) {\n Object.assign(allClaims, accessClaims);\n }\n\n // Extract from ID token (if present and JWT format)\n if (upstreamTokens.idToken) {\n const idClaims = await this.claimsExtractor.extract(\n upstreamTokens.idToken,\n \"id\",\n );\n if (idClaims) {\n // Access token claims take precedence over ID token claims\n for (const [key, value] of Object.entries(idClaims)) {\n if (!(key in allClaims)) {\n allClaims[key] = value;\n }\n }\n }\n }\n\n return Object.keys(allClaims).length > 0 ? allClaims : null;\n }\n\n /**\n * Generate authorization code for client\n */\n private generateAuthorizationCode(\n transaction: OAuthTransaction,\n upstreamTokens: UpstreamTokenSet,\n ): string {\n const code = this.generateId();\n\n const clientCode: ClientCode = {\n clientId: transaction.clientId,\n code,\n codeChallenge: transaction.clientCodeChallenge,\n codeChallengeMethod: transaction.clientCodeChallengeMethod,\n createdAt: new Date(),\n expiresAt: new Date(\n Date.now() + (this.config.authorizationCodeTtl || 300) * 1000,\n ),\n transactionId: transaction.id,\n upstreamTokens,\n };\n\n this.clientCodes.set(code, clientCode);\n\n return code;\n }\n\n /**\n * Generate secure random ID\n */\n private generateId(): string {\n return randomBytes(32).toString(\"base64url\");\n }\n\n /**\n * Generate signing key for consent cookies\n */\n private generateSigningKey(): string {\n return randomBytes(32).toString(\"hex\");\n }\n\n /**\n * Generate Basic auth header value for upstream token endpoint\n * Per RFC 6749 Section 2.3.1, credentials must be URL-encoded before base64 encoding\n */\n private getBasicAuthHeader(): string {\n const encodedClientId = encodeURIComponent(this.config.upstreamClientId);\n const encodedClientSecret = encodeURIComponent(\n this.config.upstreamClientSecret,\n );\n return `Basic ${Buffer.from(`${encodedClientId}:${encodedClientSecret}`).toString(\"base64\")}`;\n }\n\n /**\n * Get provider name for display\n */\n private getProviderName(): string {\n const url = new URL(this.config.upstreamAuthorizationEndpoint);\n return url.hostname;\n }\n\n /**\n * Handle passthrough mode refresh - forward refresh token directly to upstream\n */\n private async handlePassthroughRefresh(\n request: RefreshRequest,\n ): Promise<TokenResponse> {\n const useBasicAuth =\n this.config.upstreamTokenEndpointAuthMethod === \"client_secret_basic\";\n\n const bodyParams: Record<string, string> = {\n grant_type: \"refresh_token\",\n refresh_token: request.refresh_token,\n ...(request.scope && { scope: request.scope }),\n };\n\n // Include client credentials in body only for client_secret_post\n if (!useBasicAuth) {\n bodyParams.client_id = this.config.upstreamClientId;\n bodyParams.client_secret = this.config.upstreamClientSecret;\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n };\n\n // Add Basic Auth header for client_secret_basic\n if (useBasicAuth) {\n headers[\"Authorization\"] = this.getBasicAuthHeader();\n }\n\n // Exchange refresh token with upstream provider\n const tokenResponse = await fetch(this.config.upstreamTokenEndpoint, {\n body: new URLSearchParams(bodyParams),\n headers,\n method: \"POST\",\n });\n\n if (!tokenResponse.ok) {\n const error = (await tokenResponse.json()) as {\n error?: string;\n error_description?: string;\n };\n throw new OAuthProxyError(\n error.error || \"invalid_grant\",\n error.error_description,\n );\n }\n\n const tokens = await this.parseTokenResponse(tokenResponse);\n\n return {\n access_token: tokens.access_token,\n expires_in: tokens.expires_in || 3600,\n id_token: tokens.id_token,\n refresh_token: tokens.refresh_token,\n scope: tokens.scope,\n token_type: tokens.token_type || \"Bearer\",\n };\n }\n\n /**\n * Handle swap mode refresh - verify FastMCP JWT and issue new tokens\n */\n private async handleSwapModeRefresh(\n request: RefreshRequest,\n ): Promise<TokenResponse> {\n if (!this.jwtIssuer) {\n throw new Error(\"JWT issuer not initialized\");\n }\n\n const verifyResult = await this.jwtIssuer.verify(request.refresh_token);\n if (!verifyResult.valid) {\n throw new OAuthProxyError(\n \"invalid_grant\",\n \"Invalid or expired refresh token\",\n );\n }\n\n const jti = verifyResult.claims?.jti;\n if (!jti) {\n throw new OAuthProxyError(\"invalid_grant\", \"Refresh token missing JTI\");\n }\n\n const mapping = (await this.tokenStorage.get(`mapping:${jti}`)) as {\n clientId: string;\n scope: string[];\n upstreamTokenKey: string;\n } | null;\n\n if (!mapping) {\n throw new OAuthProxyError(\n \"invalid_grant\",\n \"Refresh token already used or expired\",\n );\n }\n\n const upstreamTokens = (await this.tokenStorage.get(\n `upstream:${mapping.upstreamTokenKey}`,\n )) as null | UpstreamTokenSet;\n\n if (!upstreamTokens) {\n throw new OAuthProxyError(\n \"invalid_grant\",\n \"Upstream tokens not found or expired\",\n );\n }\n\n if (!upstreamTokens.refreshToken) {\n throw new OAuthProxyError(\n \"invalid_grant\",\n \"No upstream refresh token available\",\n );\n }\n\n const refreshedUpstreamTokens = await this.refreshUpstreamTokens(\n upstreamTokens.refreshToken,\n request.scope,\n );\n\n if (refreshedUpstreamTokens.scope.length === 0) {\n refreshedUpstreamTokens.scope = upstreamTokens.scope;\n }\n\n const refreshTokenTtl =\n refreshedUpstreamTokens.refreshExpiresIn ??\n this.config.refreshTokenTtl ??\n DEFAULT_REFRESH_TOKEN_TTL;\n const accessTokenTtl = this.calculateAccessTokenTtl(\n refreshedUpstreamTokens,\n );\n const upstreamStorageTtl = Math.max(accessTokenTtl, refreshTokenTtl, 1);\n\n await this.tokenStorage.save(\n `upstream:${mapping.upstreamTokenKey}`,\n refreshedUpstreamTokens,\n upstreamStorageTtl,\n );\n\n return await this.issueSwappedTokensForRefresh(\n mapping.clientId,\n refreshedUpstreamTokens,\n mapping.upstreamTokenKey,\n jti,\n );\n }\n\n /**\n * Issue swapped tokens (JWT pattern)\n * Issues short-lived FastMCP JWTs and stores upstream tokens securely\n */\n private async issueSwappedTokens(\n clientId: string,\n upstreamTokens: UpstreamTokenSet,\n ): Promise<TokenResponse> {\n if (!this.jwtIssuer) {\n throw new Error(\"JWT issuer not initialized\");\n }\n\n // Extract custom claims from upstream tokens\n const customClaims = await this.extractUpstreamClaims(upstreamTokens);\n\n // Determine access token TTL (hierarchical: upstream → config → default)\n let accessTokenTtl: number;\n if (upstreamTokens.expiresIn > 0) {\n accessTokenTtl = upstreamTokens.expiresIn;\n } else if (this.config.accessTokenTtl) {\n accessTokenTtl = this.config.accessTokenTtl;\n } else if (upstreamTokens.refreshToken) {\n accessTokenTtl = DEFAULT_ACCESS_TOKEN_TTL;\n } else {\n accessTokenTtl = DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH;\n }\n\n // Determine refresh token TTL early (needed for upstream storage TTL)\n // Use upstream's refresh_expires_in if provided, otherwise fall back to config/default\n const refreshTokenTtl = upstreamTokens.refreshToken\n ? (upstreamTokens.refreshExpiresIn ??\n this.config.refreshTokenTtl ??\n DEFAULT_REFRESH_TOKEN_TTL)\n : 0;\n\n // Store upstream tokens with longest-lived token TTL (min 1s for safety)\n const upstreamStorageTtl = Math.max(accessTokenTtl, refreshTokenTtl, 1);\n const upstreamTokenKey = this.generateId();\n await this.tokenStorage.save(\n `upstream:${upstreamTokenKey}`,\n upstreamTokens,\n upstreamStorageTtl,\n );\n\n // Issue FastMCP access token with custom claims\n const accessToken = this.jwtIssuer.issueAccessToken(\n clientId,\n upstreamTokens.scope,\n customClaims || undefined,\n accessTokenTtl,\n );\n\n // Decode JWT to get JTI\n const accessJti = await this.extractJti(accessToken);\n\n // Store token mapping\n await this.tokenStorage.save(\n `mapping:${accessJti}`,\n {\n clientId,\n createdAt: new Date(),\n expiresAt: new Date(Date.now() + accessTokenTtl * 1000),\n jti: accessJti,\n scope: upstreamTokens.scope,\n upstreamTokenKey,\n },\n accessTokenTtl,\n );\n\n const response: TokenResponse = {\n access_token: accessToken,\n expires_in: accessTokenTtl,\n scope: upstreamTokens.scope.join(\" \"),\n token_type: \"Bearer\",\n };\n\n // Issue refresh token if upstream provided one\n if (upstreamTokens.refreshToken) {\n const refreshToken = this.jwtIssuer.issueRefreshToken(\n clientId,\n upstreamTokens.scope,\n customClaims || undefined,\n refreshTokenTtl,\n );\n const refreshJti = await this.extractJti(refreshToken);\n\n // Store refresh token mapping\n await this.tokenStorage.save(\n `mapping:${refreshJti}`,\n {\n clientId,\n createdAt: new Date(),\n expiresAt: new Date(Date.now() + refreshTokenTtl * 1000),\n jti: refreshJti,\n scope: upstreamTokens.scope,\n upstreamTokenKey,\n },\n refreshTokenTtl,\n );\n\n response.refresh_token = refreshToken;\n }\n\n return response;\n }\n\n /**\n * Issue swapped tokens for refresh flow\n */\n private async issueSwappedTokensForRefresh(\n clientId: string,\n upstreamTokens: UpstreamTokenSet,\n upstreamTokenKey: string,\n oldJti: string,\n ): Promise<TokenResponse> {\n if (!this.jwtIssuer) {\n throw new Error(\"JWT issuer not initialized\");\n }\n\n await this.tokenStorage.delete(`mapping:${oldJti}`);\n\n const customClaims = await this.extractUpstreamClaims(upstreamTokens);\n\n const accessTokenTtl = this.calculateAccessTokenTtl(upstreamTokens);\n const refreshTokenTtl = upstreamTokens.refreshToken\n ? (upstreamTokens.refreshExpiresIn ??\n this.config.refreshTokenTtl ??\n DEFAULT_REFRESH_TOKEN_TTL)\n : 0;\n\n const accessToken = this.jwtIssuer.issueAccessToken(\n clientId,\n upstreamTokens.scope,\n customClaims || undefined,\n accessTokenTtl,\n );\n\n const accessJti = await this.extractJti(accessToken);\n await this.tokenStorage.save(\n `mapping:${accessJti}`,\n {\n clientId,\n createdAt: new Date(),\n expiresAt: new Date(Date.now() + accessTokenTtl * 1000),\n jti: accessJti,\n scope: upstreamTokens.scope,\n upstreamTokenKey,\n },\n accessTokenTtl,\n );\n\n const response: TokenResponse = {\n access_token: accessToken,\n expires_in: accessTokenTtl,\n scope: upstreamTokens.scope.join(\" \"),\n token_type: \"Bearer\",\n };\n\n if (upstreamTokens.refreshToken) {\n const refreshToken = this.jwtIssuer.issueRefreshToken(\n clientId,\n upstreamTokens.scope,\n customClaims || undefined,\n refreshTokenTtl,\n );\n const refreshJti = await this.extractJti(refreshToken);\n\n await this.tokenStorage.save(\n `mapping:${refreshJti}`,\n {\n clientId,\n createdAt: new Date(),\n expiresAt: new Date(Date.now() + refreshTokenTtl * 1000),\n jti: refreshJti,\n scope: upstreamTokens.scope,\n upstreamTokenKey,\n },\n refreshTokenTtl,\n );\n\n response.refresh_token = refreshToken;\n }\n\n return response;\n }\n\n /**\n * Match URI against pattern (supports wildcards)\n */\n private matchesPattern(uri: string, pattern: string): boolean {\n const regex = new RegExp(\n \"^\" + pattern.replace(/\\*/g, \".*\").replace(/\\?/g, \".\") + \"$\",\n );\n return regex.test(uri);\n }\n\n /**\n * Parse token response that can be either JSON or URL-encoded\n * GitHub Apps return URL-encoded format, most providers return JSON\n */\n private async parseTokenResponse(response: Response): Promise<{\n access_token: string;\n expires_in?: number;\n id_token?: string;\n refresh_expires_in?: number;\n refresh_token?: string;\n scope?: string;\n token_type?: string;\n }> {\n const contentType = (\n response.headers.get(\"content-type\") || \"\"\n ).toLowerCase();\n\n // Define Zod schema for token response validation\n const tokenResponseSchema = z.object({\n access_token: z.string().min(1, \"access_token cannot be empty\"),\n expires_in: z.coerce.number().int().positive().optional(),\n id_token: z.string().optional(),\n refresh_expires_in: z.coerce.number().int().positive().optional(),\n refresh_token: z.string().optional(),\n scope: z.string().optional(),\n token_type: z.string().optional(),\n });\n\n // Check if response is URL-encoded (e.g., GitHub Apps)\n if (contentType.includes(\"application/x-www-form-urlencoded\")) {\n const text = await response.text();\n const params = new URLSearchParams(text);\n\n const rawData = {\n access_token: params.get(\"access_token\") || \"\",\n expires_in: params.get(\"expires_in\")\n ? parseInt(params.get(\"expires_in\")!)\n : undefined,\n id_token: params.get(\"id_token\") || undefined,\n refresh_expires_in: params.get(\"refresh_expires_in\")\n ? parseInt(params.get(\"refresh_expires_in\")!)\n : undefined,\n refresh_token: params.get(\"refresh_token\") || undefined,\n scope: params.get(\"scope\") || undefined,\n token_type: params.get(\"token_type\") || undefined,\n };\n\n return tokenResponseSchema.parse(rawData);\n }\n\n // Default to JSON parsing\n const rawJson = await response.json();\n return tokenResponseSchema.parse(rawJson);\n }\n\n /**\n * Redirect to upstream OAuth provider\n */\n private redirectToUpstream(transaction: OAuthTransaction): Response {\n const authUrl = new URL(this.config.upstreamAuthorizationEndpoint);\n\n authUrl.searchParams.set(\"client_id\", this.config.upstreamClientId);\n authUrl.searchParams.set(\n \"redirect_uri\",\n `${this.config.baseUrl}${this.config.redirectPath}`,\n );\n authUrl.searchParams.set(\"response_type\", \"code\");\n authUrl.searchParams.set(\"state\", transaction.id);\n\n if (transaction.scope.length > 0) {\n authUrl.searchParams.set(\"scope\", transaction.scope.join(\" \"));\n }\n\n // Add PKCE if not forwarding client PKCE\n if (!this.config.forwardPkce) {\n authUrl.searchParams.set(\n \"code_challenge\",\n transaction.proxyCodeChallenge,\n );\n authUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n }\n\n return new Response(null, {\n headers: {\n Location: authUrl.toString(),\n },\n status: 302,\n });\n }\n\n /**\n * Refresh upstream tokens with provider\n */\n private async refreshUpstreamTokens(\n upstreamRefreshToken: string,\n requestedScope?: string,\n ): Promise<UpstreamTokenSet> {\n const useBasicAuth =\n this.config.upstreamTokenEndpointAuthMethod === \"client_secret_basic\";\n\n const bodyParams: Record<string, string> = {\n grant_type: \"refresh_token\",\n refresh_token: upstreamRefreshToken,\n ...(requestedScope && { scope: requestedScope }),\n };\n\n // Include client credentials in body only for client_secret_post\n if (!useBasicAuth) {\n bodyParams.client_id = this.config.upstreamClientId;\n bodyParams.client_secret = this.config.upstreamClientSecret;\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n };\n\n // Add Basic Auth header for client_secret_basic\n if (useBasicAuth) {\n headers[\"Authorization\"] = this.getBasicAuthHeader();\n }\n\n // Exchange refresh token with upstream provider\n const tokenResponse = await fetch(this.config.upstreamTokenEndpoint, {\n body: new URLSearchParams(bodyParams),\n headers,\n method: \"POST\",\n });\n\n if (!tokenResponse.ok) {\n const error = (await tokenResponse.json()) as {\n error?: string;\n error_description?: string;\n };\n throw new OAuthProxyError(\n error.error || \"invalid_grant\",\n error.error_description || \"Upstream refresh failed\",\n );\n }\n\n const tokens = await this.parseTokenResponse(tokenResponse);\n\n // Handle token rotation: if upstream doesn't return new refresh token,\n // preserve the original one\n return {\n accessToken: tokens.access_token,\n expiresIn: tokens.expires_in || 3600,\n idToken: tokens.id_token,\n issuedAt: new Date(),\n refreshExpiresIn: tokens.refresh_expires_in,\n refreshToken: tokens.refresh_token || upstreamRefreshToken,\n scope: tokens.scope ? tokens.scope.split(\" \") : [],\n tokenType: tokens.token_type || \"Bearer\",\n };\n }\n\n /**\n * Start periodic cleanup of expired transactions and codes\n */\n private startCleanup(): void {\n this.cleanupInterval = setInterval(() => {\n this.cleanup();\n }, 60000); // Run every minute\n }\n\n /**\n * Validate redirect URI against allowed patterns\n */\n private validateRedirectUri(uri: string): boolean {\n try {\n const url = new URL(uri);\n const patterns = this.config.allowedRedirectUriPatterns || [];\n\n for (const pattern of patterns) {\n if (this.matchesPattern(uri, pattern)) {\n return true;\n }\n }\n\n // Default: allow https and localhost\n return (\n url.protocol === \"https:\" ||\n url.hostname === \"localhost\" ||\n url.hostname === \"127.0.0.1\"\n );\n } catch {\n return false;\n }\n }\n}\n\n/**\n * OAuth Proxy Error\n */\nexport class OAuthProxyError extends Error {\n constructor(\n public code: string,\n public description?: string,\n public statusCode: number = 400,\n ) {\n super(code);\n this.name = \"OAuthProxyError\";\n }\n\n toJSON(): OAuthError {\n return {\n error: this.code,\n error_description: this.description,\n };\n }\n\n toResponse(): Response {\n return new Response(JSON.stringify(this.toJSON()), {\n headers: { \"Content-Type\": \"application/json\" },\n status: this.statusCode,\n });\n }\n}\n","/**\n * OAuth Proxy Types\n * Type definitions for the OAuth 2.1 Proxy implementation\n */\n\n/**\n * Default TTL values for token expiration (in seconds)\n */\nexport const DEFAULT_ACCESS_TOKEN_TTL = 3600; // 1 hour\nexport const DEFAULT_ACCESS_TOKEN_TTL_NO_REFRESH = 31536000; // 1 year\nexport const DEFAULT_REFRESH_TOKEN_TTL = 2592000; // 30 days\nexport const DEFAULT_AUTHORIZATION_CODE_TTL = 300; // 5 minutes\nexport const DEFAULT_TRANSACTION_TTL = 600; // 10 minutes\n\n/**\n * OAuth authorization request parameters\n */\nexport interface AuthorizationParams {\n [key: string]: unknown;\n client_id: string;\n code_challenge?: string;\n code_challenge_method?: string;\n redirect_uri: string;\n response_type: string;\n scope?: string;\n state?: string;\n}\n\n/**\n * Authorization code storage with PKCE validation\n */\nexport interface ClientCode {\n /** Client ID that owns this code */\n clientId: string;\n /** Authorization code */\n code: string;\n /** PKCE code challenge for validation */\n codeChallenge: string;\n /** PKCE code challenge method */\n codeChallengeMethod: string;\n /** Code creation timestamp */\n createdAt: Date;\n /** Code expiration timestamp */\n expiresAt: Date;\n /** Associated transaction ID */\n transactionId: string;\n /** Upstream tokens obtained from provider */\n upstreamTokens: UpstreamTokenSet;\n /** Whether code has been used */\n used?: boolean;\n}\n\n/**\n * Consent data for user approval\n */\nexport interface ConsentData {\n clientName: string;\n provider: string;\n scope: string[];\n timestamp: number;\n transactionId: string;\n}\n\n/**\n * Custom claims passthrough configuration\n */\nexport interface CustomClaimsPassthroughConfig {\n /** Allow nested objects/arrays in claims. Default: false (only primitives) */\n allowComplexClaims?: boolean;\n\n /** Only passthrough these specific claims (allowlist). Default: undefined (allow all non-protected) */\n allowedClaims?: string[];\n\n /** Never passthrough these claims (blocklist, in addition to protected claims). Default: [] */\n blockedClaims?: string[];\n\n /** Prefix upstream claims to prevent collisions. Default: false (no prefix) */\n claimPrefix?: false | string;\n\n /** Enable passthrough from upstream access token (if JWT format). Default: true */\n fromAccessToken?: boolean;\n\n /** Enable passthrough from upstream ID token. Default: true */\n fromIdToken?: boolean;\n\n /** Maximum length for claim values. Default: 2000 */\n maxClaimValueSize?: number;\n}\n\n/**\n * Client metadata for storage\n */\nexport interface DCRClientMetadata {\n client_name?: string;\n client_uri?: string;\n contacts?: string[];\n jwks?: Record<string, unknown>;\n jwks_uri?: string;\n logo_uri?: string;\n policy_uri?: string;\n scope?: string;\n software_id?: string;\n software_version?: string;\n tos_uri?: string;\n}\n\n/**\n * RFC 7591 Dynamic Client Registration Request\n */\nexport interface DCRRequest {\n /** Client name */\n client_name?: string;\n /** Client homepage URL */\n client_uri?: string;\n /** Contact email addresses */\n contacts?: string[];\n /** Allowed grant types */\n grant_types?: string[];\n /** JWKS object */\n jwks?: Record<string, unknown>;\n /** JWKS URI */\n jwks_uri?: string;\n /** Client logo URL */\n logo_uri?: string;\n /** Privacy policy URL */\n policy_uri?: string;\n /** REQUIRED: Array of redirect URIs */\n redirect_uris: string[];\n /** Allowed response types */\n response_types?: string[];\n /** Requested scope */\n scope?: string;\n /** Software identifier */\n software_id?: string;\n /** Software version */\n software_version?: string;\n /** Token endpoint authentication method */\n token_endpoint_auth_method?: string;\n /** Terms of service URL */\n tos_uri?: string;\n}\n\n/**\n * RFC 7591 Dynamic Client Registration Response\n */\nexport interface DCRResponse {\n /** REQUIRED: Client identifier */\n client_id: string;\n /** Client ID issued timestamp */\n client_id_issued_at?: number;\n client_name?: string;\n /** Client secret */\n client_secret?: string;\n /** Client secret expiration (0 = never) */\n client_secret_expires_at?: number;\n client_uri?: string;\n contacts?: string[];\n grant_types?: string[];\n jwks?: Record<string, unknown>;\n jwks_uri?: string;\n logo_uri?: string;\n policy_uri?: string;\n /** Echo back all registered metadata */\n redirect_uris: string[];\n /** Registration access token */\n registration_access_token?: string;\n /** Registration client URI */\n registration_client_uri?: string;\n response_types?: string[];\n scope?: string;\n software_id?: string;\n software_version?: string;\n token_endpoint_auth_method?: string;\n tos_uri?: string;\n}\n\n/**\n * OAuth error response\n */\nexport interface OAuthError {\n error: string;\n error_description?: string;\n error_uri?: string;\n}\n\n/**\n * OAuth Proxy provider for pre-configured providers\n */\nexport interface OAuthProviderConfig {\n baseUrl: string;\n clientId: string;\n clientSecret: string;\n consentRequired?: boolean;\n scopes?: string[];\n}\n\n/**\n * Configuration for the OAuth Proxy\n */\nexport interface OAuthProxyConfig {\n /** Access token TTL in seconds (default: 3600) */\n accessTokenTtl?: number;\n /** Allowed redirect URI patterns for client registration */\n allowedRedirectUriPatterns?: string[];\n /** Authorization code TTL in seconds (default: 300) */\n authorizationCodeTtl?: number;\n /** Base URL of this proxy server */\n baseUrl: string;\n /** Require user consent (default: true) */\n consentRequired?: boolean;\n /** Secret key for signing consent cookies */\n consentSigningKey?: string;\n /**\n * Custom claims passthrough configuration.\n * When enabled (default), extracts custom claims from upstream access token and ID token\n * and includes them in the proxy's issued JWT tokens.\n * This enables authorization based on upstream roles, permissions, etc.\n * Set to false to disable claims passthrough entirely.\n * Default: true (enabled with default settings)\n */\n customClaimsPassthrough?: boolean | CustomClaimsPassthroughConfig;\n /** Enable token swap pattern (default: true) - issues short-lived JWTs instead of passing through upstream tokens */\n enableTokenSwap?: boolean;\n /** Encryption key for token storage (default: auto-generated). Set to false to disable encryption. */\n encryptionKey?: false | string;\n /** Forward client's PKCE to upstream (default: false) */\n forwardPkce?: boolean;\n /** Secret key for signing JWTs when token swap is enabled */\n jwtSigningKey?: string;\n /** OAuth callback path (default: /oauth/callback) */\n redirectPath?: string;\n /** Refresh token TTL in seconds (default: 2592000) */\n refreshTokenTtl?: number;\n /** Scopes to request from upstream provider */\n scopes?: string[];\n /** Custom token storage backend */\n tokenStorage?: TokenStorage;\n /** Custom token verifier for validating upstream tokens */\n tokenVerifier?: TokenVerifier;\n /** Transaction TTL in seconds (default: 600) */\n transactionTtl?: number;\n /** Upstream provider's authorization endpoint URL */\n upstreamAuthorizationEndpoint: string;\n /** Pre-registered client ID with upstream provider */\n upstreamClientId: string;\n /** Pre-registered client secret with upstream provider */\n upstreamClientSecret: string;\n /** Upstream provider's token endpoint URL */\n upstreamTokenEndpoint: string;\n /** Upstream token endpoint authentication method (default: \"client_secret_basic\") */\n upstreamTokenEndpointAuthMethod?:\n | \"client_secret_basic\"\n | \"client_secret_post\";\n}\n\n/**\n * OAuth transaction tracking active authorization flows\n */\nexport interface OAuthTransaction {\n /** Client's callback URL */\n clientCallbackUrl: string;\n /** Client's PKCE code challenge */\n clientCodeChallenge: string;\n /** Client's PKCE code challenge method (S256 or plain) */\n clientCodeChallengeMethod: string;\n /** Client ID from registration */\n clientId: string;\n /** Whether user consent was given */\n consentGiven?: boolean;\n /** Transaction creation timestamp */\n createdAt: Date;\n /** Transaction expiration timestamp */\n expiresAt: Date;\n /** Unique transaction ID */\n id: string;\n /** Additional state data */\n metadata?: Record<string, unknown>;\n /** Proxy-generated PKCE challenge for upstream */\n proxyCodeChallenge: string;\n /** Proxy-generated PKCE verifier for upstream */\n proxyCodeVerifier: string;\n /** Requested scopes */\n scope: string[];\n /** OAuth state parameter */\n state: string;\n}\n\n/**\n * PKCE pair\n */\nexport interface PKCEPair {\n challenge: string;\n verifier: string;\n}\n\n/**\n * Dynamic client registration data\n */\nexport interface ProxyDCRClient {\n /** Registered callback URL */\n callbackUrl: string;\n /** Generated or assigned client ID */\n clientId: string;\n /** Client secret (optional) */\n clientSecret?: string;\n /** Client metadata from registration request */\n metadata?: DCRClientMetadata;\n /** Client registration timestamp */\n registeredAt: Date;\n}\n\n/**\n * OAuth refresh token request\n */\nexport interface RefreshRequest {\n client_id: string;\n client_secret?: string;\n grant_type: \"refresh_token\";\n refresh_token: string;\n scope?: string;\n}\n\n/**\n * Token mapping for JWT swap pattern\n * Maps JTI to upstream token reference\n */\nexport interface TokenMapping {\n /** Client ID */\n clientId: string;\n /** Creation timestamp */\n createdAt: Date;\n /** Expiration timestamp */\n expiresAt: Date;\n /** JTI from FastMCP JWT */\n jti: string;\n /** Scopes */\n scope: string[];\n /** Reference to upstream token set */\n upstreamTokenKey: string;\n}\n\n/**\n * OAuth token request\n */\nexport interface TokenRequest {\n client_id: string;\n client_secret?: string;\n code: string;\n code_verifier?: string;\n grant_type: \"authorization_code\";\n redirect_uri: string;\n}\n\n/**\n * OAuth token response\n */\nexport interface TokenResponse {\n access_token: string;\n expires_in: number;\n id_token?: string;\n refresh_token?: string;\n scope?: string;\n token_type: string;\n}\n\n/**\n * Token storage interface\n */\nexport interface TokenStorage {\n /** Clean up expired entries */\n cleanup(): Promise<void>;\n /** Delete a value */\n delete(key: string): Promise<void>;\n /** Retrieve a value */\n get(key: string): Promise<null | unknown>;\n /** Save a value with optional TTL */\n save(key: string, value: unknown, ttl?: number): Promise<void>;\n}\n\n/**\n * Token verification result\n */\nexport interface TokenVerificationResult {\n claims?: Record<string, unknown>;\n error?: string;\n valid: boolean;\n}\n\n/**\n * Token verifier for validating upstream tokens\n */\nexport interface TokenVerifier {\n verify(token: string): Promise<TokenVerificationResult>;\n}\n\n/**\n * Token set from upstream OAuth provider\n */\nexport interface UpstreamTokenSet {\n /** Access token */\n accessToken: string;\n /** Token expiration in seconds */\n expiresIn: number;\n /** ID token (for OIDC) */\n idToken?: string;\n /** Token issuance timestamp */\n issuedAt: Date;\n /** Refresh token expiration in seconds (if provided by upstream) */\n refreshExpiresIn?: number;\n /** Refresh token (if provided) */\n refreshToken?: string;\n /** Granted scopes */\n scope: string[];\n /** Token type (usually \"Bearer\") */\n tokenType: string;\n}\n","/**\n * ClaimsExtractor\n * Securely extracts and filters custom claims from upstream OAuth tokens\n */\n\nimport type { CustomClaimsPassthroughConfig } from \"../types.js\";\n\nexport class ClaimsExtractor {\n private config: CustomClaimsPassthroughConfig;\n\n // Claims that MUST NOT be copied from upstream (protect proxy's JWT integrity)\n private readonly PROTECTED_CLAIMS = new Set([\n \"aud\",\n \"client_id\",\n \"exp\",\n \"iat\",\n \"iss\",\n \"jti\",\n \"nbf\",\n ]);\n\n constructor(config: boolean | CustomClaimsPassthroughConfig) {\n // Handle boolean shorthand: true = default config, false = disabled\n if (typeof config === \"boolean\") {\n config = config ? {} : { fromAccessToken: false, fromIdToken: false };\n }\n\n // Apply defaults\n this.config = {\n allowComplexClaims: config.allowComplexClaims || false,\n allowedClaims: config.allowedClaims,\n blockedClaims: config.blockedClaims || [],\n claimPrefix:\n config.claimPrefix !== undefined ? config.claimPrefix : false, // Default: no prefix\n fromAccessToken: config.fromAccessToken !== false, // Default: true\n fromIdToken: config.fromIdToken !== false, // Default: true\n maxClaimValueSize: config.maxClaimValueSize || 2000,\n };\n }\n\n /**\n * Extract claims from a token (access token or ID token)\n */\n async extract(\n token: string,\n tokenType: \"access\" | \"id\",\n ): Promise<null | Record<string, unknown>> {\n // Check if this token type is enabled\n if (tokenType === \"access\" && !this.config.fromAccessToken) {\n return null;\n }\n if (tokenType === \"id\" && !this.config.fromIdToken) {\n return null;\n }\n\n // Detect if token is JWT format (3 parts separated by dots)\n if (!this.isJWT(token)) {\n // Opaque token - no claims to extract\n return null;\n }\n\n // Decode JWT payload (base64url decode only, no signature verification)\n // We trust the token because it came from upstream via server-to-server exchange\n const payload = this.decodeJWTPayload(token);\n if (!payload) {\n return null;\n }\n\n // Filter and validate claims\n const filtered = this.filterClaims(payload);\n\n // Apply prefix if configured\n return this.applyPrefix(filtered);\n }\n\n /**\n * Apply prefix to claim names (if configured)\n */\n private applyPrefix(\n claims: Record<string, unknown>,\n ): Record<string, unknown> {\n const prefix = this.config.claimPrefix;\n\n // No prefix configured or explicitly disabled\n if (prefix === false || prefix === \"\" || prefix === undefined) {\n return claims;\n }\n\n // Apply prefix to all claim names\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(claims)) {\n result[`${prefix}${key}`] = value;\n }\n\n return result;\n }\n\n /**\n * Decode JWT payload without signature verification\n * Safe because token came from trusted upstream via server-to-server exchange\n */\n private decodeJWTPayload(token: string): null | Record<string, unknown> {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n\n // Decode the payload (middle part)\n const payload = Buffer.from(parts[1], \"base64url\").toString(\"utf-8\");\n return JSON.parse(payload) as Record<string, unknown>;\n } catch (error) {\n // Invalid JWT format or JSON\n console.warn(`Failed to decode JWT payload: ${error}`);\n return null;\n }\n }\n\n /**\n * Filter claims based on security rules\n */\n private filterClaims(\n claims: Record<string, unknown>,\n ): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(claims)) {\n // RULE 1: Skip protected claims (ALWAYS enforced)\n if (this.PROTECTED_CLAIMS.has(key)) {\n continue;\n }\n\n // RULE 2: Skip blocked claims\n if (this.config.blockedClaims?.includes(key)) {\n continue;\n }\n\n // RULE 3: If allowlist exists, only include allowed claims\n if (\n this.config.allowedClaims &&\n !this.config.allowedClaims.includes(key)\n ) {\n continue;\n }\n\n // RULE 4: Validate claim value\n if (!this.isValidClaimValue(value)) {\n console.warn(`Skipping claim '${key}' due to invalid value`);\n continue;\n }\n\n result[key] = value;\n }\n\n return result;\n }\n\n /**\n * Check if a token is in JWT format\n */\n private isJWT(token: string): boolean {\n return token.split(\".\").length === 3;\n }\n\n /**\n * Validate a claim value (type and size checks)\n */\n private isValidClaimValue(value: unknown): boolean {\n if (value === null || value === undefined) {\n return false;\n }\n\n const type = typeof value;\n\n // Primitive types (string, number, boolean) are always allowed\n if (type === \"string\") {\n const maxSize = this.config.maxClaimValueSize ?? 2000;\n return (value as string).length <= maxSize;\n }\n\n if (type === \"number\" || type === \"boolean\") {\n return true;\n }\n\n // Arrays and objects only if explicitly allowed\n if (Array.isArray(value) || type === \"object\") {\n // Complex types not allowed by default (security)\n if (!this.config.allowComplexClaims) {\n return false;\n }\n\n // Check serialized size\n try {\n const stringified = JSON.stringify(value);\n const maxSize = this.config.maxClaimValueSize ?? 2000;\n return stringified.length <= maxSize;\n } catch {\n // Can't serialize - reject\n return false;\n }\n }\n\n // Unknown type - reject\n return false;\n }\n}\n","/**\n * Consent Management\n * Handles user consent flow for OAuth authorization\n */\n\nimport { createHmac } from \"crypto\";\n\nimport type { ConsentData, OAuthTransaction } from \"../types.js\";\n\n/**\n * Manages consent screens and cookie signing\n */\nexport class ConsentManager {\n private signingKey: string;\n\n constructor(signingKey: string) {\n this.signingKey = signingKey || this.generateDefaultKey();\n }\n\n /**\n * Create HTTP response with consent screen\n */\n createConsentResponse(\n transaction: OAuthTransaction,\n provider: string,\n ): Response {\n const consentData: ConsentData = {\n clientName: \"MCP Client\",\n provider,\n scope: transaction.scope,\n timestamp: Date.now(),\n transactionId: transaction.id,\n };\n\n const html = this.generateConsentScreen(consentData);\n\n return new Response(html, {\n headers: {\n \"Content-Type\": \"text/html; charset=utf-8\",\n },\n status: 200,\n });\n }\n\n /**\n * Generate HTML for consent screen\n */\n generateConsentScreen(data: ConsentData): string {\n const { clientName, provider, scope, transactionId } = data;\n\n return `\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Authorization Request</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 20px;\n }\n\n .consent-container {\n background: white;\n border-radius: 12px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n max-width: 480px;\n width: 100%;\n padding: 40px;\n }\n\n .header {\n text-align: center;\n margin-bottom: 30px;\n }\n\n .header h1 {\n color: #1a202c;\n font-size: 24px;\n margin-bottom: 8px;\n }\n\n .header p {\n color: #718096;\n font-size: 14px;\n }\n\n .app-info {\n background: #f7fafc;\n border-radius: 8px;\n padding: 20px;\n margin-bottom: 24px;\n }\n\n .app-info h2 {\n color: #2d3748;\n font-size: 18px;\n margin-bottom: 12px;\n }\n\n .app-name {\n color: #667eea;\n font-weight: 600;\n }\n\n .permissions {\n margin-top: 16px;\n }\n\n .permissions h3 {\n color: #4a5568;\n font-size: 14px;\n margin-bottom: 8px;\n font-weight: 600;\n }\n\n .permissions ul {\n list-style: none;\n }\n\n .permissions li {\n color: #718096;\n font-size: 14px;\n padding: 6px 0;\n padding-left: 24px;\n position: relative;\n }\n\n .permissions li:before {\n content: \"✓\";\n position: absolute;\n left: 0;\n color: #48bb78;\n font-weight: bold;\n }\n\n .warning {\n background: #fffaf0;\n border-left: 4px solid #ed8936;\n padding: 12px 16px;\n margin-bottom: 24px;\n border-radius: 4px;\n }\n\n .warning p {\n color: #744210;\n font-size: 13px;\n line-height: 1.5;\n }\n\n .actions {\n display: flex;\n gap: 12px;\n }\n\n button {\n flex: 1;\n padding: 14px 24px;\n border: none;\n border-radius: 6px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.2s;\n }\n\n .approve {\n background: #667eea;\n color: white;\n }\n\n .approve:hover {\n background: #5a67d8;\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);\n }\n\n .deny {\n background: #e2e8f0;\n color: #4a5568;\n }\n\n .deny:hover {\n background: #cbd5e0;\n }\n\n .footer {\n margin-top: 24px;\n text-align: center;\n color: #a0aec0;\n font-size: 12px;\n }\n </style>\n</head>\n<body>\n <div class=\"consent-container\">\n <div class=\"header\">\n <h1>🔐 Authorization Request</h1>\n <p>via ${this.escapeHtml(provider)}</p>\n </div>\n\n <div class=\"app-info\">\n <h2>\n <span class=\"app-name\">${this.escapeHtml(clientName || \"An application\")}</span>\n requests access\n </h2>\n\n <div class=\"permissions\">\n <h3>This will allow the app to:</h3>\n <ul>\n ${scope.map((s) => `<li>${this.escapeHtml(this.formatScope(s))}</li>`).join(\"\")}\n </ul>\n </div>\n </div>\n\n <div class=\"warning\">\n <p>\n <strong>⚠️ Important:</strong> Only approve if you trust this application.\n By approving, you authorize it to access your account information.\n </p>\n </div>\n\n <form method=\"POST\" action=\"/oauth/consent\">\n <input type=\"hidden\" name=\"transaction_id\" value=\"${this.escapeHtml(transactionId)}\">\n <div class=\"actions\">\n <button type=\"submit\" name=\"action\" value=\"deny\" class=\"deny\">\n Deny\n </button>\n <button type=\"submit\" name=\"action\" value=\"approve\" class=\"approve\">\n Approve\n </button>\n </div>\n </form>\n\n <div class=\"footer\">\n <p>This consent is required to prevent unauthorized access.</p>\n </div>\n </div>\n</body>\n</html>\n `.trim();\n }\n\n /**\n * Sign consent data for cookie\n */\n signConsentCookie(data: ConsentData): string {\n const payload = JSON.stringify(data);\n const signature = this.sign(payload);\n\n return `${Buffer.from(payload).toString(\"base64\")}.${signature}`;\n }\n\n /**\n * Validate and parse consent cookie\n */\n validateConsentCookie(cookie: string): ConsentData | null {\n try {\n const [payloadB64, signature] = cookie.split(\".\");\n\n if (!payloadB64 || !signature) {\n return null;\n }\n\n const payload = Buffer.from(payloadB64, \"base64\").toString(\"utf8\");\n const expectedSignature = this.sign(payload);\n\n if (signature !== expectedSignature) {\n return null;\n }\n\n const data = JSON.parse(payload) as ConsentData;\n\n // Check if consent is still valid (5 minutes)\n const age = Date.now() - data.timestamp;\n if (age > 5 * 60 * 1000) {\n return null;\n }\n\n return data;\n } catch {\n return null;\n }\n }\n\n /**\n * Escape HTML to prevent XSS\n */\n private escapeHtml(text: string): string {\n const map: Record<string, string> = {\n \"'\": \"&#x27;\",\n '\"': \"&quot;\",\n \"/\": \"&#x2F;\",\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n };\n\n return text.replace(/[&<>\"'/]/g, (char) => map[char] || char);\n }\n\n /**\n * Format scope for display\n */\n private formatScope(scope: string): string {\n // Convert scope names to readable format\n const scopeMap: Record<string, string> = {\n email: \"Access your email address\",\n openid: \"Verify your identity\",\n profile: \"View your basic profile information\",\n \"read:user\": \"Read your user information\",\n \"write:user\": \"Modify your user information\",\n };\n\n return scopeMap[scope] || scope.replace(/_/g, \" \").replace(/:/g, \" - \");\n }\n\n /**\n * Generate default signing key if none provided\n */\n private generateDefaultKey(): string {\n return `fastmcp-consent-${Date.now()}-${Math.random()}`;\n }\n\n /**\n * Sign a payload using HMAC-SHA256\n */\n private sign(payload: string): string {\n return createHmac(\"sha256\", this.signingKey).update(payload).digest(\"hex\");\n }\n}\n","/**\n * JWT Issuer for OAuth Proxy\n * Issues and validates short-lived JWTs that reference upstream provider tokens\n */\n\nimport { createHmac, pbkdf2, randomBytes } from \"crypto\";\nimport { promisify } from \"util\";\n\nimport {\n DEFAULT_ACCESS_TOKEN_TTL,\n DEFAULT_REFRESH_TOKEN_TTL,\n} from \"../types.js\";\n\nconst pbkdf2Async = promisify(pbkdf2);\n\n/**\n * JWT Claims for FastMCP tokens\n */\nexport interface JWTClaims {\n /** Additional custom claims from upstream tokens */\n [key: string]: unknown;\n /** Audience */\n aud: string;\n /** Client ID */\n client_id: string;\n /** Expiration time (seconds since epoch) */\n exp: number;\n /** Issued at time (seconds since epoch) */\n iat: number;\n /** Issuer */\n iss: string;\n /** JWT ID (unique identifier) */\n jti: string;\n /** Scopes */\n scope: string[];\n}\n\n/**\n * JWT Issuer configuration\n */\nexport interface JWTIssuerConfig {\n /** Token expiration in seconds (default: 3600 = 1 hour) */\n accessTokenTtl?: number;\n /** Audience for issued tokens */\n audience: string;\n /** Issuer identifier */\n issuer: string;\n /** Refresh token expiration in seconds (default: 2592000 = 30 days) */\n refreshTokenTtl?: number;\n /** Secret key for signing tokens */\n signingKey: string;\n}\n\n/**\n * Token validation result\n */\nexport interface TokenValidationResult {\n /** Decoded claims if valid */\n claims?: JWTClaims;\n /** Error message if invalid */\n error?: string;\n /** Whether token is valid */\n valid: boolean;\n}\n\n/**\n * JWT Header\n */\ninterface JWTHeader {\n alg: string;\n typ: string;\n}\n\n/**\n * JWT Issuer\n * Issues and validates HS256-signed JWTs for the OAuth proxy\n */\nexport class JWTIssuer {\n private accessTokenTtl: number;\n private audience: string;\n private issuer: string;\n private refreshTokenTtl: number;\n private signingKey: Buffer;\n\n constructor(config: JWTIssuerConfig) {\n this.issuer = config.issuer;\n this.audience = config.audience;\n this.accessTokenTtl = config.accessTokenTtl || DEFAULT_ACCESS_TOKEN_TTL;\n this.refreshTokenTtl = config.refreshTokenTtl || DEFAULT_REFRESH_TOKEN_TTL;\n this.signingKey = Buffer.from(config.signingKey);\n }\n\n /**\n * Derive a signing key from a secret\n * Uses PBKDF2 for key derivation\n */\n static async deriveKey(\n secret: string,\n iterations: number = 100000,\n ): Promise<string> {\n const salt = Buffer.from(\"fastmcp-oauth-proxy\");\n const key = await pbkdf2Async(secret, salt, iterations, 32, \"sha256\");\n return key.toString(\"base64\");\n }\n\n /**\n * Issue an access token\n */\n issueAccessToken(\n clientId: string,\n scope: string[],\n additionalClaims?: Record<string, unknown>,\n expiresIn?: number,\n ): string {\n const now = Math.floor(Date.now() / 1000);\n const jti = this.generateJti();\n\n const claims: JWTClaims = {\n aud: this.audience,\n client_id: clientId,\n exp: now + (expiresIn ?? this.accessTokenTtl),\n iat: now,\n iss: this.issuer,\n jti,\n scope,\n // Merge additional claims (custom claims from upstream)\n ...(additionalClaims || {}),\n };\n\n return this.signToken(claims);\n }\n\n /**\n * Issue a refresh token\n */\n issueRefreshToken(\n clientId: string,\n scope: string[],\n additionalClaims?: Record<string, unknown>,\n expiresIn?: number,\n ): string {\n const now = Math.floor(Date.now() / 1000);\n const jti = this.generateJti();\n\n const claims: JWTClaims = {\n aud: this.audience,\n client_id: clientId,\n exp: now + (expiresIn ?? this.refreshTokenTtl),\n iat: now,\n iss: this.issuer,\n jti,\n scope,\n // Merge additional claims (custom claims from upstream)\n ...(additionalClaims || {}),\n };\n\n return this.signToken(claims);\n }\n\n /**\n * Validate a JWT token\n */\n async verify(token: string): Promise<TokenValidationResult> {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return {\n error: \"Invalid token format\",\n valid: false,\n };\n }\n\n const [headerB64, payloadB64, signatureB64] = parts;\n\n // Verify signature\n const expectedSignature = this.sign(`${headerB64}.${payloadB64}`);\n if (signatureB64 !== expectedSignature) {\n return {\n error: \"Invalid signature\",\n valid: false,\n };\n }\n\n // Decode claims\n const claims: JWTClaims = JSON.parse(\n Buffer.from(payloadB64, \"base64url\").toString(\"utf-8\"),\n );\n\n // Validate claims\n const now = Math.floor(Date.now() / 1000);\n\n if (claims.exp <= now) {\n return {\n claims,\n error: \"Token expired\",\n valid: false,\n };\n }\n\n if (claims.iss !== this.issuer) {\n return {\n claims,\n error: \"Invalid issuer\",\n valid: false,\n };\n }\n\n if (claims.aud !== this.audience) {\n return {\n claims,\n error: \"Invalid audience\",\n valid: false,\n };\n }\n\n return {\n claims,\n valid: true,\n };\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : \"Validation failed\",\n valid: false,\n };\n }\n }\n\n /**\n * Generate unique JWT ID\n */\n private generateJti(): string {\n return randomBytes(16).toString(\"base64url\");\n }\n\n /**\n * Sign data with HMAC-SHA256\n */\n private sign(data: string): string {\n const hmac = createHmac(\"sha256\", this.signingKey);\n hmac.update(data);\n return hmac.digest(\"base64url\");\n }\n\n /**\n * Sign a JWT token\n */\n private signToken(claims: JWTClaims): string {\n const header: JWTHeader = {\n alg: \"HS256\",\n typ: \"JWT\",\n };\n\n const headerB64 = Buffer.from(JSON.stringify(header)).toString(\"base64url\");\n const payloadB64 = Buffer.from(JSON.stringify(claims)).toString(\n \"base64url\",\n );\n\n const signature = this.sign(`${headerB64}.${payloadB64}`);\n\n return `${headerB64}.${payloadB64}.${signature}`;\n }\n}\n","/**\n * PKCE (Proof Key for Code Exchange) Utilities\n * Implements RFC 7636 for OAuth 2.0 public clients\n */\n\nimport { createHash, randomBytes } from \"crypto\";\n\nimport type { PKCEPair } from \"../types.js\";\n\n/**\n * PKCE utility class for generating and validating code challenges\n */\nexport class PKCEUtils {\n /**\n * Generate a code challenge from a verifier\n * @param verifier The code verifier\n * @param method Challenge method: 'S256' or 'plain' (default: 'S256')\n * @returns Base64URL-encoded challenge string\n */\n static generateChallenge(\n verifier: string,\n method: \"plain\" | \"S256\" = \"S256\",\n ): string {\n if (method === \"plain\") {\n return verifier;\n }\n\n if (method === \"S256\") {\n const hash = createHash(\"sha256\");\n hash.update(verifier);\n return PKCEUtils.base64URLEncode(hash.digest());\n }\n\n throw new Error(`Unsupported challenge method: ${method}`);\n }\n\n /**\n * Generate a complete PKCE pair (verifier + challenge)\n * @param method Challenge method: 'S256' or 'plain' (default: 'S256')\n * @returns Object containing verifier and challenge\n */\n static generatePair(method: \"plain\" | \"S256\" = \"S256\"): PKCEPair {\n const verifier = PKCEUtils.generateVerifier();\n const challenge = PKCEUtils.generateChallenge(verifier, method);\n\n return {\n challenge,\n verifier,\n };\n }\n\n /**\n * Generate a cryptographically secure code verifier\n * @param length Length of verifier (43-128 characters, default: 128)\n * @returns Base64URL-encoded verifier string\n */\n static generateVerifier(length: number = 128): string {\n if (length < 43 || length > 128) {\n throw new Error(\"PKCE verifier length must be between 43 and 128\");\n }\n\n // Generate random bytes and encode as base64url\n // Need more bytes because base64 encoding expands data\n const byteLength = Math.ceil((length * 3) / 4);\n const randomBytesBuffer = randomBytes(byteLength);\n\n return PKCEUtils.base64URLEncode(randomBytesBuffer).slice(0, length);\n }\n\n /**\n * Validate a code verifier against a challenge\n * @param verifier The code verifier to validate\n * @param challenge The expected challenge\n * @param method The challenge method used\n * @returns True if verifier matches challenge\n */\n static validateChallenge(\n verifier: string,\n challenge: string,\n method: string,\n ): boolean {\n if (!verifier || !challenge) {\n return false;\n }\n\n if (method === \"plain\") {\n return verifier === challenge;\n }\n\n if (method === \"S256\") {\n const computedChallenge = PKCEUtils.generateChallenge(verifier, \"S256\");\n return computedChallenge === challenge;\n }\n\n // Unknown method\n return false;\n }\n\n /**\n * Encode a buffer as base64url (RFC 4648)\n * @param buffer Buffer to encode\n * @returns Base64URL-encoded string\n */\n private static base64URLEncode(buffer: Buffer): string {\n return buffer\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=/g, \"\");\n }\n}\n","/**\n * Token Storage Implementations\n * Secure storage for OAuth tokens and transaction state\n */\n\nimport {\n createCipheriv,\n createDecipheriv,\n randomBytes,\n scryptSync,\n} from \"crypto\";\n\nimport type { TokenStorage } from \"../types.js\";\n\ninterface StorageEntry {\n expiresAt: number;\n value: unknown;\n}\n\n/**\n * Encrypted token storage wrapper\n * Encrypts values using AES-256-GCM before storing\n */\nexport class EncryptedTokenStorage implements TokenStorage {\n private algorithm = \"aes-256-gcm\";\n private backend: TokenStorage;\n private encryptionKey: Buffer;\n\n constructor(backend: TokenStorage, encryptionKey: string) {\n this.backend = backend;\n // Synchronously derive key using scrypt\n const salt = Buffer.from(\"fastmcp-oauth-proxy-salt\");\n this.encryptionKey = scryptSync(encryptionKey, salt, 32);\n }\n\n async cleanup(): Promise<void> {\n await this.backend.cleanup();\n }\n\n async delete(key: string): Promise<void> {\n await this.backend.delete(key);\n }\n\n async get(key: string): Promise<null | unknown> {\n const encrypted = await this.backend.get(key);\n\n if (!encrypted) {\n return null;\n }\n\n try {\n const decrypted = await this.decrypt(\n encrypted as string,\n this.encryptionKey,\n );\n return JSON.parse(decrypted);\n } catch (error) {\n console.error(\"Failed to decrypt value:\", error);\n return null;\n }\n }\n\n async save(key: string, value: unknown, ttl?: number): Promise<void> {\n const encrypted = await this.encrypt(\n JSON.stringify(value),\n this.encryptionKey,\n );\n await this.backend.save(key, encrypted, ttl);\n }\n\n private async decrypt(ciphertext: string, key: Buffer): Promise<string> {\n const parts = ciphertext.split(\":\");\n if (parts.length !== 3) {\n throw new Error(\"Invalid encrypted data format\");\n }\n\n const [ivHex, authTagHex, encrypted] = parts;\n const iv = Buffer.from(ivHex, \"hex\");\n const authTag = Buffer.from(authTagHex, \"hex\");\n\n const decipher = createDecipheriv(this.algorithm, key, iv);\n // Use type assertion for GCM-specific method\n (decipher as unknown as { setAuthTag(buffer: Buffer): void }).setAuthTag(\n authTag,\n );\n\n let decrypted = decipher.update(encrypted, \"hex\", \"utf8\");\n decrypted += decipher.final(\"utf8\");\n\n return decrypted;\n }\n\n private async encrypt(plaintext: string, key: Buffer): Promise<string> {\n const iv = randomBytes(16);\n const cipher = createCipheriv(this.algorithm, key, iv);\n\n let encrypted = cipher.update(plaintext, \"utf8\", \"hex\");\n encrypted += cipher.final(\"hex\");\n\n // Use type assertion for GCM-specific method\n const authTag = (\n cipher as unknown as { getAuthTag(): Buffer }\n ).getAuthTag();\n\n // Return format: iv:authTag:encrypted\n return `${iv.toString(\"hex\")}:${authTag.toString(\"hex\")}:${encrypted}`;\n }\n}\n\n/**\n * In-memory token storage with TTL support\n */\nexport class MemoryTokenStorage implements TokenStorage {\n private cleanupInterval: NodeJS.Timeout | null = null;\n private store: Map<string, StorageEntry> = new Map();\n\n constructor(cleanupIntervalMs: number = 60000) {\n // Run cleanup every minute by default\n this.cleanupInterval = setInterval(\n () => void this.cleanup(),\n cleanupIntervalMs,\n );\n }\n\n async cleanup(): Promise<void> {\n const now = Date.now();\n const keysToDelete: string[] = [];\n\n for (const [key, entry] of this.store.entries()) {\n if (entry.expiresAt < now) {\n keysToDelete.push(key);\n }\n }\n\n for (const key of keysToDelete) {\n this.store.delete(key);\n }\n }\n\n async delete(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n /**\n * Destroy the storage and clear cleanup interval\n */\n destroy(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n this.store.clear();\n }\n\n async get(key: string): Promise<null | unknown> {\n const entry = this.store.get(key);\n\n if (!entry) {\n return null;\n }\n\n if (entry.expiresAt < Date.now()) {\n this.store.delete(key);\n return null;\n }\n\n return entry.value;\n }\n\n async save(key: string, value: unknown, ttl?: number): Promise<void> {\n const expiresAt = ttl ? Date.now() + ttl * 1000 : Number.MAX_SAFE_INTEGER;\n\n this.store.set(key, {\n expiresAt,\n value,\n });\n }\n\n /**\n * Get the number of stored items\n */\n size(): number {\n return this.store.size;\n }\n}\n","/**\n * AuthProvider Base Class\n * High-level abstraction for OAuth authentication that simplifies configuration\n */\n\nimport type { IncomingMessage } from \"node:http\";\n\nimport type { TokenStorage, UpstreamTokenSet } from \"../types.js\";\n\nimport { OAuthProxy } from \"../OAuthProxy.js\";\n\n/**\n * Configuration common to all OAuth providers.\n */\nexport interface AuthProviderConfig {\n /** Allowed redirect URI patterns (default: [\"http://localhost:*\", \"https://*\"]) */\n allowedRedirectUriPatterns?: string[];\n /** Base URL where the MCP server is accessible */\n baseUrl: string;\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Require user consent screen (default: true) */\n consentRequired?: boolean;\n /** Encryption key for token storage (auto-generated if not provided, set to false to disable) */\n encryptionKey?: false | string;\n /** JWT signing key (auto-generated if not provided) */\n jwtSigningKey?: string;\n /** Scopes to request (defaults vary by provider) */\n scopes?: string[];\n /** Token storage backend (default: MemoryTokenStorage) */\n tokenStorage?: TokenStorage;\n}\n\n/**\n * Configuration for generic OAuth provider (user-specified endpoints).\n */\nexport interface GenericOAuthProviderConfig extends AuthProviderConfig {\n /** OAuth authorization endpoint URL */\n authorizationEndpoint: string;\n /** OAuth token endpoint URL */\n tokenEndpoint: string;\n /** Token endpoint auth method (default: \"client_secret_basic\") */\n tokenEndpointAuthMethod?: \"client_secret_basic\" | \"client_secret_post\";\n}\n\n/**\n * Standard session type for OAuth providers.\n * Contains the upstream access token and optional metadata.\n */\nexport interface OAuthSession {\n /** The upstream OAuth access token */\n accessToken: string;\n /** Additional claims extracted from the token (if customClaimsPassthrough enabled) */\n claims?: Record<string, unknown>;\n /** Token expiration time (Unix timestamp in seconds) */\n expiresAt?: number;\n /** ID token from OIDC providers */\n idToken?: string;\n /** Refresh token (if available) */\n refreshToken?: string;\n /** Scopes granted by the OAuth provider */\n scopes?: string[];\n}\n\n/**\n * Abstract base class for OAuth providers.\n * Encapsulates OAuthProxy creation, authenticate function, and oauth config.\n *\n * Subclasses only need to implement the endpoint and default scope methods.\n */\nexport abstract class AuthProvider<\n TSession extends OAuthSession = OAuthSession,\n> {\n protected config: AuthProviderConfig;\n /**\n * Get the proxy, creating it lazily if needed.\n */\n protected get proxy(): OAuthProxy {\n if (!this._proxy) {\n this._proxy = this.createProxy();\n }\n return this._proxy;\n }\n\n private _proxy: OAuthProxy | undefined;\n\n constructor(config: AuthProviderConfig) {\n this.config = config;\n // Note: proxy is created lazily to allow subclass constructors to run first\n }\n\n /**\n * Authenticate function to be used by FastMCP.\n * Extracts Bearer token, validates it, and returns session with upstream access token.\n */\n async authenticate(\n request: IncomingMessage | undefined,\n ): Promise<TSession | undefined> {\n if (!request) {\n // stdio transport - no HTTP authentication\n return undefined;\n }\n\n const authHeader = request.headers?.authorization;\n if (!authHeader || !authHeader.startsWith(\"Bearer \")) {\n return undefined;\n }\n\n const token = authHeader.slice(7);\n const upstreamTokens = await this.proxy.loadUpstreamTokens(token);\n\n if (!upstreamTokens) {\n return undefined;\n }\n\n return this.createSession(upstreamTokens);\n }\n\n /**\n * Get the OAuth configuration object for FastMCP ServerOptions.\n */\n getOAuthConfig(): {\n authorizationServer: ReturnType<\n OAuthProxy[\"getAuthorizationServerMetadata\"]\n >;\n enabled: true;\n protectedResource: {\n authorizationServers: string[];\n resource: string;\n scopesSupported: string[];\n };\n proxy: OAuthProxy;\n } {\n return {\n authorizationServer: this.proxy.getAuthorizationServerMetadata(),\n enabled: true,\n protectedResource: {\n authorizationServers: [this.config.baseUrl],\n resource: this.config.baseUrl,\n scopesSupported: this.config.scopes ?? this.getDefaultScopes(),\n },\n proxy: this.proxy,\n };\n }\n\n /**\n * Get the OAuthProxy instance (for advanced use cases).\n */\n getProxy(): OAuthProxy {\n return this.proxy;\n }\n\n /** Create the underlying OAuthProxy with provider-specific configuration */\n protected abstract createProxy(): OAuthProxy;\n\n /**\n * Create a session object from upstream tokens.\n * Override in subclasses to add provider-specific session data.\n */\n protected createSession(upstreamTokens: UpstreamTokenSet): TSession {\n return {\n accessToken: upstreamTokens.accessToken,\n expiresAt: upstreamTokens.expiresIn\n ? Math.floor(Date.now() / 1000) + upstreamTokens.expiresIn\n : undefined,\n idToken: upstreamTokens.idToken,\n refreshToken: upstreamTokens.refreshToken,\n scopes: upstreamTokens.scope,\n } as TSession;\n }\n\n /** Get the authorization endpoint for this provider */\n protected abstract getAuthorizationEndpoint(): string;\n\n /** Default scopes for this provider */\n protected abstract getDefaultScopes(): string[];\n\n /** Get the token endpoint for this provider */\n protected abstract getTokenEndpoint(): string;\n}\n","/**\n * Microsoft Azure/Entra ID OAuth Provider\n * Pre-configured OAuth provider for Microsoft Identity Platform\n */\n\nimport { OAuthProxy } from \"../OAuthProxy.js\";\nimport {\n AuthProvider,\n type AuthProviderConfig,\n type OAuthSession,\n} from \"./AuthProvider.js\";\n\n/**\n * Azure-specific configuration\n */\nexport interface AzureProviderConfig extends AuthProviderConfig {\n /** Tenant ID or 'common', 'organizations', 'consumers' (default: 'common') */\n tenantId?: string;\n}\n\n/**\n * Azure-specific session with additional user info\n */\nexport interface AzureSession extends OAuthSession {\n upn?: string;\n}\n\n/**\n * Microsoft Azure AD / Entra ID OAuth 2.0 Provider\n * Callback URL: {baseUrl}/oauth/callback\n */\nexport class AzureProvider extends AuthProvider<AzureSession> {\n private tenantId: string;\n\n constructor(config: AzureProviderConfig) {\n super(config);\n this.tenantId = config.tenantId ?? \"common\";\n }\n\n protected createProxy(): OAuthProxy {\n return new OAuthProxy({\n allowedRedirectUriPatterns: this.config.allowedRedirectUriPatterns ?? [\n \"http://localhost:*\",\n \"https://*\",\n ],\n baseUrl: this.config.baseUrl,\n consentRequired: this.config.consentRequired ?? true,\n encryptionKey: this.config.encryptionKey,\n jwtSigningKey: this.config.jwtSigningKey,\n scopes: this.config.scopes ?? this.getDefaultScopes(),\n tokenStorage: this.config.tokenStorage,\n upstreamAuthorizationEndpoint: this.getAuthorizationEndpoint(),\n upstreamClientId: this.config.clientId,\n upstreamClientSecret: this.config.clientSecret,\n upstreamTokenEndpoint: this.getTokenEndpoint(),\n });\n }\n\n protected getAuthorizationEndpoint(): string {\n return `https://login.microsoftonline.com/${this.tenantId}/oauth2/v2.0/authorize`;\n }\n\n protected getDefaultScopes(): string[] {\n return [\"openid\", \"profile\", \"email\"];\n }\n\n protected getTokenEndpoint(): string {\n return `https://login.microsoftonline.com/${this.tenantId}/oauth2/v2.0/token`;\n }\n}\n","/**\n * GitHub OAuth Provider\n * Pre-configured OAuth provider for GitHub OAuth Apps\n */\n\nimport { OAuthProxy } from \"../OAuthProxy.js\";\nimport {\n AuthProvider,\n type AuthProviderConfig,\n type OAuthSession,\n} from \"./AuthProvider.js\";\n\n/**\n * GitHub-specific session with additional user info\n */\nexport interface GitHubSession extends OAuthSession {\n username?: string;\n}\n\n/**\n * GitHub OAuth 2.0 Provider\n * Callback URL: {baseUrl}/oauth/callback\n */\nexport class GitHubProvider extends AuthProvider<GitHubSession> {\n constructor(config: AuthProviderConfig) {\n super(config);\n }\n\n protected createProxy(): OAuthProxy {\n return new OAuthProxy({\n allowedRedirectUriPatterns: this.config.allowedRedirectUriPatterns ?? [\n \"http://localhost:*\",\n \"https://*\",\n ],\n baseUrl: this.config.baseUrl,\n consentRequired: this.config.consentRequired ?? true,\n encryptionKey: this.config.encryptionKey,\n jwtSigningKey: this.config.jwtSigningKey,\n scopes: this.config.scopes ?? this.getDefaultScopes(),\n tokenStorage: this.config.tokenStorage,\n upstreamAuthorizationEndpoint: this.getAuthorizationEndpoint(),\n upstreamClientId: this.config.clientId,\n upstreamClientSecret: this.config.clientSecret,\n upstreamTokenEndpoint: this.getTokenEndpoint(),\n });\n }\n\n protected getAuthorizationEndpoint(): string {\n return \"https://github.com/login/oauth/authorize\";\n }\n\n protected getDefaultScopes(): string[] {\n return [\"read:user\", \"user:email\"];\n }\n\n protected getTokenEndpoint(): string {\n return \"https://github.com/login/oauth/access_token\";\n }\n}\n","/**\n * Google OAuth Provider\n * Pre-configured OAuth provider for Google Identity Platform\n */\n\nimport { OAuthProxy } from \"../OAuthProxy.js\";\nimport {\n AuthProvider,\n type AuthProviderConfig,\n type OAuthSession,\n} from \"./AuthProvider.js\";\n\n/**\n * Google-specific session with additional user info\n */\nexport interface GoogleSession extends OAuthSession {\n email?: string;\n}\n\n/**\n * Google OAuth 2.0 Provider\n * Callback URL: {baseUrl}/oauth/callback\n */\nexport class GoogleProvider extends AuthProvider<GoogleSession> {\n constructor(config: AuthProviderConfig) {\n super(config);\n }\n\n protected createProxy(): OAuthProxy {\n return new OAuthProxy({\n allowedRedirectUriPatterns: this.config.allowedRedirectUriPatterns ?? [\n \"http://localhost:*\",\n \"https://*\",\n ],\n baseUrl: this.config.baseUrl,\n consentRequired: this.config.consentRequired ?? true,\n encryptionKey: this.config.encryptionKey,\n jwtSigningKey: this.config.jwtSigningKey,\n scopes: this.config.scopes ?? this.getDefaultScopes(),\n tokenStorage: this.config.tokenStorage,\n upstreamAuthorizationEndpoint: this.getAuthorizationEndpoint(),\n upstreamClientId: this.config.clientId,\n upstreamClientSecret: this.config.clientSecret,\n upstreamTokenEndpoint: this.getTokenEndpoint(),\n });\n }\n\n protected getAuthorizationEndpoint(): string {\n return \"https://accounts.google.com/o/oauth2/v2/auth\";\n }\n\n protected getDefaultScopes(): string[] {\n return [\"openid\", \"profile\", \"email\"];\n }\n\n protected getTokenEndpoint(): string {\n return \"https://oauth2.googleapis.com/token\";\n }\n}\n","/**\n * Generic OAuth Provider\n * For any OAuth 2.0 compliant authorization server\n */\n\nimport { OAuthProxy } from \"../OAuthProxy.js\";\nimport {\n AuthProvider,\n type GenericOAuthProviderConfig,\n type OAuthSession,\n} from \"./AuthProvider.js\";\n\n/**\n * Generic OAuth provider for any OAuth 2.0 compliant authorization server.\n * Use when there's no built-in provider for your identity provider.\n */\nexport class OAuthProvider<\n TSession extends OAuthSession = OAuthSession,\n> extends AuthProvider<TSession> {\n protected genericConfig: GenericOAuthProviderConfig;\n\n constructor(config: GenericOAuthProviderConfig) {\n super(config);\n this.genericConfig = config;\n }\n\n protected createProxy(): OAuthProxy {\n return new OAuthProxy({\n allowedRedirectUriPatterns: this.config.allowedRedirectUriPatterns ?? [\n \"http://localhost:*\",\n \"https://*\",\n ],\n baseUrl: this.config.baseUrl,\n consentRequired: this.config.consentRequired ?? true,\n encryptionKey: this.config.encryptionKey,\n jwtSigningKey: this.config.jwtSigningKey,\n scopes: this.config.scopes ?? this.getDefaultScopes(),\n tokenStorage: this.config.tokenStorage,\n upstreamAuthorizationEndpoint: this.getAuthorizationEndpoint(),\n upstreamClientId: this.config.clientId,\n upstreamClientSecret: this.config.clientSecret,\n upstreamTokenEndpoint: this.getTokenEndpoint(),\n upstreamTokenEndpointAuthMethod:\n this.genericConfig.tokenEndpointAuthMethod ?? \"client_secret_basic\",\n });\n }\n\n protected getAuthorizationEndpoint(): string {\n return this.genericConfig.authorizationEndpoint;\n }\n\n protected getDefaultScopes(): string[] {\n return [\"openid\"];\n }\n\n protected getTokenEndpoint(): string {\n return this.genericConfig.tokenEndpoint;\n }\n}\n","/**\n * Disk-based Token Storage Implementation\n * Provides persistent file-based storage for OAuth tokens and transaction state\n */\n\nimport { mkdir, readdir, readFile, rm, stat, writeFile } from \"fs/promises\";\nimport { join } from \"path\";\n\nimport type { TokenStorage } from \"../types.js\";\n\nexport interface DiskStoreOptions {\n /**\n * How often to run cleanup (in milliseconds)\n * @default 60000 (1 minute)\n */\n cleanupIntervalMs?: number;\n\n /**\n * Directory path for storing data\n */\n directory: string;\n\n /**\n * File extension for stored files\n * @default \".json\"\n */\n fileExtension?: string;\n}\n\ninterface StorageEntry {\n expiresAt: number;\n value: unknown;\n}\n\n/**\n * Disk-based token storage with TTL support\n * Persists tokens to filesystem for survival across server restarts\n */\nexport class DiskStore implements TokenStorage {\n private cleanupInterval: NodeJS.Timeout | null = null;\n private directory: string;\n private fileExtension: string;\n\n constructor(options: DiskStoreOptions) {\n this.directory = options.directory;\n this.fileExtension = options.fileExtension || \".json\";\n\n // Ensure directory exists\n void this.ensureDirectory();\n\n // Start periodic cleanup\n const cleanupIntervalMs = options.cleanupIntervalMs || 60000;\n this.cleanupInterval = setInterval(() => {\n void this.cleanup();\n }, cleanupIntervalMs);\n }\n\n /**\n * Clean up expired entries\n */\n async cleanup(): Promise<void> {\n try {\n await this.ensureDirectory();\n const files = await readdir(this.directory);\n const now = Date.now();\n\n for (const file of files) {\n if (!file.endsWith(this.fileExtension)) {\n continue;\n }\n\n try {\n const filePath = join(this.directory, file);\n const content = await readFile(filePath, \"utf-8\");\n const entry: StorageEntry = JSON.parse(content);\n\n if (entry.expiresAt < now) {\n await rm(filePath);\n }\n } catch (error) {\n // If file is corrupted or can't be read, delete it\n console.warn(`Failed to read/parse file ${file}, deleting:`, error);\n try {\n await rm(join(this.directory, file));\n } catch {\n // Ignore deletion errors\n }\n }\n }\n } catch (error) {\n console.error(\"Cleanup failed:\", error);\n }\n }\n\n /**\n * Delete a value\n */\n async delete(key: string): Promise<void> {\n const filePath = this.getFilePath(key);\n try {\n await rm(filePath);\n } catch (error) {\n // File might not exist, which is fine\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") {\n console.error(`Failed to delete key ${key}:`, error);\n }\n }\n }\n\n /**\n * Destroy the storage and clear cleanup interval\n */\n destroy(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n }\n\n /**\n * Retrieve a value\n */\n async get(key: string): Promise<null | unknown> {\n const filePath = this.getFilePath(key);\n\n try {\n const content = await readFile(filePath, \"utf-8\");\n const entry: StorageEntry = JSON.parse(content);\n\n // Check if expired\n if (entry.expiresAt < Date.now()) {\n await rm(filePath);\n return null;\n }\n\n return entry.value;\n } catch (error) {\n // File doesn't exist or is corrupted\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n console.error(`Failed to read key ${key}:`, error);\n return null;\n }\n }\n\n /**\n * Save a value with optional TTL\n */\n async save(key: string, value: unknown, ttl?: number): Promise<void> {\n await this.ensureDirectory();\n\n const filePath = this.getFilePath(key);\n const expiresAt = ttl ? Date.now() + ttl * 1000 : Number.MAX_SAFE_INTEGER;\n\n const entry: StorageEntry = {\n expiresAt,\n value,\n };\n\n try {\n await writeFile(filePath, JSON.stringify(entry, null, 2), \"utf-8\");\n } catch (error) {\n console.error(`Failed to save key ${key}:`, error);\n throw error;\n }\n }\n\n /**\n * Get the number of stored items\n */\n async size(): Promise<number> {\n try {\n await this.ensureDirectory();\n const files = await readdir(this.directory);\n return files.filter((f) => f.endsWith(this.fileExtension)).length;\n } catch {\n return 0;\n }\n }\n\n /**\n * Ensure storage directory exists\n */\n private async ensureDirectory(): Promise<void> {\n try {\n const stats = await stat(this.directory);\n if (!stats.isDirectory()) {\n throw new Error(`Path ${this.directory} exists but is not a directory`);\n }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n await mkdir(this.directory, { recursive: true });\n } else {\n throw error;\n }\n }\n }\n\n /**\n * Get file path for a key\n */\n private getFilePath(key: string): string {\n // Sanitize key to prevent directory traversal\n const sanitizedKey = key.replace(/[^a-zA-Z0-9_-]/g, \"_\");\n return join(this.directory, `${sanitizedKey}${this.fileExtension}`);\n }\n}\n","/* eslint-disable @typescript-eslint/no-explicit-any */\n/**\n * JWKS (JSON Web Key Set) Verifier\n * Provides JWT verification using public keys from JWKS endpoints\n *\n * Requires the 'jose' package as an optional peer dependency.\n * Install with: npm install jose\n */\n\nimport type { TokenVerificationResult, TokenVerifier } from \"../types.js\";\nimport type { JWTClaims } from \"./jwtIssuer.js\";\n\n/**\n * Token verification result\n */\nexport interface JWKSVerificationResult {\n claims?: JWTClaims;\n error?: string;\n valid: boolean;\n}\n\n/**\n * JWKS configuration options\n */\nexport interface JWKSVerifierConfig {\n /**\n * Expected token audience\n */\n audience?: string;\n\n /**\n * Cache duration for JWKS keys in milliseconds\n * @default 3600000 (1 hour)\n */\n cacheDuration?: number;\n\n /**\n * Cooldown duration between JWKS refetches in milliseconds\n * @default 30000 (30 seconds)\n */\n cooldownDuration?: number;\n\n /**\n * Expected token issuer\n */\n issuer?: string;\n\n /**\n * JWKS endpoint URL (e.g., https://provider.com/.well-known/jwks.json)\n */\n jwksUri: string;\n}\n\n/**\n * JWKS Verifier\n * Verifies JWTs using public keys from a JWKS endpoint\n *\n * This class requires the 'jose' package to be installed:\n * ```bash\n * npm install jose\n * ```\n *\n * @example\n * ```typescript\n * const verifier = new JWKSVerifier({\n * jwksUri: 'https://accounts.google.com/.well-known/jwks.json',\n * audience: 'your-client-id',\n * issuer: 'https://accounts.google.com'\n * });\n *\n * const result = await verifier.verify(token);\n * if (result.valid) {\n * console.log('Token claims:', result.claims);\n * }\n * ```\n */\nexport class JWKSVerifier implements TokenVerifier {\n private config: Required<JWKSVerifierConfig>;\n private jose: any;\n private joseLoaded = false;\n private jwksCache: any;\n\n constructor(config: JWKSVerifierConfig) {\n this.config = {\n cacheDuration: 3600000, // 1 hour\n cooldownDuration: 30000, // 30 seconds\n ...config,\n audience: config.audience || \"\",\n issuer: config.issuer || \"\",\n };\n }\n\n /**\n * Get the JWKS URI being used\n */\n getJwksUri(): string {\n return this.config.jwksUri;\n }\n\n /**\n * Refresh the JWKS cache\n * Useful if you need to force a key refresh\n */\n async refreshKeys(): Promise<void> {\n await this.loadJose();\n\n // Recreate the JWKS cache to force a refresh\n this.jwksCache = this.jose.createRemoteJWKSet(\n new URL(this.config.jwksUri),\n {\n cacheMaxAge: this.config.cacheDuration,\n cooldownDuration: this.config.cooldownDuration,\n },\n );\n }\n\n /**\n * Verify a JWT token using JWKS\n *\n * @param token - The JWT token to verify\n * @returns Verification result with claims if valid\n *\n * @example\n * ```typescript\n * const result = await verifier.verify(token);\n * if (result.valid) {\n * console.log('User:', result.claims?.client_id);\n * } else {\n * console.error('Invalid token:', result.error);\n * }\n * ```\n */\n async verify(token: string): Promise<TokenVerificationResult> {\n try {\n // Ensure jose is loaded\n await this.loadJose();\n\n // Verify the token using JWKS\n const verifyOptions: any = {};\n\n if (this.config.audience) {\n verifyOptions.audience = this.config.audience;\n }\n\n if (this.config.issuer) {\n verifyOptions.issuer = this.config.issuer;\n }\n\n const { payload } = await this.jose.jwtVerify(\n token,\n this.jwksCache,\n verifyOptions,\n );\n\n // Map jose claims to TokenVerificationResult format\n // Store all claims as Record<string, unknown> for compatibility\n const claims: Record<string, unknown> = {\n aud: payload.aud,\n client_id: payload.client_id || payload.sub,\n exp: payload.exp,\n iat: payload.iat,\n iss: payload.iss,\n jti: payload.jti || \"\",\n scope: this.parseScope(payload.scope),\n ...payload, // Include all other claims\n };\n\n return {\n claims,\n valid: true,\n };\n } catch (error: any) {\n return {\n error: error.message || \"Token verification failed\",\n valid: false,\n };\n }\n }\n\n /**\n * Lazy load the jose library\n * Only loads when verification is first attempted\n */\n private async loadJose(): Promise<void> {\n if (this.joseLoaded) {\n return;\n }\n\n try {\n this.jose = await import(\"jose\");\n this.joseLoaded = true;\n\n // Create the JWKS cache with the configured URI\n this.jwksCache = this.jose.createRemoteJWKSet(\n new URL(this.config.jwksUri),\n {\n cacheMaxAge: this.config.cacheDuration,\n cooldownDuration: this.config.cooldownDuration,\n },\n );\n } catch (error: any) {\n throw new Error(\n `JWKS verification requires the 'jose' package.\\n` +\n `Install it with: npm install jose\\n\\n` +\n `If you don't need JWKS support, use HS256 signing instead (default).\\n\\n` +\n `Original error: ${error.message}`,\n );\n }\n }\n\n /**\n * Parse scope from token payload\n * Handles both string (space-separated) and array formats\n */\n private parseScope(scope: unknown): string[] {\n if (!scope) {\n return [];\n }\n\n if (typeof scope === \"string\") {\n return scope.split(\" \").filter(Boolean);\n }\n\n if (Array.isArray(scope)) {\n return scope;\n }\n\n return [];\n }\n}\n"]}
@@ -981,7 +981,9 @@ ${error instanceof Error ? error.stack : JSON.stringify(error)}`
981
981
  properties: {},
982
982
  type: "object"
983
983
  },
984
- name: tool.name
984
+ name: tool.name,
985
+ // Pass through _meta for MCP ext-apps UI support (issue #229)
986
+ ...tool._meta && { _meta: tool._meta }
985
987
  };
986
988
  })
987
989
  );
@@ -1535,9 +1537,10 @@ var FastMCP = class extends FastMCPEventEmitter {
1535
1537
  this.#serverState = "running" /* Running */;
1536
1538
  } else if (config.transportType === "httpStream") {
1537
1539
  const httpConfig = config.httpStream;
1540
+ const protocol = httpConfig.sslCert || httpConfig.sslKey ? "https" : "http";
1538
1541
  if (httpConfig.stateless) {
1539
1542
  this.#logger.info(
1540
- `[FastMCP info] Starting server in stateless mode on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
1543
+ `[FastMCP info] Starting server in stateless mode on HTTP Stream at ${protocol}://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
1541
1544
  );
1542
1545
  this.#httpStreamServer = await _mcpproxy.startHTTPServer.call(void 0, {
1543
1546
  ...this.#authenticate ? { authenticate: this.#authenticate } : {},
@@ -1580,6 +1583,9 @@ var FastMCP = class extends FastMCPEventEmitter {
1580
1583
  );
1581
1584
  },
1582
1585
  port: httpConfig.port,
1586
+ sslCa: httpConfig.sslCa,
1587
+ sslCert: httpConfig.sslCert,
1588
+ sslKey: httpConfig.sslKey,
1583
1589
  stateless: true,
1584
1590
  streamEndpoint: httpConfig.endpoint
1585
1591
  });
@@ -1628,11 +1634,14 @@ var FastMCP = class extends FastMCPEventEmitter {
1628
1634
  );
1629
1635
  },
1630
1636
  port: httpConfig.port,
1637
+ sslCa: httpConfig.sslCa,
1638
+ sslCert: httpConfig.sslCert,
1639
+ sslKey: httpConfig.sslKey,
1631
1640
  stateless: httpConfig.stateless,
1632
1641
  streamEndpoint: httpConfig.endpoint
1633
1642
  });
1634
1643
  this.#logger.info(
1635
- `[FastMCP info] server is running on HTTP Stream at http://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
1644
+ `[FastMCP info] server is running on HTTP Stream at ${protocol}://${httpConfig.host}:${httpConfig.port}${httpConfig.endpoint}`
1636
1645
  );
1637
1646
  }
1638
1647
  this.#serverState = "running" /* Running */;
@@ -2009,6 +2018,9 @@ var FastMCP = class extends FastMCPEventEmitter {
2009
2018
  const enableJsonResponse = _optionalChain([overrides, 'optionalAccess', _65 => _65.httpStream, 'optionalAccess', _66 => _66.enableJsonResponse]) || false;
2010
2019
  const stateless = _optionalChain([overrides, 'optionalAccess', _67 => _67.httpStream, 'optionalAccess', _68 => _68.stateless]) || statelessArg === "true" || envStateless === "true" || false;
2011
2020
  const eventStore = _optionalChain([overrides, 'optionalAccess', _69 => _69.httpStream, 'optionalAccess', _70 => _70.eventStore]);
2021
+ const sslCa = _optionalChain([overrides, 'optionalAccess', _71 => _71.httpStream, 'optionalAccess', _72 => _72.sslCa]);
2022
+ const sslCert = _optionalChain([overrides, 'optionalAccess', _73 => _73.httpStream, 'optionalAccess', _74 => _74.sslCert]);
2023
+ const sslKey = _optionalChain([overrides, 'optionalAccess', _75 => _75.httpStream, 'optionalAccess', _76 => _76.sslKey]);
2012
2024
  return {
2013
2025
  httpStream: {
2014
2026
  enableJsonResponse,
@@ -2016,6 +2028,9 @@ var FastMCP = class extends FastMCPEventEmitter {
2016
2028
  eventStore,
2017
2029
  host,
2018
2030
  port,
2031
+ sslCa,
2032
+ sslCert,
2033
+ sslKey,
2019
2034
  stateless
2020
2035
  },
2021
2036
  transportType: "httpStream"
@@ -2076,4 +2091,4 @@ var FastMCP = class extends FastMCPEventEmitter {
2076
2091
 
2077
2092
 
2078
2093
  exports.DiscoveryDocumentCache = DiscoveryDocumentCache; exports.imageContent = imageContent; exports.audioContent = audioContent; exports.UnexpectedStateError = UnexpectedStateError; exports.UserError = UserError; exports.ServerState = ServerState; exports.FastMCPSession = FastMCPSession; exports.FastMCP = FastMCP;
2079
- //# sourceMappingURL=chunk-W6DQCBJ3.cjs.map
2094
+ //# sourceMappingURL=chunk-EVM4KQGR.cjs.map