skybridge 2.0.0-beta.dba0bc8 → 2.0.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 (56) hide show
  1. package/README.md +7 -7
  2. package/dist/server/app.d.ts +21 -31
  3. package/dist/server/app.js +12 -9
  4. package/dist/server/app.js.map +1 -1
  5. package/dist/server/app.test.js +3 -3
  6. package/dist/server/app.test.js.map +1 -1
  7. package/dist/server/auth/index.d.ts +18 -0
  8. package/dist/server/auth/index.js.map +1 -1
  9. package/dist/server/auth/providers/auth0.d.ts +2 -2
  10. package/dist/server/auth/providers/auth0.js +13 -8
  11. package/dist/server/auth/providers/auth0.js.map +1 -1
  12. package/dist/server/auth/providers/auth0.test.js +1 -1
  13. package/dist/server/auth/providers/auth0.test.js.map +1 -1
  14. package/dist/server/auth/providers/authplane.d.ts +2 -2
  15. package/dist/server/auth/providers/authplane.js.map +1 -1
  16. package/dist/server/auth/providers/authplane.test.js +10 -7
  17. package/dist/server/auth/providers/authplane.test.js.map +1 -1
  18. package/dist/server/auth/providers/clerk.d.ts +2 -2
  19. package/dist/server/auth/providers/clerk.js.map +1 -1
  20. package/dist/server/auth/providers/clerk.test.js +1 -1
  21. package/dist/server/auth/providers/clerk.test.js.map +1 -1
  22. package/dist/server/auth/providers/custom.d.ts +3 -3
  23. package/dist/server/auth/providers/custom.js +5 -2
  24. package/dist/server/auth/providers/custom.js.map +1 -1
  25. package/dist/server/auth/providers/custom.test.js +13 -10
  26. package/dist/server/auth/providers/custom.test.js.map +1 -1
  27. package/dist/server/auth/providers/descope.d.ts +2 -2
  28. package/dist/server/auth/providers/descope.js.map +1 -1
  29. package/dist/server/auth/providers/descope.test.js +3 -3
  30. package/dist/server/auth/providers/descope.test.js.map +1 -1
  31. package/dist/server/auth/providers/stytch.d.ts +2 -2
  32. package/dist/server/auth/providers/stytch.js.map +1 -1
  33. package/dist/server/auth/providers/workos.d.ts +2 -2
  34. package/dist/server/auth/providers/workos.js.map +1 -1
  35. package/dist/server/auth/setup.test.js +0 -3
  36. package/dist/server/auth/setup.test.js.map +1 -1
  37. package/dist/server/auth-extra.test-d.js +12 -9
  38. package/dist/server/auth-extra.test-d.js.map +1 -1
  39. package/dist/server/auth.d.ts +4 -4
  40. package/dist/server/auth.js +2 -2
  41. package/dist/server/auth.js.map +1 -1
  42. package/dist/server/build-manifest.test.js +1 -1
  43. package/dist/server/build-manifest.test.js.map +1 -1
  44. package/dist/server/index.d.ts +4 -3
  45. package/dist/server/index.js +2 -1
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/server/server.d.ts +3 -26
  48. package/dist/server/server.js +8 -56
  49. package/dist/server/server.js.map +1 -1
  50. package/dist/server/skills-integration.test.js +5 -3
  51. package/dist/server/skills-integration.test.js.map +1 -1
  52. package/dist/test/utils.js +1 -1
  53. package/dist/test/utils.js.map +1 -1
  54. package/dist/test/view.test.js +0 -1
  55. package/dist/test/view.test.js.map +1 -1
  56. package/package.json +2 -3
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  <p align="center">
17
17
  <a href="https://docs.skybridge.tech">Documentation</a> ·
18
- <a href="https://docs.skybridge.tech/quickstart/create-new-app">Quickstart</a> ·
18
+ <a href="https://docs.skybridge.tech/get-started/quickstart">Quickstart</a> ·
19
19
  <a href="https://github.com/alpic-ai/skybridge/tree/main/examples">Examples</a>
20
20
  </p>
21
21
 
@@ -62,7 +62,7 @@ They chose to build their MCP apps with Skybridge:
62
62
 
63
63
  **For agents**
64
64
 
