skybridge 0.0.0-dev.a8fd02e → 0.0.0-dev.a955b71

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 (135) hide show
  1. package/README.md +9 -0
  2. package/dist/server/auth/discovery.d.ts +30 -5
  3. package/dist/server/auth/discovery.js +34 -29
  4. package/dist/server/auth/discovery.js.map +1 -1
  5. package/dist/server/auth/discovery.test.js +12 -0
  6. package/dist/server/auth/discovery.test.js.map +1 -1
  7. package/dist/server/auth/providers/auth0.d.ts +18 -0
  8. package/dist/server/auth/providers/auth0.js +31 -0
  9. package/dist/server/auth/providers/auth0.js.map +1 -0
  10. package/dist/server/auth/providers/auth0.test.js +48 -0
  11. package/dist/server/auth/providers/auth0.test.js.map +1 -0
  12. package/dist/server/auth/providers/clerk.d.ts +14 -0
  13. package/dist/server/auth/providers/clerk.js +16 -0
  14. package/dist/server/auth/providers/clerk.js.map +1 -0
  15. package/dist/server/auth/providers/clerk.test.js +28 -0
  16. package/dist/server/auth/providers/clerk.test.js.map +1 -0
  17. package/dist/server/auth/providers/custom.d.ts +12 -2
  18. package/dist/server/auth/providers/custom.js +19 -10
  19. package/dist/server/auth/providers/custom.js.map +1 -1
  20. package/dist/server/auth/providers/custom.test.js +19 -25
  21. package/dist/server/auth/providers/custom.test.js.map +1 -1
  22. package/dist/server/auth/providers/descope.d.ts +9 -6
  23. package/dist/server/auth/providers/descope.js +27 -7
  24. package/dist/server/auth/providers/descope.js.map +1 -1
  25. package/dist/server/auth/providers/descope.test.js +9 -7
  26. package/dist/server/auth/providers/descope.test.js.map +1 -1
  27. package/dist/server/auth/providers/shared.d.ts +2 -0
  28. package/dist/server/auth/providers/shared.js +6 -0
  29. package/dist/server/auth/providers/shared.js.map +1 -0
  30. package/dist/server/auth/providers/shared.test.d.ts +1 -0
  31. package/dist/server/auth/providers/shared.test.js +10 -0
  32. package/dist/server/auth/providers/shared.test.js.map +1 -0
  33. package/dist/server/auth/providers/stytch.d.ts +2 -1
  34. package/dist/server/auth/providers/stytch.js +1 -5
  35. package/dist/server/auth/providers/stytch.js.map +1 -1
  36. package/dist/server/auth/providers/workos.d.ts +2 -1
  37. package/dist/server/auth/providers/workos.js +1 -5
  38. package/dist/server/auth/providers/workos.js.map +1 -1
  39. package/dist/server/auth/setup.js +2 -2
  40. package/dist/server/auth/setup.js.map +1 -1
  41. package/dist/server/auth/verify.d.ts +3 -2
  42. package/dist/server/auth/verify.js +2 -1
  43. package/dist/server/auth/verify.js.map +1 -1
  44. package/dist/server/auth/verify.test.js +13 -0
  45. package/dist/server/auth/verify.test.js.map +1 -1
  46. package/dist/server/index.d.ts +2 -0
  47. package/dist/server/index.js +2 -0
  48. package/dist/server/index.js.map +1 -1
  49. package/dist/server/server.d.ts +12 -4
  50. package/dist/server/server.js +66 -99
  51. package/dist/server/server.js.map +1 -1
  52. package/dist/server/view-resource-resolution.test.js +24 -9
  53. package/dist/server/view-resource-resolution.test.js.map +1 -1
  54. package/dist/test/view.test.js +69 -101
  55. package/dist/test/view.test.js.map +1 -1
  56. package/dist/web/bridges/adaptor.d.ts +51 -0
  57. package/dist/web/bridges/adaptor.js +310 -0
  58. package/dist/web/bridges/adaptor.js.map +1 -0
  59. package/dist/web/bridges/adaptor.test.d.ts +1 -0
  60. package/dist/web/bridges/adaptor.test.js +197 -0
  61. package/dist/web/bridges/adaptor.test.js.map +1 -0
  62. package/dist/web/bridges/apps-sdk/bridge.d.ts +6 -2
  63. package/dist/web/bridges/apps-sdk/bridge.js +15 -4
  64. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  65. package/dist/web/bridges/apps-sdk/index.d.ts +0 -1
  66. package/dist/web/bridges/apps-sdk/index.js +0 -1
  67. package/dist/web/bridges/apps-sdk/index.js.map +1 -1
  68. package/dist/web/bridges/get-adaptor.d.ts +4 -4
  69. package/dist/web/bridges/get-adaptor.js +6 -11
  70. package/dist/web/bridges/get-adaptor.js.map +1 -1
  71. package/dist/web/bridges/get-adaptor.test.d.ts +1 -0
  72. package/dist/web/bridges/get-adaptor.test.js +32 -0
  73. package/dist/web/bridges/get-adaptor.test.js.map +1 -0
  74. package/dist/web/bridges/mcp-app/bridge.d.ts +13 -2
  75. package/dist/web/bridges/mcp-app/bridge.js +46 -4
  76. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  77. package/dist/web/bridges/mcp-app/bridge.test.d.ts +1 -0
  78. package/dist/web/bridges/mcp-app/bridge.test.js +17 -0
  79. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -0
  80. package/dist/web/bridges/mcp-app/index.d.ts +0 -1
  81. package/dist/web/bridges/mcp-app/index.js +0 -1
  82. package/dist/web/bridges/mcp-app/index.js.map +1 -1
  83. package/dist/web/bridges/mcp-app/view-tools.test.js +5 -6
  84. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
  85. package/dist/web/bridges/types.d.ts +10 -0
  86. package/dist/web/bridges/types.js +14 -1
  87. package/dist/web/bridges/types.js.map +1 -1
  88. package/dist/web/bridges/types.test.d.ts +1 -0
  89. package/dist/web/bridges/types.test.js +19 -0
  90. package/dist/web/bridges/types.test.js.map +1 -0
  91. package/dist/web/components/modal-provider.js +4 -3
  92. package/dist/web/components/modal-provider.js.map +1 -1
  93. package/dist/web/create-store.test.js +8 -5
  94. package/dist/web/create-store.test.js.map +1 -1
  95. package/dist/web/data-llm.test.js +9 -5
  96. package/dist/web/data-llm.test.js.map +1 -1
  97. package/dist/web/hooks/use-call-tool.test.js +37 -23
  98. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  99. package/dist/web/hooks/use-display-mode.test.js +56 -20
  100. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  101. package/dist/web/hooks/use-download.test.js +11 -3
  102. package/dist/web/hooks/use-download.test.js.map +1 -1
  103. package/dist/web/hooks/use-files.test.js +10 -2
  104. package/dist/web/hooks/use-files.test.js.map +1 -1
  105. package/dist/web/hooks/use-layout.test.js +59 -26
  106. package/dist/web/hooks/use-layout.test.js.map +1 -1
  107. package/dist/web/hooks/use-open-external.test.js +13 -3
  108. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  109. package/dist/web/hooks/use-request-close.test.js +35 -40
  110. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  111. package/dist/web/hooks/use-request-modal.test.js +10 -0
  112. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  113. package/dist/web/hooks/use-request-size.test.js +35 -53
  114. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  115. package/dist/web/hooks/use-set-open-in-app-url.test.js +28 -8
  116. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  117. package/dist/web/hooks/use-tool-info.test.js +37 -27
  118. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  119. package/dist/web/hooks/use-user.test.js +81 -35
  120. package/dist/web/hooks/use-user.test.js.map +1 -1
  121. package/dist/web/hooks/use-view-state.test.js +6 -2
  122. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  123. package/package.json +1 -1
  124. package/dist/server/auth/providers/stytch.test.js +0 -33
  125. package/dist/server/auth/providers/stytch.test.js.map +0 -1
  126. package/dist/server/auth/providers/workos.test.js +0 -41
  127. package/dist/server/auth/providers/workos.test.js.map +0 -1
  128. package/dist/web/bridges/apps-sdk/adaptor.d.ts +0 -29
  129. package/dist/web/bridges/apps-sdk/adaptor.js +0 -117
  130. package/dist/web/bridges/apps-sdk/adaptor.js.map +0 -1
  131. package/dist/web/bridges/mcp-app/adaptor.d.ts +0 -53
  132. package/dist/web/bridges/mcp-app/adaptor.js +0 -283
  133. package/dist/web/bridges/mcp-app/adaptor.js.map +0 -1
  134. /package/dist/server/auth/providers/{stytch.test.d.ts → auth0.test.d.ts} +0 -0
  135. /package/dist/server/auth/providers/{workos.test.d.ts → clerk.test.d.ts} +0 -0
