skybridge 2.0.4 → 2.1.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.
Files changed (117) hide show
  1. package/README.md +9 -9
  2. package/dist/cli/run-plain.js +2 -1
  3. package/dist/cli/run-plain.js.map +1 -1
  4. package/dist/cli/use-nodemon.d.ts +11 -1
  5. package/dist/cli/use-nodemon.js +19 -5
  6. package/dist/cli/use-nodemon.js.map +1 -1
  7. package/dist/commands/dev.js +3 -3
  8. package/dist/commands/dev.js.map +1 -1
  9. package/dist/server/app.d.ts +22 -5
  10. package/dist/server/app.js +24 -13
  11. package/dist/server/app.js.map +1 -1
  12. package/dist/server/app.test.js +81 -0
  13. package/dist/server/app.test.js.map +1 -1
  14. package/dist/server/auth/providers/custom.d.ts +4 -2
  15. package/dist/server/auth/providers/custom.js.map +1 -1
  16. package/dist/server/auth/providers/workos.d.ts +3 -2
  17. package/dist/server/auth/providers/workos.js +2 -1
  18. package/dist/server/auth/providers/workos.js.map +1 -1
  19. package/dist/server/auth/verify.d.ts +5 -3
  20. package/dist/server/auth/verify.js.map +1 -1
  21. package/dist/server/auth/verify.test.js +16 -0
  22. package/dist/server/auth/verify.test.js.map +1 -1
  23. package/dist/server/events.d.ts +113 -0
  24. package/dist/server/events.js +318 -0
  25. package/dist/server/events.js.map +1 -0
  26. package/dist/server/events.test.d.ts +1 -0
  27. package/dist/server/events.test.js +172 -0
  28. package/dist/server/events.test.js.map +1 -0
  29. package/dist/server/express.d.ts +3 -2
  30. package/dist/server/express.js +17 -11
  31. package/dist/server/express.js.map +1 -1
  32. package/dist/server/express.test.js +45 -0
  33. package/dist/server/express.test.js.map +1 -1
  34. package/dist/server/form-input.d.ts +120 -0
  35. package/dist/server/form-input.js +89 -0
  36. package/dist/server/form-input.js.map +1 -0
  37. package/dist/server/index.d.ts +4 -2
  38. package/dist/server/index.js +3 -1
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/mcp-errors.test.js +188 -0
  41. package/dist/server/mcp-errors.test.js.map +1 -1
  42. package/dist/server/metric.d.ts +5 -3
  43. package/dist/server/metric.js +5 -5
  44. package/dist/server/metric.js.map +1 -1
  45. package/dist/server/register-tool.test.js +203 -0
  46. package/dist/server/register-tool.test.js.map +1 -1
  47. package/dist/server/server.d.ts +254 -10
  48. package/dist/server/server.js +262 -9
  49. package/dist/server/server.js.map +1 -1
  50. package/dist/server/tool-extra.test.js +79 -0
  51. package/dist/server/tool-extra.test.js.map +1 -1
  52. package/dist/web/bridges/adaptor.d.ts +14 -3
  53. package/dist/web/bridges/adaptor.js +164 -14
  54. package/dist/web/bridges/adaptor.js.map +1 -1
  55. package/dist/web/bridges/adaptor.test.js +62 -6
  56. package/dist/web/bridges/adaptor.test.js.map +1 -1
  57. package/dist/web/bridges/apps-sdk/bridge.js +1 -1
  58. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  59. package/dist/web/bridges/apps-sdk/types.d.ts +2 -3
  60. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  61. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +3 -3
  62. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +3 -3
  63. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  64. package/dist/web/bridges/mcp-app/bridge.d.ts +1 -1
  65. package/dist/web/bridges/mcp-app/bridge.js +9 -2
  66. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  67. package/dist/web/bridges/mcp-app/types.d.ts +18 -3
  68. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  69. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +2 -2
  70. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
  71. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  72. package/dist/web/bridges/types.d.ts +57 -4
  73. package/dist/web/bridges/types.js +1 -1
  74. package/dist/web/bridges/types.js.map +1 -1
  75. package/dist/web/data-llm.js +3 -2
  76. package/dist/web/data-llm.js.map +1 -1
  77. package/dist/web/data-llm.test.js +6 -0
  78. package/dist/web/data-llm.test.js.map +1 -1
  79. package/dist/web/generate-helpers.d.ts +2 -2
  80. package/dist/web/generate-helpers.js +2 -2
  81. package/dist/web/generate-helpers.js.map +1 -1
  82. package/dist/web/hooks/index.d.ts +5 -1
  83. package/dist/web/hooks/index.js +5 -1
  84. package/dist/web/hooks/index.js.map +1 -1
  85. package/dist/web/hooks/openai-extensions.test.d.ts +1 -0
  86. package/dist/web/hooks/openai-extensions.test.js +107 -0
  87. package/dist/web/hooks/openai-extensions.test.js.map +1 -0
  88. package/dist/web/hooks/use-call-tool.d.ts +2 -2
  89. package/dist/web/hooks/use-call-tool.js +15 -10
  90. package/dist/web/hooks/use-call-tool.js.map +1 -1
  91. package/dist/web/hooks/use-call-tool.test.js +13 -2
  92. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  93. package/dist/web/hooks/use-deep-link.d.ts +22 -0
  94. package/dist/web/hooks/use-deep-link.js +34 -0
  95. package/dist/web/hooks/use-deep-link.js.map +1 -0
  96. package/dist/web/hooks/use-file-resource.d.ts +37 -0
  97. package/dist/web/hooks/use-file-resource.js +98 -0
  98. package/dist/web/hooks/use-file-resource.js.map +1 -0
  99. package/dist/web/hooks/use-file-resource.test.d.ts +1 -0
  100. package/dist/web/hooks/use-file-resource.test.js +65 -0
  101. package/dist/web/hooks/use-file-resource.test.js.map +1 -0
  102. package/dist/web/hooks/use-host.d.ts +27 -5
  103. package/dist/web/hooks/use-host.js +20 -5
  104. package/dist/web/hooks/use-host.js.map +1 -1
  105. package/dist/web/hooks/use-model-context.d.ts +44 -0
  106. package/dist/web/hooks/use-model-context.js +63 -0
  107. package/dist/web/hooks/use-model-context.js.map +1 -0
  108. package/dist/web/hooks/use-open-file.d.ts +20 -0
  109. package/dist/web/hooks/use-open-file.js +25 -0
  110. package/dist/web/hooks/use-open-file.js.map +1 -0
  111. package/dist/web/hooks/use-register-view-tool.d.ts +1 -1
  112. package/dist/web/hooks/use-register-view-tool.js +1 -1
  113. package/dist/web/hooks/use-register-view-tool.js.map +1 -1
  114. package/dist/web/hooks/use-send-follow-up-message.d.ts +8 -2
  115. package/dist/web/hooks/use-send-follow-up-message.js +6 -0
  116. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
  117. package/package.json +6 -5
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport type {\n Implementation,\n Server as SdkServer,\n ServerOptions,\n} from \"@modelcontextprotocol/server\";\nimport type { ErrorRequestHandler, Express, RequestHandler } from \"express\";\nimport type { OAuthConfig, OAuthProvider } from \"./auth/index.js\";\nimport { type ResourceMetadataUrlResolver, setupOAuth } from \"./auth/setup.js\";\nimport type { ExtraClaims } from \"./auth.js\";\nimport { buildMcpHandler, createApp, createBaseApp } from \"./express.js\";\nimport { createMiddlewareEntry } from \"./metric.js\";\nimport type { McpMiddlewareEntry } from \"./middleware.js\";\nimport { buildMiddlewareChain, getHandlerMaps } from \"./middleware.js\";\nimport {\n type JsonOptions,\n McpServer,\n type McpServerTypes,\n type ToolDef,\n} from \"./server.js\";\n\nconst SLOW_HANDLER_THRESHOLD_MS = 50;\n\ntype ErrorMiddlewareConfig = {\n path?: string;\n handlers: ErrorRequestHandler[];\n};\n\n/**\n * The `handler` field of {@link SkybridgeConfig}: builds the app's MCP\n * surface. Runs again for **every incoming request**, on a fresh\n * {@link McpServer}, so keep it to registration: pools, clients and any other\n * one-time work belong in `setup`, whose result is the second argument. It\n * must **return** the chained server so `typeof app` carries the registered\n * tool types.\n *\n * @typeParam TConfig - What `setup` resolved to, passed as the second argument.\n */\nexport type SkybridgeHandler<\n TTools extends Record<string, ToolDef>,\n TConfig,\n TAuthExtra extends ExtraClaims,\n> = (\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n config: TConfig,\n) => McpServer<TTools, TAuthExtra>;\n\n/**\n * What the `oauth` field accepts: an {@link OAuthConfig}, a provider\n * (`oauth: auth0Provider(...)`), or a function of the `setup` result returning\n * either. Providers and functions resolve once, at {@link Skybridge.run} or on\n * the first request, never at module import. Anything asynchronous belongs in\n * a provider's `resolve`.\n */\nexport type SkybridgeOAuthInput<TConfig, TExtra extends ExtraClaims> =\n | OAuthConfig<TExtra>\n | OAuthProvider<TExtra>\n | ((config: TConfig) => OAuthConfig<TExtra> | OAuthProvider<TExtra>);\n\nasync function resolveOAuthInput<TConfig, TExtra extends ExtraClaims>(\n input: SkybridgeOAuthInput<TConfig, TExtra> | undefined,\n config: TConfig,\n): Promise<OAuthConfig<TExtra> | undefined> {\n const resolved = typeof input === \"function\" ? input(config) : input;\n return resolved && \"resolve\" in resolved\n ? await resolved.resolve()\n : resolved;\n}\n\n/**\n * Everything a Skybridge app needs in one bag: the MCP implementation info\n * (`name`, `version`, …), the SDK's {@link ServerOptions} (`capabilities`,\n * `instructions`, …), the Express and skills options, and the app's behavior\n * (`setup`, `oauth`, `handler`).\n *\n * All type parameters are inferred from the value: the config from `setup`,\n * the auth claims from `oauth`, and the tool registry from the server\n * `handler` returns.\n */\nexport type SkybridgeConfig<\n TTools extends Record<string, ToolDef> = Record<never, ToolDef>,\n TConfig = undefined,\n TAuthExtra extends ExtraClaims = ExtraClaims,\n> = Implementation &\n ServerOptions & {\n /** Options for the built-in `express.json()` middleware, e.g. `{ limit: \"10mb\" }`. */\n json?: JsonOptions;\n /**\n * @experimental Serve Agent Skills from `src/skills` over MCP (SEP-2640).\n * API may change.\n */\n skills?: boolean;\n /**\n * Loads whatever the app needs up front (remote config, secrets, datasets,\n * …). Runs **once** — at {@link Skybridge.run} or on the first request,\n * never at module import — and its awaited return value is passed to an\n * `oauth` function and to `handler` as the second argument.\n */\n setup?: () => TConfig;\n /**\n * Resource-server OAuth. When set, mounts the well-known metadata routes\n * and bearer auth on `/mcp`, and the verifier's claims type\n * `extra.http.authInfo.extra` in tool handlers.\n */\n oauth?: SkybridgeOAuthInput<Awaited<TConfig>, TAuthExtra>;\n /** Registers the MCP surface, per request. See {@link SkybridgeHandler}. */\n handler: SkybridgeHandler<TTools, Awaited<TConfig>, TAuthExtra>;\n };\n\n/**\n * A Skybridge app: the HTTP surface (Express, OAuth metadata, the `/mcp`\n * route) plus a handler that builds the MCP server for each request.\n *\n * The handler runs for every request, so tools, resources, prompts and views\n * are always registered on the instance that serves the request. Anything in\n * the handler body other than registration therefore runs per request too.\n * Anything asynchronous the app needs (remote config, secrets, …) goes in\n * `setup`, which runs once and feeds the handler's second argument.\n *\n * @example\n * ```ts\n * export const app = new Skybridge({\n * name: \"my-app\",\n * version: \"1.0.0\",\n * setup: async () => loadConfig(),\n * oauth: (config) => descopeProvider({ url: config.mcpServerUrl }),\n * handler: (server, config) =>\n * server.registerTool(\n * {\n * name: \"search\",\n * inputSchema: { query: z.string() },\n * view: { component: \"search\" },\n * },\n * async ({ query }) => ({ content: `Results for ${query}` }),\n * ),\n * });\n *\n * export type AppType = typeof app;\n * ```\n *\n * @see https://docs.skybridge.tech/api-reference/mcp-server\n */\nexport class Skybridge<\n TTools extends Record<string, ToolDef> = Record<never, ToolDef>,\n TConfig = undefined,\n TAuthExtra extends ExtraClaims = ExtraClaims,\n> {\n declare readonly $types: McpServerTypes<TTools>;\n private readonly serverInfo: Implementation;\n private readonly serverOptions: ServerOptions;\n private readonly skills?: boolean;\n private oauthEnabled = false;\n private readonly handler: SkybridgeHandler<\n TTools,\n Awaited<TConfig>,\n TAuthExtra\n >;\n private readonly setup?: () => TConfig;\n private readonly oauthInput?: SkybridgeOAuthInput<\n Awaited<TConfig>,\n TAuthExtra\n >;\n private config?: Awaited<TConfig>;\n private readonly expressApp: Express;\n private readonly errorMiddleware: ErrorMiddlewareConfig[] = [];\n private readonly monitoringEntry: McpMiddlewareEntry | null =\n createMiddlewareEntry();\n private resolveResourceMetadataUrl?: ResourceMetadataUrlResolver;\n private readyPromise?: Promise<void>;\n private slowHandlerWarned = false;\n\n constructor({\n name,\n title,\n version,\n description,\n icons,\n websiteUrl,\n json,\n skills,\n setup,\n oauth,\n handler,\n ...serverOptions\n }: SkybridgeConfig<TTools, TConfig, TAuthExtra>) {\n this.serverInfo = { name, title, version, description, icons, websiteUrl };\n this.serverOptions = serverOptions;\n this.skills = skills;\n this.setup = setup;\n this.oauthInput = oauth;\n this.handler = handler;\n this.expressApp = createBaseApp(json);\n }\n\n /**\n * Resolve `setup` and `oauth`, then wire OAuth onto the Express app. Runs\n * once; every entry point that needs a built server awaits it, and a failed\n * attempt is retried on the next call.\n *\n * @internal\n */\n ready(): Promise<void> {\n this.readyPromise ??= (async () => {\n if (this.setup) {\n this.config = await this.setup();\n }\n const oauth = await resolveOAuthInput(\n this.oauthInput,\n this.config as Awaited<TConfig>,\n );\n this.oauthEnabled = Boolean(oauth);\n const sample = this.buildServer();\n if (oauth) {\n this.resolveResourceMetadataUrl = setupOAuth(\n this.expressApp,\n oauth,\n sample.securitySchemesByTool,\n );\n }\n })().catch((error) => {\n this.readyPromise = undefined;\n throw error;\n });\n return this.readyPromise;\n }\n\n /**\n * The underlying Express app. Use this to extend the HTTP server with\n * custom routes, middleware, or settings — e.g.\n * `app.express.get(\"/health\", ...)`.\n *\n * `express.json()` is pre-applied — tune it via the `json` config field,\n * e.g. `new Skybridge({ name, version, json: { limit: \"10mb\" }, handler })`.\n * Register your handlers before `run()`; after `run()`, dev-mode middleware,\n * the `/mcp` route, and the default error handler are appended in that order.\n *\n * Note: Alpic Cloud only routes traffic to `/mcp` — custom routes work\n * locally and on self-hosted deployments.\n */\n get express(): Express {\n return this.expressApp;\n }\n\n /**\n * Build a fresh server for one stateless HTTP request, as\n * `createMcpHandler`'s factory contract requires: the handler runs again so\n * the SDK's handler closures belong to the instance whose protocol era it\n * stamps. Sharing one instance's handler maps instead would bind them to an\n * instance that is never marked, pinning every request to the 2025 codec and\n * letting concurrent callers overwrite each other's negotiated version.\n *\n * Awaits `setup` and the `oauth` input on first use.\n */\n async createServerInstance(): Promise<SdkServer> {\n await this.ready();\n const server = this.buildServer();\n this.instrumentHandlers(server);\n return server.server;\n }\n\n /**\n * Connect a Skybridge app to an MCP transport. Use this when you're\n * embedding Skybridge in a host that already manages its own transport\n * (e.g. stdio for desktop apps); for HTTP, prefer {@link Skybridge.run}\n * which sets the transport up for you.\n */\n async connect(transport: Parameters<SdkServer[\"connect\"]>[0]): Promise<void> {\n const instance = await this.createServerInstance();\n await instance.connect(transport);\n }\n\n /**\n * Register Express middleware on the underlying app. Mirrors `app.use` —\n * pass handlers directly or a path-prefixed handler list. Register before\n * {@link Skybridge.run}; ordering matches Express.\n *\n * Note: Alpic Cloud only routes traffic to `/mcp`. Custom paths work\n * locally and on self-hosted deployments.\n */\n use(...handlers: RequestHandler[]): this;\n use(path: string, ...handlers: RequestHandler[]): this;\n use(\n pathOrHandler: string | RequestHandler,\n ...handlers: RequestHandler[]\n ): this {\n // Branching is load-bearing: Express's `app.use` overloads can't be\n // resolved against a `string | RequestHandler` union, so we narrow.\n if (typeof pathOrHandler === \"string\") {\n this.expressApp.use(pathOrHandler, ...handlers);\n } else {\n this.expressApp.use(pathOrHandler, ...handlers);\n }\n return this;\n }\n\n /**\n * Register Express error-handling middleware to run after the built-in\n * `/mcp` route (or your custom route). Use this to log or transform errors\n * thrown by tool handlers before the default error handler responds.\n *\n * @example\n * ```ts\n * app.useOnError((err, _req, _res, next) => {\n * logger.error(err);\n * next(err);\n * });\n * ```\n */\n useOnError(...handlers: ErrorRequestHandler[]): this;\n useOnError(path: string, ...handlers: ErrorRequestHandler[]): this;\n useOnError(\n pathOrHandler: string | ErrorRequestHandler,\n ...handlers: ErrorRequestHandler[]\n ): this {\n if (typeof pathOrHandler === \"string\") {\n this.errorMiddleware.push({ path: pathOrHandler, handlers });\n } else {\n this.errorMiddleware.push({ handlers: [pathOrHandler, ...handlers] });\n }\n return this;\n }\n\n /**\n * Start the HTTP server. Listens on `process.env.__PORT` (default `3000`),\n * mounts the `/mcp` route, and applies any custom Express middleware\n * registered via {@link Skybridge.use} / {@link Skybridge.useOnError}.\n *\n * On Cloudflare Workers / workerd, returns an object exposing `fetch` so\n * the runtime can bridge incoming requests to the Node HTTP server. On\n * Vercel (`VERCEL === \"1\"`), returns the Express app directly so the\n * serverless function entry can call it as a `(req, res)` handler. On\n * Node, returns `undefined` once listening. When the process was spawned\n * with an IPC channel, the bound port is reported to the parent as\n * `{ type: \"skybridge:listening\", port }`, the readiness signal test runners\n * wait on instead of polling.\n */\n async run(): Promise<\n { fetch: (...args: unknown[]) => unknown } | Express | undefined\n > {\n if (process.env.VERCEL === \"1\") {\n // createApp only reads httpServer inside its dev-only branch\n // (viewsDevServer); under VERCEL=1 + NODE_ENV=production it's a\n // bare object passed to satisfy the required parameter.\n const httpServer = http.createServer();\n await createApp({\n app: this,\n httpServer,\n errorMiddleware: this.errorMiddleware,\n });\n return this.expressApp;\n }\n\n const httpServer = http.createServer();\n const mcpHandler = buildMcpHandler(this);\n\n await createApp({\n app: this,\n httpServer,\n mcpHandler,\n errorMiddleware: this.errorMiddleware,\n });\n\n httpServer.on(\"request\", this.expressApp);\n const intendedPort = parseInt(process.env.__PORT ?? \"3000\", 10);\n await new Promise<void>((resolve, reject) => {\n httpServer.on(\"error\", (error: Error) => {\n console.error(\"Failed to start server:\", error);\n reject(error);\n });\n httpServer.listen(intendedPort, () => {\n resolve();\n });\n });\n\n const { port } = httpServer.address() as AddressInfo;\n process.send?.({ type: \"skybridge:listening\", port });\n\n // On workerd, bridge the Node http server to a Workers fetch handler.\n // The specifier is held in a variable to sidestep tsc's module resolution\n // (`cloudflare:node` only exists under wrangler/workerd).\n if (\n typeof navigator !== \"undefined\" &&\n navigator.userAgent === \"Cloudflare-Workers\"\n ) {\n const cloudflareNode = \"cloudflare:node\";\n const { httpServerHandler } = await import(cloudflareNode);\n return httpServerHandler({ port });\n }\n\n const shutdown = () => {\n // Drop both handlers so a second signal falls through to Node's default\n // (force-quit on a second Ctrl+C while drain is hanging).\n process.off(\"SIGTERM\", shutdown);\n process.off(\"SIGINT\", shutdown);\n mcpHandler.close().catch(() => {});\n httpServer.close(() => process.exit(0));\n // Force exit if connections don't drain in time so the port is still\n // released promptly (e.g. for nodemon restarts).\n setTimeout(() => process.exit(0), 3000).unref();\n };\n process.on(\"SIGTERM\", shutdown);\n process.on(\"SIGINT\", shutdown);\n return undefined;\n }\n\n private buildServer(): McpServer<TTools, TAuthExtra> {\n const server = new McpServer<Record<never, ToolDef>, TAuthExtra>(\n this.serverInfo,\n this.serverOptions,\n { skills: this.skills, oauth: this.oauthEnabled },\n );\n if (this.resolveResourceMetadataUrl) {\n server.setResourceMetadataUrlResolver(this.resolveResourceMetadataUrl);\n }\n const startedAt = performance.now();\n const built = this.handler(server, this.config as Awaited<TConfig>);\n const elapsed = performance.now() - startedAt;\n if (typeof (built as { then?: unknown }).then === \"function\") {\n throw new Error(\n \"The Skybridge handler must be synchronous — it runs on every request. Load config or secrets in `setup` instead and read them from the handler's second argument.\",\n );\n }\n if (elapsed > SLOW_HANDLER_THRESHOLD_MS && !this.slowHandlerWarned) {\n this.slowHandlerWarned = true;\n console.warn(\n `The Skybridge handler took ${Math.round(elapsed)}ms — it runs on every request, so this cost is paid per request. Move expensive work into \\`setup\\`, whose result is passed to the handler.`,\n );\n }\n return built;\n }\n\n private middlewareEntries(\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n ): McpMiddlewareEntry[] {\n return [\n ...(this.monitoringEntry ? [this.monitoringEntry] : []),\n ...server.protocolMiddlewareEntries(),\n ];\n }\n\n private instrumentHandlers(\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n ): void {\n const entries = this.middlewareEntries(server);\n const { requestHandlers, notificationHandlers } = getHandlerMaps(\n server.server,\n );\n\n const instrumentMap = (\n map: Map<string, (...args: unknown[]) => Promise<unknown>>,\n isNotification: boolean,\n ) => {\n for (const [method, handler] of map) {\n map.set(\n method,\n buildMiddlewareChain(method, isNotification, handler, entries),\n );\n }\n const originalSet = map.set.bind(map);\n map.set = (\n method: string,\n handler: (...args: unknown[]) => Promise<unknown>,\n ) =>\n originalSet(\n method,\n buildMiddlewareChain(method, isNotification, handler, entries),\n );\n };\n\n instrumentMap(requestHandlers, false);\n instrumentMap(notificationHandlers, true);\n }\n}\n"]}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport type {\n Implementation,\n Server as SdkServer,\n ServerOptions,\n} from \"@modelcontextprotocol/server\";\nimport type { ErrorRequestHandler, Express, RequestHandler } from \"express\";\nimport type { OAuthConfig, OAuthProvider } from \"./auth/index.js\";\nimport { type ResourceMetadataUrlResolver, setupOAuth } from \"./auth/setup.js\";\nimport type { ExtraClaims } from \"./auth.js\";\nimport { buildMcpHandler, createApp, createBaseApp } from \"./express.js\";\nimport { registerMonitoring } from \"./metric.js\";\nimport type { McpMiddlewareEntry } from \"./middleware.js\";\nimport { buildMiddlewareChain, getHandlerMaps } from \"./middleware.js\";\nimport {\n type JsonOptions,\n McpServer,\n type McpServerTypes,\n type ToolDef,\n} from \"./server.js\";\n\nconst SLOW_HANDLER_THRESHOLD_MS = 50;\n\ntype ErrorMiddlewareConfig = {\n path?: string;\n handlers: ErrorRequestHandler[];\n};\n\n/**\n * The `handler` field of {@link SkybridgeConfig}: builds the app's MCP\n * surface. Runs again for **every incoming request**, on a fresh\n * {@link McpServer}, so keep it to registration: pools, clients and any other\n * one-time work belong in `setup`, whose result is the second argument. It\n * must **return** the chained server so `typeof app` carries the registered\n * tool types.\n *\n * @typeParam TConfig - What `setup` resolved to, passed as the second argument.\n */\nexport type SkybridgeHandler<\n TTools extends Record<string, ToolDef>,\n TConfig,\n TAuthExtra extends ExtraClaims,\n> = (\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n config: TConfig,\n) => McpServer<TTools, TAuthExtra>;\n\n/**\n * What the `oauth` field accepts: an {@link OAuthConfig}, a provider\n * (`oauth: auth0Provider(...)`), or a function of the `setup` result returning\n * either. Providers and functions resolve once, at {@link Skybridge.run} or on\n * the first request, never at module import. Anything asynchronous belongs in\n * a provider's `resolve`.\n */\nexport type SkybridgeOAuthInput<TConfig, TExtra extends ExtraClaims> =\n | OAuthConfig<TExtra>\n | OAuthProvider<TExtra>\n | ((config: TConfig) => OAuthConfig<TExtra> | OAuthProvider<TExtra>);\n\nasync function resolveOAuthInput<TConfig, TExtra extends ExtraClaims>(\n input: SkybridgeOAuthInput<TConfig, TExtra> | undefined,\n config: TConfig,\n): Promise<OAuthConfig<TExtra> | undefined> {\n const resolved = typeof input === \"function\" ? input(config) : input;\n return resolved && \"resolve\" in resolved\n ? await resolved.resolve()\n : resolved;\n}\n\n/**\n * Everything a Skybridge app needs in one bag: the MCP implementation info\n * (`name`, `version`, …), the SDK's {@link ServerOptions} (`capabilities`,\n * `instructions`, …), the Express and skills options, and the app's behavior\n * (`setup`, `oauth`, `handler`).\n *\n * All type parameters are inferred from the value: the config from `setup`,\n * the auth claims from `oauth`, and the tool registry from the server\n * `handler` returns.\n */\nexport type SkybridgeConfig<\n TTools extends Record<string, ToolDef> = Record<never, ToolDef>,\n TConfig = undefined,\n TAuthExtra extends ExtraClaims = ExtraClaims,\n> = Implementation &\n ServerOptions & {\n /** Options for the built-in `express.json()` middleware, e.g. `{ limit: \"10mb\" }`. */\n json?: JsonOptions;\n /**\n * @experimental Serve Agent Skills from `src/skills` over MCP (SEP-2640).\n * API may change.\n */\n skills?: boolean;\n /**\n * Replaces the error reporter of the built-in `/mcp` handler. Receives every\n * error the MCP SDK handler or its Node adapter reports: rejected requests,\n * including the `UnsupportedProtocolVersionError` the default skips, and\n * failures to build or serve the per-request server. Reporting only: the\n * response does not change. It can run before the response is sent, so keep\n * it fast and synchronous. Default: `console.error` for every error the MCP\n * SDK handler reports, except `UnsupportedProtocolVersionError`. Node\n * adapter errors are not logged by default.\n */\n onMcpError?: (error: Error) => void;\n /**\n * Loads whatever the app needs up front (remote config, secrets, datasets,\n * …). Runs **once** — at {@link Skybridge.run} or on the first request,\n * never at module import — and its awaited return value is passed to an\n * `oauth` function and to `handler` as the second argument.\n */\n setup?: () => TConfig;\n /**\n * Resource-server OAuth. When set, mounts the well-known metadata routes\n * and bearer auth on `/mcp`, and the verifier's claims type\n * `extra.http.authInfo.extra` in tool handlers.\n */\n oauth?: SkybridgeOAuthInput<Awaited<TConfig>, TAuthExtra>;\n /** Registers the MCP surface, per request. See {@link SkybridgeHandler}. */\n handler: SkybridgeHandler<TTools, Awaited<TConfig>, TAuthExtra>;\n };\n\n/**\n * A Skybridge app: the HTTP surface (Express, OAuth metadata, the `/mcp`\n * route) plus a handler that builds the MCP server for each request.\n *\n * The handler runs for every request, so tools, resources, prompts and views\n * are always registered on the instance that serves the request. Anything in\n * the handler body other than registration therefore runs per request too.\n * Anything asynchronous the app needs (remote config, secrets, …) goes in\n * `setup`, which runs once and feeds the handler's second argument.\n *\n * @example\n * ```ts\n * export const app = new Skybridge({\n * name: \"my-app\",\n * version: \"1.0.0\",\n * setup: async () => loadConfig(),\n * oauth: (config) => descopeProvider({ url: config.mcpServerUrl }),\n * handler: (server, config) =>\n * server.registerTool(\n * {\n * name: \"search\",\n * inputSchema: { query: z.string() },\n * view: { component: \"search\" },\n * },\n * async ({ query }) => ({ content: `Results for ${query}` }),\n * ),\n * });\n *\n * export type AppType = typeof app;\n * ```\n *\n * @see https://docs.skybridge.tech/api-reference/mcp-server\n */\nexport class Skybridge<\n TTools extends Record<string, ToolDef> = Record<never, ToolDef>,\n TConfig = undefined,\n TAuthExtra extends ExtraClaims = ExtraClaims,\n> {\n declare readonly $types: McpServerTypes<TTools>;\n private readonly serverInfo: Implementation;\n private readonly serverOptions: ServerOptions;\n private readonly skills?: boolean;\n private oauthEnabled = false;\n private readonly handler: SkybridgeHandler<\n TTools,\n Awaited<TConfig>,\n TAuthExtra\n >;\n private readonly setup?: () => TConfig;\n private readonly oauthInput?: SkybridgeOAuthInput<\n Awaited<TConfig>,\n TAuthExtra\n >;\n private config?: Awaited<TConfig>;\n private readonly expressApp: Express;\n private readonly onMcpError?: (error: Error) => void;\n private readonly errorMiddleware: ErrorMiddlewareConfig[] = [];\n private readonly mcpMiddlewareEntries: McpMiddlewareEntry[] = [];\n private resolveResourceMetadataUrl?: ResourceMetadataUrlResolver;\n private readyPromise?: Promise<void>;\n private slowHandlerWarned = false;\n\n constructor({\n name,\n title,\n version,\n description,\n icons,\n websiteUrl,\n json,\n skills,\n onMcpError,\n setup,\n oauth,\n handler,\n ...serverOptions\n }: SkybridgeConfig<TTools, TConfig, TAuthExtra>) {\n this.serverInfo = { name, title, version, description, icons, websiteUrl };\n this.serverOptions = serverOptions;\n this.skills = skills;\n this.onMcpError = onMcpError;\n this.setup = setup;\n this.oauthInput = oauth;\n this.handler = handler;\n this.expressApp = createBaseApp(json);\n registerMonitoring(this);\n }\n\n /**\n * Resolve `setup` and `oauth`, then wire OAuth onto the Express app. Runs\n * once; every entry point that needs a built server awaits it, and a failed\n * attempt is retried on the next call.\n *\n * @internal\n */\n ready(): Promise<void> {\n this.readyPromise ??= (async () => {\n if (this.setup) {\n this.config = await this.setup();\n }\n const oauth = await resolveOAuthInput(\n this.oauthInput,\n this.config as Awaited<TConfig>,\n );\n this.oauthEnabled = Boolean(oauth);\n const sample = this.buildServer();\n if (oauth) {\n this.resolveResourceMetadataUrl = setupOAuth(\n this.expressApp,\n oauth,\n sample.securitySchemesByTool,\n );\n }\n })().catch((error) => {\n this.readyPromise = undefined;\n throw error;\n });\n return this.readyPromise;\n }\n\n /**\n * The underlying Express app. Use this to extend the HTTP server with\n * custom routes, middleware, or settings — e.g.\n * `app.express.get(\"/health\", ...)`.\n *\n * `express.json()` is pre-applied — tune it via the `json` config field,\n * e.g. `new Skybridge({ name, version, json: { limit: \"10mb\" }, handler })`.\n * Register your handlers before `run()`; after `run()`, dev-mode middleware,\n * the `/mcp` route, and the default error handler are appended in that order.\n *\n * Note: Alpic Cloud only routes traffic to `/mcp` — custom routes work\n * locally and on self-hosted deployments.\n */\n get express(): Express {\n return this.expressApp;\n }\n\n /**\n * Build a fresh server for one stateless HTTP request, as\n * `createMcpHandler`'s factory contract requires: the handler runs again so\n * the SDK's handler closures belong to the instance whose protocol era it\n * stamps. Sharing one instance's handler maps instead would bind them to an\n * instance that is never marked, pinning every request to the 2025 codec and\n * letting concurrent callers overwrite each other's negotiated version.\n *\n * Awaits `setup` and the `oauth` input on first use. Additional middleware\n * wraps the app pipeline for this instance only; it is not registered on the app.\n *\n * @internal\n */\n async createServerInstance(\n middleware: McpMiddlewareEntry[] = [],\n ): Promise<SdkServer> {\n await this.ready();\n const server = this.buildServer();\n this.instrumentHandlers(server, middleware);\n return server.server;\n }\n\n /** Register outer MCP middleware on this app, in registration order. @internal */\n withMcpMiddleware(entry: McpMiddlewareEntry): this {\n this.mcpMiddlewareEntries.push(entry);\n return this;\n }\n\n /**\n * Connect a Skybridge app to an MCP transport. Use this when you're\n * embedding Skybridge in a host that already manages its own transport\n * (e.g. stdio for desktop apps); for HTTP, prefer {@link Skybridge.run}\n * which sets the transport up for you.\n */\n async connect(transport: Parameters<SdkServer[\"connect\"]>[0]): Promise<void> {\n const instance = await this.createServerInstance();\n await instance.connect(transport);\n }\n\n /**\n * Register Express middleware on the underlying app. Mirrors `app.use` —\n * pass handlers directly or a path-prefixed handler list. Register before\n * {@link Skybridge.run}; ordering matches Express.\n *\n * Note: Alpic Cloud only routes traffic to `/mcp`. Custom paths work\n * locally and on self-hosted deployments.\n */\n use(...handlers: RequestHandler[]): this;\n use(path: string, ...handlers: RequestHandler[]): this;\n use(\n pathOrHandler: string | RequestHandler,\n ...handlers: RequestHandler[]\n ): this {\n // Branching is load-bearing: Express's `app.use` overloads can't be\n // resolved against a `string | RequestHandler` union, so we narrow.\n if (typeof pathOrHandler === \"string\") {\n this.expressApp.use(pathOrHandler, ...handlers);\n } else {\n this.expressApp.use(pathOrHandler, ...handlers);\n }\n return this;\n }\n\n /**\n * Register Express error-handling middleware to run after the built-in\n * `/mcp` route (or your custom route). Use this to log or transform errors\n * thrown by tool handlers before the default error handler responds.\n *\n * @example\n * ```ts\n * app.useOnError((err, _req, _res, next) => {\n * logger.error(err);\n * next(err);\n * });\n * ```\n */\n useOnError(...handlers: ErrorRequestHandler[]): this;\n useOnError(path: string, ...handlers: ErrorRequestHandler[]): this;\n useOnError(\n pathOrHandler: string | ErrorRequestHandler,\n ...handlers: ErrorRequestHandler[]\n ): this {\n if (typeof pathOrHandler === \"string\") {\n this.errorMiddleware.push({ path: pathOrHandler, handlers });\n } else {\n this.errorMiddleware.push({ handlers: [pathOrHandler, ...handlers] });\n }\n return this;\n }\n\n /**\n * Start the HTTP server. Listens on `process.env.__PORT` (default `3000`),\n * mounts the `/mcp` route, and applies any custom Express middleware\n * registered via {@link Skybridge.use} / {@link Skybridge.useOnError}.\n *\n * On Cloudflare Workers / workerd, returns an object exposing `fetch` so\n * the runtime can bridge incoming requests to the Node HTTP server. On\n * Vercel (`VERCEL === \"1\"`), returns the Express app directly so the\n * serverless function entry can call it as a `(req, res)` handler. On\n * Node, returns `undefined` once listening. When the process was spawned\n * with an IPC channel, the bound port is reported to the parent as\n * `{ type: \"skybridge:listening\", port }`, the readiness signal test runners\n * wait on instead of polling.\n */\n async run(): Promise<\n { fetch: (...args: unknown[]) => unknown } | Express | undefined\n > {\n if (process.env.VERCEL === \"1\") {\n // createApp only reads httpServer inside its dev-only branch\n // (viewsDevServer); under VERCEL=1 + NODE_ENV=production it's a\n // bare object passed to satisfy the required parameter.\n const httpServer = http.createServer();\n await createApp({\n app: this,\n httpServer,\n onMcpError: this.onMcpError,\n errorMiddleware: this.errorMiddleware,\n });\n return this.expressApp;\n }\n\n const httpServer = http.createServer();\n const mcpHandler = buildMcpHandler(this, this.onMcpError);\n\n await createApp({\n app: this,\n httpServer,\n mcpHandler,\n onMcpError: this.onMcpError,\n errorMiddleware: this.errorMiddleware,\n });\n\n httpServer.on(\"request\", this.expressApp);\n const intendedPort = parseInt(process.env.__PORT ?? \"3000\", 10);\n await new Promise<void>((resolve, reject) => {\n httpServer.on(\"error\", (error: Error) => {\n console.error(\"Failed to start server:\", error);\n reject(error);\n });\n httpServer.listen(intendedPort, () => {\n resolve();\n });\n });\n\n const { port } = httpServer.address() as AddressInfo;\n process.send?.({ type: \"skybridge:listening\", port });\n\n // On workerd, bridge the Node http server to a Workers fetch handler.\n // The specifier is held in a variable to sidestep tsc's module resolution\n // (`cloudflare:node` only exists under wrangler/workerd).\n if (\n typeof navigator !== \"undefined\" &&\n navigator.userAgent === \"Cloudflare-Workers\"\n ) {\n const cloudflareNode = \"cloudflare:node\";\n const { httpServerHandler } = await import(cloudflareNode);\n return httpServerHandler({ port });\n }\n\n const shutdown = () => {\n // Drop both handlers so a second signal falls through to Node's default\n // (force-quit on a second Ctrl+C while drain is hanging).\n process.off(\"SIGTERM\", shutdown);\n process.off(\"SIGINT\", shutdown);\n mcpHandler.close().catch(() => {});\n httpServer.close(() => process.exit(0));\n // Force exit if connections don't drain in time so the port is still\n // released promptly (e.g. for nodemon restarts).\n setTimeout(() => process.exit(0), 3000).unref();\n };\n process.on(\"SIGTERM\", shutdown);\n process.on(\"SIGINT\", shutdown);\n return undefined;\n }\n\n private buildServer(): McpServer<TTools, TAuthExtra> {\n const server = new McpServer<Record<never, ToolDef>, TAuthExtra>(\n this.serverInfo,\n this.serverOptions,\n { skills: this.skills, oauth: this.oauthEnabled },\n );\n if (this.resolveResourceMetadataUrl) {\n server.setResourceMetadataUrlResolver(this.resolveResourceMetadataUrl);\n }\n const startedAt = performance.now();\n const built = this.handler(server, this.config as Awaited<TConfig>);\n const elapsed = performance.now() - startedAt;\n if (typeof (built as { then?: unknown }).then === \"function\") {\n throw new Error(\n \"The Skybridge handler must be synchronous — it runs on every request. Load config or secrets in `setup` instead and read them from the handler's second argument.\",\n );\n }\n if (elapsed > SLOW_HANDLER_THRESHOLD_MS && !this.slowHandlerWarned) {\n this.slowHandlerWarned = true;\n console.warn(\n `The Skybridge handler took ${Math.round(elapsed)}ms — it runs on every request, so this cost is paid per request. Move expensive work into \\`setup\\`, whose result is passed to the handler.`,\n );\n }\n return built;\n }\n\n private instrumentHandlers(\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n middleware: McpMiddlewareEntry[],\n ): void {\n const entries = [\n ...middleware,\n ...this.mcpMiddlewareEntries,\n ...server.protocolMiddlewareEntries(),\n ];\n const { requestHandlers, notificationHandlers } = getHandlerMaps(\n server.server,\n );\n\n const instrumentMap = (\n map: Map<string, (...args: unknown[]) => Promise<unknown>>,\n isNotification: boolean,\n ) => {\n for (const [method, handler] of map) {\n map.set(\n method,\n buildMiddlewareChain(method, isNotification, handler, entries),\n );\n }\n const originalSet = map.set.bind(map);\n map.set = (\n method: string,\n handler: (...args: unknown[]) => Promise<unknown>,\n ) =>\n originalSet(\n method,\n buildMiddlewareChain(method, isNotification, handler, entries),\n );\n };\n\n instrumentMap(requestHandlers, false);\n instrumentMap(notificationHandlers, true);\n }\n}\n"]}
@@ -94,4 +94,85 @@ describe("Skybridge setup and handler", () => {
94
94
  expect(seen).toEqual({ issuer: "https://issuer.example.com" });
95
95
  });
