skybridge 0.0.0-next.45dfd29 → 0.0.0-next.4d31386

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 (61) hide show
  1. package/dist/server/auth/index.d.ts +30 -4
  2. package/dist/server/auth/index.js.map +1 -1
  3. package/dist/server/auth/providers/auth0.d.ts +24 -2
  4. package/dist/server/auth/providers/auth0.js.map +1 -1
  5. package/dist/server/auth/providers/auth0.test.js +4 -2
  6. package/dist/server/auth/providers/auth0.test.js.map +1 -1
  7. package/dist/server/auth/providers/authplane.d.ts +3 -1
  8. package/dist/server/auth/providers/authplane.js.map +1 -1
  9. package/dist/server/auth/providers/authplane.test.js +13 -11
  10. package/dist/server/auth/providers/authplane.test.js.map +1 -1
  11. package/dist/server/auth/providers/clerk.d.ts +39 -2
  12. package/dist/server/auth/providers/clerk.js +4 -1
  13. package/dist/server/auth/providers/clerk.js.map +1 -1
  14. package/dist/server/auth/providers/clerk.test.js +5 -3
  15. package/dist/server/auth/providers/clerk.test.js.map +1 -1
  16. package/dist/server/auth/providers/custom.d.ts +10 -2
  17. package/dist/server/auth/providers/custom.js +10 -3
  18. package/dist/server/auth/providers/custom.js.map +1 -1
  19. package/dist/server/auth/providers/custom.test.js +11 -9
  20. package/dist/server/auth/providers/custom.test.js.map +1 -1
  21. package/dist/server/auth/providers/descope.d.ts +31 -2
  22. package/dist/server/auth/providers/descope.js.map +1 -1
  23. package/dist/server/auth/providers/descope.test.js +6 -4
  24. package/dist/server/auth/providers/descope.test.js.map +1 -1
  25. package/dist/server/auth/providers/stytch.d.ts +15 -2
  26. package/dist/server/auth/providers/stytch.js +4 -1
  27. package/dist/server/auth/providers/stytch.js.map +1 -1
  28. package/dist/server/auth/providers/verify-spy.d.ts +12 -0
  29. package/dist/server/auth/providers/verify-spy.js +21 -0
  30. package/dist/server/auth/providers/verify-spy.js.map +1 -0
  31. package/dist/server/auth/providers/workos.d.ts +24 -2
  32. package/dist/server/auth/providers/workos.js +4 -1
  33. package/dist/server/auth/providers/workos.js.map +1 -1
  34. package/dist/server/auth/setup.d.ts +1 -1
  35. package/dist/server/auth/setup.js +3 -3
  36. package/dist/server/auth/setup.js.map +1 -1
  37. package/dist/server/auth/setup.test.js +39 -5
  38. package/dist/server/auth/setup.test.js.map +1 -1
  39. package/dist/server/auth/verify.d.ts +37 -3
  40. package/dist/server/auth/verify.js +26 -2
  41. package/dist/server/auth/verify.js.map +1 -1
  42. package/dist/server/auth/verify.test.js +49 -0
  43. package/dist/server/auth/verify.test.js.map +1 -1
  44. package/dist/server/auth-extra.test-d.d.ts +1 -0
  45. package/dist/server/auth-extra.test-d.js +82 -0
  46. package/dist/server/auth-extra.test-d.js.map +1 -0
  47. package/dist/server/auth.d.ts +39 -1
  48. package/dist/server/auth.js.map +1 -1
  49. package/dist/server/index.d.ts +8 -6
  50. package/dist/server/index.js +3 -1
  51. package/dist/server/index.js.map +1 -1
  52. package/dist/server/middleware.d.ts +31 -6
  53. package/dist/server/middleware.js +33 -0
  54. package/dist/server/middleware.js.map +1 -1
  55. package/dist/server/middleware.test.js +37 -1
  56. package/dist/server/middleware.test.js.map +1 -1
  57. package/dist/server/server.d.ts +26 -16
  58. package/dist/server/server.js +12 -4
  59. package/dist/server/server.js.map +1 -1
  60. package/dist/test/utils.d.ts +3 -3
  61. package/package.json +1 -1
@@ -2,6 +2,8 @@
2
2
  import http from "node:http";
3
3
  import { afterEach, describe, expect, it, vi } from "vitest";
4
4
  import { customProvider } from "./custom.js";
5
+ import { lastJwksConfig as jwks } from "./verify-spy.js";
6
+ vi.mock("../verify.js", () => import("./verify-spy.js"));
5
7
  const servers = [];