package/README.md CHANGED
@@ -26,6 +26,15 @@
26
26
  <a href="https://github.com/alpic-ai/skybridge/blob/main/LICENSE"><picture><source media="(prefers-color-scheme: dark)" srcset="https://img.shields.io/github/license/alpic-ai/skybridge?color=D7FFC8&amp;labelColor=161B22&amp;style=for-the-badge"><img alt="License: MIT" src="https://img.shields.io/github/license/alpic-ai/skybridge?color=E8FBD9&amp;labelColor=F6F8FA&amp;style=for-the-badge"></picture></a>
27
27
  </p>
28
28
 
29
+ <p align="center">
30
+ <a href="https://www.producthunt.com/products/skybridge?embed=true&amp;utm_source=badge-featured&amp;utm_medium=badge&amp;utm_campaign=badge-skybridge" target="_blank" rel="noopener noreferrer">
31
+ <picture>
32
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1162357&amp;theme=dark" />
33
+ <img alt="Skybridge - The full-stack open source React framework for MCP Apps | Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1162357&amp;theme=light" />
34
+ </picture>
35
+ </a>
36
+ </p>
37
+
29
38
  ## About Skybridge
30
39
 
31
40
  Skybridge helps developers build type-safe MCP apps for Claude, ChatGPT and other UI-enabled MCP clients, with a complete set of tooling designed for both humans and agents.