96
96
  });
97
+ it("registers outer middleware on the app while preserving its setup lifecycle", async () => {
98
+ let loads = 0;
99
+ const events = [];
100
+ const app = new Skybridge({
101
+ name: "outer-middleware",
102
+ version: "1",
103
+ setup: () => ++loads,
104
+ handler: (server) => server
105
+ .registerTool({ name: "echo" }, () => ({ content: "live" }))
106
+ .mcpMiddleware("tools/call", async (_request, _extra, next) => {
107
+ events.push("app:before");
108
+ const result = await next();
109
+ events.push("app:after");
110
+ return result;
111
+ }),
112
+ });
113
+ const configured = app
114
+ .withMcpMiddleware({
115
+ filter: "tools/call",
116
+ handler: async (_request, _extra, next) => {
117
+ events.push("outer:before");
118
+ const result = await next();
119
+ events.push("outer:after");
120
+ return result;
121
+ },
122
+ })
123
+ .withMcpMiddleware({
124
+ filter: "tools/list",
125
+ handler: (_request, _extra, next) => {
126
+ events.push("list");
127
+ return next();
128
+ },
129
+ });
130
+ const [outer, ordinary, conversation] = await Promise.all([
131
+ configured.createServerInstance(),
132
+ app.createServerInstance(),
133
+ app.createServerInstance([
134
+ {
135
+ filter: "tools/call",
136
+ handler: async (_request, _extra, next) => {
137
+ events.push("conversation:before");
138
+ const result = await next();
139
+ events.push("conversation:after");
140
+ return result;
141
+ },
142
+ },
143
+ ]),
144
+ ]);
145
+ expect(configured).toBe(app);
146
+ expect(outer).not.toBe(ordinary);
147
+ expect(loads).toBe(1);
148
+ for (const [server, expected] of [
149
+ [
150
+ conversation,
151
+ [
152
+ "conversation:before",
153
+ "outer:before",
154
+ "app:before",
155
+ "app:after",
156
+ "outer:after",
157
+ "conversation:after",
158
+ ],
159
+ ],
160
+ [outer, ["outer:before", "app:before", "app:after", "outer:after"]],
161
+ [ordinary, ["outer:before", "app:before", "app:after", "outer:after"]],
162
+ ]) {
163
+ const client = new Client({ name: "test", version: "1" });
164
+ const [a, b] = InMemoryTransport.createLinkedPair();
165
+ try {
166
+ await server.connect(b);
167
+ await client.connect(a);
168
+ events.length = 0;
169
+ await client.callTool({ name: "echo" });
170
+ expect(events).toEqual(expected);
171
+ }
172
+ finally {
173
+ await client.close();
174
+ await server.close();
175
+ }
176
+ }
177
+ });
97
178
  //# sourceMappingURL=app.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"app.test.js","sourceRoot":"","sources":["../../src/server/app.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { Client, InMemoryTransport } from \"@modelcontextprotocol/client\";\nimport { describe, expect, it } from \"vitest\";\nimport { Skybridge } from \"./app.js\";\n\nconst oauthConfig = {\n oauthMetadata: {\n issuer: \"https://issuer.example.com\",\n authorization_endpoint: \"https://issuer.example.com/authorize\",\n token_endpoint: \"https://issuer.example.com/token\",\n response_types_supported: [\"code\"],\n },\n verifier: {\n verifyAccessToken: async () => ({\n token: \"t\",\n clientId: \"c\",\n scopes: [],\n extra: {},\n }),\n },\n};\n\ndescribe(\"Skybridge setup and handler\", () => {\n it(\"resolves setup once and passes the context to the per-request handler\", async () => {\n let loads = 0;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: async () => {\n loads++;\n return { greeting: await Promise.resolve(\"hello\") };\n },\n handler: (server, { greeting }) =>\n server.registerTool(\n { name: \"greet\", description: \"greet\" },\n async () => ({ content: [{ type: \"text\", text: greeting }] }),\n ),\n });\n\n expect(loads).toBe(0);\n\n const call = async () => {\n const client = new Client({ name: \"client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await app.connect(serverTransport);\n await client.connect(clientTransport);\n const result = await client.callTool({ name: \"greet\" });\n await client.close();\n return result;\n };\n\n expect(await call()).toMatchObject({\n content: [{ type: \"text\", text: \"hello\" }],\n });\n expect(await call()).toMatchObject({\n content: [{ type: \"text\", text: \"hello\" }],\n });\n expect(loads).toBe(1);\n });\n\n it(\"retries setup after a failed first resolution\", async () => {\n let attempts = 0;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: async () => {\n attempts++;\n if (attempts === 1) {\n throw new Error(\"transient\");\n }\n },\n handler: (server) => server,\n });\n\n const [, first] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(first)).rejects.toThrow(\"transient\");\n\n const [, second] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(second)).resolves.toBeUndefined();\n expect(attempts).toBe(2);\n });\n\n it(\"rejects a handler that returns a promise\", async () => {\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n // @ts-expect-error the handler runs per request and must stay synchronous\n handler: async (server) => server,\n });\n const [, serverTransport] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(serverTransport)).rejects.toThrow(\n /must be synchronous/,\n );\n });\n\n it(\"resolves an oauth function with the setup config before wiring the routes\", async () => {\n let seen: unknown;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: () => ({ issuer: \"https://issuer.example.com\" }),\n oauth: (config) => {\n seen = config;\n return oauthConfig;\n },\n handler: (server) => server,\n });\n\n expect(seen).toBeUndefined();\n const [, serverTransport] = InMemoryTransport.createLinkedPair();\n await app.connect(serverTransport);\n expect(seen).toEqual({ issuer: \"https://issuer.example.com\" });\n });\n});\n"]}