6
8
  afterEach(() => {
7
9
  for (const srv of servers.splice(0)) {
@@ -46,7 +48,7 @@ describe("customProvider", () => {
46
48
  scopes: ["openid"],
47
49
  });
48
50
  expect(config.baseUrl).toBe("https://app.example.test");
49
- expect(config.verify).toEqual({
51
+ expect(jwks()).toEqual({
50
52
  issuer: base,
51
53
  audience: "my-api",
52
54
  jwksUri: `${base}/jwks`,
@@ -61,23 +63,23 @@ describe("customProvider", () => {
61
63
  audience: "a",
62
64
  metadataOverrides: { issuer: "https://evil.test" },
63
65
  });
64
- expect(config.verify.issuer).toBe(base);
66
+ expect(jwks().issuer).toBe(base);
65
67
  expect(config.oauthMetadata.issuer).toBe(base);
66
68
  });
67
69
  it("ignores a runtime jwks_uri override (keeps the discovered signing keys)", async () => {
68
70
  const base = await serveDiscovery();
69
- const config = await customProvider({
71
+ await customProvider({
70
72
  issuer: base,
71
73
  audience: "a",
72
74
  metadataOverrides: { jwks_uri: "https://evil.test/keys" },
73
75
  });
74
- expect(config.verify.jwksUri).toBe(`${base}/jwks`);
76
+ expect(jwks().jwksUri).toBe(`${base}/jwks`);
75
77
  });
76
78
  it("serves metadata without a registration_endpoint when the IdP has no DCR", async () => {
77
79
  const base = await serveDiscovery({ registration_endpoint: undefined });
78
80
  const config = await customProvider({ issuer: base, audience: "a" });
79
81
  expect(config.oauthMetadata.registration_endpoint).toBeUndefined();
80
- expect(config.verify.issuer).toBe(base);
82
+ expect(jwks().issuer).toBe(base);
81
83
  });
82
84
  it("throws when discovery has no jwks_uri (token verification needs it)", async () => {
83
85
  const base = await serveDiscovery({ jwks_uri: undefined });
@@ -95,7 +97,7 @@ describe("customProvider", () => {
95
97
  expect(config.oauthMetadata.issuer).toBe("https://app.example.test");
96
98
  expect(config.oauthMetadata.scopes_supported).toEqual(["openid"]);
97
99
  // but the token's issuer is still verified against the IdP.
98
- expect(config.verify.issuer).toBe(base);
100
+ expect(jwks().issuer).toBe(base);
99
101
  });
100
102
  it("authorizationServer supplies the advertised metadata and the verified issuer", async () => {
101
103
  const base = await serveDiscovery();
@@ -109,7 +111,7 @@ describe("customProvider", () => {
109
111
  expect(config.oauthMetadata.authorization_endpoint).toBe(`${as}/authorize`);
110
112
  expect(config.oauthMetadata.registration_endpoint).toBe(`${as}/register`);
111
113
  expect(config.scopesSupported).toEqual(["checkout"]);
112
- expect(config.verify).toEqual({
114
+ expect(jwks()).toEqual({
113
115
  issuer: as,
114
116
  audience: "a",
115
117
  jwksUri: `${as}/jwks`,
@@ -126,7 +128,7 @@ describe("customProvider", () => {
126
128
  expect(warn).toHaveBeenCalledOnce();
127
129
  expect(config.oauthMetadata.issuer).toBe(`${base}/agentic/P1/MS1`);
128
130
  expect(config.oauthMetadata.authorization_endpoint).toBe(`${base}/authorize`);
129
- expect(config.verify.issuer).toBe(base);
131
+ expect(jwks().issuer).toBe(base);
130
132
  warn.mockRestore();
131
133
  });
132
134
  it("serverUrl takes precedence over authorizationServer", async () => {
@@ -140,7 +142,7 @@ describe("customProvider", () => {
140
142
  });
141
143
  expect(config.oauthMetadata.issuer).toBe("https://app.example.test");
142
144
  expect(config.oauthMetadata.token_endpoint).toBe(`${as}/token`);
143
- expect(config.verify.issuer).toBe(as);
145
+ expect(jwks().issuer).toBe(as);
144
146
  });
145
147
  it("applies metadataOverrides over discovered values", async () => {
146
148
  const base = await serveDiscovery();
@@ -1 +1 @@
1
- {"version":3,"file":"custom.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { customProvider } from \"./custom.js\";\n\nconst servers: http.Server[] = [];\nafterEach(() => {\n for (const srv of servers.splice(0)) {\n srv.close();\n }\n});\n\nfunction doc(origin: string, extra: Record<string, unknown> = {}) {\n return {\n issuer: origin,\n authorization_endpoint: `${origin}/authorize`,\n token_endpoint: `${origin}/token`,\n registration_endpoint: `${origin}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\", \"email\", \"profile\"],\n jwks_uri: `${origin}/jwks`,\n ...extra,\n };\n}\n\n// Serves the discovery doc (built from the live origin) at the OIDC well-known path.\nasync function serveDiscovery(extra: Record<string, unknown> = {}) {\n let origin = \"\";\n const srv = http.createServer((req, res) => {\n if (req.url !== \"/.well-known/openid-configuration\") {\n res.writeHead(404).end();\n return;\n }\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(doc(origin, extra)));\n });\n await new Promise<void>((resolve) => srv.listen(0, resolve));\n servers.push(srv);\n origin = `http://localhost:${(srv.address() as { port: number }).port}`;\n return origin;\n}\n\ndescribe(\"customProvider\", () => {\n it(\"builds an OAuthConfig from discovery\", async () => {\n const base = await serveDiscovery();\n\n const config = await customProvider({\n issuer: base,\n audience: \"my-api\",\n baseUrl: \"https://app.example.test\",\n scopes: [\"openid\"],\n });\n\n expect(config.baseUrl).toBe(\"https://app.example.test\");\n expect(config.verify).toEqual({\n issuer: base,\n audience: \"my-api\",\n jwksUri: `${base}/jwks`,\n });\n expect(config.scopesSupported).toEqual([\"openid\"]);\n expect(config.oauthMetadata.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"ignores a runtime issuer override (keeps the discovered trust anchor)\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n metadataOverrides: { issuer: \"https://evil.test\" } as never,\n });\n expect(config.verify.issuer).toBe(base);\n expect(config.oauthMetadata.issuer).toBe(base);\n });\n\n it(\"ignores a runtime jwks_uri override (keeps the discovered signing keys)\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n metadataOverrides: { jwks_uri: \"https://evil.test/keys\" } as never,\n });\n expect(config.verify.jwksUri).toBe(`${base}/jwks`);\n });\n\n it(\"serves metadata without a registration_endpoint when the IdP has no DCR\", async () => {\n const base = await serveDiscovery({ registration_endpoint: undefined });\n const config = await customProvider({ issuer: base, audience: \"a\" });\n expect(config.oauthMetadata.registration_endpoint).toBeUndefined();\n expect(config.verify.issuer).toBe(base);\n });\n\n it(\"throws when discovery has no jwks_uri (token verification needs it)\", async () => {\n const base = await serveDiscovery({ jwks_uri: undefined });\n await expect(\n customProvider({ issuer: base, audience: \"a\" }),\n ).rejects.toThrow(/jwks_uri/);\n });\n\n it(\"serverUrl advertises this server as the AS, keeping the IdP as the token issuer\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n serverUrl: \"https://app.example.test/\",\n scopes: [\"openid\"],\n });\n // AS metadata + advertised scopes reflect this server.\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.test\");\n expect(config.oauthMetadata.scopes_supported).toEqual([\"openid\"]);\n // but the token's issuer is still verified against the IdP.\n expect(config.verify.issuer).toBe(base);\n });\n\n it(\"authorizationServer supplies the advertised metadata and the verified issuer\", async () => {\n const base = await serveDiscovery();\n const as = await serveDiscovery({ scopes_supported: [\"checkout\"] });\n\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n authorizationServer: `${as}/`,\n });\n\n expect(config.oauthMetadata.issuer).toBe(as);\n expect(config.oauthMetadata.authorization_endpoint).toBe(`${as}/authorize`);\n expect(config.oauthMetadata.registration_endpoint).toBe(`${as}/register`);\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n expect(config.verify).toEqual({\n issuer: as,\n audience: \"a\",\n jwksUri: `${as}/jwks`,\n });\n });\n\n it(\"falls back to the issuer's metadata when the authorizationServer is undiscoverable\", async () => {\n const base = await serveDiscovery();\n const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n authorizationServer: `${base}/agentic/P1/MS1`,\n });\n\n expect(warn).toHaveBeenCalledOnce();\n expect(config.oauthMetadata.issuer).toBe(`${base}/agentic/P1/MS1`);\n expect(config.oauthMetadata.authorization_endpoint).toBe(\n `${base}/authorize`,\n );\n expect(config.verify.issuer).toBe(base);\n warn.mockRestore();\n });\n\n it(\"serverUrl takes precedence over authorizationServer\", async () => {\n const base = await serveDiscovery();\n const as = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n serverUrl: \"https://app.example.test/\",\n authorizationServer: as,\n });\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.test\");\n expect(config.oauthMetadata.token_endpoint).toBe(`${as}/token`);\n expect(config.verify.issuer).toBe(as);\n });\n\n it(\"applies metadataOverrides over discovered values\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n baseUrl: \"https://app.example.test\",\n metadataOverrides: { token_endpoint: \"https://override/token\" },\n });\n expect(config.oauthMetadata.token_endpoint).toBe(\"https://override/token\");\n });\n});\n"]}
1
+ {"version":3,"file":"custom.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { customProvider } from \"./custom.js\";\nimport { lastJwksConfig as jwks } from \"./verify-spy.js\";\n\nvi.mock(\"../verify.js\", () => import(\"./verify-spy.js\"));\n\nconst servers: http.Server[] = [];\nafterEach(() => {\n for (const srv of servers.splice(0)) {\n srv.close();\n }\n});\n\nfunction doc(origin: string, extra: Record<string, unknown> = {}) {\n return {\n issuer: origin,\n authorization_endpoint: `${origin}/authorize`,\n token_endpoint: `${origin}/token`,\n registration_endpoint: `${origin}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\", \"email\", \"profile\"],\n jwks_uri: `${origin}/jwks`,\n ...extra,\n };\n}\n\n// Serves the discovery doc (built from the live origin) at the OIDC well-known path.\nasync function serveDiscovery(extra: Record<string, unknown> = {}) {\n let origin = \"\";\n const srv = http.createServer((req, res) => {\n if (req.url !== \"/.well-known/openid-configuration\") {\n res.writeHead(404).end();\n return;\n }\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(doc(origin, extra)));\n });\n await new Promise<void>((resolve) => srv.listen(0, resolve));\n servers.push(srv);\n origin = `http://localhost:${(srv.address() as { port: number }).port}`;\n return origin;\n}\n\ndescribe(\"customProvider\", () => {\n it(\"builds an OAuthConfig from discovery\", async () => {\n const base = await serveDiscovery();\n\n const config = await customProvider({\n issuer: base,\n audience: \"my-api\",\n baseUrl: \"https://app.example.test\",\n scopes: [\"openid\"],\n });\n\n expect(config.baseUrl).toBe(\"https://app.example.test\");\n expect(jwks()).toEqual({\n issuer: base,\n audience: \"my-api\",\n jwksUri: `${base}/jwks`,\n });\n expect(config.scopesSupported).toEqual([\"openid\"]);\n expect(config.oauthMetadata.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"ignores a runtime issuer override (keeps the discovered trust anchor)\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n metadataOverrides: { issuer: \"https://evil.test\" } as never,\n });\n expect(jwks().issuer).toBe(base);\n expect(config.oauthMetadata.issuer).toBe(base);\n });\n\n it(\"ignores a runtime jwks_uri override (keeps the discovered signing keys)\", async () => {\n const base = await serveDiscovery();\n await customProvider({\n issuer: base,\n audience: \"a\",\n metadataOverrides: { jwks_uri: \"https://evil.test/keys\" } as never,\n });\n expect(jwks().jwksUri).toBe(`${base}/jwks`);\n });\n\n it(\"serves metadata without a registration_endpoint when the IdP has no DCR\", async () => {\n const base = await serveDiscovery({ registration_endpoint: undefined });\n const config = await customProvider({ issuer: base, audience: \"a\" });\n expect(config.oauthMetadata.registration_endpoint).toBeUndefined();\n expect(jwks().issuer).toBe(base);\n });\n\n it(\"throws when discovery has no jwks_uri (token verification needs it)\", async () => {\n const base = await serveDiscovery({ jwks_uri: undefined });\n await expect(\n customProvider({ issuer: base, audience: \"a\" }),\n ).rejects.toThrow(/jwks_uri/);\n });\n\n it(\"serverUrl advertises this server as the AS, keeping the IdP as the token issuer\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n serverUrl: \"https://app.example.test/\",\n scopes: [\"openid\"],\n });\n // AS metadata + advertised scopes reflect this server.\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.test\");\n expect(config.oauthMetadata.scopes_supported).toEqual([\"openid\"]);\n // but the token's issuer is still verified against the IdP.\n expect(jwks().issuer).toBe(base);\n });\n\n it(\"authorizationServer supplies the advertised metadata and the verified issuer\", async () => {\n const base = await serveDiscovery();\n const as = await serveDiscovery({ scopes_supported: [\"checkout\"] });\n\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n authorizationServer: `${as}/`,\n });\n\n expect(config.oauthMetadata.issuer).toBe(as);\n expect(config.oauthMetadata.authorization_endpoint).toBe(`${as}/authorize`);\n expect(config.oauthMetadata.registration_endpoint).toBe(`${as}/register`);\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n expect(jwks()).toEqual({\n issuer: as,\n audience: \"a\",\n jwksUri: `${as}/jwks`,\n });\n });\n\n it(\"falls back to the issuer's metadata when the authorizationServer is undiscoverable\", async () => {\n const base = await serveDiscovery();\n const warn = vi.spyOn(console, \"warn\").mockImplementation(() => {});\n\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n authorizationServer: `${base}/agentic/P1/MS1`,\n });\n\n expect(warn).toHaveBeenCalledOnce();\n expect(config.oauthMetadata.issuer).toBe(`${base}/agentic/P1/MS1`);\n expect(config.oauthMetadata.authorization_endpoint).toBe(\n `${base}/authorize`,\n );\n expect(jwks().issuer).toBe(base);\n warn.mockRestore();\n });\n\n it(\"serverUrl takes precedence over authorizationServer\", async () => {\n const base = await serveDiscovery();\n const as = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n serverUrl: \"https://app.example.test/\",\n authorizationServer: as,\n });\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.test\");\n expect(config.oauthMetadata.token_endpoint).toBe(`${as}/token`);\n expect(jwks().issuer).toBe(as);\n });\n\n it(\"applies metadataOverrides over discovered values\", async () => {\n const base = await serveDiscovery();\n const config = await customProvider({\n issuer: base,\n audience: \"a\",\n baseUrl: \"https://app.example.test\",\n metadataOverrides: { token_endpoint: \"https://override/token\" },\n });\n expect(config.oauthMetadata.token_endpoint).toBe(\"https://override/token\");\n });\n});\n"]}
@@ -1,5 +1,34 @@
1
+ import type { ExtraClaims } from "../../auth.js";
1
2
  import type { OAuthConfig } from "../index.js";
3
+ import type { RegisteredClaims } from "../verify.js";
2
4
  import { type CustomProviderOptions } from "./custom.js";
5
+ /**
6
+ * Claims a Descope session JWT carries. Roles and permissions are nested per
7
+ * tenant when the project uses tenants, and flat otherwise. `email` arrives only
8
+ * through a Custom Claims flow action or a JWT template.
9
+ *
10
+ * @see https://docs.descope.com/authorization/session-management
11
+ * @see https://docs.descope.com/management/token/jwt-templates
12
+ */
13
+ export type DescopeClaims = {
14
+ /** Authentication methods used, e.g. `["otp"]`. */
15
+ amr?: string[];
16
+ /** Descope Resource Name: where the token is stored. */
17
+ drn?: string;
18
+ /** The user's active tenant. */
19
+ dct?: string;
20
+ /** Associated tenants, keyed by tenant id. */
21
+ tenants?: Record<string, {
22
+ roles?: string[];
23
+ permissions?: string[];
24
+ }>;
25
+ /** Roles, when the project has no tenants. */
26
+ roles?: string[];
27
+ /** Permissions, when the project has no tenants. */
28
+ permissions?: string[];
29
+ /** Only via a Custom Claims action or JWT template; not a default claim. */
30
+ email?: string;
31
+ };
3
32
  /**
4
33
  * OAuth provider for a Descope MCP Server. `url` is the MCP Server's **Discovery
5
34
  * URL** (a.k.a. Issuer) from the console's Connection Information, e.g.
@@ -10,6 +39,6 @@ import { type CustomProviderOptions } from "./custom.js";
10
39
  * the **Project ID** derived from the URL — Descope binds `aud` to [DCR client
11
40
  * id, project id], not the server URL; pass `audience` to override.
12
41
  */
13
- export declare function descopeProvider(opts: {
42
+ export declare function descopeProvider<TCustom extends ExtraClaims = Record<never, never>>(opts: {
14
43
  url: string;
15
- } & Omit<CustomProviderOptions, "issuer">): Promise<OAuthConfig>;
44
+ } & Omit<CustomProviderOptions, "issuer">): Promise<OAuthConfig<DescopeClaims & TCustom & RegisteredClaims>>;
@@ -1 +1 @@
1
- {"version":3,"file":"descope.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AAEzE;;;;GAIG;AACH,SAAS,wBAAwB,CAAC,YAAoB;IACpD,OAAO,YAAY,CAAC,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,iDAAiD,GAAG,mCAAmC,CACxF,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAC7B,IAA6D;IAE7D,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACxC,MAAM,KAAK,GAAG,wBAAwB,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC;IAC5E,OAAO,cAAc,CAAC;QACpB,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,SAAS;QAC/B,mBAAmB,EAAE,KAAK;QAC1B,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\n\n/**\n * Turns the console's Discovery URL into the authorization server's base URL:\n * drops a `/.well-known/openid-configuration` (or `oauth-authorization-server`)\n * suffix, since discovery appends that path itself, and any trailing slash.\n */\nfunction toAuthorizationServerUrl(discoveryUrl: string): string {\n return discoveryUrl.replace(/\\/\\.well-known\\/[^?#]*$/, \"\").replace(/\\/$/, \"\");\n}\n\n/**\n * Derives the Descope Project ID from an MCP Server URL\n * (`…/agentic/<projectId>/<mcpServerId>`). Descope binds the token `aud` to the\n * project id, so it doubles as the audience.\n */\nfunction projectIdFromUrl(url: string): string {\n const projectId = url.match(/\\/agentic\\/([^/]+)\\/[^/]+/)?.[1];\n if (!projectId) {\n throw new Error(\n `Could not derive the Descope project id from \"${url}\"; pass an explicit \\`audience\\`.`,\n );\n }\n return projectId;\n}\n\n/**\n * OAuth provider for a Descope MCP Server. `url` is the MCP Server's **Discovery\n * URL** (a.k.a. Issuer) from the console's Connection Information, e.g.\n * `https://api.descope.com/v1/apps/agentic/<projectId>/<mcpServerId>` (or your\n * custom domain). Requires DCR enabled on the MCP Server. For Descope with DCR\n * disabled and Alpic's DCR proxy, use {@link customProvider} with `serverUrl`\n * instead (see `examples/auth-descope-alpic`). The token `audience` defaults to\n * the **Project ID** derived from the URL — Descope binds `aud` to [DCR client\n * id, project id], not the server URL; pass `audience` to override.\n */\nexport function descopeProvider(\n opts: { url: string } & Omit<CustomProviderOptions, \"issuer\">,\n): Promise<OAuthConfig> {\n const { url, audience, ...rest } = opts;\n const asUrl = toAuthorizationServerUrl(url);\n const projectId = projectIdFromUrl(asUrl);\n const issuer = `${asUrl.slice(0, asUrl.indexOf(\"/agentic/\"))}/${projectId}`;\n return customProvider({\n issuer,\n audience: audience ?? projectId,\n authorizationServer: asUrl,\n ...rest,\n });\n}\n"]}
1
+ {"version":3,"file":"descope.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AA2BzE;;;;GAIG;AACH,SAAS,wBAAwB,CAAC,YAAoB;IACpD,OAAO,YAAY,CAAC,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAChF,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,2BAA2B,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,iDAAiD,GAAG,mCAAmC,CACxF,CAAC;IACJ,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,eAAe,CAG7B,IAA6D;IAE7D,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACxC,MAAM,KAAK,GAAG,wBAAwB,CAAC,GAAG,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,IAAI,SAAS,EAAE,CAAC;IAC5E,OAAO,cAAc,CAA0B;QAC7C,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,SAAS;QAC/B,mBAAmB,EAAE,KAAK;QAC1B,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/**\n * Claims a Descope session JWT carries. Roles and permissions are nested per\n * tenant when the project uses tenants, and flat otherwise. `email` arrives only\n * through a Custom Claims flow action or a JWT template.\n *\n * @see https://docs.descope.com/authorization/session-management\n * @see https://docs.descope.com/management/token/jwt-templates\n */\nexport type DescopeClaims = {\n /** Authentication methods used, e.g. `[\"otp\"]`. */\n amr?: string[];\n /** Descope Resource Name: where the token is stored. */\n drn?: string;\n /** The user's active tenant. */\n dct?: string;\n /** Associated tenants, keyed by tenant id. */\n tenants?: Record<string, { roles?: string[]; permissions?: string[] }>;\n /** Roles, when the project has no tenants. */\n roles?: string[];\n /** Permissions, when the project has no tenants. */\n permissions?: string[];\n /** Only via a Custom Claims action or JWT template; not a default claim. */\n email?: string;\n};\n\n/**\n * Turns the console's Discovery URL into the authorization server's base URL:\n * drops a `/.well-known/openid-configuration` (or `oauth-authorization-server`)\n * suffix, since discovery appends that path itself, and any trailing slash.\n */\nfunction toAuthorizationServerUrl(discoveryUrl: string): string {\n return discoveryUrl.replace(/\\/\\.well-known\\/[^?#]*$/, \"\").replace(/\\/$/, \"\");\n}\n\n/**\n * Derives the Descope Project ID from an MCP Server URL\n * (`…/agentic/<projectId>/<mcpServerId>`). Descope binds the token `aud` to the\n * project id, so it doubles as the audience.\n */\nfunction projectIdFromUrl(url: string): string {\n const projectId = url.match(/\\/agentic\\/([^/]+)\\/[^/]+/)?.[1];\n if (!projectId) {\n throw new Error(\n `Could not derive the Descope project id from \"${url}\"; pass an explicit \\`audience\\`.`,\n );\n }\n return projectId;\n}\n\n/**\n * OAuth provider for a Descope MCP Server. `url` is the MCP Server's **Discovery\n * URL** (a.k.a. Issuer) from the console's Connection Information, e.g.\n * `https://api.descope.com/v1/apps/agentic/<projectId>/<mcpServerId>` (or your\n * custom domain). Requires DCR enabled on the MCP Server. For Descope with DCR\n * disabled and Alpic's DCR proxy, use {@link customProvider} with `serverUrl`\n * instead (see `examples/auth-descope-alpic`). The token `audience` defaults to\n * the **Project ID** derived from the URL — Descope binds `aud` to [DCR client\n * id, project id], not the server URL; pass `audience` to override.\n */\nexport function descopeProvider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: { url: string } & Omit<CustomProviderOptions, \"issuer\">,\n): Promise<OAuthConfig<DescopeClaims & TCustom & RegisteredClaims>> {\n const { url, audience, ...rest } = opts;\n const asUrl = toAuthorizationServerUrl(url);\n const projectId = projectIdFromUrl(asUrl);\n const issuer = `${asUrl.slice(0, asUrl.indexOf(\"/agentic/\"))}/${projectId}`;\n return customProvider<DescopeClaims & TCustom>({\n issuer,\n audience: audience ?? projectId,\n authorizationServer: asUrl,\n ...rest,\n });\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  // @vitest-environment node
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  import { descopeProvider } from "./descope.js";
4
+ import { lastJwksConfig as jwks } from "./verify-spy.js";
5
+ vi.mock("../verify.js", () => import("./verify-spy.js"));
4
6
  afterEach(() => vi.restoreAllMocks());
5
7
  function discoveryDoc(issuer, scopes) {
6
8
  return {
@@ -37,8 +39,8 @@ describe("descopeProvider", () => {
37
39
  });
38
40
  const config = await descopeProvider({ url: AGENTIC });
39
41
  expect(fetchSpy).toHaveBeenCalledWith(`${PROJECT}/.well-known/openid-configuration`, expect.anything());
40
- expect(config.verify.issuer).toBe(AGENTIC);
41
- expect(config.verify.audience).toBe("P123");
42
+ expect(jwks().issuer).toBe(AGENTIC);
43
+ expect(jwks().audience).toBe("P123");
42
44
  expect(config.oauthMetadata.issuer).toBe(AGENTIC);
43
45
  expect(config.oauthMetadata.registration_endpoint).toBe(`${AGENTIC}/register`);
44
46
  expect(config.scopesSupported).toEqual(["checkout"]);
@@ -53,8 +55,8 @@ describe("descopeProvider", () => {
53
55
  });
54
56
  it("lets an explicit audience override the derived project id", async () => {
55
57
  mockDiscovery({ [PROJECT]: ["openid"], [AGENTIC]: ["checkout"] });
56
- const config = await descopeProvider({ url: AGENTIC, audience: "custom" });
57
- expect(config.verify.audience).toBe("custom");
58
+ await descopeProvider({ url: AGENTIC, audience: "custom" });
59
+ expect(jwks().audience).toBe("custom");
58
60
  });
59
61
  });
60
62
  //# sourceMappingURL=descope.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"descope.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { descopeProvider } from \"./descope.js\";\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction discoveryDoc(issuer: string, scopes: string[]) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/authorize`,\n token_endpoint: `${issuer}/token`,\n registration_endpoint: `${issuer}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: scopes,\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\nconst PROJECT = \"https://api.descope.com/v1/apps/P123\";\nconst AGENTIC = \"https://api.descope.com/v1/apps/agentic/P123/MS456\";\n\nfunction mockDiscovery(docs: Record<string, string[]>) {\n return vi\n .spyOn(globalThis, \"fetch\")\n .mockImplementation(async (input: RequestInfo | URL) => {\n const url = String(input);\n const entry = Object.entries(docs).find(\n ([issuer]) => url === `${issuer}/.well-known/openid-configuration`,\n );\n if (entry === undefined) {\n return new Response(null, { status: 404 });\n }\n return new Response(JSON.stringify(discoveryDoc(...entry)), {\n headers: { \"content-type\": \"application/json\" },\n });\n });\n}\n\ndescribe(\"descopeProvider\", () => {\n it(\"verifies against the agentic issuer it advertises\", async () => {\n const fetchSpy = mockDiscovery({\n [PROJECT]: [\"openid\", \"profile\", \"email\", \"phone\"],\n [AGENTIC]: [\"checkout\"],\n });\n\n const config = await descopeProvider({ url: AGENTIC });\n\n expect(fetchSpy).toHaveBeenCalledWith(\n `${PROJECT}/.well-known/openid-configuration`,\n expect.anything(),\n );\n expect(config.verify.issuer).toBe(AGENTIC);\n expect(config.verify.audience).toBe(\"P123\");\n expect(config.oauthMetadata.issuer).toBe(AGENTIC);\n expect(config.oauthMetadata.registration_endpoint).toBe(\n `${AGENTIC}/register`,\n );\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n });\n\n it(\"accepts a URL carrying the well-known suffix\", async () => {\n mockDiscovery({ [PROJECT]: [\"openid\"], [AGENTIC]: [\"checkout\"] });\n\n const config = await descopeProvider({\n url: `${AGENTIC}/.well-known/openid-configuration`,\n });\n\n expect(config.oauthMetadata.issuer).toBe(AGENTIC);\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n });\n\n it(\"lets an explicit audience override the derived project id\", async () => {\n mockDiscovery({ [PROJECT]: [\"openid\"], [AGENTIC]: [\"checkout\"] });\n\n const config = await descopeProvider({ url: AGENTIC, audience: \"custom\" });\n\n expect(config.verify.audience).toBe(\"custom\");\n });\n});\n"]}
1
+ {"version":3,"file":"descope.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/descope.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { descopeProvider } from \"./descope.js\";\nimport { lastJwksConfig as jwks } from \"./verify-spy.js\";\n\nvi.mock(\"../verify.js\", () => import(\"./verify-spy.js\"));\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction discoveryDoc(issuer: string, scopes: string[]) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/authorize`,\n token_endpoint: `${issuer}/token`,\n registration_endpoint: `${issuer}/register`,\n response_types_supported: [\"code\"],\n scopes_supported: scopes,\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\nconst PROJECT = \"https://api.descope.com/v1/apps/P123\";\nconst AGENTIC = \"https://api.descope.com/v1/apps/agentic/P123/MS456\";\n\nfunction mockDiscovery(docs: Record<string, string[]>) {\n return vi\n .spyOn(globalThis, \"fetch\")\n .mockImplementation(async (input: RequestInfo | URL) => {\n const url = String(input);\n const entry = Object.entries(docs).find(\n ([issuer]) => url === `${issuer}/.well-known/openid-configuration`,\n );\n if (entry === undefined) {\n return new Response(null, { status: 404 });\n }\n return new Response(JSON.stringify(discoveryDoc(...entry)), {\n headers: { \"content-type\": \"application/json\" },\n });\n });\n}\n\ndescribe(\"descopeProvider\", () => {\n it(\"verifies against the agentic issuer it advertises\", async () => {\n const fetchSpy = mockDiscovery({\n [PROJECT]: [\"openid\", \"profile\", \"email\", \"phone\"],\n [AGENTIC]: [\"checkout\"],\n });\n\n const config = await descopeProvider({ url: AGENTIC });\n\n expect(fetchSpy).toHaveBeenCalledWith(\n `${PROJECT}/.well-known/openid-configuration`,\n expect.anything(),\n );\n expect(jwks().issuer).toBe(AGENTIC);\n expect(jwks().audience).toBe(\"P123\");\n expect(config.oauthMetadata.issuer).toBe(AGENTIC);\n expect(config.oauthMetadata.registration_endpoint).toBe(\n `${AGENTIC}/register`,\n );\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n });\n\n it(\"accepts a URL carrying the well-known suffix\", async () => {\n mockDiscovery({ [PROJECT]: [\"openid\"], [AGENTIC]: [\"checkout\"] });\n\n const config = await descopeProvider({\n url: `${AGENTIC}/.well-known/openid-configuration`,\n });\n\n expect(config.oauthMetadata.issuer).toBe(AGENTIC);\n expect(config.scopesSupported).toEqual([\"checkout\"]);\n });\n\n it(\"lets an explicit audience override the derived project id\", async () => {\n mockDiscovery({ [PROJECT]: [\"openid\"], [AGENTIC]: [\"checkout\"] });\n\n await descopeProvider({ url: AGENTIC, audience: \"custom\" });\n\n expect(jwks().audience).toBe(\"custom\");\n });\n});\n"]}
@@ -1,12 +1,25 @@
1
+ import type { ExtraClaims } from "../../auth.js";
1
2
  import type { OAuthConfig } from "../index.js";
3
+ import type { RegisteredClaims } from "../verify.js";
2
4
  import { type CustomProviderOptions } from "./custom.js";
5
+ /**
6
+ * Claims a Stytch Connected Apps access token carries. Beyond the subject,
7
+ * everything is project-configured through the client's access-token template.
8
+ *
9
+ * @see https://stytch.com/docs/api-reference/consumer/api/connected-apps/tokens/connected-app-access-token-object
10
+ * @see https://stytch.com/docs/api/connected-apps-create
11
+ */
12
+ export type StytchClaims = {
13
+ /** Only when the client's access-token template adds it. */
14
+ email?: string;
15
+ };
3
16
  /**
4
17
  * OAuth provider for Stytch Connected Apps. `domain` is the project domain,
5
18
  * e.g. `acme.customers.stytch.dev`, or a configured custom domain. Requires
6
19
  * DCR enabled in the Stytch dashboard. `audience` is the Stytch Project ID
7
20
  * (the default token audience).
8
21
  */
9
- export declare function stytchProvider(opts: {
22
+ export declare function stytchProvider<TCustom extends ExtraClaims = Record<never, never>>(opts: {
10
23
  domain: string;
11
24
  audience: string;
12
- } & Omit<CustomProviderOptions, "issuer" | "audience">): Promise<OAuthConfig>;
25
+ } & Omit<CustomProviderOptions, "issuer" | "audience">): Promise<OAuthConfig<StytchClaims & TCustom & RegisteredClaims>>;
@@ -8,6 +8,9 @@ import { toIssuerUrl } from "./shared.js";
8
8
  */
9
9
  export function stytchProvider(opts) {
10
10
  const { domain, ...rest } = opts;
11
- return customProvider({ issuer: toIssuerUrl(domain), ...rest });
11
+ return customProvider({
12
+ issuer: toIssuerUrl(domain),
13
+ ...rest,
14
+ });
12
15
  }
13
16
  //# sourceMappingURL=stytch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stytch.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/stytch.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACjC,OAAO,cAAc,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;AAClE,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\nimport { toIssuerUrl } from \"./shared.js\";\n\n/**\n * OAuth provider for Stytch Connected Apps. `domain` is the project domain,\n * e.g. `acme.customers.stytch.dev`, or a configured custom domain. Requires\n * DCR enabled in the Stytch dashboard. `audience` is the Stytch Project ID\n * (the default token audience).\n */\nexport function stytchProvider(\n opts: { domain: string; audience: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\"\n >,\n): Promise<OAuthConfig> {\n const { domain, ...rest } = opts;\n return customProvider({ issuer: toIssuerUrl(domain), ...rest });\n}\n"]}
1
+ {"version":3,"file":"stytch.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/stytch.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAc1C;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAG5B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACjC,OAAO,cAAc,CAAyB;QAC5C,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;QAC3B,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ExtraClaims } from \"../../auth.js\";\nimport type { 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 a Stytch Connected Apps access token carries. Beyond the subject,\n * everything is project-configured through the client's access-token template.\n *\n * @see https://stytch.com/docs/api-reference/consumer/api/connected-apps/tokens/connected-app-access-token-object\n * @see https://stytch.com/docs/api/connected-apps-create\n */\nexport type StytchClaims = {\n /** Only when the client's access-token template adds it. */\n email?: string;\n};\n\n/**\n * OAuth provider for Stytch Connected Apps. `domain` is the project domain,\n * e.g. `acme.customers.stytch.dev`, or a configured custom domain. Requires\n * DCR enabled in the Stytch dashboard. `audience` is the Stytch Project ID\n * (the default token audience).\n */\nexport function stytchProvider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: { domain: string; audience: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\"\n >,\n): Promise<OAuthConfig<StytchClaims & TCustom & RegisteredClaims>> {\n const { domain, ...rest } = opts;\n return customProvider<StytchClaims & TCustom>({\n issuer: toIssuerUrl(domain),\n ...rest,\n });\n}\n"]}
@@ -0,0 +1,12 @@
1
+ export declare const createJwksVerifier: import("vitest").Mock<typeof import("../verify.js").createJwksVerifier>;
2
+ /**
3
+ * The JWKS parameters a provider handed to `createJwksVerifier`: the issuer and
4
+ * JWKS URL it derived from discovery, and the audience it expects. Substitute
5
+ * this module for `../verify.js` to assert on them:
6
+ *
7
+ * ```ts
8
+ * vi.mock("../verify.js", () => import("./verify-spy.js"));
9
+ * import { lastJwksConfig as jwks } from "./verify-spy.js";
10
+ * ```
11
+ */
12
+ export declare const lastJwksConfig: () => import("../verify.js").JwksVerifyConfig;
@@ -0,0 +1,21 @@
1
+ import { vi } from "vitest";
2
+ const actual = await vi.importActual("../verify.js");
3
+ export const createJwksVerifier = vi.fn(actual.createJwksVerifier);
4
+ /**
5
+ * The JWKS parameters a provider handed to `createJwksVerifier`: the issuer and
6
+ * JWKS URL it derived from discovery, and the audience it expects. Substitute
7
+ * this module for `../verify.js` to assert on them:
8
+ *
9
+ * ```ts
10
+ * vi.mock("../verify.js", () => import("./verify-spy.js"));
11
+ * import { lastJwksConfig as jwks } from "./verify-spy.js";
12
+ * ```
13
+ */
14
+ export const lastJwksConfig = () => {
15
+ const call = createJwksVerifier.mock.lastCall;
16
+ if (!call) {
17
+ throw new Error("createJwksVerifier was not called");
18
+ }
19
+ return call[0];
20
+ };
21
+ //# sourceMappingURL=verify-spy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-spy.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/verify-spy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE5B,MAAM,MAAM,GACV,MAAM,EAAE,CAAC,YAAY,CAAgC,cAAc,CAAC,CAAC;AAEvE,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC;AAEnE;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,GAAG,EAAE;IACjC,MAAM,IAAI,GAAG,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC;IAC9C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;AACjB,CAAC,CAAC","sourcesContent":["import { vi } from \"vitest\";\n\nconst actual =\n await vi.importActual<typeof import(\"../verify.js\")>(\"../verify.js\");\n\nexport const createJwksVerifier = vi.fn(actual.createJwksVerifier);\n\n/**\n * The JWKS parameters a provider handed to `createJwksVerifier`: the issuer and\n * JWKS URL it derived from discovery, and the audience it expects. Substitute\n * this module for `../verify.js` to assert on them:\n *\n * ```ts\n * vi.mock(\"../verify.js\", () => import(\"./verify-spy.js\"));\n * import { lastJwksConfig as jwks } from \"./verify-spy.js\";\n * ```\n */\nexport const lastJwksConfig = () => {\n const call = createJwksVerifier.mock.lastCall;\n if (!call) {\n throw new Error(\"createJwksVerifier was not called\");\n }\n return call[0];\n};\n"]}
@@ -1,11 +1,33 @@
1
+ import type { ExtraClaims } from "../../auth.js";
1
2
  import type { OAuthConfig } from "../index.js";
3
+ import type { RegisteredClaims } from "../verify.js";
2
4
  import { type CustomProviderOptions } from "./custom.js";
5
+ /**
6
+ * Claims a WorkOS AuthKit access token carries. All optional: `org_id`, `role`
7
+ * and `permissions` appear only when an organization is selected at sign-in, and
8
+ * `email` only when a JWT template adds it.
9
+ *
10
+ * @see https://workos.com/docs/authkit/sessions/integrating-sessions/access-token
11
+ * @see https://workos.com/docs/authkit/jwt-templates
12
+ */
13
+ export type WorkosClaims = {
14
+ /** Session id, used for signing out. */
15
+ sid?: string;
16
+ /** Organization selected at sign-in. */
17
+ org_id?: string;
18
+ /** Role of the selected organization membership. */
19
+ role?: string;
20
+ /** Permissions assigned to the role. */
21
+ permissions?: string[];
22
+ /** Only when a JWT template adds it; not a default AuthKit claim. */
23
+ email?: string;
24
+ };
3
25
  /**
4
26
  * OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.
5
27
  * `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard
6
28
  * (Connect → Configuration). `audience` is the MCP server's Resource Indicator.
7
29
  */
8
- export declare function workosProvider(opts: {
30
+ export declare function workosProvider<TCustom extends ExtraClaims = Record<never, never>>(opts: {
9
31
  domain: string;
10
32
  audience: string;
11
- } & Omit<CustomProviderOptions, "issuer" | "audience">): Promise<OAuthConfig>;
33
+ } & Omit<CustomProviderOptions, "issuer" | "audience">): Promise<OAuthConfig<WorkosClaims & TCustom & RegisteredClaims>>;
@@ -7,6 +7,9 @@ import { toIssuerUrl } from "./shared.js";
7
7
  */
8
8
  export function workosProvider(opts) {
9
9
  const { domain, ...rest } = opts;
10
- return customProvider({ issuer: toIssuerUrl(domain), ...rest });
10
+ return customProvider({
11
+ issuer: toIssuerUrl(domain),
12
+ ...rest,
13
+ });
11
14
  }
12
15
  //# sourceMappingURL=workos.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"workos.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/workos.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACjC,OAAO,cAAc,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;AAClE,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\nimport { toIssuerUrl } from \"./shared.js\";\n\n/**\n * OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.\n * `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard\n * (Connect → Configuration). `audience` is the MCP server's Resource Indicator.\n */\nexport function workosProvider(\n opts: { domain: string; audience: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\"\n >,\n): Promise<OAuthConfig> {\n const { domain, ...rest } = opts;\n return customProvider({ issuer: toIssuerUrl(domain), ...rest });\n}\n"]}
1
+ {"version":3,"file":"workos.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/workos.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB1C;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAG5B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IACjC,OAAO,cAAc,CAAyB;QAC5C,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;QAC3B,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ExtraClaims } from \"../../auth.js\";\nimport type { 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 a WorkOS AuthKit access token carries. All optional: `org_id`, `role`\n * and `permissions` appear only when an organization is selected at sign-in, and\n * `email` only when a JWT template adds it.\n *\n * @see https://workos.com/docs/authkit/sessions/integrating-sessions/access-token\n * @see https://workos.com/docs/authkit/jwt-templates\n */\nexport type WorkosClaims = {\n /** Session id, used for signing out. */\n sid?: string;\n /** Organization selected at sign-in. */\n org_id?: string;\n /** Role of the selected organization membership. */\n role?: string;\n /** Permissions assigned to the role. */\n permissions?: string[];\n /** Only when a JWT template adds it; not a default AuthKit claim. */\n email?: string;\n};\n\n/**\n * OAuth provider for WorkOS AuthKit. `domain` is the AuthKit domain, e.g.\n * `acme.authkit.app`. Requires DCR enabled in the WorkOS dashboard\n * (Connect → Configuration). `audience` is the MCP server's Resource Indicator.\n */\nexport function workosProvider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: { domain: string; audience: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\"\n >,\n): Promise<OAuthConfig<WorkosClaims & TCustom & RegisteredClaims>> {\n const { domain, ...rest } = opts;\n return customProvider<WorkosClaims & TCustom>({\n issuer: toIssuerUrl(domain),\n ...rest,\n });\n}\n"]}
@@ -3,4 +3,4 @@ import type { SecurityScheme } from "../server.js";
3
3
  import type { OAuthConfig } from "./index.js";
4
4
  export type ResourceMetadataUrlResolver = (getHeader: (key: string) => string | undefined) => string;
5
5
  /** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */
6
- export declare function setupOAuth(app: Express, config: OAuthConfig, schemesByTool: Map<string, SecurityScheme[] | undefined>): ResourceMetadataUrlResolver;
6
+ export declare function setupOAuth(app: Express, config: OAuthConfig<Record<string, unknown>>, schemesByTool: Map<string, SecurityScheme[] | undefined>): ResourceMetadataUrlResolver;
@@ -6,11 +6,11 @@ import { clientPrefersInBandChallenge, evaluateSecuritySchemes, httpStatusForFai
6
6
  import { createJwksVerifier } from "./verify.js";
7
7
  /** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */
8
8
  export function setupOAuth(app, config, schemesByTool) {
9
- if (!config.verify?.issuer) {
10
- throw new Error("oauth.verify requires an `issuer`");
9
+ if (!config.verifier && !config.verify) {
10
+ throw new Error("oauth requires a `verifier`");
11
11
  }
12
12
  const acceptsAnonymous = () => [...schemesByTool.values()].some(securitySchemesAllowAnonymous);
13
- const verifier = createJwksVerifier(config.verify);
13
+ const verifier = config.verifier ?? createJwksVerifier(config.verify);
14
14
  const bearer = (options) => {
15
15
  const required = requireBearerAuth(options);
16
16
  const optional = optionalBearerAuth(options);
@@ -1 +1 @@
1
- {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/server/auth/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BearerAuthMiddlewareOptions } from \"@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js\";\nimport {\n getOAuthProtectedResourceMetadataUrl,\n mcpAuthMetadataRouter,\n} from \"@modelcontextprotocol/sdk/server/auth/router.js\";\nimport cors from \"cors\";\nimport type { Express, Request, RequestHandler } from \"express\";\nimport type { AuthInfo } from \"../auth.js\";\nimport { optionalBearerAuth, requireBearerAuth } from \"../auth.js\";\nimport { resolveServerOrigin } from \"../requestOrigin.js\";\nimport type { SecurityScheme } from \"../server.js\";\nimport type { OAuthConfig } from \"./index.js\";\nimport {\n clientPrefersInBandChallenge,\n evaluateSecuritySchemes,\n httpStatusForFailure,\n inBandChallengeResult,\n type SchemeFailure,\n securitySchemesAllowAnonymous,\n wwwAuthenticateHeader,\n} from \"./security-schemes.js\";\nimport { createJwksVerifier } from \"./verify.js\";\n\nexport type ResourceMetadataUrlResolver = (\n getHeader: (key: string) => string | undefined,\n) => string;\n\n/** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */\nexport function setupOAuth(\n app: Express,\n config: OAuthConfig,\n schemesByTool: Map<string, SecurityScheme[] | undefined>,\n): ResourceMetadataUrlResolver {\n if (!config.verify?.issuer) {\n throw new Error(\"oauth.verify requires an `issuer`\");\n }\n\n const acceptsAnonymous = () =>\n [...schemesByTool.values()].some(securitySchemesAllowAnonymous);\n const verifier = createJwksVerifier(config.verify);\n const bearer = (options: BearerAuthMiddlewareOptions): RequestHandler => {\n const required = requireBearerAuth(options);\n const optional = optionalBearerAuth(options);\n return (req, res, next) =>\n (acceptsAnonymous() ? optional : required)(req, res, next);\n };\n\n let resourceMetadataUrl: ResourceMetadataUrlResolver;\n\n // baseUrl known at boot: bake the resource URLs once, no Host-header trust.\n if (config.baseUrl !== undefined) {\n let baseUrl: URL;\n try {\n baseUrl = new URL(config.baseUrl);\n } catch {\n throw new Error(\n `oauth.baseUrl must be a valid absolute URL, got: ${JSON.stringify(\n config.baseUrl,\n )}`,\n );\n }\n app.use(\n mcpAuthMetadataRouter({\n oauthMetadata: config.oauthMetadata,\n resourceServerUrl: baseUrl,\n scopesSupported: config.scopesSupported,\n }),\n );\n const url = getOAuthProtectedResourceMetadataUrl(baseUrl);\n resourceMetadataUrl = () => url;\n } else {\n // No baseUrl: resolve the resource origin per request from forwarded headers\n // (same precedence the framework uses for view serverUrl). Origin headers are\n // pathless, so the PRM path stays at root, matching the SDK's static layout.\n const resolveOrigin = (getHeader: (key: string) => string | undefined) =>\n new URL(resolveServerOrigin(getHeader));\n app.use(\n \"/.well-known/oauth-authorization-server\",\n cors(),\n (_req, res) => void res.json(config.oauthMetadata),\n );\n app.use(\"/.well-known/oauth-protected-resource\", cors(), (req, res) => {\n res.json({\n resource: resolveOrigin((key) => req.get(key)).href,\n authorization_servers: [config.oauthMetadata.issuer],\n scopes_supported: config.scopesSupported,\n });\n });\n resourceMetadataUrl = (getHeader) =>\n getOAuthProtectedResourceMetadataUrl(resolveOrigin(getHeader));\n }\n\n app.use(\"/mcp\", (req, res, next) =>\n bearer({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl: resourceMetadataUrl((key) => req.get(key)),\n })(req, res, next),\n );\n\n app.use(\"/mcp\", (req, res, next) => {\n type Call = {\n id?: string | number | null;\n method?: string;\n params?: { name?: string };\n };\n const body = req.body as Call | Call[] | undefined;\n const calls = Array.isArray(body) ? body : [body];\n const auth = (req as Request & { auth?: AuthInfo }).auth;\n let failure: SchemeFailure | undefined;\n for (const call of calls) {\n const tool = call?.params?.name;\n if (call?.method === \"tools/call\" && tool && schemesByTool.has(tool)) {\n failure = evaluateSecuritySchemes(schemesByTool.get(tool), auth);\n if (failure) {\n break;\n }\n }\n }\n if (!failure) {\n return next();\n }\n const url = resourceMetadataUrl((key) => req.get(key));\n if (\n !Array.isArray(body) &&\n clientPrefersInBandChallenge(req.get(\"user-agent\"))\n ) {\n res.json({\n jsonrpc: \"2.0\",\n id: body?.id ?? null,\n result: inBandChallengeResult(failure, url),\n });\n return;\n }\n res.set(\"WWW-Authenticate\", wwwAuthenticateHeader(failure, url));\n res.status(httpStatusForFailure(failure)).json({\n error: failure.error,\n error_description: failure.description,\n });\n });\n\n return resourceMetadataUrl;\n}\n"]}
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../src/server/auth/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BearerAuthMiddlewareOptions } from \"@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js\";\nimport {\n getOAuthProtectedResourceMetadataUrl,\n mcpAuthMetadataRouter,\n} from \"@modelcontextprotocol/sdk/server/auth/router.js\";\nimport cors from \"cors\";\nimport type { Express, Request, RequestHandler } from \"express\";\nimport type { AuthInfo } from \"../auth.js\";\nimport { optionalBearerAuth, requireBearerAuth } from \"../auth.js\";\nimport { resolveServerOrigin } from \"../requestOrigin.js\";\nimport type { SecurityScheme } from \"../server.js\";\nimport type { OAuthConfig } from \"./index.js\";\nimport {\n clientPrefersInBandChallenge,\n evaluateSecuritySchemes,\n httpStatusForFailure,\n inBandChallengeResult,\n type SchemeFailure,\n securitySchemesAllowAnonymous,\n wwwAuthenticateHeader,\n} from \"./security-schemes.js\";\nimport { createJwksVerifier } from \"./verify.js\";\n\nexport type ResourceMetadataUrlResolver = (\n getHeader: (key: string) => string | undefined,\n) => string;\n\n/** Mounts the well-known OAuth metadata and bearer auth on `/mcp`. */\nexport function setupOAuth(\n app: Express,\n config: OAuthConfig<Record<string, unknown>>,\n schemesByTool: Map<string, SecurityScheme[] | undefined>,\n): ResourceMetadataUrlResolver {\n if (!config.verifier && !config.verify) {\n throw new Error(\"oauth requires a `verifier`\");\n }\n\n const acceptsAnonymous = () =>\n [...schemesByTool.values()].some(securitySchemesAllowAnonymous);\n const verifier = config.verifier ?? createJwksVerifier(config.verify);\n const bearer = (options: BearerAuthMiddlewareOptions): RequestHandler => {\n const required = requireBearerAuth(options);\n const optional = optionalBearerAuth(options);\n return (req, res, next) =>\n (acceptsAnonymous() ? optional : required)(req, res, next);\n };\n\n let resourceMetadataUrl: ResourceMetadataUrlResolver;\n\n // baseUrl known at boot: bake the resource URLs once, no Host-header trust.\n if (config.baseUrl !== undefined) {\n let baseUrl: URL;\n try {\n baseUrl = new URL(config.baseUrl);\n } catch {\n throw new Error(\n `oauth.baseUrl must be a valid absolute URL, got: ${JSON.stringify(\n config.baseUrl,\n )}`,\n );\n }\n app.use(\n mcpAuthMetadataRouter({\n oauthMetadata: config.oauthMetadata,\n resourceServerUrl: baseUrl,\n scopesSupported: config.scopesSupported,\n }),\n );\n const url = getOAuthProtectedResourceMetadataUrl(baseUrl);\n resourceMetadataUrl = () => url;\n } else {\n // No baseUrl: resolve the resource origin per request from forwarded headers\n // (same precedence the framework uses for view serverUrl). Origin headers are\n // pathless, so the PRM path stays at root, matching the SDK's static layout.\n const resolveOrigin = (getHeader: (key: string) => string | undefined) =>\n new URL(resolveServerOrigin(getHeader));\n app.use(\n \"/.well-known/oauth-authorization-server\",\n cors(),\n (_req, res) => void res.json(config.oauthMetadata),\n );\n app.use(\"/.well-known/oauth-protected-resource\", cors(), (req, res) => {\n res.json({\n resource: resolveOrigin((key) => req.get(key)).href,\n authorization_servers: [config.oauthMetadata.issuer],\n scopes_supported: config.scopesSupported,\n });\n });\n resourceMetadataUrl = (getHeader) =>\n getOAuthProtectedResourceMetadataUrl(resolveOrigin(getHeader));\n }\n\n app.use(\"/mcp\", (req, res, next) =>\n bearer({\n verifier,\n requiredScopes: config.requiredScopes,\n resourceMetadataUrl: resourceMetadataUrl((key) => req.get(key)),\n })(req, res, next),\n );\n\n app.use(\"/mcp\", (req, res, next) => {\n type Call = {\n id?: string | number | null;\n method?: string;\n params?: { name?: string };\n };\n const body = req.body as Call | Call[] | undefined;\n const calls = Array.isArray(body) ? body : [body];\n const auth = (req as Request & { auth?: AuthInfo }).auth;\n let failure: SchemeFailure | undefined;\n for (const call of calls) {\n const tool = call?.params?.name;\n if (call?.method === \"tools/call\" && tool && schemesByTool.has(tool)) {\n failure = evaluateSecuritySchemes(schemesByTool.get(tool), auth);\n if (failure) {\n break;\n }\n }\n }\n if (!failure) {\n return next();\n }\n const url = resourceMetadataUrl((key) => req.get(key));\n if (\n !Array.isArray(body) &&\n clientPrefersInBandChallenge(req.get(\"user-agent\"))\n ) {\n res.json({\n jsonrpc: \"2.0\",\n id: body?.id ?? null,\n result: inBandChallengeResult(failure, url),\n });\n return;\n }\n res.set(\"WWW-Authenticate\", wwwAuthenticateHeader(failure, url));\n res.status(httpStatusForFailure(failure)).json({\n error: failure.error,\n error_description: failure.description,\n });\n });\n\n return resourceMetadataUrl;\n}\n"]}
@@ -5,6 +5,7 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
5
5
  import * as jose from "jose";
6
6
  import { afterEach, describe, expect, it, vi } from "vitest";
7
7
  import { McpServer } from "../server.js";
8
+ import { createJwksVerifier } from "./verify.js";
8
9
  vi.mock("@skybridge/devtools", () => ({
9
10
  devtoolsStaticServer: () => ((_req, _res, next) => next()),
10
11
  }));
@@ -48,7 +49,7 @@ function signToken(key, scope = "openid email") {
48
49
  .setExpirationTime("1h")
49
50
  .sign(key);
50
51
  }
51
- async function bootServer(jwksUri, { baseUrl = "https://app.example.test" } = {}) {
52
+ async function bootServer(jwksUri, { baseUrl = "https://app.example.test", legacyVerify = false, } = {}) {
52
53
  const { createApp } = await import("../express.js");
53
54
  const server = new McpServer({ name: "auth-test", version: "0.0.0" }, { capabilities: {} }, {
54
55
  oauth: {
@@ -59,7 +60,15 @@ async function bootServer(jwksUri, { baseUrl = "https://app.example.test" } = {}
59
60
  token_endpoint: `${ISSUER}/token`,
60
61
  response_types_supported: ["code"],
61
62
  },
62
- verify: { issuer: ISSUER, audience: AUDIENCE, jwksUri },
63
+ ...(legacyVerify
64
+ ? { verify: { issuer: ISSUER, audience: AUDIENCE, jwksUri } }
65
+ : {
66
+ verifier: createJwksVerifier({
67
+ issuer: ISSUER,
68
+ audience: AUDIENCE,
69
+ jwksUri,
70
+ }),
71
+ }),
63
72
  scopesSupported: ["openid", "email"],
64
73
  requiredScopes: ["openid"],
65
74
  },
@@ -115,6 +124,20 @@ describe("setupOAuth wiring", () => {
115
124
  expect(result.content[0]?.text).toBe("client-1");
116
125
  await client.close();
117
126
  });
127
+ it("builds the verifier from the legacy verify config", async () => {
128
+ const { privateKey, jwksUri } = await startJwks();
129
+ const base = await bootServer(jwksUri, { legacyVerify: true });
130
+ const token = await signToken(privateKey);
131
+ const client = new Client({ name: "test-client", version: "0.0.0" });
132
+ const transport = new StreamableHTTPClientTransport(new URL(`${base}/mcp`), { requestInit: { headers: { Authorization: `Bearer ${token}` } } });
133
+ await client.connect(transport);
134
+ const result = (await client.callTool({
135
+ name: "whoami",
136
+ arguments: {},
137
+ }));
138
+ expect(result.content[0]?.text).toBe("client-1");
139
+ await client.close();
140
+ });
118
141
  });
119
142
  describe("baseUrl inferred from headers", () => {
120
143
  it("derives the resource origin from x-forwarded-host", async () => {
@@ -176,7 +199,11 @@ async function bootMixedServer(jwksUri) {
176
199
  token_endpoint: `${ISSUER}/token`,
177
200
  response_types_supported: ["code"],
178
201
  },
179
- verify: { issuer: ISSUER, audience: AUDIENCE, jwksUri },
202
+ verifier: createJwksVerifier({
203
+ issuer: ISSUER,
204
+ audience: AUDIENCE,
205
+ jwksUri,
206
+ }),
180
207
  },
181
208
  })
182
209
  .registerTool({
@@ -379,7 +406,11 @@ async function bootScopedServer(jwksUri) {
379
406
  token_endpoint: `${ISSUER}/token`,
380
407
  response_types_supported: ["code"],
381
408
  },
382
- verify: { issuer: ISSUER, audience: AUDIENCE, jwksUri },
409
+ verifier: createJwksVerifier({
410
+ issuer: ISSUER,
411
+ audience: AUDIENCE,
412
+ jwksUri,
413
+ }),
383
414
  },
384
415
  }).registerTool({
385
416
  name: "checkout",
@@ -466,7 +497,10 @@ describe("oauth config validation", () => {
466
497
  oauth: {
467
498
  baseUrl: "not-a-url",
468
499
  oauthMetadata: validMetadata,
469
- verify: { issuer: ISSUER, audience: AUDIENCE },
500
+ verifier: createJwksVerifier({
501
+ issuer: ISSUER,
502
+ audience: AUDIENCE,
503
+ }),
470
504
  },
471
505
  })).toThrow(/baseUrl must be a valid absolute URL/);
472
506
  });