@@ -1,7 +1,32 @@
1
- import { type OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
2
- /** Discovered metadata; `jwks_uri` is an RFC field the SDK type doesn't expose. */
3
- export type DiscoveredMetadata = OAuthMetadata & {
4
- jwks_uri?: string;
5
- };
1
+ import { z } from "zod";
2
+ /**
3
+ * Discovery doc validated as OAuth AS metadata + `jwks_uri`. The SDK's
4
+ * `OAuthMetadataSchema` omits `jwks_uri` (its client never verifies tokens), but
5
+ * we need it for JWKS token verification, so we extend the schema to keep it.
6
+ */
7
+ declare const DiscoverySchema: z.ZodObject<{
8
+ issuer: z.ZodString;
9
+ authorization_endpoint: z.ZodURL;
10
+ token_endpoint: z.ZodURL;
11
+ registration_endpoint: z.ZodOptional<z.ZodURL>;
12
+ scopes_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
13
+ response_types_supported: z.ZodArray<z.ZodString>;
14
+ response_modes_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
15
+ grant_types_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
16
+ token_endpoint_auth_methods_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
17
+ token_endpoint_auth_signing_alg_values_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
+ service_documentation: z.ZodOptional<z.ZodURL>;
19
+ revocation_endpoint: z.ZodOptional<z.ZodURL>;
20
+ revocation_endpoint_auth_methods_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ revocation_endpoint_auth_signing_alg_values_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
+ introspection_endpoint: z.ZodOptional<z.ZodString>;
23
+ introspection_endpoint_auth_methods_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
24
+ introspection_endpoint_auth_signing_alg_values_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
25
+ code_challenge_methods_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
+ client_id_metadata_document_supported: z.ZodOptional<z.ZodBoolean>;
27
+ jwks_uri: z.ZodOptional<z.ZodString>;
28
+ }, z.core.$loose>;
29
+ export type DiscoveredMetadata = z.infer<typeof DiscoverySchema>;
6
30
  /** Fetches and validates an authorization server's discovery document. */
7
31
  export declare function discoverAuthorizationServer(issuer: string): Promise<DiscoveredMetadata>;
32
+ export {};
@@ -1,4 +1,13 @@
1
- import { OAuthMetadataSchema, } from "@modelcontextprotocol/sdk/shared/auth.js";
1
+ import { OAuthMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
2
+ import { z } from "zod";
3
+ /**
4
+ * Discovery doc validated as OAuth AS metadata + `jwks_uri`. The SDK's
5
+ * `OAuthMetadataSchema` omits `jwks_uri` (its client never verifies tokens), but
6
+ * we need it for JWKS token verification, so we extend the schema to keep it.
7
+ */
8
+ const DiscoverySchema = OAuthMetadataSchema.extend({
9
+ jwks_uri: z.string().optional(),
10
+ });
2
11
  const WELL_KNOWN = [
3
12
  "/.well-known/openid-configuration",
4
13
  "/.well-known/oauth-authorization-server",
@@ -8,43 +17,39 @@ const DISCOVERY_TIMEOUT_MS = 5000;
8
17
  export async function discoverAuthorizationServer(issuer) {
9
18
  const base = issuer.replace(/\/$/, "");
10
19
  const errors = [];
20
+ // First valid doc lacking a registration_endpoint, kept as a fallback so a
21
+ // genuinely non-DCR IdP still resolves (and is rejected later by the caller).
22
+ let fallback;
11
23
  for (const path of WELL_KNOWN) {
12
24
  const url = `${base}${path}`;
13
- let res;
25
+ // Any failure (network/timeout, non-2xx, non-JSON, invalid metadata, issuer
26
+ // mismatch) records a per-URL reason and falls through to the next path.
14
27
  try {
15
- res = await fetch(url, {
28
+ const res = await fetch(url, {
16
29
  signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),
17
30
  });
31
+ if (!res.ok) {
32
+ throw new Error(`HTTP ${res.status}`);
33
+ }
34
+ const meta = DiscoverySchema.parse(await res.json());
35
+ // RFC 8414 §3.3: issuer must match the fetch origin (slash-insensitive).
36
+ if (meta.issuer.replace(/\/$/, "") !== base) {
37
+ throw new Error(`issuer mismatch: ${meta.issuer}`);
38
+ }
39
+ // Prefer the document that advertises DCR: some IdPs (e.g. Clerk) serve a
40
+ // valid openid-configuration without `registration_endpoint` yet expose
41
+ // it at oauth-authorization-server. Keep looking past a doc that omits it.
42
+ if (meta.registration_endpoint) {
43
+ return meta;
44
+ }
45
+ fallback ??= meta;
18
46
  }
19
47
  catch (err) {
20
48
  errors.push(`${url}: ${err instanceof Error ? err.message : String(err)}`);
21
- continue;
22
49
  }
23
- if (!res.ok) {
24
- errors.push(`${url}: HTTP ${res.status}`);
25
- continue;
26
- }
27
- let json;
28
- try {
29
- json = await res.json();
30
- }
31
- catch {
32
- errors.push(`${url}: response is not valid JSON`);
33
- continue;
34
- }
35
- const parsed = OAuthMetadataSchema.safeParse(json);
36
- if (!parsed.success) {
37
- errors.push(`${url}: invalid metadata (${parsed.error.message})`);
38
- continue;
39
- }
40
- // RFC 8414 §3.3: the document's issuer must match the URL it came from.
41
- // Compare slash-insensitively so a canonically slash-terminated issuer
42
- // isn't rejected by the trailing-slash strip applied to `base`.
43
- if (parsed.data.issuer.replace(/\/$/, "") !== base) {
44
- errors.push(`${url}: issuer mismatch (${parsed.data.issuer} !== ${base})`);
45
- continue;
46
- }
47
- return parsed.data;
50
+ }
51
+ if (fallback) {
52
+ return fallback;
48
53
  }
49
54
  throw new Error(`OAuth discovery failed for ${issuer}: ${errors.join("; ")}`);
50
55
  }
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/server/auth/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,mBAAmB,GACpB,MAAM,0CAA0C,CAAC;AAKlD,MAAM,UAAU,GAAG;IACjB,mCAAmC;IACnC,yCAAyC;CAC1C,CAAC;AAEF,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAElC,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,MAAc;IAEd,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;QAC7B,IAAI,GAAa,CAAC;QAClB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBACrB,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,oBAAoB,CAAC;aAClD,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CACT,GAAG,GAAG,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC9D,CAAC;YACF,SAAS;QACX,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,UAAU,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1C,SAAS;QACX,CAAC;QACD,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,8BAA8B,CAAC,CAAC;YAClD,SAAS;QACX,CAAC;QACD,MAAM,MAAM,GAAG,mBAAmB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACnD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,GAAG,GAAG,uBAAuB,MAAM,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC;YAClE,SAAS;QACX,CAAC;QACD,wEAAwE;QACxE,uEAAuE;QACvE,gEAAgE;QAChE,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;YACnD,MAAM,CAAC,IAAI,CACT,GAAG,GAAG,sBAAsB,MAAM,CAAC,IAAI,CAAC,MAAM,QAAQ,IAAI,GAAG,CAC9D,CAAC;YACF,SAAS;QACX,CAAC;QACD,OAAO,MAAM,CAAC,IAA0B,CAAC;IAC3C,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAChF,CAAC","sourcesContent":["import {\n type OAuthMetadata,\n OAuthMetadataSchema,\n} from \"@modelcontextprotocol/sdk/shared/auth.js\";\n\n/** Discovered metadata; `jwks_uri` is an RFC field the SDK type doesn't expose. */\nexport type DiscoveredMetadata = OAuthMetadata & { jwks_uri?: string };\n\nconst WELL_KNOWN = [\n \"/.well-known/openid-configuration\",\n \"/.well-known/oauth-authorization-server\",\n];\n\nconst DISCOVERY_TIMEOUT_MS = 5000;\n\n/** Fetches and validates an authorization server's discovery document. */\nexport async function discoverAuthorizationServer(\n issuer: string,\n): Promise<DiscoveredMetadata> {\n const base = issuer.replace(/\\/$/, \"\");\n const errors: string[] = [];\n\n for (const path of WELL_KNOWN) {\n const url = `${base}${path}`;\n let res: Response;\n try {\n res = await fetch(url, {\n signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),\n });\n } catch (err) {\n errors.push(\n `${url}: ${err instanceof Error ? err.message : String(err)}`,\n );\n continue;\n }\n if (!res.ok) {\n errors.push(`${url}: HTTP ${res.status}`);\n continue;\n }\n let json: unknown;\n try {\n json = await res.json();\n } catch {\n errors.push(`${url}: response is not valid JSON`);\n continue;\n }\n const parsed = OAuthMetadataSchema.safeParse(json);\n if (!parsed.success) {\n errors.push(`${url}: invalid metadata (${parsed.error.message})`);\n continue;\n }\n // RFC 8414 §3.3: the document's issuer must match the URL it came from.\n // Compare slash-insensitively so a canonically slash-terminated issuer\n // isn't rejected by the trailing-slash strip applied to `base`.\n if (parsed.data.issuer.replace(/\\/$/, \"\") !== base) {\n errors.push(\n `${url}: issuer mismatch (${parsed.data.issuer} !== ${base})`,\n );\n continue;\n }\n return parsed.data as DiscoveredMetadata;\n }\n\n throw new Error(`OAuth discovery failed for ${issuer}: ${errors.join(\"; \")}`);\n}\n"]}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/server/auth/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0CAA0C,CAAC;AAC/E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;GAIG;AACH,MAAM,eAAe,GAAG,mBAAmB,CAAC,MAAM,CAAC;IACjD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAChC,CAAC,CAAC;AAGH,MAAM,UAAU,GAAG;IACjB,mCAAmC;IACnC,yCAAyC;CAC1C,CAAC;AAEF,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAElC,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,MAAc;IAEd,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACvC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,2EAA2E;IAC3E,8EAA8E;IAC9E,IAAI,QAAwC,CAAC;IAE7C,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;QAC7B,4EAA4E;QAC5E,yEAAyE;QACzE,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,oBAAoB,CAAC;aAClD,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YACxC,CAAC;YACD,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;YACrD,yEAAyE;YACzE,IAAI,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;gBAC5C,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;YACrD,CAAC;YACD,0EAA0E;YAC1E,wEAAwE;YACxE,2EAA2E;YAC3E,IAAI,IAAI,CAAC,qBAAqB,EAAE,CAAC;gBAC/B,OAAO,IAAI,CAAC;YACd,CAAC;YACD,QAAQ,KAAK,IAAI,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CACT,GAAG,GAAG,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAC9D,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,8BAA8B,MAAM,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAChF,CAAC","sourcesContent":["import { OAuthMetadataSchema } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport { z } from \"zod\";\n\n/**\n * Discovery doc validated as OAuth AS metadata + `jwks_uri`. The SDK's\n * `OAuthMetadataSchema` omits `jwks_uri` (its client never verifies tokens), but\n * we need it for JWKS token verification, so we extend the schema to keep it.\n */\nconst DiscoverySchema = OAuthMetadataSchema.extend({\n jwks_uri: z.string().optional(),\n});\nexport type DiscoveredMetadata = z.infer<typeof DiscoverySchema>;\n\nconst WELL_KNOWN = [\n \"/.well-known/openid-configuration\",\n \"/.well-known/oauth-authorization-server\",\n];\n\nconst DISCOVERY_TIMEOUT_MS = 5000;\n\n/** Fetches and validates an authorization server's discovery document. */\nexport async function discoverAuthorizationServer(\n issuer: string,\n): Promise<DiscoveredMetadata> {\n const base = issuer.replace(/\\/$/, \"\");\n const errors: string[] = [];\n // First valid doc lacking a registration_endpoint, kept as a fallback so a\n // genuinely non-DCR IdP still resolves (and is rejected later by the caller).\n let fallback: DiscoveredMetadata | undefined;\n\n for (const path of WELL_KNOWN) {\n const url = `${base}${path}`;\n // Any failure (network/timeout, non-2xx, non-JSON, invalid metadata, issuer\n // mismatch) records a per-URL reason and falls through to the next path.\n try {\n const res = await fetch(url, {\n signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS),\n });\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}`);\n }\n const meta = DiscoverySchema.parse(await res.json());\n // RFC 8414 §3.3: issuer must match the fetch origin (slash-insensitive).\n if (meta.issuer.replace(/\\/$/, \"\") !== base) {\n throw new Error(`issuer mismatch: ${meta.issuer}`);\n }\n // Prefer the document that advertises DCR: some IdPs (e.g. Clerk) serve a\n // valid openid-configuration without `registration_endpoint` yet expose\n // it at oauth-authorization-server. Keep looking past a doc that omits it.\n if (meta.registration_endpoint) {\n return meta;\n }\n fallback ??= meta;\n } catch (err) {\n errors.push(\n `${url}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n if (fallback) {\n return fallback;\n }\n throw new Error(`OAuth discovery failed for ${issuer}: ${errors.join(\"; \")}`);\n}\n"]}
@@ -71,6 +71,18 @@ describe("discoverAuthorizationServer", () => {
71
71
  const meta = await discoverAuthorizationServer(base);
72
72
  expect(meta.registration_endpoint).toBe(`${base}/register`);
73
73
  });