1
+ {"version":3,"file":"app.test.js","sourceRoot":"","sources":["../../src/server/app.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { Client, InMemoryTransport } from \"@modelcontextprotocol/client\";\nimport { describe, expect, it } from \"vitest\";\nimport { Skybridge } from \"./app.js\";\n\nconst oauthConfig = {\n oauthMetadata: {\n issuer: \"https://issuer.example.com\",\n authorization_endpoint: \"https://issuer.example.com/authorize\",\n token_endpoint: \"https://issuer.example.com/token\",\n response_types_supported: [\"code\"],\n },\n verifier: {\n verifyAccessToken: async () => ({\n token: \"t\",\n clientId: \"c\",\n scopes: [],\n extra: {},\n }),\n },\n};\n\ndescribe(\"Skybridge setup and handler\", () => {\n it(\"resolves setup once and passes the context to the per-request handler\", async () => {\n let loads = 0;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: async () => {\n loads++;\n return { greeting: await Promise.resolve(\"hello\") };\n },\n handler: (server, { greeting }) =>\n server.registerTool(\n { name: \"greet\", description: \"greet\" },\n async () => ({ content: [{ type: \"text\", text: greeting }] }),\n ),\n });\n\n expect(loads).toBe(0);\n\n const call = async () => {\n const client = new Client({ name: \"client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await app.connect(serverTransport);\n await client.connect(clientTransport);\n const result = await client.callTool({ name: \"greet\" });\n await client.close();\n return result;\n };\n\n expect(await call()).toMatchObject({\n content: [{ type: \"text\", text: \"hello\" }],\n });\n expect(await call()).toMatchObject({\n content: [{ type: \"text\", text: \"hello\" }],\n });\n expect(loads).toBe(1);\n });\n\n it(\"retries setup after a failed first resolution\", async () => {\n let attempts = 0;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: async () => {\n attempts++;\n if (attempts === 1) {\n throw new Error(\"transient\");\n }\n },\n handler: (server) => server,\n });\n\n const [, first] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(first)).rejects.toThrow(\"transient\");\n\n const [, second] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(second)).resolves.toBeUndefined();\n expect(attempts).toBe(2);\n });\n\n it(\"rejects a handler that returns a promise\", async () => {\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n // @ts-expect-error the handler runs per request and must stay synchronous\n handler: async (server) => server,\n });\n const [, serverTransport] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(serverTransport)).rejects.toThrow(\n /must be synchronous/,\n );\n });\n\n it(\"resolves an oauth function with the setup config before wiring the routes\", async () => {\n let seen: unknown;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: () => ({ issuer: \"https://issuer.example.com\" }),\n oauth: (config) => {\n seen = config;\n return oauthConfig;\n },\n handler: (server) => server,\n });\n\n expect(seen).toBeUndefined();\n const [, serverTransport] = InMemoryTransport.createLinkedPair();\n await app.connect(serverTransport);\n expect(seen).toEqual({ issuer: \"https://issuer.example.com\" });\n });\n});\n\nit(\"registers outer middleware on the app while preserving its setup lifecycle\", async () => {\n let loads = 0;\n const events: string[] = [];\n const app = new Skybridge({\n name: \"outer-middleware\",\n version: \"1\",\n setup: () => ++loads,\n handler: (server) =>\n server\n .registerTool({ name: \"echo\" }, () => ({ content: \"live\" }))\n .mcpMiddleware(\"tools/call\", async (_request, _extra, next) => {\n events.push(\"app:before\");\n const result = await next();\n events.push(\"app:after\");\n return result;\n }),\n });\n const configured = app\n .withMcpMiddleware({\n filter: \"tools/call\",\n handler: async (_request, _extra, next) => {\n events.push(\"outer:before\");\n const result = await next();\n events.push(\"outer:after\");\n return result;\n },\n })\n .withMcpMiddleware({\n filter: \"tools/list\",\n handler: (_request, _extra, next) => {\n events.push(\"list\");\n return next();\n },\n });\n const [outer, ordinary, conversation] = await Promise.all([\n configured.createServerInstance(),\n app.createServerInstance(),\n app.createServerInstance([\n {\n filter: \"tools/call\",\n handler: async (_request, _extra, next) => {\n events.push(\"conversation:before\");\n const result = await next();\n events.push(\"conversation:after\");\n return result;\n },\n },\n ]),\n ]);\n expect(configured).toBe(app);\n expect(outer).not.toBe(ordinary);\n expect(loads).toBe(1);\n for (const [server, expected] of [\n [\n conversation,\n [\n \"conversation:before\",\n \"outer:before\",\n \"app:before\",\n \"app:after\",\n \"outer:after\",\n \"conversation:after\",\n ],\n ],\n [outer, [\"outer:before\", \"app:before\", \"app:after\", \"outer:after\"]],\n [ordinary, [\"outer:before\", \"app:before\", \"app:after\", \"outer:after\"]],\n ] as const) {\n const client = new Client({ name: \"test\", version: \"1\" });\n const [a, b] = InMemoryTransport.createLinkedPair();\n try {\n await server.connect(b);\n await client.connect(a);\n events.length = 0;\n await client.callTool({ name: \"echo\" });\n expect(events).toEqual(expected);\n } finally {\n await client.close();\n await server.close();\n }\n }\n});\n"]}
@@ -5,10 +5,12 @@ import { type RegisteredClaims } from "../verify.js";
5
5
  /** Options accepted by {@link customProvider} and the branded providers. */