65
- Install our [skill](https://docs.skybridge.tech/devtools/skills) for building MCP apps and ChatGPT apps:
65
+ Install our [skill](https://docs.skybridge.tech/guides/migrate) for building MCP apps and ChatGPT apps:
66
66
  ```bash
67
67
  npx skills add alpic-ai/skybridge -s skybridge
68
68
  ```
@@ -78,22 +78,22 @@ Bootstrap a new project with:
78
78
  ```bash
79
79
  npm create skybridge@latest my-app
80
80
  ```
81
- For full install instructions, read our [**Quickstart guide**](https://docs.skybridge.tech/quickstart/create-new-app).
81
+ For full install instructions, read our [**Quickstart guide**](https://docs.skybridge.tech/get-started/quickstart).
82
82
 
83
83
  ## Documentation
84
84
 
85
85
  The [Skybridge documentation](https://docs.skybridge.tech) covers the full lifecycle of building MCP Apps:
86
86
 
87
- - [Fundamentals](https://docs.skybridge.tech/fundamentals): understand MCP Apps, ChatGPT Apps, and how Skybridge bridges both runtimes.
88
- - [Core concepts](https://docs.skybridge.tech/concepts): learn about server <> model <> UI data flows, LLM context sync, type safety, and instant local iteration with our devtools.
89
- - [Guides](https://docs.skybridge.tech/guides/fetching-data): build real app behavior with tools, views, state, and model communication.
87
+ - [Fundamentals](https://docs.skybridge.tech/get-started/architecture): understand MCP Apps, ChatGPT Apps, and how Skybridge bridges both runtimes.
88
+ - [Core concepts](https://docs.skybridge.tech/get-started/architecture): learn about server <> model <> UI data flows, LLM context sync, type safety, and instant local iteration with our devtools.
89
+ - [Guides](https://docs.skybridge.tech/build/tools): build real app behavior with tools, views, state, and model communication.
90
90
  - [API Reference](https://docs.skybridge.tech/api-reference): browse our MCP server APIs, React hooks, CLI commands, and runtime compatibility.
91
91
 
92
92
  ## Deploy
93
93
 
94
94
  Deploy Skybridge apps instantly on [Alpic](https://alpic.ai) for scalable hosting, MCP-specific analytics, permanent tunneling, app store compliance auditing and submission help. You can also self-host on any Node.js-compatible platform.
95
95
 
96
- See our [deployment guide](https://docs.skybridge.tech/quickstart/deploy) for the full production path.
96
+ See our [deployment guide](https://docs.skybridge.tech/ship/deploy) for the full production path.
97
97
 
98
98
  ## Community & Contributing
99
99
 
@@ -1,48 +1,38 @@
1
1
  import type { Implementation, Server as SdkServer, ServerOptions } from "@modelcontextprotocol/server";
2
2
  import type { ErrorRequestHandler, Express, RequestHandler } from "express";
3
- import type { OAuthConfig } from "./auth/index.js";
3
+ import type { OAuthConfig, OAuthProvider } from "./auth/index.js";
4
4
  import type { ExtraClaims } from "./auth.js";
5
5
  import { type JsonOptions, McpServer, type McpServerTypes, type ToolDef } from "./server.js";
6
- /**
7
- * The bare {@link McpServer} a {@link SkybridgeHandler} receives: no tools
8
- * registered yet. Use it to annotate a handler extracted into its own
9
- * declaration — `(server: SkybridgeServer) => server.registerTool(…)` — and
10
- * pass the claims your OAuth verifier produces to type
11
- * `extra.http.authInfo.extra` in handlers. Leave the handler's return type
12
- * inferred: the returned chain is what carries the tool registry into
13
- * `typeof app`.
14
- */
15
- export type SkybridgeServer<TAuthExtra extends ExtraClaims = ExtraClaims> = McpServer<Record<never, ToolDef>, TAuthExtra>;
16
6
  /**
17
7
  * The `handler` field of {@link SkybridgeConfig}: builds the app's MCP
18
8
  * surface. Runs again for **every incoming request**, on a fresh
19
- * {@link McpServer}, so keep it to registration: hoist pools, timers, clients
20
- * and any other side effect to module scope (or into `setup`) and close over
21
- * them. It must **return** the chained server so `typeof app` carries the
22
- * registered tool types.
9
+ * {@link McpServer}, so keep it to registration: pools, clients and any other
10
+ * one-time work belong in `setup`, whose result is the second argument. It
11
+ * must **return** the chained server so `typeof app` carries the registered
12
+ * tool types.
23
13
  *
24
- * @typeParam TContext - What `setup` resolved to, passed as the second argument.
14
+ * @typeParam TConfig - What `setup` resolved to, passed as the second argument.
25
15
  */
26
- export type SkybridgeHandler<TTools extends Record<string, ToolDef>, TContext, TAuthExtra extends ExtraClaims> = (server: McpServer<Record<never, ToolDef>, TAuthExtra>, context: TContext) => McpServer<TTools, TAuthExtra>;
16
+ export type SkybridgeHandler<TTools extends Record<string, ToolDef>, TConfig, TAuthExtra extends ExtraClaims> = (server: McpServer<Record<never, ToolDef>, TAuthExtra>, config: TConfig) => McpServer<TTools, TAuthExtra>;
27
17
  /**
28
- * What the `oauth` field accepts: a resolved {@link OAuthConfig}, a promise of
29
- * one (the branded providers are async), or a function of the `setup` result.
30
- * A function or promise is resolved once at {@link Skybridge.run} or on the
31
- * first request, never at module import so prefer a function when building
32
- * the config has side effects (network discovery, secrets).
18
+ * What the `oauth` field accepts: an {@link OAuthConfig}, a provider
19
+ * (`oauth: auth0Provider(...)`), or a function of the `setup` result returning
20
+ * either. Providers and functions resolve once, at {@link Skybridge.run} or on
21
+ * the first request, never at module import. Anything asynchronous belongs in
22
+ * a provider's `resolve`.
33
23
  */
34
- export type SkybridgeOAuthInput<TContext, TExtra extends ExtraClaims> = OAuthConfig<TExtra> | Promise<OAuthConfig<TExtra>> | ((context: TContext) => OAuthConfig<TExtra> | Promise<OAuthConfig<TExtra>>);
24
+ export type SkybridgeOAuthInput<TConfig, TExtra extends ExtraClaims> = OAuthConfig<TExtra> | OAuthProvider<TExtra> | ((config: TConfig) => OAuthConfig<TExtra> | OAuthProvider<TExtra>);
35
25
  /**
36
26
  * Everything a Skybridge app needs in one bag: the MCP implementation info
37
27
  * (`name`, `version`, …), the SDK's {@link ServerOptions} (`capabilities`,
38
28
  * `instructions`, …), the Express and skills options, and the app's behavior
39
29
  * (`setup`, `oauth`, `handler`).
40
30
  *
41
- * All type parameters are inferred from the value: the context from `setup`,
31
+ * All type parameters are inferred from the value: the config from `setup`,
42
32
  * the auth claims from `oauth`, and the tool registry from the server
43
33
  * `handler` returns.
44
34
  */
45
- export type SkybridgeConfig<TTools extends Record<string, ToolDef> = Record<never, ToolDef>, TContext = undefined, TAuthExtra extends ExtraClaims = ExtraClaims> = Implementation & ServerOptions & {
35
+ export type SkybridgeConfig<TTools extends Record<string, ToolDef> = Record<never, ToolDef>, TConfig = undefined, TAuthExtra extends ExtraClaims = ExtraClaims> = Implementation & ServerOptions & {
46
36
  /** Options for the built-in `express.json()` middleware, e.g. `{ limit: "10mb" }`. */
47
37
  json?: JsonOptions;
48
38
  /**
@@ -56,15 +46,15 @@ export type SkybridgeConfig<TTools extends Record<string, ToolDef> = Record<neve
56
46
  * never at module import — and its awaited return value is passed to an
57
47
  * `oauth` function and to `handler` as the second argument.
58
48
  */
59
- setup?: () => TContext;
49
+ setup?: () => TConfig;
60
50
  /**
61
51
  * Resource-server OAuth. When set, mounts the well-known metadata routes
62
52
  * and bearer auth on `/mcp`, and the verifier's claims type
63
53
  * `extra.http.authInfo.extra` in tool handlers.
64
54
  */
65
- oauth?: SkybridgeOAuthInput<Awaited<TContext>, TAuthExtra>;
55
+ oauth?: SkybridgeOAuthInput<Awaited<TConfig>, TAuthExtra>;
66
56
  /** Registers the MCP surface, per request. See {@link SkybridgeHandler}. */
67
- handler: SkybridgeHandler<TTools, Awaited<TContext>, TAuthExtra>;
57
+ handler: SkybridgeHandler<TTools, Awaited<TConfig>, TAuthExtra>;
68
58
  };
69
59
  /**
70
60
  * A Skybridge app: the HTTP surface (Express, OAuth metadata, the `/mcp`
@@ -99,7 +89,7 @@ export type SkybridgeConfig<TTools extends Record<string, ToolDef> = Record<neve
99
89
  *
100
90
  * @see https://docs.skybridge.tech/api-reference/mcp-server
101
91
  */
102
- export declare class Skybridge<TTools extends Record<string, ToolDef> = Record<never, ToolDef>, TContext = undefined, TAuthExtra extends ExtraClaims = ExtraClaims> {
92
+ export declare class Skybridge<TTools extends Record<string, ToolDef> = Record<never, ToolDef>, TConfig = undefined, TAuthExtra extends ExtraClaims = ExtraClaims> {
103
93
  readonly $types: McpServerTypes<TTools>;
104
94
  private readonly serverInfo;
105
95
  private readonly serverOptions;
@@ -108,14 +98,14 @@ export declare class Skybridge<TTools extends Record<string, ToolDef> = Record<n
108
98
  private readonly handler;
109
99
  private readonly setup?;
110
100
  private readonly oauthInput?;
111
- private context?;
101
+ private config?;
112
102
  private readonly expressApp;
113
103
  private readonly errorMiddleware;
114
104
  private readonly monitoringEntry;
115
105
  private resolveResourceMetadataUrl?;
116
106
  private readyPromise?;
117
107
  private slowHandlerWarned;
118
- constructor(config: SkybridgeConfig<TTools, TContext, TAuthExtra>);
108
+ constructor({ name, title, version, description, icons, websiteUrl, json, skills, setup, oauth, handler, ...serverOptions }: SkybridgeConfig<TTools, TConfig, TAuthExtra>);
119
109
  /**
120
110
  * Resolve `setup` and `oauth`, then wire OAuth onto the Express app. Runs
121
111
  * once; every entry point that needs a built server awaits it, and a failed
@@ -5,6 +5,12 @@ import { createMiddlewareEntry } from "./metric.js";
5
5
  import { buildMiddlewareChain, getHandlerMaps } from "./middleware.js";
6
6
  import { McpServer, } from "./server.js";
7
7
  const SLOW_HANDLER_THRESHOLD_MS = 50;
8
+ async function resolveOAuthInput(input, config) {
9
+ const resolved = typeof input === "function" ? input(config) : input;
10
+ return resolved && "resolve" in resolved
11
+ ? await resolved.resolve()
12
+ : resolved;
13
+ }
8
14
  /**
9
15
  * A Skybridge app: the HTTP surface (Express, OAuth metadata, the `/mcp`
10
16
  * route) plus a handler that builds the MCP server for each request.
@@ -46,15 +52,14 @@ export class Skybridge {
46
52
  handler;
47
53
  setup;
48
54
  oauthInput;
49
- context;
55
+ config;
50
56
  expressApp;
51
57
  errorMiddleware = [];
52
58
  monitoringEntry = createMiddlewareEntry();
53
59
  resolveResourceMetadataUrl;
54
60
  readyPromise;
55
61
  slowHandlerWarned = false;
56
- constructor(config) {
57
- const { name, title, version, description, icons, websiteUrl, json, skills, setup, oauth, handler, ...serverOptions } = config;
62
+ constructor({ name, title, version, description, icons, websiteUrl, json, skills, setup, oauth, handler, ...serverOptions }) {
58
63
  this.serverInfo = { name, title, version, description, icons, websiteUrl };
59
64
  this.serverOptions = serverOptions;
60
65
  this.skills = skills;
@@ -73,11 +78,9 @@ export class Skybridge {
73
78
  ready() {
74
79
  this.readyPromise ??= (async () => {
75
80
  if (this.setup) {
76
- this.context = await this.setup();
81
+ this.config = await this.setup();
77
82
  }
78
- const oauth = typeof this.oauthInput === "function"
79
- ? await this.oauthInput(this.context)
80
- : await this.oauthInput;
83
+ const oauth = await resolveOAuthInput(this.oauthInput, this.config);
81
84
  this.oauthEnabled = Boolean(oauth);
82
85
  const sample = this.buildServer();
83
86
  if (oauth) {
@@ -229,14 +232,14 @@ export class Skybridge {
229
232
  server.setResourceMetadataUrlResolver(this.resolveResourceMetadataUrl);
230
233
  }
231
234
  const startedAt = performance.now();
232
- const built = this.handler(server, this.context);
235
+ const built = this.handler(server, this.config);
233
236
  const elapsed = performance.now() - startedAt;
234
237
  if (typeof built.then === "function") {
235
238
  throw new Error("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.");
236
239
  }
237
240
  if (elapsed > SLOW_HANDLER_THRESHOLD_MS && !this.slowHandlerWarned) {
238
241
  this.slowHandlerWarned = true;
239
- console.warn(`The Skybridge handler took ${Math.round(elapsed)}ms — it runs on every request, so this cost is paid per request. Hoist expensive work to module scope or into \`setup\`, whose result is passed to the handler.`);
242
+ console.warn(`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.`);
240
243
  }
241
244
  return built;
242
245
  }
@@ -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 } 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 bare {@link McpServer} a {@link SkybridgeHandler} receives: no tools\n * registered yet. Use it to annotate a handler extracted into its own\n * declaration — `(server: SkybridgeServer) => server.registerTool(…)` — and\n * pass the claims your OAuth verifier produces to type\n * `extra.http.authInfo.extra` in handlers. Leave the handler's return type\n * inferred: the returned chain is what carries the tool registry into\n * `typeof app`.\n */\nexport type SkybridgeServer<TAuthExtra extends ExtraClaims = ExtraClaims> =\n McpServer<Record<never, ToolDef>, TAuthExtra>;\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: hoist pools, timers, clients\n * and any other side effect to module scope (or into `setup`) and close over\n * them. It must **return** the chained server so `typeof app` carries the\n * registered tool types.\n *\n * @typeParam TContext - What `setup` resolved to, passed as the second argument.\n */\nexport type SkybridgeHandler<\n TTools extends Record<string, ToolDef>,\n TContext,\n TAuthExtra extends ExtraClaims,\n> = (\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n context: TContext,\n) => McpServer<TTools, TAuthExtra>;\n\n/**\n * What the `oauth` field accepts: a resolved {@link OAuthConfig}, a promise of\n * one (the branded providers are async), or a function of the `setup` result.\n * A function or promise is resolved once — at {@link Skybridge.run} or on the\n * first request, never at module import — so prefer a function when building\n * the config has side effects (network discovery, secrets).\n */\nexport type SkybridgeOAuthInput<TContext, TExtra extends ExtraClaims> =\n | OAuthConfig<TExtra>\n | Promise<OAuthConfig<TExtra>>\n | ((context: TContext) => OAuthConfig<TExtra> | Promise<OAuthConfig<TExtra>>);\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 context 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 TContext = 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?: () => TContext;\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<TContext>, TAuthExtra>;\n /** Registers the MCP surface, per request. See {@link SkybridgeHandler}. */\n handler: SkybridgeHandler<TTools, Awaited<TContext>, 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 TContext = 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<TContext>,\n TAuthExtra\n >;\n private readonly setup?: () => TContext;\n private readonly oauthInput?: SkybridgeOAuthInput<\n Awaited<TContext>,\n TAuthExtra\n >;\n private context?: Awaited<TContext>;\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(config: SkybridgeConfig<TTools, TContext, TAuthExtra>) {\n const {\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 } = config;\n\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.context = await this.setup();\n }\n const oauth =\n typeof this.oauthInput === \"function\"\n ? await this.oauthInput(this.context as Awaited<TContext>)\n : await this.oauthInput;\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.context as Awaited<TContext>);\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. Hoist expensive work to module scope or 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 { 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"]}
@@ -76,14 +76,14 @@ describe("Skybridge setup and handler", () => {
76
76
  const [, serverTransport] = InMemoryTransport.createLinkedPair();
77
77
  await expect(app.connect(serverTransport)).rejects.toThrow(/must be synchronous/);
78
78
  });
79
- it("resolves an oauth function with the setup context before wiring the routes", async () => {
79
+ it("resolves an oauth function with the setup config before wiring the routes", async () => {
80
80
  let seen;
81
81
  const app = new Skybridge({
82
82
  name: "test",
83
83
  version: "1.0.0",
84
84
  setup: () => ({ issuer: "https://issuer.example.com" }),
85
- oauth: (context) => {
86
- seen = context;
85
+ oauth: (config) => {
86
+ seen = config;
87
87
  return oauthConfig;
88
88
  },
89
89
  handler: (server) => server,
@@ -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 context 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: (context) => {\n seen = context;\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"]}
@@ -28,3 +28,21 @@ export type OAuthConfig<TExtra extends ExtraClaims = ExtraClaims> = {
28
28
  */
29
29
  verifier: TokenVerifier<TExtra>;
30
30
  };
31
+ /**
32
+ * What the branded providers return: a deferred {@link OAuthConfig}. Discovery
33
+ * runs in `resolve`, so `oauth: auth0Provider(...)` at module scope performs
34
+ * no network access until `Skybridge.run`. Call `resolve` yourself
35
+ * (`await customProvider(...).resolve()`) when wiring the middleware by hand.
36
+ *
37
+ * This is an object rather than a bare `() => Promise<OAuthConfig>` on
38
+ * purpose. TypeScript defers, during the first inference pass, any argument
39
+ * that is a call to a generic function returning a function type. The
40
+ * providers are generic (`workosProvider<TCustom>`), so a function-shaped
41
+ * return written inline in `new Skybridge({ oauth: workosProvider(...) })`
42
+ * would be skipped and `TAuthExtra` would fall back to `ExtraClaims`, losing
43
+ * the claims typing in tool handlers. A non-function shape is inferred
44
+ * eagerly.
45
+ */
46
+ export type OAuthProvider<TExtra extends ExtraClaims = ExtraClaims> = {
47
+ resolve: () => Promise<OAuthConfig<TExtra>>;
48
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/auth/index.ts"],"names":[],"mappings":"","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/server\";\nimport type { ExtraClaims, TokenVerifier } from \"../auth.js\";\n\n/**\n * Resource-server OAuth config for the `oauth` field of `SkybridgeConfig`.\n *\n * `TExtra` comes from the `verifier` and flows on to tool handlers and\n * `mcpMiddleware`, so the claim shape is declared once by whoever checks the\n * token. The branded providers set it from the claims their IdP documents.\n *\n * @typeParam TExtra - Claims the verifier populates in `AuthInfo[\"extra\"]`.\n */\nexport type OAuthConfig<TExtra extends ExtraClaims = ExtraClaims> = {\n /**\n * Public URL of this server; sets `resourceServerUrl` and the\n * `resource_metadata` URL. When omitted, it is inferred per request from\n * `x-forwarded-host`/`origin`/`host` headers.\n */\n baseUrl?: string;\n /** AS metadata served at `/.well-known/oauth-authorization-server`. */\n oauthMetadata: OAuthMetadata;\n /** Scopes advertised in protected-resource metadata. */\n scopesSupported?: string[];\n /** Server-wide required-scope floor. */\n requiredScopes?: string[];\n /**\n * Checks each bearer token. Build one with `createJwksVerifier` for a\n * JWT-issuing IdP, or supply your own for opaque tokens.\n */\n verifier: TokenVerifier<TExtra>;\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/auth/index.ts"],"names":[],"mappings":"","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/server\";\nimport type { ExtraClaims, TokenVerifier } from \"../auth.js\";\n\n/**\n * Resource-server OAuth config for the `oauth` field of `SkybridgeConfig`.\n *\n * `TExtra` comes from the `verifier` and flows on to tool handlers and\n * `mcpMiddleware`, so the claim shape is declared once by whoever checks the\n * token. The branded providers set it from the claims their IdP documents.\n *\n * @typeParam TExtra - Claims the verifier populates in `AuthInfo[\"extra\"]`.\n */\nexport type OAuthConfig<TExtra extends ExtraClaims = ExtraClaims> = {\n /**\n * Public URL of this server; sets `resourceServerUrl` and the\n * `resource_metadata` URL. When omitted, it is inferred per request from\n * `x-forwarded-host`/`origin`/`host` headers.\n */\n baseUrl?: string;\n /** AS metadata served at `/.well-known/oauth-authorization-server`. */\n oauthMetadata: OAuthMetadata;\n /** Scopes advertised in protected-resource metadata. */\n scopesSupported?: string[];\n /** Server-wide required-scope floor. */\n requiredScopes?: string[];\n /**\n * Checks each bearer token. Build one with `createJwksVerifier` for a\n * JWT-issuing IdP, or supply your own for opaque tokens.\n */\n verifier: TokenVerifier<TExtra>;\n};\n\n/**\n * What the branded providers return: a deferred {@link OAuthConfig}. Discovery\n * runs in `resolve`, so `oauth: auth0Provider(...)` at module scope performs\n * no network access until `Skybridge.run`. Call `resolve` yourself\n * (`await customProvider(...).resolve()`) when wiring the middleware by hand.\n *\n * This is an object rather than a bare `() => Promise<OAuthConfig>` on\n * purpose. TypeScript defers, during the first inference pass, any argument\n * that is a call to a generic function returning a function type. The\n * providers are generic (`workosProvider<TCustom>`), so a function-shaped\n * return written inline in `new Skybridge({ oauth: workosProvider(...) })`\n * would be skipped and `TAuthExtra` would fall back to `ExtraClaims`, losing\n * the claims typing in tool handlers. A non-function shape is inferred\n * eagerly.\n */\nexport type OAuthProvider<TExtra extends ExtraClaims = ExtraClaims> = {\n resolve: () => Promise<OAuthConfig<TExtra>>;\n};\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { ExtraClaims } from "../../auth.js";
2
- import type { OAuthConfig } from "../index.js";
2
+ import type { OAuthProvider } from "../index.js";
3
3
  import type { RegisteredClaims } from "../verify.js";
4
4
  import { type CustomProviderOptions } from "./custom.js";
5
5
  /**
@@ -37,4 +37,4 @@ export declare function auth0Provider<TCustom extends ExtraClaims = Record<never
37
37
  domain: string;
38
38
  audience: string;
39
39
  serverUrl: string;
40
- } & Omit<CustomProviderOptions, "issuer" | "audience" | "baseUrl" | "serverUrl">): Promise<OAuthConfig<Auth0Claims & TCustom & RegisteredClaims>>;
40
+ } & Omit<CustomProviderOptions, "issuer" | "audience" | "baseUrl" | "serverUrl">): OAuthProvider<Auth0Claims & TCustom & RegisteredClaims>;
@@ -11,20 +11,25 @@ import { toIssuerUrl } from "./shared.js";
11
11
  * `authorization_endpoint`. The token's `aud` is the API; `verify.issuer` stays
12
12
  * Auth0 (the token's real `iss`).
13
13
  */
14
- export async function auth0Provider(opts) {
14
+ export function auth0Provider(opts) {
15
15
  const { domain, audience, ...rest } = opts;
16
- const config = await customProvider({
16
+ const provider = customProvider({
17
17
  issuer: toIssuerUrl(domain),
18
18
  audience,
19
19
  ...rest,
20
20
  });
21
- const authUrl = new URL(config.oauthMetadata.authorization_endpoint);
22
- authUrl.searchParams.set("audience", audience);
23
21
  return {
24
- ...config,
25
- oauthMetadata: {
26
- ...config.oauthMetadata,
27
- authorization_endpoint: authUrl.href,
22
+ resolve: async () => {
23
+ const config = await provider.resolve();
24
+ const authUrl = new URL(config.oauthMetadata.authorization_endpoint);
25
+ authUrl.searchParams.set("audience", audience);
26
+ return {
27
+ ...config,
28
+ oauthMetadata: {
29
+ ...config.oauthMetadata,
30
+ authorization_endpoint: authUrl.href,
31
+ },
32
+ };
28
33
  },
29
34
  };
30
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth0.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAGjC,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,cAAc,CAAwB;QACzD,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;QAC3B,QAAQ;QACR,GAAG,IAAI;KACR,CAAC,CAAC;IACH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;IACrE,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;IAC/C,OAAO;QACL,GAAG,MAAM;QACT,aAAa,EAAE;YACb,GAAG,MAAM,CAAC,aAAa;YACvB,sBAAsB,EAAE,OAAO,CAAC,IAAI;SACrC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { ExtraClaims } from \"../../auth.js\";\nimport type { OAuthConfig } 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 an Auth0 access token carries for a custom API audience. Auth0 puts no\n * user attributes beyond the id in the token, so there is no `email`: call\n * `/userinfo` for the profile. Custom claims must be namespaced (a bare `email`\n * key is silently dropped), so add them through the type parameter under their\n * full URI.\n *\n * @see https://auth0.com/docs/secure/tokens/access-tokens\n * @see https://auth0.com/docs/secure/tokens/json-web-tokens/create-custom-claims\n */\nexport type Auth0Claims = {\n /** Authorized party: the client id the token was issued to. */\n azp?: string;\n /** Only when the API has RBAC and \"Add Permissions in the Access Token\" on. */\n permissions?: string[];\n /** Organization id, when the tenant uses Organizations. */\n org_id?: string;\n /** Organization name, when the tenant uses Organizations. */\n org_name?: string;\n};\n\n/**\n * OAuth provider for Auth0. `domain` is the tenant domain (e.g.\n * `acme.us.auth0.com`); `audience` is the API Identifier; `serverUrl` is this\n * server's public URL. Requires DCR enabled on the tenant.\n *\n * Auth0 can't use the client's resource indicator — `audience` must be *in* the\n * authorize request. So it runs as skybridge-as-AS (`serverUrl`, see\n * {@link customProvider}) and bakes `?audience=<id>` into the advertised\n * `authorization_endpoint`. The token's `aud` is the API; `verify.issuer` stays\n * Auth0 (the token's real `iss`).\n */\nexport async function auth0Provider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: { domain: string; audience: string; serverUrl: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\" | \"baseUrl\" | \"serverUrl\"\n >,\n): Promise<OAuthConfig<Auth0Claims & TCustom & RegisteredClaims>> {\n const { domain, audience, ...rest } = opts;\n const config = await customProvider<Auth0Claims & TCustom>({\n issuer: toIssuerUrl(domain),\n audience,\n ...rest,\n });\n const authUrl = new URL(config.oauthMetadata.authorization_endpoint);\n authUrl.searchParams.set(\"audience\", audience);\n return {\n ...config,\n oauthMetadata: {\n ...config.oauthMetadata,\n authorization_endpoint: authUrl.href,\n },\n };\n}\n"]}
1
+ {"version":3,"file":"auth0.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB1C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAG3B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAC3C,MAAM,QAAQ,GAAG,cAAc,CAAwB;QACrD,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;QAC3B,QAAQ;QACR,GAAG,IAAI;KACR,CAAC,CAAC;IACH,OAAO;QACL,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;YACxC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;YACrE,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YAC/C,OAAO;gBACL,GAAG,MAAM;gBACT,aAAa,EAAE;oBACb,GAAG,MAAM,CAAC,aAAa;oBACvB,sBAAsB,EAAE,OAAO,CAAC,IAAI;iBACrC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,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 an Auth0 access token carries for a custom API audience. Auth0 puts no\n * user attributes beyond the id in the token, so there is no `email`: call\n * `/userinfo` for the profile. Custom claims must be namespaced (a bare `email`\n * key is silently dropped), so add them through the type parameter under their\n * full URI.\n *\n * @see https://auth0.com/docs/secure/tokens/access-tokens\n * @see https://auth0.com/docs/secure/tokens/json-web-tokens/create-custom-claims\n */\nexport type Auth0Claims = {\n /** Authorized party: the client id the token was issued to. */\n azp?: string;\n /** Only when the API has RBAC and \"Add Permissions in the Access Token\" on. */\n permissions?: string[];\n /** Organization id, when the tenant uses Organizations. */\n org_id?: string;\n /** Organization name, when the tenant uses Organizations. */\n org_name?: string;\n};\n\n/**\n * OAuth provider for Auth0. `domain` is the tenant domain (e.g.\n * `acme.us.auth0.com`); `audience` is the API Identifier; `serverUrl` is this\n * server's public URL. Requires DCR enabled on the tenant.\n *\n * Auth0 can't use the client's resource indicator — `audience` must be *in* the\n * authorize request. So it runs as skybridge-as-AS (`serverUrl`, see\n * {@link customProvider}) and bakes `?audience=<id>` into the advertised\n * `authorization_endpoint`. The token's `aud` is the API; `verify.issuer` stays\n * Auth0 (the token's real `iss`).\n */\nexport function auth0Provider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: { domain: string; audience: string; serverUrl: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\" | \"baseUrl\" | \"serverUrl\"\n >,\n): OAuthProvider<Auth0Claims & TCustom & RegisteredClaims> {\n const { domain, audience, ...rest } = opts;\n const provider = customProvider<Auth0Claims & TCustom>({\n issuer: toIssuerUrl(domain),\n audience,\n ...rest,\n });\n return {\n resolve: async () => {\n const config = await provider.resolve();\n const authUrl = new URL(config.oauthMetadata.authorization_endpoint);\n authUrl.searchParams.set(\"audience\", audience);\n return {\n ...config,\n oauthMetadata: {\n ...config.oauthMetadata,\n authorization_endpoint: authUrl.href,\n },\n };\n },\n };\n}\n"]}
@@ -26,7 +26,7 @@ describe("auth0Provider", () => {
26
26
  audience: "https://api.example.com/",
27
27
  serverUrl: "https://app.example.com/",
28
28
  scopes: ["openid", "profile", "email"],
29
- });
29
+ }).resolve();
30
30
  // skybridge is the advertised AS: static issuer = serverUrl (trailing slash stripped).
31
31
  expect(config.oauthMetadata.issuer).toBe("https://app.example.com");
32
32
  // AS metadata scopes match the advertised set (clients read scopes from here).
@@ -1 +1 @@
1
- {"version":3,"file":"auth0.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.test.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,IAAI,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAEzD,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAEzD,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;AAEtC,SAAS,GAAG,CAAC,MAAc;IACzB,OAAO;QACL,MAAM;QACN,sBAAsB,EAAE,GAAG,MAAM,YAAY;QAC7C,cAAc,EAAE,GAAG,MAAM,cAAc;QACvC,qBAAqB,EAAE,GAAG,MAAM,gBAAgB;QAChD,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC;QAChD,QAAQ,EAAE,GAAG,MAAM,wBAAwB;KAC5C,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,2FAA2F,EAAE,KAAK,IAAI,EAAE;QACzG,MAAM,MAAM,GAAG,2BAA2B,CAAC;QAC3C,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE;YACxC,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CACH,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC;YACjC,MAAM,EAAE,mBAAmB;YAC3B,QAAQ,EAAE,0BAA0B;YACpC,SAAS,EAAE,0BAA0B;YACrC,MAAM,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC;SACvC,CAAC,CAAC;QAEH,uFAAuF;QACvF,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QACpE,+EAA+E;QAC/E,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC;YACpD,QAAQ;YACR,SAAS;YACT,OAAO;SACR,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;QACvE,mFAAmF;QACnF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;QACjE,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,YAAY,CAAC,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QAC1E,4EAA4E;QAC5E,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,CAAC;QACvC,oEAAoE;QACpE,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { auth0Provider } from \"./auth0.js\";\nimport { lastJwksConfig as jwks } from \"./verify-spy.js\";\n\nvi.mock(\"../verify.js\", () => import(\"./verify-spy.js\"));\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction doc(issuer: string) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/authorize`,\n token_endpoint: `${issuer}/oauth/token`,\n registration_endpoint: `${issuer}/oidc/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\", \"profile\", \"email\"],\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\ndescribe(\"auth0Provider\", () => {\n it(\"advertises skybridge as the AS (static issuer) and bakes audience; verifies against Auth0\", async () => {\n const issuer = \"https://acme.us.auth0.com\";\n vi.spyOn(globalThis, \"fetch\").mockResolvedValue(\n new Response(JSON.stringify(doc(issuer)), {\n headers: { \"content-type\": \"application/json\" },\n }),\n );\n\n const config = await auth0Provider({\n domain: \"acme.us.auth0.com\",\n audience: \"https://api.example.com/\",\n serverUrl: \"https://app.example.com/\",\n scopes: [\"openid\", \"profile\", \"email\"],\n });\n\n // skybridge is the advertised AS: static issuer = serverUrl (trailing slash stripped).\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.com\");\n // AS metadata scopes match the advertised set (clients read scopes from here).\n expect(config.oauthMetadata.scopes_supported).toEqual([\n \"openid\",\n \"profile\",\n \"email\",\n ]);\n expect(config.scopesSupported).toEqual([\"openid\", \"profile\", \"email\"]);\n // audience baked into the advertised authorize endpoint (still pointing at Auth0).\n const url = new URL(config.oauthMetadata.authorization_endpoint);\n expect(url.origin + url.pathname).toBe(`${issuer}/authorize`);\n expect(url.searchParams.get(\"audience\")).toBe(\"https://api.example.com/\");\n // baseUrl left unset so the PRM resource resolves per request (Alpic-safe).\n expect(config.baseUrl).toBeUndefined();\n // token verification still uses Auth0 as issuer + the API audience.\n expect(jwks().issuer).toBe(issuer);\n expect(jwks().audience).toBe(\"https://api.example.com/\");\n });\n});\n"]}
1
+ {"version":3,"file":"auth0.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.test.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,IAAI,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAEzD,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAEzD,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;AAEtC,SAAS,GAAG,CAAC,MAAc;IACzB,OAAO;QACL,MAAM;QACN,sBAAsB,EAAE,GAAG,MAAM,YAAY;QAC7C,cAAc,EAAE,GAAG,MAAM,cAAc;QACvC,qBAAqB,EAAE,GAAG,MAAM,gBAAgB;QAChD,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC;QAChD,QAAQ,EAAE,GAAG,MAAM,wBAAwB;KAC5C,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,2FAA2F,EAAE,KAAK,IAAI,EAAE;QACzG,MAAM,MAAM,GAAG,2BAA2B,CAAC;QAC3C,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE;YACxC,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CACH,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC;YACjC,MAAM,EAAE,mBAAmB;YAC3B,QAAQ,EAAE,0BAA0B;YACpC,SAAS,EAAE,0BAA0B;YACrC,MAAM,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC;SACvC,CAAC,CAAC,OAAO,EAAE,CAAC;QAEb,uFAAuF;QACvF,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QACpE,+EAA+E;QAC/E,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC;YACpD,QAAQ;YACR,SAAS;YACT,OAAO;SACR,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;QACvE,mFAAmF;QACnF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;QACjE,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,YAAY,CAAC,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QAC1E,4EAA4E;QAC5E,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,CAAC;QACvC,oEAAoE;QACpE,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { auth0Provider } from \"./auth0.js\";\nimport { lastJwksConfig as jwks } from \"./verify-spy.js\";\n\nvi.mock(\"../verify.js\", () => import(\"./verify-spy.js\"));\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction doc(issuer: string) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/authorize`,\n token_endpoint: `${issuer}/oauth/token`,\n registration_endpoint: `${issuer}/oidc/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\", \"profile\", \"email\"],\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\ndescribe(\"auth0Provider\", () => {\n it(\"advertises skybridge as the AS (static issuer) and bakes audience; verifies against Auth0\", async () => {\n const issuer = \"https://acme.us.auth0.com\";\n vi.spyOn(globalThis, \"fetch\").mockResolvedValue(\n new Response(JSON.stringify(doc(issuer)), {\n headers: { \"content-type\": \"application/json\" },\n }),\n );\n\n const config = await auth0Provider({\n domain: \"acme.us.auth0.com\",\n audience: \"https://api.example.com/\",\n serverUrl: \"https://app.example.com/\",\n scopes: [\"openid\", \"profile\", \"email\"],\n }).resolve();\n\n // skybridge is the advertised AS: static issuer = serverUrl (trailing slash stripped).\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.com\");\n // AS metadata scopes match the advertised set (clients read scopes from here).\n expect(config.oauthMetadata.scopes_supported).toEqual([\n \"openid\",\n \"profile\",\n \"email\",\n ]);\n expect(config.scopesSupported).toEqual([\"openid\", \"profile\", \"email\"]);\n // audience baked into the advertised authorize endpoint (still pointing at Auth0).\n const url = new URL(config.oauthMetadata.authorization_endpoint);\n expect(url.origin + url.pathname).toBe(`${issuer}/authorize`);\n expect(url.searchParams.get(\"audience\")).toBe(\"https://api.example.com/\");\n // baseUrl left unset so the PRM resource resolves per request (Alpic-safe).\n expect(config.baseUrl).toBeUndefined();\n // token verification still uses Auth0 as issuer + the API audience.\n expect(jwks().issuer).toBe(issuer);\n expect(jwks().audience).toBe(\"https://api.example.com/\");\n });\n});\n"]}
@@ -1,5 +1,5 @@
1
1
  import type { ExtraClaims } from "../../auth.js";
2
- import type { OAuthConfig } from "../index.js";
2
+ import type { OAuthProvider } from "../index.js";
3
3
  import type { RegisteredClaims } from "../verify.js";
4
4
  import { type CustomProviderOptions } from "./custom.js";
5
5
  /** Options accepted by {@link authplaneProvider}. */
@@ -46,4 +46,4 @@ export type AuthplaneProviderOptions = {
46
46
  * of the authorization path. Pass `serverUrl` to advertise this server as the
47
47
  * authorization server instead (see {@link customProvider}).
48
48
  */
49
- export declare function authplaneProvider<TCustom extends ExtraClaims = Record<never, never>>(opts: AuthplaneProviderOptions): Promise<OAuthConfig<TCustom & RegisteredClaims>>;
49
+ export declare function authplaneProvider<TCustom extends ExtraClaims = Record<never, never>>(opts: AuthplaneProviderOptions): OAuthProvider<TCustom & RegisteredClaims>;
@@ -1 +1 @@
1
- {"version":3,"file":"authplane.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/authplane.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AAqCzE;;;;GAIG;AACH,SAAS,eAAe,CAAC,KAAa,EAAE,MAAc;IACpD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,mCAAmC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACzF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,6CAA6C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACnG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,+BAA+B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACrF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,uCAAuC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAC7F,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAG/B,IAA8B;IAE9B,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAErD,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAClC,MAAM,cAAc,GAAG,eAAe,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAE7D,0EAA0E;IAC1E,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,gEAAgE;IAChE,IAAI,cAAc,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,mFAAmF;YACjF,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI;YACvF,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,6GAA6G,CAC1J,CAAC;IACJ,CAAC;IAED,OAAO,cAAc,CAAU;QAC7B,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,QAAQ;QAC9B,OAAO,EAAE,QAAQ;QACjB,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ExtraClaims } from \"../../auth.js\";\nimport type { OAuthConfig } from \"../index.js\";\nimport type { RegisteredClaims } from \"../verify.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\n\n/** Options accepted by {@link authplaneProvider}. */\nexport type AuthplaneProviderOptions = {\n /**\n * The authorization server's issuer identifier (RFC 8414 §2) — your\n * Authplane deployment, e.g. `https://auth.acme.com` (or\n * `http://localhost:9000` in local development).\n */\n issuer: string;\n /**\n * This server's resource identifier (RFC 9728 §1.2): the public URL clients\n * reach, advertised as the `resource` field of its protected-resource\n * metadata. Required, unlike the other providers.\n *\n * Authplane binds the access token's `aud` to the RFC 8707 `resource`\n * parameter the client sends, and the client takes that value from the\n * advertised metadata. Setting it explicitly gives the deployment one fixed\n * identifier, which is what the audience is checked against.\n *\n * Resource identifiers are compared by exact string match, so give it in the\n * form it will be advertised and register that same string in Authplane. RFC\n * 8707 §2 asks for the most specific URI available, e.g.\n * `https://acme.example.com/mcp`.\n */\n resource: string;\n /**\n * Expected token `aud`. Defaults to `resource`.\n *\n * RFC 8707 §2 lets an authorization server use the resource identifier\n * verbatim as the audience or map it to another value; set this only for a\n * resource configured in Authplane with such an override, and pass that\n * value verbatim.\n */\n audience?: string;\n} & Omit<CustomProviderOptions, \"issuer\" | \"audience\" | \"baseUrl\">;\n\n/**\n * Rejects anything that cannot serve as an OAuth identifier: RFC 8707 §2\n * requires an absolute URI and forbids a fragment, and the discovery and\n * protected-resource metadata URLs are both built from the scheme and host.\n */\nfunction parseIdentifier(value: string, option: string): URL {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must be an absolute URL, got ${JSON.stringify(value)}`,\n );\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must use the http or https scheme, got ${JSON.stringify(value)}`,\n );\n }\n if (!parsed.host) {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must include a host, got ${JSON.stringify(value)}`,\n );\n }\n if (parsed.hash) {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must not include a fragment, got ${JSON.stringify(value)}`,\n );\n }\n return parsed;\n}\n\n/**\n * OAuth provider for Authplane. `issuer` is the authorization server's issuer\n * identifier and `resource` is this server's resource identifier, which also\n * supplies the expected token audience.\n *\n * Dynamic Client Registration is supported natively, so no registration proxy\n * is needed: clients register with Authplane directly and this server stays out\n * of the authorization path. Pass `serverUrl` to advertise this server as the\n * authorization server instead (see {@link customProvider}).\n */\nexport function authplaneProvider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: AuthplaneProviderOptions,\n): Promise<OAuthConfig<TCustom & RegisteredClaims>> {\n const { issuer, resource, audience, ...rest } = opts;\n\n parseIdentifier(issuer, \"issuer\");\n const parsedResource = parseIdentifier(resource, \"resource\");\n\n // The advertised resource is the URL-normalised form of this value. Where\n // normalisation would change the string — a bare origin gaining a root path,\n // an uppercase host, an explicit default port — the configured and advertised\n // identifiers would differ, and the audience check compares them exactly.\n // Require the advertised form up front so the two always match.\n if (parsedResource.href !== resource) {\n throw new Error(\n `authplaneProvider: \\`resource\\` must be given in the form it will be advertised. ` +\n `${JSON.stringify(resource)} is advertised as ${JSON.stringify(parsedResource.href)}. ` +\n `Use ${JSON.stringify(parsedResource.href)}, or a path-qualified URL such as \"https://acme.example.com/mcp\", and register the same value in Authplane.`,\n );\n }\n\n return customProvider<TCustom>({\n issuer,\n audience: audience ?? resource,\n baseUrl: resource,\n ...rest,\n });\n}\n"]}
1
+ {"version":3,"file":"authplane.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/authplane.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AAqCzE;;;;GAIG;AACH,SAAS,eAAe,CAAC,KAAa,EAAE,MAAc;IACpD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,mCAAmC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACzF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,6CAA6C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACnG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,+BAA+B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACrF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,uCAAuC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAC7F,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAE/B,IAA8B;IAC9B,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAErD,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAClC,MAAM,cAAc,GAAG,eAAe,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAE7D,0EAA0E;IAC1E,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,gEAAgE;IAChE,IAAI,cAAc,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,mFAAmF;YACjF,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI;YACvF,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,6GAA6G,CAC1J,CAAC;IACJ,CAAC;IAED,OAAO,cAAc,CAAU;QAC7B,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,QAAQ;QAC9B,OAAO,EAAE,QAAQ;QACjB,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\";\n\n/** Options accepted by {@link authplaneProvider}. */\nexport type AuthplaneProviderOptions = {\n /**\n * The authorization server's issuer identifier (RFC 8414 §2) — your\n * Authplane deployment, e.g. `https://auth.acme.com` (or\n * `http://localhost:9000` in local development).\n */\n issuer: string;\n /**\n * This server's resource identifier (RFC 9728 §1.2): the public URL clients\n * reach, advertised as the `resource` field of its protected-resource\n * metadata. Required, unlike the other providers.\n *\n * Authplane binds the access token's `aud` to the RFC 8707 `resource`\n * parameter the client sends, and the client takes that value from the\n * advertised metadata. Setting it explicitly gives the deployment one fixed\n * identifier, which is what the audience is checked against.\n *\n * Resource identifiers are compared by exact string match, so give it in the\n * form it will be advertised and register that same string in Authplane. RFC\n * 8707 §2 asks for the most specific URI available, e.g.\n * `https://acme.example.com/mcp`.\n */\n resource: string;\n /**\n * Expected token `aud`. Defaults to `resource`.\n *\n * RFC 8707 §2 lets an authorization server use the resource identifier\n * verbatim as the audience or map it to another value; set this only for a\n * resource configured in Authplane with such an override, and pass that\n * value verbatim.\n */\n audience?: string;\n} & Omit<CustomProviderOptions, \"issuer\" | \"audience\" | \"baseUrl\">;\n\n/**\n * Rejects anything that cannot serve as an OAuth identifier: RFC 8707 §2\n * requires an absolute URI and forbids a fragment, and the discovery and\n * protected-resource metadata URLs are both built from the scheme and host.\n */\nfunction parseIdentifier(value: string, option: string): URL {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must be an absolute URL, got ${JSON.stringify(value)}`,\n );\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must use the http or https scheme, got ${JSON.stringify(value)}`,\n );\n }\n if (!parsed.host) {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must include a host, got ${JSON.stringify(value)}`,\n );\n }\n if (parsed.hash) {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must not include a fragment, got ${JSON.stringify(value)}`,\n );\n }\n return parsed;\n}\n\n/**\n * OAuth provider for Authplane. `issuer` is the authorization server's issuer\n * identifier and `resource` is this server's resource identifier, which also\n * supplies the expected token audience.\n *\n * Dynamic Client Registration is supported natively, so no registration proxy\n * is needed: clients register with Authplane directly and this server stays out\n * of the authorization path. Pass `serverUrl` to advertise this server as the\n * authorization server instead (see {@link customProvider}).\n */\nexport function authplaneProvider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(opts: AuthplaneProviderOptions): OAuthProvider<TCustom & RegisteredClaims> {\n const { issuer, resource, audience, ...rest } = opts;\n\n parseIdentifier(issuer, \"issuer\");\n const parsedResource = parseIdentifier(resource, \"resource\");\n\n // The advertised resource is the URL-normalised form of this value. Where\n // normalisation would change the string — a bare origin gaining a root path,\n // an uppercase host, an explicit default port — the configured and advertised\n // identifiers would differ, and the audience check compares them exactly.\n // Require the advertised form up front so the two always match.\n if (parsedResource.href !== resource) {\n throw new Error(\n `authplaneProvider: \\`resource\\` must be given in the form it will be advertised. ` +\n `${JSON.stringify(resource)} is advertised as ${JSON.stringify(parsedResource.href)}. ` +\n `Use ${JSON.stringify(parsedResource.href)}, or a path-qualified URL such as \"https://acme.example.com/mcp\", and register the same value in Authplane.`,\n );\n }\n\n return customProvider<TCustom>({\n issuer,\n audience: audience ?? resource,\n baseUrl: resource,\n ...rest,\n });\n}\n"]}