74
+ it("prefers the doc advertising DCR when oidc omits registration_endpoint", async () => {
75
+ // Clerk-shaped: valid openid-configuration without a registration_endpoint,
76
+ // DCR exposed only at oauth-authorization-server.
77
+ const base = await serve((o) => ({
78
+ "/.well-known/openid-configuration": doc(o, {
79
+ registration_endpoint: undefined,
80
+ }),
81
+ "/.well-known/oauth-authorization-server": doc(o),
82
+ }));
83
+ const meta = await discoverAuthorizationServer(base);
84
+ expect(meta.registration_endpoint).toBe(`${base}/register`);
85
+ });
74
86
  it("rejects a doc whose issuer does not match the fetch origin", async () => {
75
87
  const base = await serve(() => ({
76
88
  "/.well-known/openid-configuration": doc("https://evil.test"),
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.test.js","sourceRoot":"","sources":["../../../src/server/auth/discovery.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { discoverAuthorizationServer } from \"./discovery.js\";\n\nlet server: http.Server | undefined;\nafterEach(() => server?.close());\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\"],\n jwks_uri: `${origin}/jwks`,\n ...extra,\n };\n}\n\n// Serves JSON bodies built from the live origin at their well-known paths;\n// returns the origin URL.\nasync function serve(build: (origin: string) => Record<string, unknown>) {\n let origin = \"\";\n const srv = http.createServer((req, res) => {\n const body = build(origin)[req.url ?? \"\"];\n if (body === undefined) {\n res.writeHead(404).end();\n return;\n }\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(body));\n });\n await new Promise<void>((resolve) => srv.listen(0, resolve));\n server = srv;\n origin = `http://localhost:${(srv.address() as { port: number }).port}`;\n return origin;\n}\n\ndescribe(\"discoverAuthorizationServer\", () => {\n it(\"fetches and validates openid-configuration\", async () => {\n const base = await serve((o) => ({\n \"/.well-known/openid-configuration\": doc(o),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n expect(meta.jwks_uri).toBe(`${base}/jwks`);\n });\n\n it(\"accepts a canonically slash-terminated issuer\", async () => {\n const base = await serve((o) => ({\n \"/.well-known/openid-configuration\": doc(o, { issuer: `${o}/` }),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"falls back to oauth-authorization-server when oidc is 404\", async () => {\n const base = await serve((o) => ({\n \"/.well-known/oauth-authorization-server\": doc(o),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"throws when no well-known doc is reachable\", async () => {\n const base = await serve(() => ({}));\n await expect(discoverAuthorizationServer(base)).rejects.toThrow(\n /discovery failed/i,\n );\n });\n\n it(\"falls through to oauth-authorization-server when oidc doc is schema-invalid\", async () => {\n const base = await serve((o) => ({\n \"/.well-known/openid-configuration\": doc(o, {\n response_types_supported: undefined,\n }),\n \"/.well-known/oauth-authorization-server\": doc(o),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"rejects a doc whose issuer does not match the fetch origin\", async () => {\n const base = await serve(() => ({\n \"/.well-known/openid-configuration\": doc(\"https://evil.test\"),\n }));\n await expect(discoverAuthorizationServer(base)).rejects.toThrow(\n /discovery failed/i,\n );\n });\n});\n"]}
1
+ {"version":3,"file":"discovery.test.js","sourceRoot":"","sources":["../../../src/server/auth/discovery.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment node\nimport http from \"node:http\";\nimport { afterEach, describe, expect, it } from \"vitest\";\nimport { discoverAuthorizationServer } from \"./discovery.js\";\n\nlet server: http.Server | undefined;\nafterEach(() => server?.close());\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\"],\n jwks_uri: `${origin}/jwks`,\n ...extra,\n };\n}\n\n// Serves JSON bodies built from the live origin at their well-known paths;\n// returns the origin URL.\nasync function serve(build: (origin: string) => Record<string, unknown>) {\n let origin = \"\";\n const srv = http.createServer((req, res) => {\n const body = build(origin)[req.url ?? \"\"];\n if (body === undefined) {\n res.writeHead(404).end();\n return;\n }\n res.setHeader(\"content-type\", \"application/json\");\n res.end(JSON.stringify(body));\n });\n await new Promise<void>((resolve) => srv.listen(0, resolve));\n server = srv;\n origin = `http://localhost:${(srv.address() as { port: number }).port}`;\n return origin;\n}\n\ndescribe(\"discoverAuthorizationServer\", () => {\n it(\"fetches and validates openid-configuration\", async () => {\n const base = await serve((o) => ({\n \"/.well-known/openid-configuration\": doc(o),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n expect(meta.jwks_uri).toBe(`${base}/jwks`);\n });\n\n it(\"accepts a canonically slash-terminated issuer\", async () => {\n const base = await serve((o) => ({\n \"/.well-known/openid-configuration\": doc(o, { issuer: `${o}/` }),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"falls back to oauth-authorization-server when oidc is 404\", async () => {\n const base = await serve((o) => ({\n \"/.well-known/oauth-authorization-server\": doc(o),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"throws when no well-known doc is reachable\", async () => {\n const base = await serve(() => ({}));\n await expect(discoverAuthorizationServer(base)).rejects.toThrow(\n /discovery failed/i,\n );\n });\n\n it(\"falls through to oauth-authorization-server when oidc doc is schema-invalid\", async () => {\n const base = await serve((o) => ({\n \"/.well-known/openid-configuration\": doc(o, {\n response_types_supported: undefined,\n }),\n \"/.well-known/oauth-authorization-server\": doc(o),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"prefers the doc advertising DCR when oidc omits registration_endpoint\", async () => {\n // Clerk-shaped: valid openid-configuration without a registration_endpoint,\n // DCR exposed only at oauth-authorization-server.\n const base = await serve((o) => ({\n \"/.well-known/openid-configuration\": doc(o, {\n registration_endpoint: undefined,\n }),\n \"/.well-known/oauth-authorization-server\": doc(o),\n }));\n const meta = await discoverAuthorizationServer(base);\n expect(meta.registration_endpoint).toBe(`${base}/register`);\n });\n\n it(\"rejects a doc whose issuer does not match the fetch origin\", async () => {\n const base = await serve(() => ({\n \"/.well-known/openid-configuration\": doc(\"https://evil.test\"),\n }));\n await expect(discoverAuthorizationServer(base)).rejects.toThrow(\n /discovery failed/i,\n );\n });\n});\n"]}
@@ -0,0 +1,18 @@
1
+ import type { OAuthConfig } from "../index.js";
2
+ import { type CustomProviderOptions } from "./custom.js";
3
+ /**
4
+ * OAuth provider for Auth0. `domain` is the tenant domain (e.g.
5
+ * `acme.us.auth0.com`); `audience` is the API Identifier; `serverUrl` is this
6
+ * server's public URL. Requires DCR enabled on the tenant.
7
+ *
8
+ * Auth0 can't use the client's resource indicator — `audience` must be *in* the
9
+ * authorize request. So it runs as skybridge-as-AS (`serverUrl`, see
10
+ * {@link customProvider}) and bakes `?audience=<id>` into the advertised
11
+ * `authorization_endpoint`. The token's `aud` is the API; `verify.issuer` stays
12
+ * Auth0 (the token's real `iss`).
13
+ */
14
+ export declare function auth0Provider(opts: {
15
+ domain: string;
16
+ audience: string;
17
+ serverUrl: string;
18
+ } & Omit<CustomProviderOptions, "issuer" | "audience" | "baseUrl" | "serverUrl">): Promise<OAuthConfig>;
@@ -0,0 +1,31 @@
1
+ import { customProvider } from "./custom.js";
2
+ import { toIssuerUrl } from "./shared.js";
3
+ /**
4
+ * OAuth provider for Auth0. `domain` is the tenant domain (e.g.
5
+ * `acme.us.auth0.com`); `audience` is the API Identifier; `serverUrl` is this
6
+ * server's public URL. Requires DCR enabled on the tenant.
7
+ *
8
+ * Auth0 can't use the client's resource indicator — `audience` must be *in* the
9
+ * authorize request. So it runs as skybridge-as-AS (`serverUrl`, see
10
+ * {@link customProvider}) and bakes `?audience=<id>` into the advertised
11
+ * `authorization_endpoint`. The token's `aud` is the API; `verify.issuer` stays
12
+ * Auth0 (the token's real `iss`).
13
+ */
14
+ export async function auth0Provider(opts) {
15
+ const { domain, audience, ...rest } = opts;
16
+ const config = await customProvider({
17
+ issuer: toIssuerUrl(domain),
18
+ audience,
19
+ ...rest,
20
+ });
21
+ const authUrl = new URL(config.oauthMetadata.authorization_endpoint);
22
+ authUrl.searchParams.set("audience", audience);
23
+ return {
24
+ ...config,
25
+ oauthMetadata: {
26
+ ...config.oauthMetadata,
27
+ authorization_endpoint: authUrl.href,
28
+ },
29
+ };
30
+ }
31
+ //# sourceMappingURL=auth0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth0.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAC3C,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC;QAClC,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 { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\nimport { toIssuerUrl } from \"./shared.js\";\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 opts: { domain: string; audience: string; serverUrl: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\" | \"baseUrl\" | \"serverUrl\"\n >,\n): Promise<OAuthConfig> {\n const { domain, audience, ...rest } = opts;\n const config = await customProvider({\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"]}
@@ -0,0 +1,48 @@
1
+ // @vitest-environment node
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { auth0Provider } from "./auth0.js";
4
+ afterEach(() => vi.restoreAllMocks());
5
+ function doc(issuer) {
6
+ return {
7
+ issuer,
8
+ authorization_endpoint: `${issuer}/authorize`,
9
+ token_endpoint: `${issuer}/oauth/token`,
10
+ registration_endpoint: `${issuer}/oidc/register`,
11
+ response_types_supported: ["code"],
12
+ scopes_supported: ["openid", "profile", "email"],
13
+ jwks_uri: `${issuer}/.well-known/jwks.json`,
14
+ };
15
+ }
16
+ describe("auth0Provider", () => {
17
+ it("advertises skybridge as the AS (static issuer) and bakes audience; verifies against Auth0", async () => {
18
+ const issuer = "https://acme.us.auth0.com";
19
+ vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(doc(issuer)), {
20
+ headers: { "content-type": "application/json" },
21
+ }));
22
+ const config = await auth0Provider({
23
+ domain: "acme.us.auth0.com",
24
+ audience: "https://api.example.com/",
25
+ serverUrl: "https://app.example.com/",
26
+ scopes: ["openid", "profile", "email"],
27
+ });
28
+ // skybridge is the advertised AS: static issuer = serverUrl (trailing slash stripped).
29
+ expect(config.oauthMetadata.issuer).toBe("https://app.example.com");
30
+ // AS metadata scopes match the advertised set (clients read scopes from here).
31
+ expect(config.oauthMetadata.scopes_supported).toEqual([
32
+ "openid",
33
+ "profile",
34
+ "email",
35
+ ]);
36
+ expect(config.scopesSupported).toEqual(["openid", "profile", "email"]);
37
+ // audience baked into the advertised authorize endpoint (still pointing at Auth0).
38
+ const url = new URL(config.oauthMetadata.authorization_endpoint);
39
+ expect(url.origin + url.pathname).toBe(`${issuer}/authorize`);
40
+ expect(url.searchParams.get("audience")).toBe("https://api.example.com/");
41
+ // baseUrl left unset so the PRM resource resolves per request (Alpic-safe).
42
+ expect(config.baseUrl).toBeUndefined();
43
+ // token verification still uses Auth0 as issuer + the API audience.
44
+ expect(config.verify.issuer).toBe(issuer);
45
+ expect(config.verify.audience).toBe("https://api.example.com/");
46
+ });
47
+ });
48
+ //# sourceMappingURL=auth0.test.js.map
@@ -0,0 +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;AAE3C,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,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAClE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { auth0Provider } from \"./auth0.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(config.verify.issuer).toBe(issuer);\n expect(config.verify.audience).toBe(\"https://api.example.com/\");\n });\n});\n"]}
@@ -0,0 +1,14 @@
1
+ import type { OAuthConfig } from "../index.js";
2
+ import { type CustomProviderOptions } from "./custom.js";
3
+ /**
4
+ * OAuth provider for Clerk. `domain` is the Frontend API URL (e.g.
5
+ * `acme.clerk.accounts.dev`, or a production custom domain). Requires Dynamic
6
+ * Client Registration enabled on the instance, and the OAuth application set to
7
+ * issue JWT access tokens (opaque tokens can't be JWKS-verified).
8
+ *
9
+ * Clerk access tokens carry no `aud` claim, so there is no `audience` option —
10
+ * verification is issuer + JWKS only (matching Clerk's own `mcpAuthClerk`).
11
+ */
12
+ export declare function clerkProvider(opts: {
13
+ domain: string;
14
+ } & Omit<CustomProviderOptions, "issuer" | "audience">): Promise<OAuthConfig>;
@@ -0,0 +1,16 @@
1
+ import { customProvider } from "./custom.js";
2
+ import { toIssuerUrl } from "./shared.js";
3
+ /**
4
+ * OAuth provider for Clerk. `domain` is the Frontend API URL (e.g.
5
+ * `acme.clerk.accounts.dev`, or a production custom domain). Requires Dynamic
6
+ * Client Registration enabled on the instance, and the OAuth application set to
7
+ * issue JWT access tokens (opaque tokens can't be JWKS-verified).
8
+ *
9
+ * Clerk access tokens carry no `aud` claim, so there is no `audience` option —
10
+ * verification is issuer + JWKS only (matching Clerk's own `mcpAuthClerk`).
11
+ */
12
+ export function clerkProvider(opts) {
13
+ const { domain, ...rest } = opts;
14
+ return customProvider({ issuer: toIssuerUrl(domain), ...rest });
15
+ }
16
+ //# sourceMappingURL=clerk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clerk.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/clerk.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAC3B,IAA6E;IAE7E,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 Clerk. `domain` is the Frontend API URL (e.g.\n * `acme.clerk.accounts.dev`, or a production custom domain). Requires Dynamic\n * Client Registration enabled on the instance, and the OAuth application set to\n * issue JWT access tokens (opaque tokens can't be JWKS-verified).\n *\n * Clerk access tokens carry no `aud` claim, so there is no `audience` option —\n * verification is issuer + JWKS only (matching Clerk's own `mcpAuthClerk`).\n */\nexport function clerkProvider(\n opts: { domain: string } & Omit<CustomProviderOptions, \"issuer\" | \"audience\">,\n): Promise<OAuthConfig> {\n const { domain, ...rest } = opts;\n return customProvider({ issuer: toIssuerUrl(domain), ...rest });\n}\n"]}
@@ -0,0 +1,28 @@
1
+ // @vitest-environment node
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { clerkProvider } from "./clerk.js";
4
+ afterEach(() => vi.restoreAllMocks());
5
+ function doc(issuer) {
6
+ return {
7
+ issuer,
8
+ authorization_endpoint: `${issuer}/oauth/authorize`,
9
+ token_endpoint: `${issuer}/oauth/token`,
10
+ registration_endpoint: `${issuer}/oauth/register`,
11
+ response_types_supported: ["code"],
12
+ scopes_supported: ["profile", "email"],
13
+ jwks_uri: `${issuer}/.well-known/jwks.json`,
14
+ };
15
+ }
16
+ describe("clerkProvider", () => {
17
+ it("verifies issuer + JWKS with no audience (Clerk tokens carry no aud)", async () => {
18
+ const issuer = "https://acme.clerk.accounts.dev";
19
+ const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response(JSON.stringify(doc(issuer)), {
20
+ headers: { "content-type": "application/json" },
21
+ }));
22
+ const config = await clerkProvider({ domain: "acme.clerk.accounts.dev" });
23
+ expect(fetchSpy).toHaveBeenCalledWith(`${issuer}/.well-known/openid-configuration`, expect.anything());
24
+ expect(config.verify.issuer).toBe(issuer);
25
+ expect(config.verify.audience).toBeUndefined();
26
+ });
27
+ });
28
+ //# sourceMappingURL=clerk.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clerk.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/clerk.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;AAE3C,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,kBAAkB;QACnD,cAAc,EAAE,GAAG,MAAM,cAAc;QACvC,qBAAqB,EAAE,GAAG,MAAM,iBAAiB;QACjD,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,gBAAgB,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC;QACtC,QAAQ,EAAE,GAAG,MAAM,wBAAwB;KAC5C,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;QACnF,MAAM,MAAM,GAAG,iCAAiC,CAAC;QACjD,MAAM,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC9D,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,EAAE,MAAM,EAAE,yBAAyB,EAAE,CAAC,CAAC;QAE1E,MAAM,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CACnC,GAAG,MAAM,mCAAmC,EAC5C,MAAM,CAAC,QAAQ,EAAE,CAClB,CAAC;QACF,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,aAAa,EAAE,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { clerkProvider } from \"./clerk.js\";\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction doc(issuer: string) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/oauth/authorize`,\n token_endpoint: `${issuer}/oauth/token`,\n registration_endpoint: `${issuer}/oauth/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"profile\", \"email\"],\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\ndescribe(\"clerkProvider\", () => {\n it(\"verifies issuer + JWKS with no audience (Clerk tokens carry no aud)\", async () => {\n const issuer = \"https://acme.clerk.accounts.dev\";\n const fetchSpy = 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 clerkProvider({ domain: \"acme.clerk.accounts.dev\" });\n\n expect(fetchSpy).toHaveBeenCalledWith(\n `${issuer}/.well-known/openid-configuration`,\n expect.anything(),\n );\n expect(config.verify.issuer).toBe(issuer);\n expect(config.verify.audience).toBeUndefined();\n });\n});\n"]}
@@ -3,12 +3,22 @@ import type { OAuthConfig } from "../index.js";
3
3
  /** Options accepted by {@link customProvider} and the branded providers. */