6
6
  export type CustomProviderOptions = {
7
7
  issuer: string;
8
- /** Expected token `aud`. Omit to skip audience verification — only for IdPs
8
+ /** Expected token `aud`, or several when the server is reached at more than
9
+ * one URL (a local port and a dev tunnel): a token passes when its `aud`
10
+ * matches any of them. Omit to skip audience verification — only for IdPs
9
11
  * that don't bind an audience (e.g. Clerk). Branded providers whose IdP does
10
12
  * bind an audience re-require it in their own options. */
11
- audience?: string;
13
+ audience?: string | string[];
12
14
  /** Omit to let the server infer the resource origin from request headers. */
13
15
  baseUrl?: string;
14
16
  /** Advertise THIS server as the authorization server (skybridge-as-AS): the
@@ -1 +1 @@
1
- {"version":3,"file":"custom.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,kBAAkB,EAAyB,MAAM,cAAc,CAAC;AA+BzE;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,IAA2B;IAE3B,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,qBAAqB,CAAS,IAAI,CAAC,EAAE,CAAC;AAChE,CAAC;AAED,KAAK,UAAU,qBAAqB,CAClC,IAA2B;IAE3B,MAAM,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAElE,gFAAgF;IAChF,kFAAkF;IAClF,iFAAiF;IACjF,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,4DAA4D,CAC3E,CAAC;IACJ,CAAC;IAED,iFAAiF;IACjF,yCAAyC;IACzC,MAAM,EACJ,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,KAAK,EACf,GAAG,SAAS,EACb,GAAgC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAC;IAC9D,IAAI,UAAU,GAAG,UAAU,CAAC;IAC5B,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CACV,kDAAkD,IAAI,CAAC,mBAAmB,iBAAiB,IAAI,CAAC,MAAM,sBAAsB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC/K,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAuB,EAAE,GAAG,UAAU,EAAE,GAAG,SAAS,EAAE,CAAC;IACjE,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAE7D,2EAA2E;IAC3E,2EAA2E;IAC3E,sEAAsE;IACtE,MAAM,gBAAgB,GACpB,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,mBAAmB,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,aAAa,GAAuB,gBAAgB;QACxD,CAAC,CAAC;YACE,GAAG,IAAI;YACP,MAAM,EAAE,gBAAgB;YACxB,gBAAgB,EAAE,eAAe;SAClC;QACH,CAAC,CAAC,IAAI,CAAC;IAET,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa;QACb,QAAQ,EAAE,kBAAkB,CAAS;YACnC,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,QAAQ;SACpD,CAAC;QACF,eAAe;QACf,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAC;AACJ,CAAC","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/server\";\nimport type { ExtraClaims } from \"../../auth.js\";\nimport {\n type DiscoveredMetadata,\n discoverAuthorizationServer,\n} from \"../discovery.js\";\nimport type { OAuthConfig, OAuthProvider } from \"../index.js\";\nimport { createJwksVerifier, type RegisteredClaims } from \"../verify.js\";\n\n/** Options accepted by {@link customProvider} and the branded providers. */\nexport type CustomProviderOptions = {\n issuer: string;\n /** Expected token `aud`. Omit to skip audience verification — only for IdPs\n * that don't bind an audience (e.g. Clerk). Branded providers whose IdP does\n * bind an audience re-require it in their own options. */\n audience?: string;\n /** Omit to let the server infer the resource origin from request headers. */\n baseUrl?: string;\n /** Advertise THIS server as the authorization server (skybridge-as-AS): the\n * served AS metadata `issuer` and the PRM `authorization_servers` use this URL\n * instead of the IdP's. Needed when skybridge must sit in the auth path — Auth0\n * (the `audience` must be baked into `/authorize`) or the Alpic DCR proxy (Alpic\n * injects the registration endpoint). Use the static public URL; `verify.issuer`\n * stays the IdP (the token's real `iss`). */\n serverUrl?: string;\n /** Advertise this URL as the authorization server (served AS metadata `issuer`\n * and PRM `authorization_servers`). Its own discovery document supplies the\n * advertised endpoints, DCR registration, scopes and the `iss` tokens are\n * verified against; only `jwks_uri` still comes from `issuer`'s document. Use\n * when DCR/authorize/token live at a different URL than the discovery issuer\n * (e.g. Descope's agentic URL vs. its base-project URL). `serverUrl` wins if\n * both are set. */\n authorizationServer?: string;\n scopes?: string[];\n requiredScopes?: string[];\n metadataOverrides?: Omit<Partial<OAuthMetadata>, \"issuer\">;\n};\n\n/**\n * Builds a complete {@link OAuthConfig} from an IdP's OAuth discovery document.\n *\n * @typeParam TExtra - Claims the IdP puts in the access token, reaching handlers\n * as `extra.http?.authInfo.extra`. The branded providers pass their documented claims;\n * pass your own when wiring an IdP by hand.\n */\nexport function customProvider<TExtra extends ExtraClaims = ExtraClaims>(\n opts: CustomProviderOptions,\n): OAuthProvider<TExtra & RegisteredClaims> {\n return { resolve: () => resolveCustomProvider<TExtra>(opts) };\n}\n\nasync function resolveCustomProvider<TExtra extends ExtraClaims>(\n opts: CustomProviderOptions,\n): Promise<OAuthConfig<TExtra & RegisteredClaims>> {\n const discovered = await discoverAuthorizationServer(opts.issuer);\n\n // JWKS verification needs a signing-key URL; without it the server can't verify\n // tokens. (`registration_endpoint` is optional per RFC 8414 — a no-DCR IdP simply\n // omits it; a proxy like Alpic, or pre-registered clients, supply registration.)\n if (!discovered.jwks_uri) {\n throw new Error(\n `${opts.issuer} discovery has no jwks_uri; JWKS verification requires it.`,\n );\n }\n\n // Overrides adjust only advertised metadata; the trust anchor (issuer, jwks_uri)\n // always comes from validated discovery.\n const {\n issuer: _issuer,\n jwks_uri: _jwks,\n ...overrides\n }: Partial<DiscoveredMetadata> = opts.metadataOverrides ?? {};\n let advertised = discovered;\n if (opts.authorizationServer) {\n try {\n advertised = await discoverAuthorizationServer(opts.authorizationServer);\n } catch (err) {\n console.warn(\n `OAuth discovery failed for authorizationServer ${opts.authorizationServer}; advertising ${opts.issuer} metadata instead: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n const base: DiscoveredMetadata = { ...advertised, ...overrides };\n const scopesSupported = opts.scopes ?? base.scopes_supported;\n\n // serverUrl (skybridge-as-AS) overrides the advertised issuer, keeping the\n // IdP as the token issuer. authorizationServer re-asserts its own URL so a\n // failed AS discovery still points clients at the AS. serverUrl wins.\n const advertisedIssuer =\n opts.serverUrl?.replace(/\\/$/, \"\") ??\n opts.authorizationServer?.replace(/\\/$/, \"\");\n const oauthMetadata: DiscoveredMetadata = advertisedIssuer\n ? {\n ...base,\n issuer: advertisedIssuer,\n scopes_supported: scopesSupported,\n }\n : base;\n\n return {\n baseUrl: opts.baseUrl,\n oauthMetadata,\n verifier: createJwksVerifier<TExtra>({\n issuer: advertised.issuer,\n audience: opts.audience,\n jwksUri: advertised.jwks_uri ?? discovered.jwks_uri,\n }),\n scopesSupported,\n requiredScopes: opts.requiredScopes,\n };\n}\n"]}
1
+ {"version":3,"file":"custom.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,kBAAkB,EAAyB,MAAM,cAAc,CAAC;AAiCzE;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAC5B,IAA2B;IAE3B,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,qBAAqB,CAAS,IAAI,CAAC,EAAE,CAAC;AAChE,CAAC;AAED,KAAK,UAAU,qBAAqB,CAClC,IAA2B;IAE3B,MAAM,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAElE,gFAAgF;IAChF,kFAAkF;IAClF,iFAAiF;IACjF,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,4DAA4D,CAC3E,CAAC;IACJ,CAAC;IAED,iFAAiF;IACjF,yCAAyC;IACzC,MAAM,EACJ,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,KAAK,EACf,GAAG,SAAS,EACb,GAAgC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAC;IAC9D,IAAI,UAAU,GAAG,UAAU,CAAC;IAC5B,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAC3E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CACV,kDAAkD,IAAI,CAAC,mBAAmB,iBAAiB,IAAI,CAAC,MAAM,sBAAsB,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC/K,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,IAAI,GAAuB,EAAE,GAAG,UAAU,EAAE,GAAG,SAAS,EAAE,CAAC;IACjE,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAE7D,2EAA2E;IAC3E,2EAA2E;IAC3E,sEAAsE;IACtE,MAAM,gBAAgB,GACpB,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,mBAAmB,EAAE,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,aAAa,GAAuB,gBAAgB;QACxD,CAAC,CAAC;YACE,GAAG,IAAI;YACP,MAAM,EAAE,gBAAgB;YACxB,gBAAgB,EAAE,eAAe;SAClC;QACH,CAAC,CAAC,IAAI,CAAC;IAET,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa;QACb,QAAQ,EAAE,kBAAkB,CAAS;YACnC,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,UAAU,CAAC,QAAQ,IAAI,UAAU,CAAC,QAAQ;SACpD,CAAC;QACF,eAAe;QACf,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAC;AACJ,CAAC","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/server\";\nimport type { ExtraClaims } from \"../../auth.js\";\nimport {\n type DiscoveredMetadata,\n discoverAuthorizationServer,\n} from \"../discovery.js\";\nimport type { OAuthConfig, OAuthProvider } from \"../index.js\";\nimport { createJwksVerifier, type RegisteredClaims } from \"../verify.js\";\n\n/** Options accepted by {@link customProvider} and the branded providers. */\nexport type CustomProviderOptions = {\n issuer: string;\n /** Expected token `aud`, or several when the server is reached at more than\n * one URL (a local port and a dev tunnel): a token passes when its `aud`\n * matches any of them. Omit to skip audience verification — only for IdPs\n * that don't bind an audience (e.g. Clerk). Branded providers whose IdP does\n * bind an audience re-require it in their own options. */\n audience?: string | string[];\n /** Omit to let the server infer the resource origin from request headers. */\n baseUrl?: string;\n /** Advertise THIS server as the authorization server (skybridge-as-AS): the\n * served AS metadata `issuer` and the PRM `authorization_servers` use this URL\n * instead of the IdP's. Needed when skybridge must sit in the auth path — Auth0\n * (the `audience` must be baked into `/authorize`) or the Alpic DCR proxy (Alpic\n * injects the registration endpoint). Use the static public URL; `verify.issuer`\n * stays the IdP (the token's real `iss`). */\n serverUrl?: string;\n /** Advertise this URL as the authorization server (served AS metadata `issuer`\n * and PRM `authorization_servers`). Its own discovery document supplies the\n * advertised endpoints, DCR registration, scopes and the `iss` tokens are\n * verified against; only `jwks_uri` still comes from `issuer`'s document. Use\n * when DCR/authorize/token live at a different URL than the discovery issuer\n * (e.g. Descope's agentic URL vs. its base-project URL). `serverUrl` wins if\n * both are set. */\n authorizationServer?: string;\n scopes?: string[];\n requiredScopes?: string[];\n metadataOverrides?: Omit<Partial<OAuthMetadata>, \"issuer\">;\n};\n\n/**\n * Builds a complete {@link OAuthConfig} from an IdP's OAuth discovery document.\n *\n * @typeParam TExtra - Claims the IdP puts in the access token, reaching handlers\n * as `extra.http?.authInfo.extra`. The branded providers pass their documented claims;\n * pass your own when wiring an IdP by hand.\n */\nexport function customProvider<TExtra extends ExtraClaims = ExtraClaims>(\n opts: CustomProviderOptions,\n): OAuthProvider<TExtra & RegisteredClaims> {\n return { resolve: () => resolveCustomProvider<TExtra>(opts) };\n}\n\nasync function resolveCustomProvider<TExtra extends ExtraClaims>(\n opts: CustomProviderOptions,\n): Promise<OAuthConfig<TExtra & RegisteredClaims>> {\n const discovered = await discoverAuthorizationServer(opts.issuer);\n\n // JWKS verification needs a signing-key URL; without it the server can't verify\n // tokens. (`registration_endpoint` is optional per RFC 8414 — a no-DCR IdP simply\n // omits it; a proxy like Alpic, or pre-registered clients, supply registration.)\n if (!discovered.jwks_uri) {\n throw new Error(\n `${opts.issuer} discovery has no jwks_uri; JWKS verification requires it.`,\n );\n }\n\n // Overrides adjust only advertised metadata; the trust anchor (issuer, jwks_uri)\n // always comes from validated discovery.\n const {\n issuer: _issuer,\n jwks_uri: _jwks,\n ...overrides\n }: Partial<DiscoveredMetadata> = opts.metadataOverrides ?? {};\n let advertised = discovered;\n if (opts.authorizationServer) {\n try {\n advertised = await discoverAuthorizationServer(opts.authorizationServer);\n } catch (err) {\n console.warn(\n `OAuth discovery failed for authorizationServer ${opts.authorizationServer}; advertising ${opts.issuer} metadata instead: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n const base: DiscoveredMetadata = { ...advertised, ...overrides };\n const scopesSupported = opts.scopes ?? base.scopes_supported;\n\n // serverUrl (skybridge-as-AS) overrides the advertised issuer, keeping the\n // IdP as the token issuer. authorizationServer re-asserts its own URL so a\n // failed AS discovery still points clients at the AS. serverUrl wins.\n const advertisedIssuer =\n opts.serverUrl?.replace(/\\/$/, \"\") ??\n opts.authorizationServer?.replace(/\\/$/, \"\");\n const oauthMetadata: DiscoveredMetadata = advertisedIssuer\n ? {\n ...base,\n issuer: advertisedIssuer,\n scopes_supported: scopesSupported,\n }\n : base;\n\n return {\n baseUrl: opts.baseUrl,\n oauthMetadata,\n verifier: createJwksVerifier<TExtra>({\n issuer: advertised.issuer,\n audience: opts.audience,\n jwksUri: advertised.jwks_uri ?? discovered.jwks_uri,\n }),\n scopesSupported,\n requiredScopes: opts.requiredScopes,\n };\n}\n"]}
@@ -25,9 +25,10 @@ export type WorkosClaims = {
25
25
  /**
26
26
  * OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.
27
27
  * `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard
28
- * (Connect → Configuration). `audience` is the MCP server's Resource Indicator.
28
+ * (Connect → Configuration). `audience` is the MCP server's Resource Indicator,
29
+ * or every Resource Indicator it answers to when it is reached at more than one URL.
29
30
  */
30
31
  export declare function workosProvider<TCustom extends ExtraClaims = Record<never, never>>(opts: {
31
32
  domain: string;
32
- audience: string;
33
+ audience: string | string[];
33
34
  } & Omit<CustomProviderOptions, "issuer" | "audience">): OAuthProvider<WorkosClaims & TCustom & RegisteredClaims>;
@@ -3,7 +3,8 @@ import { toIssuerUrl } from "./shared.js";
3
3
  /**
4
4
  * OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.
5
5
  * `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard
6
- * (Connect → Configuration). `audience` is the MCP server's Resource Indicator.
6
+ * (Connect → Configuration). `audience` is the MCP server's Resource Indicator,
7
+ * or every Resource Indicator it answers to when it is reached at more than one URL.
7
8
  */
8
9
  export function workosProvider(opts) {
9
10
  const { domain, ...rest } = opts;
@@ -1 +1 @@
1
- {"version":3,"file":"workos.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/workos.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB1C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAG5B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACjC,OAAO,cAAc,CAAyB;QAC5C,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;QAC3B,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ExtraClaims } from \"../../auth.js\";\nimport type { OAuthProvider } from \"../index.js\";\nimport type { RegisteredClaims } from \"../verify.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\nimport { toIssuerUrl } from \"./shared.js\";\n\n/**\n * Claims a WorkOS AuthKit access token carries. All optional: `org_id`, `role`\n * and `permissions` appear only when an organization is selected at sign-in, and\n * `email` only when a JWT template adds it.\n *\n * @see https://workos.com/docs/authkit/sessions/integrating-sessions/access-token\n * @see https://workos.com/docs/authkit/jwt-templates\n */\nexport type WorkosClaims = {\n /** Session id, used for signing out. */\n sid?: string;\n /** Organization selected at sign-in. */\n org_id?: string;\n /** Role of the selected organization membership. */\n role?: string;\n /** Permissions assigned to the role. */\n permissions?: string[];\n /** Only when a JWT template adds it; not a default AuthKit claim. */\n email?: string;\n};\n\n/**\n * OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.\n * `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard\n * (Connect → Configuration). `audience` is the MCP server's Resource Indicator.\n */\nexport function workosProvider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: { domain: string; audience: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\"\n >,\n): OAuthProvider<WorkosClaims & TCustom & RegisteredClaims> {\n const { domain, ...rest } = opts;\n return customProvider<WorkosClaims & TCustom>({\n issuer: toIssuerUrl(domain),\n ...rest,\n });\n}\n"]}
1
+ {"version":3,"file":"workos.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/workos.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB1C;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAG5B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACjC,OAAO,cAAc,CAAyB;QAC5C,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;QAC3B,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ExtraClaims } from \"../../auth.js\";\nimport type { OAuthProvider } from \"../index.js\";\nimport type { RegisteredClaims } from \"../verify.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\nimport { toIssuerUrl } from \"./shared.js\";\n\n/**\n * Claims a WorkOS AuthKit access token carries. All optional: `org_id`, `role`\n * and `permissions` appear only when an organization is selected at sign-in, and\n * `email` only when a JWT template adds it.\n *\n * @see https://workos.com/docs/authkit/sessions/integrating-sessions/access-token\n * @see https://workos.com/docs/authkit/jwt-templates\n */\nexport type WorkosClaims = {\n /** Session id, used for signing out. */\n sid?: string;\n /** Organization selected at sign-in. */\n org_id?: string;\n /** Role of the selected organization membership. */\n role?: string;\n /** Permissions assigned to the role. */\n permissions?: string[];\n /** Only when a JWT template adds it; not a default AuthKit claim. */\n email?: string;\n};\n\n/**\n * OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.\n * `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard\n * (Connect → Configuration). `audience` is the MCP server's Resource Indicator,\n * or every Resource Indicator it answers to when it is reached at more than one URL.\n */\nexport function workosProvider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: { domain: string; audience: string | string[] } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\"\n >,\n): OAuthProvider<WorkosClaims & TCustom & RegisteredClaims> {\n const { domain, ...rest } = opts;\n return customProvider<WorkosClaims & TCustom>({\n issuer: toIssuerUrl(domain),\n ...rest,\n });\n}\n"]}
@@ -2,9 +2,11 @@ import type { ExtraClaims, TokenVerifier } from "../auth.js";
2
2
  export type JwksVerifyConfig = {
3
3
  /** Expected `iss` claim. */
4
4
  issuer: string;
5
- /** Expected `aud` claim. Omit to skip audience verification — for IdPs that
6
- * don't bind an audience to their access tokens (e.g. Clerk). */
7
- audience?: string;
5
+ /** Expected `aud` claim, or several: a token passes when its `aud` matches
6
+ * any of them, for a server reached at more than one URL (a local port and a
7
+ * dev tunnel). Omit to skip audience verification — for IdPs that don't bind
8
+ * an audience to their access tokens (e.g. Clerk). */
9
+ audience?: string | string[];
8
10
  /** Defaults to `${issuer}/.well-known/jwks.json`. */
9
11
  jwksUri?: string;
10
12
  };