4
4
  export type CustomProviderOptions = {
5
5
  issuer: string;
6
- audience: string;
6
+ /** Expected token `aud`. Omit to skip audience verification — only for IdPs
7
+ * that don't bind an audience (e.g. Clerk). Branded providers whose IdP does
8
+ * bind an audience re-require it in their own options. */
9
+ audience?: string;
7
10
  /** Omit to let the server infer the resource origin from request headers. */
8
11
  baseUrl?: string;
12
+ /** Advertise THIS server as the authorization server (skybridge-as-AS): the
13
+ * served AS metadata `issuer` and the PRM `authorization_servers` use this URL
14
+ * instead of the IdP's. Needed when skybridge must sit in the auth path — Auth0
15
+ * (the `audience` must be baked into `/authorize`) or the Alpic DCR proxy (Alpic
16
+ * injects the registration endpoint). Use the static public URL; `verify.issuer`
17
+ * stays the IdP (the token's real `iss`). */
18
+ serverUrl?: string;
9
19
  scopes?: string[];
10
20
  requiredScopes?: string[];
11
21
  metadataOverrides?: Omit<Partial<OAuthMetadata>, "issuer">;
12
22
  };
13
- /** Builds a complete {@link OAuthConfig} for a DCR-compatible IdP via discovery. */
23
+ /** Builds a complete {@link OAuthConfig} from an IdP's OAuth discovery document. */
14
24
  export declare function customProvider(opts: CustomProviderOptions): Promise<OAuthConfig>;
@@ -1,18 +1,27 @@
1
1
  import { discoverAuthorizationServer, } from "../discovery.js";
2
- /** Builds a complete {@link OAuthConfig} for a DCR-compatible IdP via discovery. */
2
+ /** Builds a complete {@link OAuthConfig} from an IdP's OAuth discovery document. */
3
3
  export async function customProvider(opts) {
4
4
  const discovered = await discoverAuthorizationServer(opts.issuer);
5
- // The IdP's own discovery must advertise DCR and a JWKS; overrides can't fake it.
6
- if (!discovered.registration_endpoint) {
7
- throw new Error(`${opts.issuer} is not DCR-compatible (no registration_endpoint); customProvider only supports DCR IdPs.`);
8
- }
5
+ // JWKS verification needs a signing-key URL; without it the server can't verify
6
+ // tokens. (`registration_endpoint` is optional per RFC 8414 — a no-DCR IdP simply
7
+ // omits it; a proxy like Alpic, or pre-registered clients, supply registration.)
9
8
  if (!discovered.jwks_uri) {
10
9
  throw new Error(`${opts.issuer} discovery has no jwks_uri; JWKS verification requires it.`);
11
10
  }
12
- // Overrides adjust only advertised metadata (e.g. proxied endpoints). The
13
- // trust anchor (issuer, jwks_uri) always comes from validated discovery.
14
- const { issuer: _issuer, jwks_uri: _jwks, ...overrides } = (opts.metadataOverrides ?? {});
15
- const oauthMetadata = { ...discovered, ...overrides };
11
+ // Overrides adjust only advertised metadata; the trust anchor (issuer, jwks_uri)
12
+ // always comes from validated discovery.
13
+ const { issuer: _issuer, jwks_uri: _jwks, ...overrides } = opts.metadataOverrides ?? {};
14
+ const base = { ...discovered, ...overrides };
15
+ const scopesSupported = opts.scopes ?? base.scopes_supported;
16
+ // serverUrl => skybridge-as-AS: advertise this server's URL as the issuer (and
17
+ // keep the served scopes in sync). The verifier still trusts the IdP's `iss`.
18
+ const oauthMetadata = opts.serverUrl
19
+ ? {
20
+ ...base,
21
+ issuer: opts.serverUrl.replace(/\/$/, ""),
22
+ scopes_supported: scopesSupported,
23
+ }
24
+ : base;
16
25
  return {
17
26
  baseUrl: opts.baseUrl,
18
27
  oauthMetadata,
@@ -21,7 +30,7 @@ export async function customProvider(opts) {
21
30
  audience: opts.audience,
22
31
  jwksUri: discovered.jwks_uri,
23
32
  },
24
- scopesSupported: opts.scopes ?? oauthMetadata.scopes_supported,
33
+ scopesSupported,
25
34
  requiredScopes: opts.requiredScopes,
26
35
  };
27
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"custom.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAczB,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAA2B;IAE3B,MAAM,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAElE,kFAAkF;IAClF,IAAI,CAAC,UAAU,CAAC,qBAAqB,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,2FAA2F,CAC1G,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,4DAA4D,CAC3E,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,yEAAyE;IACzE,MAAM,EACJ,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,KAAK,EACf,GAAG,SAAS,EACb,GAAG,CAAC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAgC,CAAC;IAClE,MAAM,aAAa,GAAuB,EAAE,GAAG,UAAU,EAAE,GAAG,SAAS,EAAE,CAAC;IAE1E,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa;QACb,MAAM,EAAE;YACN,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,UAAU,CAAC,QAAQ;SAC7B;QACD,eAAe,EAAE,IAAI,CAAC,MAAM,IAAI,aAAa,CAAC,gBAAgB;QAC9D,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAC;AACJ,CAAC","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport {\n type DiscoveredMetadata,\n discoverAuthorizationServer,\n} from \"../discovery.js\";\nimport type { OAuthConfig } from \"../index.js\";\n\n/** Options accepted by {@link customProvider} and the branded providers. */\nexport type CustomProviderOptions = {\n issuer: string;\n audience: string;\n /** Omit to let the server infer the resource origin from request headers. */\n baseUrl?: string;\n scopes?: string[];\n requiredScopes?: string[];\n metadataOverrides?: Omit<Partial<OAuthMetadata>, \"issuer\">;\n};\n\n/** Builds a complete {@link OAuthConfig} for a DCR-compatible IdP via discovery. */\nexport async function customProvider(\n opts: CustomProviderOptions,\n): Promise<OAuthConfig> {\n const discovered = await discoverAuthorizationServer(opts.issuer);\n\n // The IdP's own discovery must advertise DCR and a JWKS; overrides can't fake it.\n if (!discovered.registration_endpoint) {\n throw new Error(\n `${opts.issuer} is not DCR-compatible (no registration_endpoint); customProvider only supports DCR IdPs.`,\n );\n }\n if (!discovered.jwks_uri) {\n throw new Error(\n `${opts.issuer} discovery has no jwks_uri; JWKS verification requires it.`,\n );\n }\n\n // Overrides adjust only advertised metadata (e.g. proxied endpoints). The\n // trust anchor (issuer, jwks_uri) always comes from validated discovery.\n const {\n issuer: _issuer,\n jwks_uri: _jwks,\n ...overrides\n } = (opts.metadataOverrides ?? {}) as Partial<DiscoveredMetadata>;\n const oauthMetadata: DiscoveredMetadata = { ...discovered, ...overrides };\n\n return {\n baseUrl: opts.baseUrl,\n oauthMetadata,\n verify: {\n issuer: discovered.issuer,\n audience: opts.audience,\n jwksUri: discovered.jwks_uri,\n },\n scopesSupported: opts.scopes ?? oauthMetadata.scopes_supported,\n requiredScopes: opts.requiredScopes,\n };\n}\n"]}
1
+ {"version":3,"file":"custom.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/custom.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAwBzB,oFAAoF;AACpF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,IAA2B;IAE3B,MAAM,UAAU,GAAG,MAAM,2BAA2B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAElE,gFAAgF;IAChF,kFAAkF;IAClF,iFAAiF;IACjF,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,MAAM,4DAA4D,CAC3E,CAAC;IACJ,CAAC;IAED,iFAAiF;IACjF,yCAAyC;IACzC,MAAM,EACJ,MAAM,EAAE,OAAO,EACf,QAAQ,EAAE,KAAK,EACf,GAAG,SAAS,EACb,GAAgC,IAAI,CAAC,iBAAiB,IAAI,EAAE,CAAC;IAC9D,MAAM,IAAI,GAAuB,EAAE,GAAG,UAAU,EAAE,GAAG,SAAS,EAAE,CAAC;IACjE,MAAM,eAAe,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,gBAAgB,CAAC;IAE7D,+EAA+E;IAC/E,8EAA8E;IAC9E,MAAM,aAAa,GAAuB,IAAI,CAAC,SAAS;QACtD,CAAC,CAAC;YACE,GAAG,IAAI;YACP,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC;YACzC,gBAAgB,EAAE,eAAe;SAClC;QACH,CAAC,CAAC,IAAI,CAAC;IAET,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,aAAa;QACb,MAAM,EAAE;YACN,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,UAAU,CAAC,QAAQ;SAC7B;QACD,eAAe;QACf,cAAc,EAAE,IAAI,CAAC,cAAc;KACpC,CAAC;AACJ,CAAC","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport {\n type DiscoveredMetadata,\n discoverAuthorizationServer,\n} from \"../discovery.js\";\nimport type { OAuthConfig } from \"../index.js\";\n\n/** Options accepted by {@link customProvider} and the branded providers. */\nexport type CustomProviderOptions = {\n issuer: string;\n /** Expected token `aud`. Omit to skip audience verification — only for IdPs\n * that don't bind an audience (e.g. Clerk). Branded providers whose IdP does\n * bind an audience re-require it in their own options. */\n audience?: string;\n /** Omit to let the server infer the resource origin from request headers. */\n baseUrl?: string;\n /** Advertise THIS server as the authorization server (skybridge-as-AS): the\n * served AS metadata `issuer` and the PRM `authorization_servers` use this URL\n * instead of the IdP's. Needed when skybridge must sit in the auth path — Auth0\n * (the `audience` must be baked into `/authorize`) or the Alpic DCR proxy (Alpic\n * injects the registration endpoint). Use the static public URL; `verify.issuer`\n * stays the IdP (the token's real `iss`). */\n serverUrl?: string;\n scopes?: string[];\n requiredScopes?: string[];\n metadataOverrides?: Omit<Partial<OAuthMetadata>, \"issuer\">;\n};\n\n/** Builds a complete {@link OAuthConfig} from an IdP's OAuth discovery document. */\nexport async function customProvider(\n opts: CustomProviderOptions,\n): Promise<OAuthConfig> {\n const discovered = await discoverAuthorizationServer(opts.issuer);\n\n // JWKS verification needs a signing-key URL; without it the server can't verify\n // tokens. (`registration_endpoint` is optional per RFC 8414 — a no-DCR IdP simply\n // omits it; a proxy like Alpic, or pre-registered clients, supply registration.)\n if (!discovered.jwks_uri) {\n throw new Error(\n `${opts.issuer} discovery has no jwks_uri; JWKS verification requires it.`,\n );\n }\n\n // Overrides adjust only advertised metadata; the trust anchor (issuer, jwks_uri)\n // always comes from validated discovery.\n const {\n issuer: _issuer,\n jwks_uri: _jwks,\n ...overrides\n }: Partial<DiscoveredMetadata> = opts.metadataOverrides ?? {};\n const base: DiscoveredMetadata = { ...discovered, ...overrides };\n const scopesSupported = opts.scopes ?? base.scopes_supported;\n\n // serverUrl => skybridge-as-AS: advertise this server's URL as the issuer (and\n // keep the served scopes in sync). The verifier still trusts the IdP's `iss`.\n const oauthMetadata: DiscoveredMetadata = opts.serverUrl\n ? {\n ...base,\n issuer: opts.serverUrl.replace(/\\/$/, \"\"),\n scopes_supported: scopesSupported,\n }\n : base;\n\n return {\n baseUrl: opts.baseUrl,\n oauthMetadata,\n verify: {\n issuer: discovered.issuer,\n audience: opts.audience,\n jwksUri: discovered.jwks_uri,\n },\n scopesSupported,\n requiredScopes: opts.requiredScopes,\n };\n}\n"]}
@@ -50,20 +50,6 @@ describe("customProvider", () => {
50
50
  expect(config.scopesSupported).toEqual(["openid"]);
51
51
  expect(config.oauthMetadata.registration_endpoint).toBe(`${base}/register`);
52
52
  });