@@ -1 +1 @@
1
- {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/server/auth/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAqC7B;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAwB;IAExB,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,OAAO,GACX,MAAM,CAAC,OAAO;QACd,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,wBAAwB,CAAC;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAEvD,OAAO;QACL,KAAK,CAAC,iBAAiB,CACrB,KAAa;YAEb,IAAI,OAAwB,CAAC;YAC7B,IAAI,CAAC;gBACH,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;oBAC/C,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,yDAAyD;oBACzD,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;iBACpE,CAAC,CAAC,CAAC;YACN,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjE,+DAA+D;gBAC/D,6DAA6D;gBAC7D,oEAAoE;gBACpE,gEAAgE;gBAChE,qEAAqE;gBACrE,sEAAsE;gBACtE,iCAAiC;gBACjC,MAAM,IAAI,GAAG,OAAO;qBACjB,OAAO,CAAC,kCAAkC,EAAE,GAAG,CAAC;qBAChD,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,UAAU,CAClB,eAAe,EACf,8BAA8B,IAAI,EAAE,CACrC,CAAC;YACJ,CAAC;YAED,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,OAQ/C,CAAC;YAEF,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBACjC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;gBACnB,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ;oBACzB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;oBACpC,CAAC,CAAC,EAAE,CAAC;YAET,OAAO;gBACL,KAAK;gBACL,QAAQ,EAAE,SAAS,IAAI,EAAE;gBACzB,MAAM;gBACN,SAAS,EAAE,GAAG;gBACd,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,EACZ;aACnB,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { OAuthError } from \"@modelcontextprotocol/server\";\nimport * as jose from \"jose\";\nimport type { AuthInfo, ExtraClaims, TokenVerifier } from \"../auth.js\";\n\nexport type JwksVerifyConfig = {\n /** Expected `iss` claim. */\n issuer: string;\n /** Expected `aud` claim. Omit to skip audience verification — for IdPs that\n * don't bind an audience to their access tokens (e.g. Clerk). */\n audience?: string;\n /** Defaults to `${issuer}/.well-known/jwks.json`. */\n jwksUri?: string;\n};\n\n/**\n * Claims a JWKS-verified token always carries into `extra`, whatever the IdP:\n * `subject` (the renamed `sub`) plus the registered claims that survive the\n * mapping. `client_id`, `scope` and `exp` do not appear here, since they become\n * `AuthInfo` fields instead. Intersected into every claim shape a JWKS verifier\n * resolves.\n *\n * @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1\n */\nexport type RegisteredClaims = {\n /** The token's `sub`, renamed on the way into `extra`. */\n subject?: string;\n /** Issuer of the token. */\n iss?: string;\n /** Audience the token was minted for. */\n aud?: string | string[];\n /** Issued-at, in unix seconds. */\n iat?: number;\n /** Not-valid-before, in unix seconds. */\n nbf?: number;\n /** Unique token id. */\n jti?: string;\n};\n\n/**\n * Builds a {@link TokenVerifier} that validates JWTs against a remote JWKS.\n *\n * `extra` receives the token's claims with `sub` renamed to `subject`, minus\n * `client_id`, `scope` and `exp`, which map onto `AuthInfo` fields instead. Pass\n * `TExtra` to declare what your IdP puts there; the branded providers do this\n * for you. {@link RegisteredClaims} is always included, since those claims\n * survive the mapping.\n *\n * @typeParam TExtra - Claims the verified token carries in `extra`. An\n * assertion, not a runtime check: nothing rejects a token whose claims differ.\n */\nexport function createJwksVerifier<TExtra extends ExtraClaims = ExtraClaims>(\n config: JwksVerifyConfig,\n): TokenVerifier<TExtra & RegisteredClaims> {\n if (!config.issuer) {\n throw new Error(\"createJwksVerifier requires an `issuer`\");\n }\n const jwksUri =\n config.jwksUri ??\n `${config.issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const jwks = jose.createRemoteJWKSet(new URL(jwksUri));\n\n return {\n async verifyAccessToken(\n token: string,\n ): Promise<AuthInfo<TExtra & RegisteredClaims>> {\n let payload: jose.JWTPayload;\n try {\n ({ payload } = await jose.jwtVerify(token, jwks, {\n issuer: config.issuer,\n // jose skips the aud check when `audience` is undefined.\n ...(config.audience !== undefined && { audience: config.audience }),\n }));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // This reaches the `error_description` of a `WWW-Authenticate`\n // challenge, whose value must not include characters outside\n // %x20-21 / %x23-5B / %x5D-7E (OAuth 2.1 §5.3.1, RFC 6750 §3). jose\n // quotes claim names in its messages, and a quote would end the\n // parameter early, dropping the `resource_metadata` that follows it.\n // Each run becomes a space rather than being dropped, so removing one\n // never runs two words together.\n const safe = message\n .replace(/[^\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]+/g, \" \")\n .trim();\n throw new OAuthError(\n \"invalid_token\",\n `Token verification failed: ${safe}`,\n );\n }\n\n const { client_id, scope, exp, sub, ...rest } = payload as Record<\n string,\n unknown\n > & {\n client_id?: string;\n scope?: string | string[];\n exp?: number;\n sub?: string;\n };\n\n const scopes = Array.isArray(scope)\n ? scope.map(String)\n : typeof scope === \"string\"\n ? scope.split(/\\s+/).filter(Boolean)\n : [];\n\n return {\n token,\n clientId: client_id ?? \"\",\n scopes,\n expiresAt: exp,\n extra: { subject: sub, ...rest } as unknown as TExtra &\n RegisteredClaims,\n };\n },\n };\n}\n"]}
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../../../src/server/auth/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAuC7B;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAwB;IAExB,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC7D,CAAC;IACD,MAAM,OAAO,GACX,MAAM,CAAC,OAAO;QACd,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,wBAAwB,CAAC;IAC9D,MAAM,IAAI,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IAEvD,OAAO;QACL,KAAK,CAAC,iBAAiB,CACrB,KAAa;YAEb,IAAI,OAAwB,CAAC;YAC7B,IAAI,CAAC;gBACH,CAAC,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE;oBAC/C,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,yDAAyD;oBACzD,GAAG,CAAC,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,CAAC;iBACpE,CAAC,CAAC,CAAC;YACN,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;gBACjE,+DAA+D;gBAC/D,6DAA6D;gBAC7D,oEAAoE;gBACpE,gEAAgE;gBAChE,qEAAqE;gBACrE,sEAAsE;gBACtE,iCAAiC;gBACjC,MAAM,IAAI,GAAG,OAAO;qBACjB,OAAO,CAAC,kCAAkC,EAAE,GAAG,CAAC;qBAChD,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,UAAU,CAClB,eAAe,EACf,8BAA8B,IAAI,EAAE,CACrC,CAAC;YACJ,CAAC;YAED,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,OAQ/C,CAAC;YAEF,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBACjC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;gBACnB,CAAC,CAAC,OAAO,KAAK,KAAK,QAAQ;oBACzB,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;oBACpC,CAAC,CAAC,EAAE,CAAC;YAET,OAAO;gBACL,KAAK;gBACL,QAAQ,EAAE,SAAS,IAAI,EAAE;gBACzB,MAAM;gBACN,SAAS,EAAE,GAAG;gBACd,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,EACZ;aACnB,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { OAuthError } from \"@modelcontextprotocol/server\";\nimport * as jose from \"jose\";\nimport type { AuthInfo, ExtraClaims, TokenVerifier } from \"../auth.js\";\n\nexport type JwksVerifyConfig = {\n /** Expected `iss` claim. */\n issuer: string;\n /** Expected `aud` claim, or several: a token passes when its `aud` matches\n * any of them, for a server reached at more than one URL (a local port and a\n * dev tunnel). Omit to skip audience verification — for IdPs that don't bind\n * an audience to their access tokens (e.g. Clerk). */\n audience?: string | string[];\n /** Defaults to `${issuer}/.well-known/jwks.json`. */\n jwksUri?: string;\n};\n\n/**\n * Claims a JWKS-verified token always carries into `extra`, whatever the IdP:\n * `subject` (the renamed `sub`) plus the registered claims that survive the\n * mapping. `client_id`, `scope` and `exp` do not appear here, since they become\n * `AuthInfo` fields instead. Intersected into every claim shape a JWKS verifier\n * resolves.\n *\n * @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1\n */\nexport type RegisteredClaims = {\n /** The token's `sub`, renamed on the way into `extra`. */\n subject?: string;\n /** Issuer of the token. */\n iss?: string;\n /** Audience the token was minted for. */\n aud?: string | string[];\n /** Issued-at, in unix seconds. */\n iat?: number;\n /** Not-valid-before, in unix seconds. */\n nbf?: number;\n /** Unique token id. */\n jti?: string;\n};\n\n/**\n * Builds a {@link TokenVerifier} that validates JWTs against a remote JWKS.\n *\n * `extra` receives the token's claims with `sub` renamed to `subject`, minus\n * `client_id`, `scope` and `exp`, which map onto `AuthInfo` fields instead. Pass\n * `TExtra` to declare what your IdP puts there; the branded providers do this\n * for you. {@link RegisteredClaims} is always included, since those claims\n * survive the mapping.\n *\n * @typeParam TExtra - Claims the verified token carries in `extra`. An\n * assertion, not a runtime check: nothing rejects a token whose claims differ.\n */\nexport function createJwksVerifier<TExtra extends ExtraClaims = ExtraClaims>(\n config: JwksVerifyConfig,\n): TokenVerifier<TExtra & RegisteredClaims> {\n if (!config.issuer) {\n throw new Error(\"createJwksVerifier requires an `issuer`\");\n }\n const jwksUri =\n config.jwksUri ??\n `${config.issuer.replace(/\\/$/, \"\")}/.well-known/jwks.json`;\n const jwks = jose.createRemoteJWKSet(new URL(jwksUri));\n\n return {\n async verifyAccessToken(\n token: string,\n ): Promise<AuthInfo<TExtra & RegisteredClaims>> {\n let payload: jose.JWTPayload;\n try {\n ({ payload } = await jose.jwtVerify(token, jwks, {\n issuer: config.issuer,\n // jose skips the aud check when `audience` is undefined.\n ...(config.audience !== undefined && { audience: config.audience }),\n }));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // This reaches the `error_description` of a `WWW-Authenticate`\n // challenge, whose value must not include characters outside\n // %x20-21 / %x23-5B / %x5D-7E (OAuth 2.1 §5.3.1, RFC 6750 §3). jose\n // quotes claim names in its messages, and a quote would end the\n // parameter early, dropping the `resource_metadata` that follows it.\n // Each run becomes a space rather than being dropped, so removing one\n // never runs two words together.\n const safe = message\n .replace(/[^\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]+/g, \" \")\n .trim();\n throw new OAuthError(\n \"invalid_token\",\n `Token verification failed: ${safe}`,\n );\n }\n\n const { client_id, scope, exp, sub, ...rest } = payload as Record<\n string,\n unknown\n > & {\n client_id?: string;\n scope?: string | string[];\n exp?: number;\n sub?: string;\n };\n\n const scopes = Array.isArray(scope)\n ? scope.map(String)\n : typeof scope === \"string\"\n ? scope.split(/\\s+/).filter(Boolean)\n : [];\n\n return {\n token,\n clientId: client_id ?? \"\",\n scopes,\n expiresAt: exp,\n extra: { subject: sub, ...rest } as unknown as TExtra &\n RegisteredClaims,\n };\n },\n };\n}\n"]}
@@ -112,6 +112,22 @@ describe("createJwksVerifier", () => {
112
112
  // OAuth 2.1 §5.3.1: %x20-21 / %x23-5B / %x5D-7E and nothing else.
113
113
  expect(error?.message).not.toMatch(/[^\x20-\x21\x23-\x5B\x5D-\x7E]/);
114
114
  });