53
- it("defaults scopesSupported to the discovered scopes", async () => {
54
- const base = await serveDiscovery();
55
- const config = await customProvider({
56
- issuer: base,
57
- audience: "a",
58
- baseUrl: "https://app.example.test",
59
- });
60
- expect(config.scopesSupported).toEqual(["openid", "email", "profile"]);
61
- });
62
- it("allows omitting baseUrl (server infers it from headers)", async () => {
63
- const base = await serveDiscovery();
64
- const config = await customProvider({ issuer: base, audience: "a" });
65
- expect(config.baseUrl).toBeUndefined();
66
- });
67
53
  it("ignores a runtime issuer override (keeps the discovered trust anchor)", async () => {
68
54
  const base = await serveDiscovery();
69
55
  const config = await customProvider({
@@ -83,21 +69,29 @@ describe("customProvider", () => {
83
69
  });
84
70
  expect(config.verify.jwksUri).toBe(`${base}/jwks`);
85
71
  });
86
- it("rejects a non-DCR IdP (no registration_endpoint)", async () => {
72
+ it("serves metadata without a registration_endpoint when the IdP has no DCR", async () => {
87
73
  const base = await serveDiscovery({ registration_endpoint: undefined });
88
- await expect(customProvider({
89
- issuer: base,
90
- audience: "a",
91
- baseUrl: "https://app.example.test",
92
- })).rejects.toThrow(/not DCR-compatible/);
74
+ const config = await customProvider({ issuer: base, audience: "a" });
75
+ expect(config.oauthMetadata.registration_endpoint).toBeUndefined();
76
+ expect(config.verify.issuer).toBe(base);
93
77
  });
94
- it("rejects a non-DCR IdP even if an override supplies registration_endpoint", async () => {
95
- const base = await serveDiscovery({ registration_endpoint: undefined });
96
- await expect(customProvider({
78
+ it("throws when discovery has no jwks_uri (token verification needs it)", async () => {
79
+ const base = await serveDiscovery({ jwks_uri: undefined });
80
+ await expect(customProvider({ issuer: base, audience: "a" })).rejects.toThrow(/jwks_uri/);
81
+ });
82
+ it("serverUrl advertises this server as the AS, keeping the IdP as the token issuer", async () => {
83
+ const base = await serveDiscovery();
84
+ const config = await customProvider({
97
85
  issuer: base,
98
86
  audience: "a",
99
- metadataOverrides: { registration_endpoint: `${base}/register` },
100
- })).rejects.toThrow(/not DCR-compatible/);
87
+ serverUrl: "https://app.example.test/",
88
+ scopes: ["openid"],
89
+ });
90
+ // AS metadata + advertised scopes reflect this server.
91
+ expect(config.oauthMetadata.issuer).toBe("https://app.example.test");
92
+ expect(config.oauthMetadata.scopes_supported).toEqual(["openid"]);
93
+ // but the token's issuer is still verified against the IdP.
94
+ expect(config.verify.issuer).toBe(base);
101
95
  });
102
96
  it("applies metadataOverrides over discovered values", async () => {
103
97
  const base = await serveDiscovery();