115
+ it("accepts a token minted for any of several audiences", async () => {
116
+ const { privateKey, jwksUri } = await startJwks();
117
+ // A server reached at a local port and through a dev tunnel: tokens carry
118
+ // whichever resource the client discovered.
119
+ const verifier = createJwksVerifier({
120
+ issuer: ISSUER,
121
+ audience: [AUDIENCE, "https://tunnel.test"],
122
+ jwksUri,
123
+ });
124
+ const tunnelToken = await sign(privateKey, { client_id: "c" }, { audience: "https://tunnel.test" });
125
+ expect((await verifier.verifyAccessToken(tunnelToken)).clientId).toBe("c");
126
+ const localToken = await sign(privateKey, { client_id: "c" });
127
+ expect((await verifier.verifyAccessToken(localToken)).clientId).toBe("c");
128
+ const otherToken = await sign(privateKey, {}, { audience: "api://other" });
129
+ await expect(verifier.verifyAccessToken(otherToken)).rejects.toThrow(/unexpected {2}aud {2}claim value/);
130
+ });
115
131
  it("skips the aud check when no audience is configured (e.g. Clerk)", async () => {
116
132
  const { privateKey, jwksUri } = await startJwks();
117
133
  const verifier = createJwksVerifier({ issuer: ISSUER, jwksUri });
@@ -1 +1 @@
1
- {"version":3,"file":"verify.test.js","sourceRoot":"","sources":["../../../src/server/auth/verify.test.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,MAAM,GAAG,qBAAqB,CAAC;AACrC,MAAM,QAAQ,GAAG,eAAe,CAAC;AAEjC,IAAI,UAAmC,CAAC;AACxC,SAAS,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;AAErC,KAAK,UAAU,SAAS;IACtB,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,MAAM,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IACtE,MAAM,GAAG,GAAG;QACV,GAAG,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACpC,GAAG,EAAE,UAAU;QACf,GAAG,EAAE,OAAO;QACZ,GAAG,EAAE,KAAK;KACX,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC7C,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QAClD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IAChE,UAAU,GAAG,MAAM,CAAC;IACpB,MAAM,IAAI,GAAI,MAAM,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;IACzD,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,oBAAoB,IAAI,OAAO,EAAE,CAAC;AAClE,CAAC;AAED,SAAS,IAAI,CACX,GAAc,EACd,MAA+B,EAC/B,OAAmE,EAAE;IAErE,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;SAC5B,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;SACrD,SAAS,CAAC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC;SAChC,WAAW,CAAC,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC;SACtC,iBAAiB,CAAC,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC;SACzC,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;IACzC,MAAM,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CACtD,sBAAsB,CACvB,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,oBAAoB,EAAE,GAAG,EAAE;IAClC,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,kBAAkB,CAAC;YAClC,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,QAAQ;YAClB,OAAO;SACR,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE;YACnC,SAAS,EAAE,UAAU;YACrB,KAAK,EAAE,cAAc;YACrB,GAAG,EAAE,QAAQ;YACb,KAAK,EAAE,UAAU;SAClB,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAErD,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/B,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;QACjD,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC3C,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,kBAAkB,CAAC;YAClC,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,QAAQ;YAClB,OAAO;SACR,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,IAAI,CACtB,UAAU,EACV,EAAE,SAAS,EAAE,GAAG,EAAE,EAClB,EAAE,QAAQ,EAAE,aAAa,EAAE,CAC5B,CAAC;QAEF,MAAM,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAC7D,2BAA2B,CAC5B,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,8EAA8E;IAC9E,8EAA8E;IAC9E,0EAA0E;IAC1E,EAAE,CAAC,IAAI,CAAC;QACN;YACE,kBAAkB;YAClB,CAAC,GAAc,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;YACvD,6CAA6C;YAC7C,8DAA8D;SAC/D;QACD;YACE,qCAAqC;YACrC,CAAC,GAAc,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC;YAC9D,uCAAuC;YACvC,yDAAyD;SAC1D;QACD;YACE,6BAA6B;YAC7B,CAAC,GAAc,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;YACnE,uCAAuC;YACvC,yDAAyD;SAC1D;QACD;YACE,+BAA+B;YAC/B,CAAC,GAAc,EAAE,EAAE,CACjB,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;iBACjB,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;iBACrD,SAAS,CAAC,MAAM,CAAC;iBACjB,iBAAiB,CAAC,IAAI,CAAC;iBACvB,IAAI,CAAC,GAAG,CAAC;YACd,uCAAuC;YACvC,yDAAyD;SAC1D;KACF,CAAC,CACA,oDAAoD,EACpD,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;QAC1B,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,kBAAkB,CAAC;YAClC,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,QAAQ;YAClB,OAAO;SACR,CAAC,CAAC;QAEH,MAAM,KAAK,GAAG,MAAM,QAAQ;aACzB,iBAAiB,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,CAAC;aACzC,IAAI,CACH,GAAG,EAAE,CAAC,SAAS,EACf,CAAC,CAAQ,EAAE,EAAE,CAAC,CAAC,CAChB,CAAC;QAEJ,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACtC,kEAAkE;QAClE,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,gCAAgC,CAAC,CAAC;IACvE,CAAC,CACF,CAAC;IAEF,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QACjE,oDAAoD;QACpD,MAAM,KAAK,GAAG,MAAM,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;aAC/D,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC;aACrD,SAAS,CAAC,MAAM,CAAC;aACjB,iBAAiB,CAAC,IAAI,CAAC;aACvB,IAAI,CAAC,UAAU,CAAC,CAAC;QAEpB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,iBAAiB,CAAC,KAAK,CAAC,CAAC;QACrD,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,EAAE,CAAC;QAClD,MAAM,QAAQ,GAAG,kBAAkB,CAAC;YAClC,MAAM,EAAE,MAAM;YACd,QAAQ,EAAE,QAAQ;YAClB,OAAO;SACR,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE;YACxC,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC;SAC3B,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,MAAM,QAAQ,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACpE,QAAQ;YACR,OAAO;SACR,CAAC,CAAC;QAEH,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,CAAC,MAAM,QAAQ,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC;YACpE,QAAQ;YACR,OAAO;SACR,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport * as jose from \"jose\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { createJwksVerifier } from \"./verify.js\";\n\nconst ISSUER = \"https://issuer.test\";\nconst AUDIENCE = \"api://default\";\n\nlet jwksServer: http.Server | undefined;\nafterEach(() => jwksServer?.close());\n\nasync function startJwks() {\n const { publicKey, privateKey } = await jose.generateKeyPair(\"RS256\");\n const jwk = {\n ...(await jose.exportJWK(publicKey)),\n kid: \"test-key\",\n alg: \"RS256\",\n use: \"sig\",\n };\n const server = http.createServer((_req, res) => {\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify({ keys: [jwk] }));\n });\n await new Promise<void>((resolve) => server.listen(0, resolve));\n jwksServer = server;\n const port = (server.address() as { port: number }).port;\n return { privateKey, jwksUri: `http://localhost:${port}/jwks` };\n}\n\nfunction sign(\n key: CryptoKey,\n claims: Record<string, unknown>,\n opts: { issuer?: string; audience?: string; expiresAt?: string } = {},\n) {\n return new jose.SignJWT(claims)\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(opts.issuer ?? ISSUER)\n .setAudience(opts.audience ?? AUDIENCE)\n .setExpirationTime(opts.expiresAt ?? \"1h\")\n .sign(key);\n}\n\nit(\"rejects a config with no issuer\", () => {\n expect(() => createJwksVerifier({ issuer: \"\" })).toThrow(\n /requires an `issuer`/,\n );\n});\n\ndescribe(\"createJwksVerifier\", () => {\n it(\"verifies a valid token and maps claims to AuthInfo\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(privateKey, {\n client_id: \"client-1\",\n scope: \"openid email\",\n sub: \"user-1\",\n email: \"a@b.test\",\n });\n\n const auth = await verifier.verifyAccessToken(token);\n\n expect(auth.token).toBe(token);\n expect(auth.clientId).toBe(\"client-1\");\n expect(auth.scopes).toEqual([\"openid\", \"email\"]);\n expect(auth.extra?.subject).toBe(\"user-1\");\n expect(auth.extra?.email).toBe(\"a@b.test\");\n });\n\n it(\"rejects a token with the wrong audience\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(\n privateKey,\n { client_id: \"c\" },\n { audience: \"api://other\" },\n );\n\n await expect(verifier.verifyAccessToken(token)).rejects.toThrow(\n /Token verification failed/,\n );\n });\n\n // jose quotes claim names — `\"exp\" claim timestamp check failed`, `unexpected\n // \"aud\" claim value`. Those quotes would close `error_description=\"…\"` early,\n // dropping the `resource_metadata` that MCP clients start discovery from.\n it.each([\n [\n \"an expired token\",\n (key: CryptoKey) => sign(key, {}, { expiresAt: \"-1h\" }),\n // jose: `\"exp\" claim timestamp check failed`\n \"Token verification failed: exp claim timestamp check failed\",\n ],\n [\n \"a token issued for another resource\",\n (key: CryptoKey) => sign(key, {}, { audience: \"api://other\" }),\n // jose: `unexpected \"aud\" claim value`\n \"Token verification failed: unexpected aud claim value\",\n ],\n [\n \"a token from another issuer\",\n (key: CryptoKey) => sign(key, {}, { issuer: \"https://other.test\" }),\n // jose: `unexpected \"iss\" claim value`\n \"Token verification failed: unexpected iss claim value\",\n ],\n [\n \"a token carrying no aud claim\",\n (key: CryptoKey) =>\n new jose.SignJWT({})\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(ISSUER)\n .setExpirationTime(\"1h\")\n .sign(key),\n // jose: `missing required \"aud\" claim`\n \"Token verification failed: missing required aud claim\",\n ],\n ])(\n \"keeps the message safe inside the challenge for %s\",\n async (_, mint, expected) => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n\n const error = await verifier\n .verifyAccessToken(await mint(privateKey))\n .then(\n () => undefined,\n (e: Error) => e,\n );\n\n expect(error?.message).toBe(expected);\n // OAuth 2.1 §5.3.1: %x20-21 / %x23-5B / %x5D-7E and nothing else.\n expect(error?.message).not.toMatch(/[^\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]/);\n },\n );\n\n it(\"skips the aud check when no audience is configured (e.g. Clerk)\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({ issuer: ISSUER, jwksUri });\n // Clerk-shaped access token: no `aud` claim at all.\n const token = await new jose.SignJWT({ client_id: \"c\", sub: \"u\" })\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(ISSUER)\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n\n const auth = await verifier.verifyAccessToken(token);\n expect(auth.clientId).toBe(\"c\");\n expect(auth.extra?.subject).toBe(\"u\");\n });\n\n it(\"parses array scope claims and trims extra whitespace\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n\n const arrayToken = await sign(privateKey, {\n scope: [\"openid\", \"email\"],\n });\n expect((await verifier.verifyAccessToken(arrayToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n\n const messyToken = await sign(privateKey, { scope: \" openid email \" });\n expect((await verifier.verifyAccessToken(messyToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n });\n});\n"]}
1
+ {"version":3,"file":"verify.test.js","sourceRoot":"","sources":["../../../src/server/auth/verify.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport * as jose from \"jose\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { createJwksVerifier } from \"./verify.js\";\n\nconst ISSUER = \"https://issuer.test\";\nconst AUDIENCE = \"api://default\";\n\nlet jwksServer: http.Server | undefined;\nafterEach(() => jwksServer?.close());\n\nasync function startJwks() {\n const { publicKey, privateKey } = await jose.generateKeyPair(\"RS256\");\n const jwk = {\n ...(await jose.exportJWK(publicKey)),\n kid: \"test-key\",\n alg: \"RS256\",\n use: \"sig\",\n };\n const server = http.createServer((_req, res) => {\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify({ keys: [jwk] }));\n });\n await new Promise<void>((resolve) => server.listen(0, resolve));\n jwksServer = server;\n const port = (server.address() as { port: number }).port;\n return { privateKey, jwksUri: `http://localhost:${port}/jwks` };\n}\n\nfunction sign(\n key: CryptoKey,\n claims: Record<string, unknown>,\n opts: { issuer?: string; audience?: string; expiresAt?: string } = {},\n) {\n return new jose.SignJWT(claims)\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(opts.issuer ?? ISSUER)\n .setAudience(opts.audience ?? AUDIENCE)\n .setExpirationTime(opts.expiresAt ?? \"1h\")\n .sign(key);\n}\n\nit(\"rejects a config with no issuer\", () => {\n expect(() => createJwksVerifier({ issuer: \"\" })).toThrow(\n /requires an `issuer`/,\n );\n});\n\ndescribe(\"createJwksVerifier\", () => {\n it(\"verifies a valid token and maps claims to AuthInfo\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(privateKey, {\n client_id: \"client-1\",\n scope: \"openid email\",\n sub: \"user-1\",\n email: \"a@b.test\",\n });\n\n const auth = await verifier.verifyAccessToken(token);\n\n expect(auth.token).toBe(token);\n expect(auth.clientId).toBe(\"client-1\");\n expect(auth.scopes).toEqual([\"openid\", \"email\"]);\n expect(auth.extra?.subject).toBe(\"user-1\");\n expect(auth.extra?.email).toBe(\"a@b.test\");\n });\n\n it(\"rejects a token with the wrong audience\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n const token = await sign(\n privateKey,\n { client_id: \"c\" },\n { audience: \"api://other\" },\n );\n\n await expect(verifier.verifyAccessToken(token)).rejects.toThrow(\n /Token verification failed/,\n );\n });\n\n // jose quotes claim names — `\"exp\" claim timestamp check failed`, `unexpected\n // \"aud\" claim value`. Those quotes would close `error_description=\"…\"` early,\n // dropping the `resource_metadata` that MCP clients start discovery from.\n it.each([\n [\n \"an expired token\",\n (key: CryptoKey) => sign(key, {}, { expiresAt: \"-1h\" }),\n // jose: `\"exp\" claim timestamp check failed`\n \"Token verification failed: exp claim timestamp check failed\",\n ],\n [\n \"a token issued for another resource\",\n (key: CryptoKey) => sign(key, {}, { audience: \"api://other\" }),\n // jose: `unexpected \"aud\" claim value`\n \"Token verification failed: unexpected aud claim value\",\n ],\n [\n \"a token from another issuer\",\n (key: CryptoKey) => sign(key, {}, { issuer: \"https://other.test\" }),\n // jose: `unexpected \"iss\" claim value`\n \"Token verification failed: unexpected iss claim value\",\n ],\n [\n \"a token carrying no aud claim\",\n (key: CryptoKey) =>\n new jose.SignJWT({})\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(ISSUER)\n .setExpirationTime(\"1h\")\n .sign(key),\n // jose: `missing required \"aud\" claim`\n \"Token verification failed: missing required aud claim\",\n ],\n ])(\n \"keeps the message safe inside the challenge for %s\",\n async (_, mint, expected) => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n\n const error = await verifier\n .verifyAccessToken(await mint(privateKey))\n .then(\n () => undefined,\n (e: Error) => e,\n );\n\n expect(error?.message).toBe(expected);\n // OAuth 2.1 §5.3.1: %x20-21 / %x23-5B / %x5D-7E and nothing else.\n expect(error?.message).not.toMatch(/[^\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]/);\n },\n );\n\n it(\"accepts a token minted for any of several audiences\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n // A server reached at a local port and through a dev tunnel: tokens carry\n // whichever resource the client discovered.\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: [AUDIENCE, \"https://tunnel.test\"],\n jwksUri,\n });\n\n const tunnelToken = await sign(\n privateKey,\n { client_id: \"c\" },\n { audience: \"https://tunnel.test\" },\n );\n expect((await verifier.verifyAccessToken(tunnelToken)).clientId).toBe(\"c\");\n\n const localToken = await sign(privateKey, { client_id: \"c\" });\n expect((await verifier.verifyAccessToken(localToken)).clientId).toBe(\"c\");\n\n const otherToken = await sign(privateKey, {}, { audience: \"api://other\" });\n await expect(verifier.verifyAccessToken(otherToken)).rejects.toThrow(\n /unexpected {2}aud {2}claim value/,\n );\n });\n\n it(\"skips the aud check when no audience is configured (e.g. Clerk)\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({ issuer: ISSUER, jwksUri });\n // Clerk-shaped access token: no `aud` claim at all.\n const token = await new jose.SignJWT({ client_id: \"c\", sub: \"u\" })\n .setProtectedHeader({ alg: \"RS256\", kid: \"test-key\" })\n .setIssuer(ISSUER)\n .setExpirationTime(\"1h\")\n .sign(privateKey);\n\n const auth = await verifier.verifyAccessToken(token);\n expect(auth.clientId).toBe(\"c\");\n expect(auth.extra?.subject).toBe(\"u\");\n });\n\n it(\"parses array scope claims and trims extra whitespace\", async () => {\n const { privateKey, jwksUri } = await startJwks();\n const verifier = createJwksVerifier({\n issuer: ISSUER,\n audience: AUDIENCE,\n jwksUri,\n });\n\n const arrayToken = await sign(privateKey, {\n scope: [\"openid\", \"email\"],\n });\n expect((await verifier.verifyAccessToken(arrayToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n\n const messyToken = await sign(privateKey, { scope: \" openid email \" });\n expect((await verifier.verifyAccessToken(messyToken)).scopes).toEqual([\n \"openid\",\n \"email\",\n ]);\n });\n});\n"]}
@@ -0,0 +1,113 @@
1
+ import { type McpServer as SdkMcpServer, type StandardSchemaV1, type StandardSchemaWithJSON } from "@modelcontextprotocol/server";
2
+ /**
3
+ * A webhook subscription, as passed to `onSubscribe`. Whatever delivers the
4
+ * events POSTs them to `url`, signed with `secret`, until `expiresAt`.
5
+ */
6
+ export interface EventSubscription {
7
+ /** Stable across refreshes: the same user subscribing to the same event with the same arguments and callback gets the same `id`. */
8
+ id: string;
9
+ /** Callback URL deliveries are POSTed to. */
10
+ url: string;
11
+ /** Standard Webhooks signing secret (`whsec_…`). A refresh may carry a new one. */
12
+ secret: string;
13
+ /** When the subscription lapses unless the host refreshes it. */
14
+ expiresAt: Date;
15
+ }
16
+ /** An event occurrence, as passed to {@link deliverEvent}. */
17
+ export interface EventOccurrence {
18
+ /** Event type name, as registered with `registerEvent`. */
19
+ name: string;
20
+ /** Stable identifier the host uses to drop duplicates, ideally the upstream's. Reuse it when retrying. */
21
+ id: string;
22
+ /** When the event happened. Defaults to now. */
23
+ timestamp?: Date;
24
+ /** Payload matching the event's `payloadSchema`. */
25
+ data: unknown;
26
+ }
27
+ /** Outcome of {@link deliverEvent}. Retry with the same event `id` when `retryable`. */
28
+ export type DeliveryResult = {
29
+ ok: true;
30
+ } | {
31
+ ok: false;
32
+ reason: string;
33
+ retryable: boolean;
34
+ status?: number;
35
+ };
36
+ type EventSchema = Record<string, StandardSchemaWithJSON> | StandardSchemaWithJSON;
37
+ export type InferEventSchema<T extends EventSchema> = T extends StandardSchemaWithJSON ? StandardSchemaV1.InferOutput<T> : T extends Record<string, StandardSchemaWithJSON> ? {
38
+ [K in keyof T as undefined extends StandardSchemaV1.InferOutput<T[K]> ? never : K]: StandardSchemaV1.InferOutput<T[K]>;
39
+ } & {
40
+ [K in keyof T as undefined extends StandardSchemaV1.InferOutput<T[K]> ? K : never]?: StandardSchemaV1.InferOutput<T[K]>;
41
+ } : never;
42
+ /** The `registerEvent` config: how the event type is described in `events/list`. */
43
+ export interface EventConfig<TInput extends EventSchema> {
44
+ /** Stable, specific name such as `comment.created`. */
45
+ name: string;
46
+ /** What the event is and when it fires. */
47
+ description?: string;
48
+ /** Subscription arguments (filters): a Zod shape or a Standard Schema object. */
49
+ inputSchema?: TInput;
50
+ /** Shape of the delivered `data`: a Zod shape or a Standard Schema object. */
51
+ payloadSchema: EventSchema;
52
+ _meta?: Record<string, unknown>;
53
+ }
54
+ /** What `registerEvent` does when the host subscribes and unsubscribes. */
55
+ export interface EventHooks<TArguments, TExtra> {
56
+ /**
57
+ * Runs on every `events/subscribe`, including the host's refreshes, once
58
+ * the callback URL is verified. Hand the subscription to whatever delivers
59
+ * the events, keyed by `subscription.id`. Throw (e.g. a `ProtocolError`) to
60
+ * reject it, for instance when the user may not access what the arguments
61
+ * point to.
62
+ */
63
+ onSubscribe(args: TArguments, context: {
64
+ subscription: EventSubscription;
65
+ extra: TExtra;
66
+ }): void | Promise<void>;
67
+ /** Runs on `events/unsubscribe`. Stop delivering to `subscription.id`. */
68
+ onUnsubscribe(args: TArguments, context: {
69
+ subscription: Pick<EventSubscription, "id">;
70
+ extra: TExtra;
71
+ }): void | Promise<void>;
72
+ }
73
+ /**
74
+ * @experimental POST one event to a subscription handed out by `onSubscribe`,
75
+ * as MCP Events expects: the event envelope, Standard Webhooks signature
76
+ * headers, a 10 second deadline and, outside development, no requests to
77
+ * private addresses. Pass the old and new secrets during a rotation so both
78
+ * sign the delivery. A subscription past `expiresAt` is not posted to.
79
+ * Retrying is up to the caller. API may change.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const result = await deliverEvent(subscription, {
84
+ * name: "comment.created",
85
+ * id: comment.id,
86
+ * data: { documentId: comment.documentId, excerpt: comment.text.slice(0, 280) },
87
+ * });
88
+ * if (!result.ok && result.retryable) {
89
+ * await queue.retryLater(subscription, comment);
90
+ * }
91
+ * ```
92
+ *
93
+ * @see https://docs.skybridge.tech/api-reference/register-event
94
+ */
95
+ export declare function deliverEvent(subscription: Pick<EventSubscription, "id" | "url"> & {
96
+ secret: string | string[];
97
+ expiresAt: Date | string;
98
+ }, event: EventOccurrence): Promise<DeliveryResult>;
99
+ /**
100
+ * The event types registered on one server, and the `events/*` handlers that
101
+ * serve them.
102
+ *
103
+ * @internal
104
+ */
105
+ export declare class EventRegistry {
106
+ private readonly server;
107
+ private readonly events;
108
+ constructor(server: Pick<SdkMcpServer["server"], "setRequestHandler">);
109
+ add(config: EventConfig<EventSchema>, hooks: EventHooks<never, never>): void;
110
+ private install;
111
+ private resolve;
112
+ }
113
+ export {};