skybridge 0.0.0-dev.d2cb8f6 → 0.0.0-dev.d33e6f5

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 (327) hide show
  1. package/README.md +15 -14
  2. package/dist/cli/build-helpers.d.ts +2 -2
  3. package/dist/cli/build-helpers.js +13 -5
  4. package/dist/cli/build-helpers.js.map +1 -1
  5. package/dist/cli/build-helpers.test.js +30 -4
  6. package/dist/cli/build-helpers.test.js.map +1 -1
  7. package/dist/cli/build-steps.js +17 -6
  8. package/dist/cli/build-steps.js.map +1 -1
  9. package/dist/cli/build-steps.test.js +23 -3
  10. package/dist/cli/build-steps.test.js.map +1 -1
  11. package/dist/cli/detect-port.js +0 -1
  12. package/dist/cli/detect-port.js.map +1 -1
  13. package/dist/cli/resolve-skybridge-config.d.ts +1 -0
  14. package/dist/cli/resolve-skybridge-config.js +11 -1
  15. package/dist/cli/resolve-skybridge-config.js.map +1 -1
  16. package/dist/cli/run-plain.js +2 -1
  17. package/dist/cli/run-plain.js.map +1 -1
  18. package/dist/cli/telemetry.js +2 -0
  19. package/dist/cli/telemetry.js.map +1 -1
  20. package/dist/cli/use-execute-steps.d.ts +1 -1
  21. package/dist/cli/use-nodemon.d.ts +11 -1
  22. package/dist/cli/use-nodemon.js +30 -8
  23. package/dist/cli/use-nodemon.js.map +1 -1
  24. package/dist/cli/use-typescript-check.js +3 -1
  25. package/dist/cli/use-typescript-check.js.map +1 -1
  26. package/dist/commands/dev.js +4 -4
  27. package/dist/commands/dev.js.map +1 -1
  28. package/dist/commands/start.js +2 -2
  29. package/dist/commands/start.js.map +1 -1
  30. package/dist/context-warnings.d.ts +5 -0
  31. package/dist/context-warnings.js +31 -0
  32. package/dist/context-warnings.js.map +1 -0
  33. package/dist/context-warnings.test.js +28 -0
  34. package/dist/context-warnings.test.js.map +1 -0
  35. package/dist/server/app.d.ts +211 -0
  36. package/dist/server/app.js +277 -0
  37. package/dist/server/app.js.map +1 -0
  38. package/dist/server/app.test.js +178 -0
  39. package/dist/server/app.test.js.map +1 -0
  40. package/dist/server/auth/discovery.d.ts +2 -1
  41. package/dist/server/auth/discovery.js +2 -2
  42. package/dist/server/auth/discovery.js.map +1 -1
  43. package/dist/server/auth/index.d.ts +35 -5
  44. package/dist/server/auth/index.js.map +1 -1
  45. package/dist/server/auth/providers/auth0.d.ts +25 -3
  46. package/dist/server/auth/providers/auth0.js +13 -8
  47. package/dist/server/auth/providers/auth0.js.map +1 -1
  48. package/dist/server/auth/providers/auth0.test.js +5 -3
  49. package/dist/server/auth/providers/auth0.test.js.map +1 -1
  50. package/dist/server/auth/providers/authplane.d.ts +4 -2
  51. package/dist/server/auth/providers/authplane.js.map +1 -1
  52. package/dist/server/auth/providers/authplane.test.js +23 -18
  53. package/dist/server/auth/providers/authplane.test.js.map +1 -1
  54. package/dist/server/auth/providers/clerk.d.ts +40 -3
  55. package/dist/server/auth/providers/clerk.js +4 -1
  56. package/dist/server/auth/providers/clerk.js.map +1 -1
  57. package/dist/server/auth/providers/clerk.test.js +5 -3
  58. package/dist/server/auth/providers/clerk.test.js.map +1 -1
  59. package/dist/server/auth/providers/custom.d.ts +16 -6
  60. package/dist/server/auth/providers/custom.js +14 -4
  61. package/dist/server/auth/providers/custom.js.map +1 -1
  62. package/dist/server/auth/providers/custom.test.js +24 -19
  63. package/dist/server/auth/providers/custom.test.js.map +1 -1
  64. package/dist/server/auth/providers/descope.d.ts +32 -3
  65. package/dist/server/auth/providers/descope.js.map +1 -1
  66. package/dist/server/auth/providers/descope.test.js +8 -6
  67. package/dist/server/auth/providers/descope.test.js.map +1 -1
  68. package/dist/server/auth/providers/stytch.d.ts +16 -3
  69. package/dist/server/auth/providers/stytch.js +4 -1
  70. package/dist/server/auth/providers/stytch.js.map +1 -1
  71. package/dist/server/auth/providers/verify-spy.d.ts +12 -0
  72. package/dist/server/auth/providers/verify-spy.js +21 -0
  73. package/dist/server/auth/providers/verify-spy.js.map +1 -0
  74. package/dist/server/auth/providers/workos.d.ts +28 -5
  75. package/dist/server/auth/providers/workos.js +6 -2
  76. package/dist/server/auth/providers/workos.js.map +1 -1
  77. package/dist/server/auth/setup.d.ts +1 -1
  78. package/dist/server/auth/setup.js +4 -5
  79. package/dist/server/auth/setup.js.map +1 -1
  80. package/dist/server/auth/setup.test.js +97 -57
  81. package/dist/server/auth/setup.test.js.map +1 -1
  82. package/dist/server/auth/verify.d.ts +42 -6
  83. package/dist/server/auth/verify.js +27 -3
  84. package/dist/server/auth/verify.js.map +1 -1
  85. package/dist/server/auth/verify.test.js +65 -0
  86. package/dist/server/auth/verify.test.js.map +1 -1
  87. package/dist/server/auth-extra.test-d.js +130 -0
  88. package/dist/server/auth-extra.test-d.js.map +1 -0
  89. package/dist/server/auth.d.ts +43 -6
  90. package/dist/server/auth.js +4 -5
  91. package/dist/server/auth.js.map +1 -1
  92. package/dist/server/build-manifest.test.js +7 -6
  93. package/dist/server/build-manifest.test.js.map +1 -1
  94. package/dist/server/content-helpers.d.ts +1 -1
  95. package/dist/server/content-helpers.js.map +1 -1
  96. package/dist/server/events.d.ts +113 -0
  97. package/dist/server/events.js +318 -0
  98. package/dist/server/events.js.map +1 -0
  99. package/dist/server/events.test.js +172 -0
  100. package/dist/server/events.test.js.map +1 -0
  101. package/dist/server/express.d.ts +24 -3
  102. package/dist/server/express.js +44 -32
  103. package/dist/server/express.js.map +1 -1
  104. package/dist/server/express.test.js +265 -103
  105. package/dist/server/express.test.js.map +1 -1
  106. package/dist/server/file-ref.d.ts +1 -1
  107. package/dist/server/file-ref.js +1 -1
  108. package/dist/server/file-ref.js.map +1 -1
  109. package/dist/server/form-input.d.ts +120 -0
  110. package/dist/server/form-input.js +89 -0
  111. package/dist/server/form-input.js.map +1 -0
  112. package/dist/server/index.d.ts +15 -8
  113. package/dist/server/index.js +8 -2
  114. package/dist/server/index.js.map +1 -1
  115. package/dist/server/mcp-errors.test.js +229 -0
  116. package/dist/server/mcp-errors.test.js.map +1 -0
  117. package/dist/server/metric.d.ts +5 -3
  118. package/dist/server/metric.js +5 -5
  119. package/dist/server/metric.js.map +1 -1
  120. package/dist/server/middleware.d.ts +35 -10
  121. package/dist/server/middleware.js +33 -0
  122. package/dist/server/middleware.js.map +1 -1
  123. package/dist/server/middleware.test-d.js +1 -1
  124. package/dist/server/middleware.test-d.js.map +1 -1
  125. package/dist/server/middleware.test.js +242 -134
  126. package/dist/server/middleware.test.js.map +1 -1
  127. package/dist/server/register-tool.test.js +230 -0
  128. package/dist/server/register-tool.test.js.map +1 -0
  129. package/dist/server/server.d.ts +338 -128
  130. package/dist/server/server.js +389 -305
  131. package/dist/server/server.js.map +1 -1
  132. package/dist/server/skills-integration.test.js +44 -21
  133. package/dist/server/skills-integration.test.js.map +1 -1
  134. package/dist/server/skills.d.ts +14 -10
  135. package/dist/server/skills.js +49 -39
  136. package/dist/server/skills.js.map +1 -1
  137. package/dist/server/skills.test.js +77 -44
  138. package/dist/server/skills.test.js.map +1 -1
  139. package/dist/server/templateHelper.d.ts +1 -3
  140. package/dist/server/templateHelper.js.map +1 -1
  141. package/dist/server/templates.generated.js +28 -18
  142. package/dist/server/templates.generated.js.map +1 -1
  143. package/dist/server/tool-extra.test.d.ts +1 -0
  144. package/dist/server/tool-extra.test.js +167 -0
  145. package/dist/server/tool-extra.test.js.map +1 -0
  146. package/dist/server/view-resource-resolution.test.js +53 -13
  147. package/dist/server/view-resource-resolution.test.js.map +1 -1
  148. package/dist/test/utils.d.ts +9 -11
  149. package/dist/test/utils.js +220 -201
  150. package/dist/test/utils.js.map +1 -1
  151. package/dist/test/view.test.js +41 -37
  152. package/dist/test/view.test.js.map +1 -1
  153. package/dist/views/index.d.ts +10 -0
  154. package/dist/views/index.js +3 -0
  155. package/dist/views/index.js.map +1 -0
  156. package/dist/{web/plugin → views}/scan-views.js +1 -1
  157. package/dist/views/scan-views.js.map +1 -0
  158. package/dist/views/scan-views.test.d.ts +1 -0
  159. package/dist/views/scan-views.test.js.map +1 -0
  160. package/dist/views/validate-view.d.ts +1 -0
  161. package/dist/{web/plugin → views}/validate-view.js +2 -2
  162. package/dist/views/validate-view.js.map +1 -0
  163. package/dist/views/validate-view.test.d.ts +1 -0
  164. package/dist/views/validate-view.test.js.map +1 -0
  165. package/dist/web/bridges/adaptor.d.ts +14 -3
  166. package/dist/web/bridges/adaptor.js +168 -29
  167. package/dist/web/bridges/adaptor.js.map +1 -1
  168. package/dist/web/bridges/adaptor.test.js +77 -9
  169. package/dist/web/bridges/adaptor.test.js.map +1 -1
  170. package/dist/web/bridges/apps-sdk/bridge.js +1 -1
  171. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
  172. package/dist/web/bridges/apps-sdk/types.d.ts +3 -4
  173. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  174. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +4 -4
  175. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +4 -4
  176. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
  177. package/dist/web/bridges/get-adaptor.test.js +2 -2
  178. package/dist/web/bridges/get-adaptor.test.js.map +1 -1
  179. package/dist/web/bridges/mcp-app/bridge.d.ts +1 -1
  180. package/dist/web/bridges/mcp-app/bridge.js +10 -3
  181. package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
  182. package/dist/web/bridges/mcp-app/bridge.test.js +2 -2
  183. package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -1
  184. package/dist/web/bridges/mcp-app/types.d.ts +18 -3
  185. package/dist/web/bridges/mcp-app/types.js.map +1 -1
  186. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +2 -2
  187. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +1 -1
  188. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
  189. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +1 -1
  190. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
  191. package/dist/web/bridges/mcp-app/view-tools.test.js +1 -1
  192. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
  193. package/dist/web/bridges/types.d.ts +74 -16
  194. package/dist/web/bridges/types.js +1 -1
  195. package/dist/web/bridges/types.js.map +1 -1
  196. package/dist/web/create-store.test.js +2 -2
  197. package/dist/web/create-store.test.js.map +1 -1
  198. package/dist/web/data-llm.js +3 -2
  199. package/dist/web/data-llm.js.map +1 -1
  200. package/dist/web/data-llm.test.js +11 -4
  201. package/dist/web/data-llm.test.js.map +1 -1
  202. package/dist/web/generate-helpers.d.ts +23 -19
  203. package/dist/web/generate-helpers.js +21 -17
  204. package/dist/web/generate-helpers.js.map +1 -1
  205. package/dist/web/generate-helpers.test-d.js +3 -3
  206. package/dist/web/generate-helpers.test-d.js.map +1 -1
  207. package/dist/web/generate-helpers.test.js +2 -2
  208. package/dist/web/generate-helpers.test.js.map +1 -1
  209. package/dist/web/hooks/index.d.ts +6 -2
  210. package/dist/web/hooks/index.js +6 -2
  211. package/dist/web/hooks/index.js.map +1 -1
  212. package/dist/web/hooks/openai-extensions.test.d.ts +1 -0
  213. package/dist/web/hooks/openai-extensions.test.js +107 -0
  214. package/dist/web/hooks/openai-extensions.test.js.map +1 -0
  215. package/dist/web/hooks/use-call-tool.d.ts +2 -2
  216. package/dist/web/hooks/use-call-tool.js +15 -10
  217. package/dist/web/hooks/use-call-tool.js.map +1 -1
  218. package/dist/web/hooks/use-call-tool.test.js +15 -4
  219. package/dist/web/hooks/use-call-tool.test.js.map +1 -1
  220. package/dist/web/hooks/use-deep-link.d.ts +22 -0
  221. package/dist/web/hooks/use-deep-link.js +34 -0
  222. package/dist/web/hooks/use-deep-link.js.map +1 -0
  223. package/dist/web/hooks/use-display-mode.d.ts +1 -1
  224. package/dist/web/hooks/use-display-mode.js +1 -1
  225. package/dist/web/hooks/use-display-mode.js.map +1 -1
  226. package/dist/web/hooks/use-display-mode.test.js +1 -1
  227. package/dist/web/hooks/use-display-mode.test.js.map +1 -1
  228. package/dist/web/hooks/use-download.d.ts +1 -3
  229. package/dist/web/hooks/use-download.js +1 -2
  230. package/dist/web/hooks/use-download.js.map +1 -1
  231. package/dist/web/hooks/use-download.test.js +7 -7
  232. package/dist/web/hooks/use-download.test.js.map +1 -1
  233. package/dist/web/hooks/use-file-resource.d.ts +37 -0
  234. package/dist/web/hooks/use-file-resource.js +98 -0
  235. package/dist/web/hooks/use-file-resource.js.map +1 -0
  236. package/dist/web/hooks/use-file-resource.test.d.ts +1 -0
  237. package/dist/web/hooks/use-file-resource.test.js +65 -0
  238. package/dist/web/hooks/use-file-resource.test.js.map +1 -0
  239. package/dist/web/hooks/use-files.test.js +1 -1
  240. package/dist/web/hooks/use-files.test.js.map +1 -1
  241. package/dist/web/hooks/use-host.d.ts +44 -0
  242. package/dist/web/hooks/use-host.js +45 -0
  243. package/dist/web/hooks/use-host.js.map +1 -0
  244. package/dist/web/hooks/use-host.test.d.ts +1 -0
  245. package/dist/web/hooks/{use-host-info.test.js → use-host.test.js} +7 -7
  246. package/dist/web/hooks/use-host.test.js.map +1 -0
  247. package/dist/web/hooks/use-model-context.d.ts +44 -0
  248. package/dist/web/hooks/use-model-context.js +63 -0
  249. package/dist/web/hooks/use-model-context.js.map +1 -0
  250. package/dist/web/hooks/use-open-external.test.js +2 -2
  251. package/dist/web/hooks/use-open-external.test.js.map +1 -1
  252. package/dist/web/hooks/use-open-file.d.ts +20 -0
  253. package/dist/web/hooks/use-open-file.js +25 -0
  254. package/dist/web/hooks/use-open-file.js.map +1 -0
  255. package/dist/web/hooks/use-register-view-tool.d.ts +3 -4
  256. package/dist/web/hooks/use-register-view-tool.js +1 -1
  257. package/dist/web/hooks/use-register-view-tool.js.map +1 -1
  258. package/dist/web/hooks/use-request-close.test.js +1 -1
  259. package/dist/web/hooks/use-request-close.test.js.map +1 -1
  260. package/dist/web/hooks/use-request-modal.test.js +1 -1
  261. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  262. package/dist/web/hooks/use-request-size.d.ts +1 -1
  263. package/dist/web/hooks/use-request-size.js +1 -1
  264. package/dist/web/hooks/use-request-size.js.map +1 -1
  265. package/dist/web/hooks/use-request-size.test.js +1 -1
  266. package/dist/web/hooks/use-request-size.test.js.map +1 -1
  267. package/dist/web/hooks/use-send-follow-up-message.d.ts +8 -2
  268. package/dist/web/hooks/use-send-follow-up-message.js +6 -0
  269. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
  270. package/dist/web/hooks/use-set-open-in-app-url.test.js +2 -8
  271. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
  272. package/dist/web/hooks/use-tool-info.d.ts +1 -20
  273. package/dist/web/hooks/use-tool-info.js +0 -1
  274. package/dist/web/hooks/use-tool-info.js.map +1 -1
  275. package/dist/web/hooks/use-tool-info.test-d.js +0 -5
  276. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
  277. package/dist/web/hooks/use-tool-info.test.js +2 -7
  278. package/dist/web/hooks/use-tool-info.test.js.map +1 -1
  279. package/dist/web/hooks/use-user.d.ts +3 -2
  280. package/dist/web/hooks/use-user.js +4 -3
  281. package/dist/web/hooks/use-user.js.map +1 -1
  282. package/dist/web/hooks/use-user.test.js +5 -3
  283. package/dist/web/hooks/use-user.test.js.map +1 -1
  284. package/dist/web/hooks/use-view-state.test.js +2 -2
  285. package/dist/web/hooks/use-view-state.test.js.map +1 -1
  286. package/dist/web/hooks/use-viewport.d.ts +20 -0
  287. package/dist/web/hooks/use-viewport.js +21 -0
  288. package/dist/web/hooks/use-viewport.js.map +1 -0
  289. package/dist/web/hooks/use-viewport.test.d.ts +1 -0
  290. package/dist/web/hooks/{use-layout.test.js → use-viewport.test.js} +12 -32
  291. package/dist/web/hooks/use-viewport.test.js.map +1 -0
  292. package/dist/web/mount-view.d.ts +3 -2
  293. package/dist/web/mount-view.js +6 -11
  294. package/dist/web/mount-view.js.map +1 -1
  295. package/package.json +17 -13
  296. package/dist/web/hooks/use-host-info.d.ts +0 -22
  297. package/dist/web/hooks/use-host-info.js +0 -30
  298. package/dist/web/hooks/use-host-info.js.map +0 -1
  299. package/dist/web/hooks/use-host-info.test.js.map +0 -1
  300. package/dist/web/hooks/use-layout.d.ts +0 -24
  301. package/dist/web/hooks/use-layout.js +0 -25
  302. package/dist/web/hooks/use-layout.js.map +0 -1
  303. package/dist/web/hooks/use-layout.test.js.map +0 -1
  304. package/dist/web/plugin/data-llm.test.js +0 -81
  305. package/dist/web/plugin/data-llm.test.js.map +0 -1
  306. package/dist/web/plugin/plugin.d.ts +0 -39
  307. package/dist/web/plugin/plugin.js +0 -189
  308. package/dist/web/plugin/plugin.js.map +0 -1
  309. package/dist/web/plugin/scan-views.js.map +0 -1
  310. package/dist/web/plugin/scan-views.test.js.map +0 -1
  311. package/dist/web/plugin/transform-data-llm.d.ts +0 -12
  312. package/dist/web/plugin/transform-data-llm.js +0 -96
  313. package/dist/web/plugin/transform-data-llm.js.map +0 -1
  314. package/dist/web/plugin/transform-data-llm.test.js +0 -81
  315. package/dist/web/plugin/transform-data-llm.test.js.map +0 -1
  316. package/dist/web/plugin/validate-view.d.ts +0 -1
  317. package/dist/web/plugin/validate-view.js.map +0 -1
  318. package/dist/web/plugin/validate-view.test.js.map +0 -1
  319. /package/dist/{web/hooks/use-host-info.test.d.ts → context-warnings.test.d.ts} +0 -0
  320. /package/dist/{web/hooks/use-layout.test.d.ts → server/app.test.d.ts} +0 -0
  321. /package/dist/{web/plugin/data-llm.test.d.ts → server/auth-extra.test-d.d.ts} +0 -0
  322. /package/dist/{web/plugin/scan-views.test.d.ts → server/events.test.d.ts} +0 -0
  323. /package/dist/{web/plugin/transform-data-llm.test.d.ts → server/mcp-errors.test.d.ts} +0 -0
  324. /package/dist/{web/plugin/validate-view.test.d.ts → server/register-tool.test.d.ts} +0 -0
  325. /package/dist/{web/plugin → views}/scan-views.d.ts +0 -0
  326. /package/dist/{web/plugin → views}/scan-views.test.js +0 -0
  327. /package/dist/{web/plugin → views}/validate-view.test.js +0 -0
@@ -0,0 +1,178 @@
1
+ import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
2
+ import { describe, expect, it } from "vitest";
3
+ import { Skybridge } from "./app.js";
4
+ const oauthConfig = {
5
+ oauthMetadata: {
6
+ issuer: "https://issuer.example.com",
7
+ authorization_endpoint: "https://issuer.example.com/authorize",
8
+ token_endpoint: "https://issuer.example.com/token",
9
+ response_types_supported: ["code"],
10
+ },
11
+ verifier: {
12
+ verifyAccessToken: async () => ({
13
+ token: "t",
14
+ clientId: "c",
15
+ scopes: [],
16
+ extra: {},
17
+ }),
18
+ },
19
+ };
20
+ describe("Skybridge setup and handler", () => {
21
+ it("resolves setup once and passes the context to the per-request handler", async () => {
22
+ let loads = 0;
23
+ const app = new Skybridge({
24
+ name: "test",
25
+ version: "1.0.0",
26
+ setup: async () => {
27
+ loads++;
28
+ return { greeting: await Promise.resolve("hello") };
29
+ },
30
+ handler: (server, { greeting }) => server.registerTool({ name: "greet", description: "greet" }, async () => ({ content: [{ type: "text", text: greeting }] })),
31
+ });
32
+ expect(loads).toBe(0);
33
+ const call = async () => {
34
+ const client = new Client({ name: "client", version: "1.0.0" });
35
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
36
+ await app.connect(serverTransport);
37
+ await client.connect(clientTransport);
38
+ const result = await client.callTool({ name: "greet" });
39
+ await client.close();
40
+ return result;
41
+ };
42
+ expect(await call()).toMatchObject({
43
+ content: [{ type: "text", text: "hello" }],
44
+ });
45
+ expect(await call()).toMatchObject({
46
+ content: [{ type: "text", text: "hello" }],
47
+ });
48
+ expect(loads).toBe(1);
49
+ });
50
+ it("retries setup after a failed first resolution", async () => {
51
+ let attempts = 0;
52
+ const app = new Skybridge({
53
+ name: "test",
54
+ version: "1.0.0",
55
+ setup: async () => {
56
+ attempts++;
57
+ if (attempts === 1) {
58
+ throw new Error("transient");
59
+ }
60
+ },
61
+ handler: (server) => server,
62
+ });
63
+ const [, first] = InMemoryTransport.createLinkedPair();
64
+ await expect(app.connect(first)).rejects.toThrow("transient");
65
+ const [, second] = InMemoryTransport.createLinkedPair();
66
+ await expect(app.connect(second)).resolves.toBeUndefined();
67
+ expect(attempts).toBe(2);
68
+ });
69
+ it("rejects a handler that returns a promise", async () => {
70
+ const app = new Skybridge({
71
+ name: "test",
72
+ version: "1.0.0",
73
+ // @ts-expect-error the handler runs per request and must stay synchronous
74
+ handler: async (server) => server,
75
+ });
76
+ const [, serverTransport] = InMemoryTransport.createLinkedPair();
77
+ await expect(app.connect(serverTransport)).rejects.toThrow(/must be synchronous/);
78
+ });
79
+ it("resolves an oauth function with the setup config before wiring the routes", async () => {
80
+ let seen;
81
+ const app = new Skybridge({
82
+ name: "test",
83
+ version: "1.0.0",
84
+ setup: () => ({ issuer: "https://issuer.example.com" }),
85
+ oauth: (config) => {
86
+ seen = config;
87
+ return oauthConfig;
88
+ },
89
+ handler: (server) => server,
90
+ });
91
+ expect(seen).toBeUndefined();
92
+ const [, serverTransport] = InMemoryTransport.createLinkedPair();
93
+ await app.connect(serverTransport);
94
+ expect(seen).toEqual({ issuer: "https://issuer.example.com" });
95
+ });
96
+ });
97
+ it("registers outer middleware on the app while preserving its setup lifecycle", async () => {
98
+ let loads = 0;
99
+ const events = [];
100
+ const app = new Skybridge({
101
+ name: "outer-middleware",
102
+ version: "1",
103
+ setup: () => ++loads,
104
+ handler: (server) => server
105
+ .registerTool({ name: "echo" }, () => ({ content: "live" }))
106
+ .mcpMiddleware("tools/call", async (_request, _extra, next) => {
107
+ events.push("app:before");
108
+ const result = await next();
109
+ events.push("app:after");
110
+ return result;
111
+ }),
112
+ });
113
+ const configured = app
114
+ .withMcpMiddleware({
115
+ filter: "tools/call",
116
+ handler: async (_request, _extra, next) => {
117
+ events.push("outer:before");
118
+ const result = await next();
119
+ events.push("outer:after");
120
+ return result;
121
+ },
122
+ })
123
+ .withMcpMiddleware({
124
+ filter: "tools/list",
125
+ handler: (_request, _extra, next) => {
126
+ events.push("list");
127
+ return next();
128
+ },
129
+ });
130
+ const [outer, ordinary, conversation] = await Promise.all([
131
+ configured.createServerInstance(),
132
+ app.createServerInstance(),
133
+ app.createServerInstance([
134
+ {
135
+ filter: "tools/call",
136
+ handler: async (_request, _extra, next) => {
137
+ events.push("conversation:before");
138
+ const result = await next();
139
+ events.push("conversation:after");
140
+ return result;
141
+ },
142
+ },
143
+ ]),
144
+ ]);
145
+ expect(configured).toBe(app);
146
+ expect(outer).not.toBe(ordinary);
147
+ expect(loads).toBe(1);
148
+ for (const [server, expected] of [
149
+ [
150
+ conversation,
151
+ [
152
+ "conversation:before",
153
+ "outer:before",
154
+ "app:before",
155
+ "app:after",
156
+ "outer:after",
157
+ "conversation:after",
158
+ ],
159
+ ],
160
+ [outer, ["outer:before", "app:before", "app:after", "outer:after"]],
161
+ [ordinary, ["outer:before", "app:before", "app:after", "outer:after"]],
162
+ ]) {
163
+ const client = new Client({ name: "test", version: "1" });
164
+ const [a, b] = InMemoryTransport.createLinkedPair();
165
+ try {
166
+ await server.connect(b);
167
+ await client.connect(a);
168
+ events.length = 0;
169
+ await client.callTool({ name: "echo" });
170
+ expect(events).toEqual(expected);
171
+ }
172
+ finally {
173
+ await client.close();
174
+ await server.close();
175
+ }
176
+ }
177
+ });
178
+ //# sourceMappingURL=app.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.test.js","sourceRoot":"","sources":["../../src/server/app.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { Client, InMemoryTransport } from \"@modelcontextprotocol/client\";\nimport { describe, expect, it } from \"vitest\";\nimport { Skybridge } from \"./app.js\";\n\nconst oauthConfig = {\n oauthMetadata: {\n issuer: \"https://issuer.example.com\",\n authorization_endpoint: \"https://issuer.example.com/authorize\",\n token_endpoint: \"https://issuer.example.com/token\",\n response_types_supported: [\"code\"],\n },\n verifier: {\n verifyAccessToken: async () => ({\n token: \"t\",\n clientId: \"c\",\n scopes: [],\n extra: {},\n }),\n },\n};\n\ndescribe(\"Skybridge setup and handler\", () => {\n it(\"resolves setup once and passes the context to the per-request handler\", async () => {\n let loads = 0;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: async () => {\n loads++;\n return { greeting: await Promise.resolve(\"hello\") };\n },\n handler: (server, { greeting }) =>\n server.registerTool(\n { name: \"greet\", description: \"greet\" },\n async () => ({ content: [{ type: \"text\", text: greeting }] }),\n ),\n });\n\n expect(loads).toBe(0);\n\n const call = async () => {\n const client = new Client({ name: \"client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await app.connect(serverTransport);\n await client.connect(clientTransport);\n const result = await client.callTool({ name: \"greet\" });\n await client.close();\n return result;\n };\n\n expect(await call()).toMatchObject({\n content: [{ type: \"text\", text: \"hello\" }],\n });\n expect(await call()).toMatchObject({\n content: [{ type: \"text\", text: \"hello\" }],\n });\n expect(loads).toBe(1);\n });\n\n it(\"retries setup after a failed first resolution\", async () => {\n let attempts = 0;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: async () => {\n attempts++;\n if (attempts === 1) {\n throw new Error(\"transient\");\n }\n },\n handler: (server) => server,\n });\n\n const [, first] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(first)).rejects.toThrow(\"transient\");\n\n const [, second] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(second)).resolves.toBeUndefined();\n expect(attempts).toBe(2);\n });\n\n it(\"rejects a handler that returns a promise\", async () => {\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n // @ts-expect-error the handler runs per request and must stay synchronous\n handler: async (server) => server,\n });\n const [, serverTransport] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(serverTransport)).rejects.toThrow(\n /must be synchronous/,\n );\n });\n\n it(\"resolves an oauth function with the setup config before wiring the routes\", async () => {\n let seen: unknown;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: () => ({ issuer: \"https://issuer.example.com\" }),\n oauth: (config) => {\n seen = config;\n return oauthConfig;\n },\n handler: (server) => server,\n });\n\n expect(seen).toBeUndefined();\n const [, serverTransport] = InMemoryTransport.createLinkedPair();\n await app.connect(serverTransport);\n expect(seen).toEqual({ issuer: \"https://issuer.example.com\" });\n });\n});\n\nit(\"registers outer middleware on the app while preserving its setup lifecycle\", async () => {\n let loads = 0;\n const events: string[] = [];\n const app = new Skybridge({\n name: \"outer-middleware\",\n version: \"1\",\n setup: () => ++loads,\n handler: (server) =>\n server\n .registerTool({ name: \"echo\" }, () => ({ content: \"live\" }))\n .mcpMiddleware(\"tools/call\", async (_request, _extra, next) => {\n events.push(\"app:before\");\n const result = await next();\n events.push(\"app:after\");\n return result;\n }),\n });\n const configured = app\n .withMcpMiddleware({\n filter: \"tools/call\",\n handler: async (_request, _extra, next) => {\n events.push(\"outer:before\");\n const result = await next();\n events.push(\"outer:after\");\n return result;\n },\n })\n .withMcpMiddleware({\n filter: \"tools/list\",\n handler: (_request, _extra, next) => {\n events.push(\"list\");\n return next();\n },\n });\n const [outer, ordinary, conversation] = await Promise.all([\n configured.createServerInstance(),\n app.createServerInstance(),\n app.createServerInstance([\n {\n filter: \"tools/call\",\n handler: async (_request, _extra, next) => {\n events.push(\"conversation:before\");\n const result = await next();\n events.push(\"conversation:after\");\n return result;\n },\n },\n ]),\n ]);\n expect(configured).toBe(app);\n expect(outer).not.toBe(ordinary);\n expect(loads).toBe(1);\n for (const [server, expected] of [\n [\n conversation,\n [\n \"conversation:before\",\n \"outer:before\",\n \"app:before\",\n \"app:after\",\n \"outer:after\",\n \"conversation:after\",\n ],\n ],\n [outer, [\"outer:before\", \"app:before\", \"app:after\", \"outer:after\"]],\n [ordinary, [\"outer:before\", \"app:before\", \"app:after\", \"outer:after\"]],\n ] as const) {\n const client = new Client({ name: \"test\", version: \"1\" });\n const [a, b] = InMemoryTransport.createLinkedPair();\n try {\n await server.connect(b);\n await client.connect(a);\n events.length = 0;\n await client.callTool({ name: \"echo\" });\n expect(events).toEqual(expected);\n } finally {\n await client.close();\n await server.close();\n }\n }\n});\n"]}
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import { z } from "zod/v4";
2
2
  /**
3
3
  * Discovery doc validated as OAuth AS metadata + `jwks_uri`. The SDK's
4
4
  * `OAuthMetadataSchema` omits `jwks_uri` (its client never verifies tokens), but
@@ -24,6 +24,7 @@ declare const DiscoverySchema: z.ZodObject<{
24
24
  introspection_endpoint_auth_signing_alg_values_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
25
25
  code_challenge_methods_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
26
26
  client_id_metadata_document_supported: z.ZodOptional<z.ZodBoolean>;
27
+ authorization_response_iss_parameter_supported: z.ZodCatch<z.ZodOptional<z.ZodBoolean>>;
27
28
  jwks_uri: z.ZodOptional<z.ZodString>;
28
29
  }, z.core.$loose>;
29
30
  export type DiscoveredMetadata = z.infer<typeof DiscoverySchema>;
@@ -1,5 +1,5 @@
1
- import { OAuthMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
2
- import { z } from "zod";
1
+ import { OAuthMetadataSchema } from "@modelcontextprotocol/core";
2
+ import { z } from "zod/v4";
3
3
  /**
4
4
  * Discovery doc validated as OAuth AS metadata + `jwks_uri`. The SDK's
5
5
  * `OAuthMetadataSchema` omits `jwks_uri` (its client never verifies tokens), but
@@ -1 +1 @@
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"]}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/server/auth/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAE3B;;;;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/core\";\nimport { z } from \"zod/v4\";\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"]}
@@ -1,7 +1,15 @@
1
- import type { OAuthMetadata } from "@modelcontextprotocol/sdk/shared/auth.js";
2
- import type { JwksVerifyConfig } from "./verify.js";
3
- /** Resource-server OAuth config for `SkybridgeServerOptions.oauth`. */
4
- export type OAuthConfig = {
1
+ import type { OAuthMetadata } from "@modelcontextprotocol/server";
2
+ import type { ExtraClaims, TokenVerifier } from "../auth.js";
3
+ /**
4
+ * Resource-server OAuth config for the `oauth` field of `SkybridgeConfig`.
5
+ *
6
+ * `TExtra` comes from the `verifier` and flows on to tool handlers and
7
+ * `mcpMiddleware`, so the claim shape is declared once by whoever checks the
8
+ * token. The branded providers set it from the claims their IdP documents.
9
+ *
10
+ * @typeParam TExtra - Claims the verifier populates in `AuthInfo["extra"]`.
11
+ */
12
+ export type OAuthConfig<TExtra extends ExtraClaims = ExtraClaims> = {
5
13
  /**
6
14
  * Public URL of this server; sets `resourceServerUrl` and the
7
15
  * `resource_metadata` URL. When omitted, it is inferred per request from
@@ -10,9 +18,31 @@ export type OAuthConfig = {
10
18
  baseUrl?: string;
11
19
  /** AS metadata served at `/.well-known/oauth-authorization-server`. */
12
20
  oauthMetadata: OAuthMetadata;
13
- verify: JwksVerifyConfig;
14
21
  /** Scopes advertised in protected-resource metadata. */
15
22
  scopesSupported?: string[];
16
23
  /** Server-wide required-scope floor. */
17
24
  requiredScopes?: string[];
25
+ /**
26
+ * Checks each bearer token. Build one with `createJwksVerifier` for a
27
+ * JWT-issuing IdP, or supply your own for opaque tokens.
28
+ */
29
+ verifier: TokenVerifier<TExtra>;
30
+ };
31
+ /**
32
+ * What the branded providers return: a deferred {@link OAuthConfig}. Discovery
33
+ * runs in `resolve`, so `oauth: auth0Provider(...)` at module scope performs
34
+ * no network access until `Skybridge.run`. Call `resolve` yourself
35
+ * (`await customProvider(...).resolve()`) when wiring the middleware by hand.
36
+ *
37
+ * This is an object rather than a bare `() => Promise<OAuthConfig>` on
38
+ * purpose. TypeScript defers, during the first inference pass, any argument
39
+ * that is a call to a generic function returning a function type. The
40
+ * providers are generic (`workosProvider<TCustom>`), so a function-shaped
41
+ * return written inline in `new Skybridge({ oauth: workosProvider(...) })`
42
+ * would be skipped and `TAuthExtra` would fall back to `ExtraClaims`, losing
43
+ * the claims typing in tool handlers. A non-function shape is inferred
44
+ * eagerly.
45
+ */
46
+ export type OAuthProvider<TExtra extends ExtraClaims = ExtraClaims> = {
47
+ resolve: () => Promise<OAuthConfig<TExtra>>;
18
48
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/auth/index.ts"],"names":[],"mappings":"","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport type { JwksVerifyConfig } from \"./verify.js\";\n\n/** Resource-server OAuth config for `SkybridgeServerOptions.oauth`. */\nexport type OAuthConfig = {\n /**\n * Public URL of this server; sets `resourceServerUrl` and the\n * `resource_metadata` URL. When omitted, it is inferred per request from\n * `x-forwarded-host`/`origin`/`host` headers.\n */\n baseUrl?: string;\n /** AS metadata served at `/.well-known/oauth-authorization-server`. */\n oauthMetadata: OAuthMetadata;\n verify: JwksVerifyConfig;\n /** Scopes advertised in protected-resource metadata. */\n scopesSupported?: string[];\n /** Server-wide required-scope floor. */\n requiredScopes?: string[];\n};\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/server/auth/index.ts"],"names":[],"mappings":"","sourcesContent":["import type { OAuthMetadata } from \"@modelcontextprotocol/server\";\nimport type { ExtraClaims, TokenVerifier } from \"../auth.js\";\n\n/**\n * Resource-server OAuth config for the `oauth` field of `SkybridgeConfig`.\n *\n * `TExtra` comes from the `verifier` and flows on to tool handlers and\n * `mcpMiddleware`, so the claim shape is declared once by whoever checks the\n * token. The branded providers set it from the claims their IdP documents.\n *\n * @typeParam TExtra - Claims the verifier populates in `AuthInfo[\"extra\"]`.\n */\nexport type OAuthConfig<TExtra extends ExtraClaims = ExtraClaims> = {\n /**\n * Public URL of this server; sets `resourceServerUrl` and the\n * `resource_metadata` URL. When omitted, it is inferred per request from\n * `x-forwarded-host`/`origin`/`host` headers.\n */\n baseUrl?: string;\n /** AS metadata served at `/.well-known/oauth-authorization-server`. */\n oauthMetadata: OAuthMetadata;\n /** Scopes advertised in protected-resource metadata. */\n scopesSupported?: string[];\n /** Server-wide required-scope floor. */\n requiredScopes?: string[];\n /**\n * Checks each bearer token. Build one with `createJwksVerifier` for a\n * JWT-issuing IdP, or supply your own for opaque tokens.\n */\n verifier: TokenVerifier<TExtra>;\n};\n\n/**\n * What the branded providers return: a deferred {@link OAuthConfig}. Discovery\n * runs in `resolve`, so `oauth: auth0Provider(...)` at module scope performs\n * no network access until `Skybridge.run`. Call `resolve` yourself\n * (`await customProvider(...).resolve()`) when wiring the middleware by hand.\n *\n * This is an object rather than a bare `() => Promise<OAuthConfig>` on\n * purpose. TypeScript defers, during the first inference pass, any argument\n * that is a call to a generic function returning a function type. The\n * providers are generic (`workosProvider<TCustom>`), so a function-shaped\n * return written inline in `new Skybridge({ oauth: workosProvider(...) })`\n * would be skipped and `TAuthExtra` would fall back to `ExtraClaims`, losing\n * the claims typing in tool handlers. A non-function shape is inferred\n * eagerly.\n */\nexport type OAuthProvider<TExtra extends ExtraClaims = ExtraClaims> = {\n resolve: () => Promise<OAuthConfig<TExtra>>;\n};\n"]}
@@ -1,5 +1,27 @@
1
- import type { OAuthConfig } from "../index.js";
1
+ import type { ExtraClaims } from "../../auth.js";
2
+ import type { OAuthProvider } from "../index.js";
3
+ import type { RegisteredClaims } from "../verify.js";
2
4
  import { type CustomProviderOptions } from "./custom.js";
5
+ /**
6
+ * Claims an Auth0 access token carries for a custom API audience. Auth0 puts no
7
+ * user attributes beyond the id in the token, so there is no `email`: call
8
+ * `/userinfo` for the profile. Custom claims must be namespaced (a bare `email`
9
+ * key is silently dropped), so add them through the type parameter under their
10
+ * full URI.
11
+ *
12
+ * @see https://auth0.com/docs/secure/tokens/access-tokens
13
+ * @see https://auth0.com/docs/secure/tokens/json-web-tokens/create-custom-claims
14
+ */
15
+ export type Auth0Claims = {
16
+ /** Authorized party: the client id the token was issued to. */
17
+ azp?: string;
18
+ /** Only when the API has RBAC and "Add Permissions in the Access Token" on. */
19
+ permissions?: string[];
20
+ /** Organization id, when the tenant uses Organizations. */
21
+ org_id?: string;
22
+ /** Organization name, when the tenant uses Organizations. */
23
+ org_name?: string;
24
+ };
3
25
  /**
4
26
  * OAuth provider for Auth0. `domain` is the tenant domain (e.g.
5
27
  * `acme.us.auth0.com`); `audience` is the API Identifier; `serverUrl` is this
@@ -11,8 +33,8 @@ import { type CustomProviderOptions } from "./custom.js";
11
33
  * `authorization_endpoint`. The token's `aud` is the API; `verify.issuer` stays
12
34
  * Auth0 (the token's real `iss`).
13
35
  */
14
- export declare function auth0Provider(opts: {
36
+ export declare function auth0Provider<TCustom extends ExtraClaims = Record<never, never>>(opts: {
15
37
  domain: string;
16
38
  audience: string;
17
39
  serverUrl: string;
18
- } & Omit<CustomProviderOptions, "issuer" | "audience" | "baseUrl" | "serverUrl">): Promise<OAuthConfig>;
40
+ } & Omit<CustomProviderOptions, "issuer" | "audience" | "baseUrl" | "serverUrl">): OAuthProvider<Auth0Claims & TCustom & RegisteredClaims>;
@@ -11,20 +11,25 @@ import { toIssuerUrl } from "./shared.js";
11
11
  * `authorization_endpoint`. The token's `aud` is the API; `verify.issuer` stays
12
12
  * Auth0 (the token's real `iss`).
13
13
  */
14
- export async function auth0Provider(opts) {
14
+ export function auth0Provider(opts) {
15
15
  const { domain, audience, ...rest } = opts;
16
- const config = await customProvider({
16
+ const provider = customProvider({
17
17
  issuer: toIssuerUrl(domain),
18
18
  audience,
19
19
  ...rest,
20
20
  });
21
- const authUrl = new URL(config.oauthMetadata.authorization_endpoint);
22
- authUrl.searchParams.set("audience", audience);
23
21
  return {
24
- ...config,
25
- oauthMetadata: {
26
- ...config.oauthMetadata,
27
- authorization_endpoint: authUrl.href,
22
+ resolve: async () => {
23
+ const config = await provider.resolve();
24
+ const authUrl = new URL(config.oauthMetadata.authorization_endpoint);
25
+ authUrl.searchParams.set("audience", audience);
26
+ return {
27
+ ...config,
28
+ oauthMetadata: {
29
+ ...config.oauthMetadata,
30
+ authorization_endpoint: authUrl.href,
31
+ },
32
+ };
28
33
  },
29
34
  };
30
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth0.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.ts"],"names":[],"mappings":"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"]}
1
+ {"version":3,"file":"auth0.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAuB1C;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAG3B,IAGC;IAED,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAC3C,MAAM,QAAQ,GAAG,cAAc,CAAwB;QACrD,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC;QAC3B,QAAQ;QACR,GAAG,IAAI;KACR,CAAC,CAAC;IACH,OAAO;QACL,OAAO,EAAE,KAAK,IAAI,EAAE;YAClB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;YACxC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;YACrE,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YAC/C,OAAO;gBACL,GAAG,MAAM;gBACT,aAAa,EAAE;oBACb,GAAG,MAAM,CAAC,aAAa;oBACvB,sBAAsB,EAAE,OAAO,CAAC,IAAI;iBACrC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { ExtraClaims } from \"../../auth.js\";\nimport type { OAuthProvider } from \"../index.js\";\nimport type { RegisteredClaims } from \"../verify.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\nimport { toIssuerUrl } from \"./shared.js\";\n\n/**\n * Claims an Auth0 access token carries for a custom API audience. Auth0 puts no\n * user attributes beyond the id in the token, so there is no `email`: call\n * `/userinfo` for the profile. Custom claims must be namespaced (a bare `email`\n * key is silently dropped), so add them through the type parameter under their\n * full URI.\n *\n * @see https://auth0.com/docs/secure/tokens/access-tokens\n * @see https://auth0.com/docs/secure/tokens/json-web-tokens/create-custom-claims\n */\nexport type Auth0Claims = {\n /** Authorized party: the client id the token was issued to. */\n azp?: string;\n /** Only when the API has RBAC and \"Add Permissions in the Access Token\" on. */\n permissions?: string[];\n /** Organization id, when the tenant uses Organizations. */\n org_id?: string;\n /** Organization name, when the tenant uses Organizations. */\n org_name?: string;\n};\n\n/**\n * OAuth provider for Auth0. `domain` is the tenant domain (e.g.\n * `acme.us.auth0.com`); `audience` is the API Identifier; `serverUrl` is this\n * server's public URL. Requires DCR enabled on the tenant.\n *\n * Auth0 can't use the client's resource indicator — `audience` must be *in* the\n * authorize request. So it runs as skybridge-as-AS (`serverUrl`, see\n * {@link customProvider}) and bakes `?audience=<id>` into the advertised\n * `authorization_endpoint`. The token's `aud` is the API; `verify.issuer` stays\n * Auth0 (the token's real `iss`).\n */\nexport function auth0Provider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(\n opts: { domain: string; audience: string; serverUrl: string } & Omit<\n CustomProviderOptions,\n \"issuer\" | \"audience\" | \"baseUrl\" | \"serverUrl\"\n >,\n): OAuthProvider<Auth0Claims & TCustom & RegisteredClaims> {\n const { domain, audience, ...rest } = opts;\n const provider = customProvider<Auth0Claims & TCustom>({\n issuer: toIssuerUrl(domain),\n audience,\n ...rest,\n });\n return {\n resolve: async () => {\n const config = await provider.resolve();\n const authUrl = new URL(config.oauthMetadata.authorization_endpoint);\n authUrl.searchParams.set(\"audience\", audience);\n return {\n ...config,\n oauthMetadata: {\n ...config.oauthMetadata,\n authorization_endpoint: authUrl.href,\n },\n };\n },\n };\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  // @vitest-environment node
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  import { auth0Provider } from "./auth0.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 doc(issuer) {
6
8
  return {
@@ -24,7 +26,7 @@ describe("auth0Provider", () => {
24
26
  audience: "https://api.example.com/",
25
27
  serverUrl: "https://app.example.com/",
26
28
  scopes: ["openid", "profile", "email"],
27
- });
29
+ }).resolve();
28
30
  // skybridge is the advertised AS: static issuer = serverUrl (trailing slash stripped).
29
31
  expect(config.oauthMetadata.issuer).toBe("https://app.example.com");
30
32
  // AS metadata scopes match the advertised set (clients read scopes from here).
@@ -41,8 +43,8 @@ describe("auth0Provider", () => {
41
43
  // baseUrl left unset so the PRM resource resolves per request (Alpic-safe).
42
44
  expect(config.baseUrl).toBeUndefined();
43
45
  // 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
+ expect(jwks().issuer).toBe(issuer);
47
+ expect(jwks().audience).toBe("https://api.example.com/");
46
48
  });
47
49
  });
48
50
  //# sourceMappingURL=auth0.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"auth0.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.test.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;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"]}
1
+ {"version":3,"file":"auth0.test.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/auth0.test.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,IAAI,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAEzD,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAEzD,SAAS,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,eAAe,EAAE,CAAC,CAAC;AAEtC,SAAS,GAAG,CAAC,MAAc;IACzB,OAAO;QACL,MAAM;QACN,sBAAsB,EAAE,GAAG,MAAM,YAAY;QAC7C,cAAc,EAAE,GAAG,MAAM,cAAc;QACvC,qBAAqB,EAAE,GAAG,MAAM,gBAAgB;QAChD,wBAAwB,EAAE,CAAC,MAAM,CAAC;QAClC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC;QAChD,QAAQ,EAAE,GAAG,MAAM,wBAAwB;KAC5C,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,2FAA2F,EAAE,KAAK,IAAI,EAAE;QACzG,MAAM,MAAM,GAAG,2BAA2B,CAAC;QAC3C,EAAE,CAAC,KAAK,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,iBAAiB,CAC7C,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE;YACxC,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CACH,CAAC;QAEF,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC;YACjC,MAAM,EAAE,mBAAmB;YAC3B,QAAQ,EAAE,0BAA0B;YACpC,SAAS,EAAE,0BAA0B;YACrC,MAAM,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC;SACvC,CAAC,CAAC,OAAO,EAAE,CAAC;QAEb,uFAAuF;QACvF,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QACpE,+EAA+E;QAC/E,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC;YACpD,QAAQ;YACR,SAAS;YACT,OAAO;SACR,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;QACvE,mFAAmF;QACnF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;QACjE,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,YAAY,CAAC,CAAC;QAC9D,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;QAC1E,4EAA4E;QAC5E,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,CAAC;QACvC,oEAAoE;QACpE,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["// @vitest-environment node\nimport { afterEach, describe, expect, it, vi } from \"vitest\";\nimport { auth0Provider } from \"./auth0.js\";\nimport { lastJwksConfig as jwks } from \"./verify-spy.js\";\n\nvi.mock(\"../verify.js\", () => import(\"./verify-spy.js\"));\n\nafterEach(() => vi.restoreAllMocks());\n\nfunction doc(issuer: string) {\n return {\n issuer,\n authorization_endpoint: `${issuer}/authorize`,\n token_endpoint: `${issuer}/oauth/token`,\n registration_endpoint: `${issuer}/oidc/register`,\n response_types_supported: [\"code\"],\n scopes_supported: [\"openid\", \"profile\", \"email\"],\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n };\n}\n\ndescribe(\"auth0Provider\", () => {\n it(\"advertises skybridge as the AS (static issuer) and bakes audience; verifies against Auth0\", async () => {\n const issuer = \"https://acme.us.auth0.com\";\n vi.spyOn(globalThis, \"fetch\").mockResolvedValue(\n new Response(JSON.stringify(doc(issuer)), {\n headers: { \"content-type\": \"application/json\" },\n }),\n );\n\n const config = await auth0Provider({\n domain: \"acme.us.auth0.com\",\n audience: \"https://api.example.com/\",\n serverUrl: \"https://app.example.com/\",\n scopes: [\"openid\", \"profile\", \"email\"],\n }).resolve();\n\n // skybridge is the advertised AS: static issuer = serverUrl (trailing slash stripped).\n expect(config.oauthMetadata.issuer).toBe(\"https://app.example.com\");\n // AS metadata scopes match the advertised set (clients read scopes from here).\n expect(config.oauthMetadata.scopes_supported).toEqual([\n \"openid\",\n \"profile\",\n \"email\",\n ]);\n expect(config.scopesSupported).toEqual([\"openid\", \"profile\", \"email\"]);\n // audience baked into the advertised authorize endpoint (still pointing at Auth0).\n const url = new URL(config.oauthMetadata.authorization_endpoint);\n expect(url.origin + url.pathname).toBe(`${issuer}/authorize`);\n expect(url.searchParams.get(\"audience\")).toBe(\"https://api.example.com/\");\n // baseUrl left unset so the PRM resource resolves per request (Alpic-safe).\n expect(config.baseUrl).toBeUndefined();\n // token verification still uses Auth0 as issuer + the API audience.\n expect(jwks().issuer).toBe(issuer);\n expect(jwks().audience).toBe(\"https://api.example.com/\");\n });\n});\n"]}
@@ -1,4 +1,6 @@
1
- import type { OAuthConfig } from "../index.js";
1
+ import type { ExtraClaims } from "../../auth.js";
2
+ import type { OAuthProvider } from "../index.js";
3
+ import type { RegisteredClaims } from "../verify.js";
2
4
  import { type CustomProviderOptions } from "./custom.js";
3
5
  /** Options accepted by {@link authplaneProvider}. */
4
6
  export type AuthplaneProviderOptions = {
@@ -44,4 +46,4 @@ export type AuthplaneProviderOptions = {
44
46
  * of the authorization path. Pass `serverUrl` to advertise this server as the
45
47
  * authorization server instead (see {@link customProvider}).
46
48
  */
47
- export declare function authplaneProvider(opts: AuthplaneProviderOptions): Promise<OAuthConfig>;
49
+ export declare function authplaneProvider<TCustom extends ExtraClaims = Record<never, never>>(opts: AuthplaneProviderOptions): OAuthProvider<TCustom & RegisteredClaims>;
@@ -1 +1 @@
1
- {"version":3,"file":"authplane.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/authplane.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AAqCzE;;;;GAIG;AACH,SAAS,eAAe,CAAC,KAAa,EAAE,MAAc;IACpD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,mCAAmC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACzF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,6CAA6C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACnG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,+BAA+B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACrF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,uCAAuC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAC7F,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAA8B;IAE9B,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAErD,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAClC,MAAM,cAAc,GAAG,eAAe,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAE7D,0EAA0E;IAC1E,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,gEAAgE;IAChE,IAAI,cAAc,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,mFAAmF;YACjF,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI;YACvF,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,6GAA6G,CAC1J,CAAC;IACJ,CAAC;IAED,OAAO,cAAc,CAAC;QACpB,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,QAAQ;QAC9B,OAAO,EAAE,QAAQ;QACjB,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { OAuthConfig } from \"../index.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\n\n/** Options accepted by {@link authplaneProvider}. */\nexport type AuthplaneProviderOptions = {\n /**\n * The authorization server's issuer identifier (RFC 8414 §2) — your\n * Authplane deployment, e.g. `https://auth.acme.com` (or\n * `http://localhost:9000` in local development).\n */\n issuer: string;\n /**\n * This server's resource identifier (RFC 9728 §1.2): the public URL clients\n * reach, advertised as the `resource` field of its protected-resource\n * metadata. Required, unlike the other providers.\n *\n * Authplane binds the access token's `aud` to the RFC 8707 `resource`\n * parameter the client sends, and the client takes that value from the\n * advertised metadata. Setting it explicitly gives the deployment one fixed\n * identifier, which is what the audience is checked against.\n *\n * Resource identifiers are compared by exact string match, so give it in the\n * form it will be advertised and register that same string in Authplane. RFC\n * 8707 §2 asks for the most specific URI available, e.g.\n * `https://acme.example.com/mcp`.\n */\n resource: string;\n /**\n * Expected token `aud`. Defaults to `resource`.\n *\n * RFC 8707 §2 lets an authorization server use the resource identifier\n * verbatim as the audience or map it to another value; set this only for a\n * resource configured in Authplane with such an override, and pass that\n * value verbatim.\n */\n audience?: string;\n} & Omit<CustomProviderOptions, \"issuer\" | \"audience\" | \"baseUrl\">;\n\n/**\n * Rejects anything that cannot serve as an OAuth identifier: RFC 8707 §2\n * requires an absolute URI and forbids a fragment, and the discovery and\n * protected-resource metadata URLs are both built from the scheme and host.\n */\nfunction parseIdentifier(value: string, option: string): URL {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must be an absolute URL, got ${JSON.stringify(value)}`,\n );\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must use the http or https scheme, got ${JSON.stringify(value)}`,\n );\n }\n if (!parsed.host) {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must include a host, got ${JSON.stringify(value)}`,\n );\n }\n if (parsed.hash) {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must not include a fragment, got ${JSON.stringify(value)}`,\n );\n }\n return parsed;\n}\n\n/**\n * OAuth provider for Authplane. `issuer` is the authorization server's issuer\n * identifier and `resource` is this server's resource identifier, which also\n * supplies the expected token audience.\n *\n * Dynamic Client Registration is supported natively, so no registration proxy\n * is needed: clients register with Authplane directly and this server stays out\n * of the authorization path. Pass `serverUrl` to advertise this server as the\n * authorization server instead (see {@link customProvider}).\n */\nexport function authplaneProvider(\n opts: AuthplaneProviderOptions,\n): Promise<OAuthConfig> {\n const { issuer, resource, audience, ...rest } = opts;\n\n parseIdentifier(issuer, \"issuer\");\n const parsedResource = parseIdentifier(resource, \"resource\");\n\n // The advertised resource is the URL-normalised form of this value. Where\n // normalisation would change the string — a bare origin gaining a root path,\n // an uppercase host, an explicit default port — the configured and advertised\n // identifiers would differ, and the audience check compares them exactly.\n // Require the advertised form up front so the two always match.\n if (parsedResource.href !== resource) {\n throw new Error(\n `authplaneProvider: \\`resource\\` must be given in the form it will be advertised. ` +\n `${JSON.stringify(resource)} is advertised as ${JSON.stringify(parsedResource.href)}. ` +\n `Use ${JSON.stringify(parsedResource.href)}, or a path-qualified URL such as \"https://acme.example.com/mcp\", and register the same value in Authplane.`,\n );\n }\n\n return customProvider({\n issuer,\n audience: audience ?? resource,\n baseUrl: resource,\n ...rest,\n });\n}\n"]}
1
+ {"version":3,"file":"authplane.js","sourceRoot":"","sources":["../../../../src/server/auth/providers/authplane.ts"],"names":[],"mappings":"AAGA,OAAO,EAA8B,cAAc,EAAE,MAAM,aAAa,CAAC;AAqCzE;;;;GAIG;AACH,SAAS,eAAe,CAAC,KAAa,EAAE,MAAc;IACpD,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,mCAAmC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACzF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,6CAA6C,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACnG,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,+BAA+B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CACrF,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CACb,wBAAwB,MAAM,uCAAuC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAC7F,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAE/B,IAA8B;IAC9B,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC;IAErD,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAClC,MAAM,cAAc,GAAG,eAAe,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAE7D,0EAA0E;IAC1E,6EAA6E;IAC7E,8EAA8E;IAC9E,0EAA0E;IAC1E,gEAAgE;IAChE,IAAI,cAAc,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACrC,MAAM,IAAI,KAAK,CACb,mFAAmF;YACjF,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI;YACvF,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,6GAA6G,CAC1J,CAAC;IACJ,CAAC;IAED,OAAO,cAAc,CAAU;QAC7B,MAAM;QACN,QAAQ,EAAE,QAAQ,IAAI,QAAQ;QAC9B,OAAO,EAAE,QAAQ;QACjB,GAAG,IAAI;KACR,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ExtraClaims } from \"../../auth.js\";\nimport type { OAuthProvider } from \"../index.js\";\nimport type { RegisteredClaims } from \"../verify.js\";\nimport { type CustomProviderOptions, customProvider } from \"./custom.js\";\n\n/** Options accepted by {@link authplaneProvider}. */\nexport type AuthplaneProviderOptions = {\n /**\n * The authorization server's issuer identifier (RFC 8414 §2) — your\n * Authplane deployment, e.g. `https://auth.acme.com` (or\n * `http://localhost:9000` in local development).\n */\n issuer: string;\n /**\n * This server's resource identifier (RFC 9728 §1.2): the public URL clients\n * reach, advertised as the `resource` field of its protected-resource\n * metadata. Required, unlike the other providers.\n *\n * Authplane binds the access token's `aud` to the RFC 8707 `resource`\n * parameter the client sends, and the client takes that value from the\n * advertised metadata. Setting it explicitly gives the deployment one fixed\n * identifier, which is what the audience is checked against.\n *\n * Resource identifiers are compared by exact string match, so give it in the\n * form it will be advertised and register that same string in Authplane. RFC\n * 8707 §2 asks for the most specific URI available, e.g.\n * `https://acme.example.com/mcp`.\n */\n resource: string;\n /**\n * Expected token `aud`. Defaults to `resource`.\n *\n * RFC 8707 §2 lets an authorization server use the resource identifier\n * verbatim as the audience or map it to another value; set this only for a\n * resource configured in Authplane with such an override, and pass that\n * value verbatim.\n */\n audience?: string;\n} & Omit<CustomProviderOptions, \"issuer\" | \"audience\" | \"baseUrl\">;\n\n/**\n * Rejects anything that cannot serve as an OAuth identifier: RFC 8707 §2\n * requires an absolute URI and forbids a fragment, and the discovery and\n * protected-resource metadata URLs are both built from the scheme and host.\n */\nfunction parseIdentifier(value: string, option: string): URL {\n let parsed: URL;\n try {\n parsed = new URL(value);\n } catch {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must be an absolute URL, got ${JSON.stringify(value)}`,\n );\n }\n if (parsed.protocol !== \"https:\" && parsed.protocol !== \"http:\") {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must use the http or https scheme, got ${JSON.stringify(value)}`,\n );\n }\n if (!parsed.host) {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must include a host, got ${JSON.stringify(value)}`,\n );\n }\n if (parsed.hash) {\n throw new Error(\n `authplaneProvider: \\`${option}\\` must not include a fragment, got ${JSON.stringify(value)}`,\n );\n }\n return parsed;\n}\n\n/**\n * OAuth provider for Authplane. `issuer` is the authorization server's issuer\n * identifier and `resource` is this server's resource identifier, which also\n * supplies the expected token audience.\n *\n * Dynamic Client Registration is supported natively, so no registration proxy\n * is needed: clients register with Authplane directly and this server stays out\n * of the authorization path. Pass `serverUrl` to advertise this server as the\n * authorization server instead (see {@link customProvider}).\n */\nexport function authplaneProvider<\n TCustom extends ExtraClaims = Record<never, never>,\n>(opts: AuthplaneProviderOptions): OAuthProvider<TCustom & RegisteredClaims> {\n const { issuer, resource, audience, ...rest } = opts;\n\n parseIdentifier(issuer, \"issuer\");\n const parsedResource = parseIdentifier(resource, \"resource\");\n\n // The advertised resource is the URL-normalised form of this value. Where\n // normalisation would change the string — a bare origin gaining a root path,\n // an uppercase host, an explicit default port — the configured and advertised\n // identifiers would differ, and the audience check compares them exactly.\n // Require the advertised form up front so the two always match.\n if (parsedResource.href !== resource) {\n throw new Error(\n `authplaneProvider: \\`resource\\` must be given in the form it will be advertised. ` +\n `${JSON.stringify(resource)} is advertised as ${JSON.stringify(parsedResource.href)}. ` +\n `Use ${JSON.stringify(parsedResource.href)}, or a path-qualified URL such as \"https://acme.example.com/mcp\", and register the same value in Authplane.`,\n );\n }\n\n return customProvider<TCustom>({\n issuer,\n audience: audience ?? resource,\n baseUrl: resource,\n ...rest,\n });\n}\n"]}
@@ -1,6 +1,8 @@
1
1
  // @vitest-environment node
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  import { authplaneProvider } from "./authplane.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
  const ISSUER = "https://auth.acme.com";
6
8
  function discoveryDoc(issuer, scopes) {
@@ -34,10 +36,10 @@ describe("authplaneProvider", () => {
34
36
  const config = await authplaneProvider({
35
37
  issuer: ISSUER,
36
38
  resource: "https://coffee.example.com/mcp",
37
- });
38
- expect(config.verify.audience).toBe("https://coffee.example.com/mcp");
39
- expect(config.verify.issuer).toBe(ISSUER);
40
- expect(config.verify.jwksUri).toBe(`${ISSUER}/.well-known/jwks.json`);
39
+ }).resolve();
40
+ expect(jwks().audience).toBe("https://coffee.example.com/mcp");
41
+ expect(jwks().issuer).toBe(ISSUER);
42
+ expect(jwks().jwksUri).toBe(`${ISSUER}/.well-known/jwks.json`);
41
43
  expect(config.baseUrl).toBe("https://coffee.example.com/mcp");
42
44
  });
43
45
  it.each([
@@ -48,9 +50,12 @@ describe("authplaneProvider", () => {
48
50
  "https://coffee.example.com:8443/mcp",
49
51
  ])("passes %s through as both resource and audience", async (resource) => {
50
52
  mockDiscovery({ [ISSUER]: ["checkout"] });
51
- const config = await authplaneProvider({ issuer: ISSUER, resource });
53
+ const config = await authplaneProvider({
54
+ issuer: ISSUER,
55
+ resource,
56
+ }).resolve();
52
57
  expect(config.baseUrl).toBe(resource);
53
- expect(config.verify.audience).toBe(resource);
58
+ expect(jwks().audience).toBe(resource);
54
59
  });
55
60
  // The metadata router serialises the resource identifier through `URL` before
56
61
  // advertising it. Anything that serialisation would rewrite is refused here,
@@ -70,12 +75,12 @@ describe("authplaneProvider", () => {
70
75
  });
71
76
  it("lets an explicit audience override the default", async () => {
72
77
  mockDiscovery({ [ISSUER]: ["checkout"] });
73
- const config = await authplaneProvider({
78
+ await authplaneProvider({
74
79
  issuer: ISSUER,
75
80
  resource: "https://coffee.example.com/mcp",
76
81
  audience: "urn:acme:coffee",
77
- });
78
- expect(config.verify.audience).toBe("urn:acme:coffee");
82
+ }).resolve();
83
+ expect(jwks().audience).toBe("urn:acme:coffee");
79
84
  });
80
85
  it.each([
81
86
  ["coffee.example.com/mcp", /must be an absolute URL/],
@@ -98,21 +103,21 @@ describe("authplaneProvider", () => {
98
103
  // Discovery resolves the issuer itself; the provider passes it through
99
104
  // rather than rewriting the operator's value.
100
105
  mockDiscovery({ [ISSUER]: ["checkout"] });
101
- const config = await authplaneProvider({
106
+ await authplaneProvider({
102
107
  issuer: `${ISSUER}/`,
103
108
  resource: "https://coffee.example.com/mcp",
104
- });
105
- expect(config.verify.issuer).toBe(ISSUER);
109
+ }).resolve();
110
+ expect(jwks().issuer).toBe(ISSUER);
106
111
  });
107
112
  it("accepts a local http issuer for development", async () => {
108
113
  const local = "http://localhost:9000";
109
114
  mockDiscovery({ [local]: ["checkout"] });
110
- const config = await authplaneProvider({
115
+ await authplaneProvider({
111
116
  issuer: local,
112
117
  resource: "http://localhost:3000/mcp",
113
- });
114
- expect(config.verify.issuer).toBe(local);
115
- expect(config.verify.audience).toBe("http://localhost:3000/mcp");
118
+ }).resolve();
119
+ expect(jwks().issuer).toBe(local);
120
+ expect(jwks().audience).toBe("http://localhost:3000/mcp");
116
121
  });
117
122
  it("forwards scopes and the required-scope floor", async () => {
118
123
  mockDiscovery({ [ISSUER]: ["checkout", "profile"] });
@@ -121,7 +126,7 @@ describe("authplaneProvider", () => {
121
126
  resource: "https://coffee.example.com/mcp",
122
127
  scopes: ["checkout"],
123
128
  requiredScopes: ["checkout"],
124
- });
129
+ }).resolve();
125
130
  expect(config.scopesSupported).toEqual(["checkout"]);
126
131
  expect(config.requiredScopes).toEqual(["checkout"]);
127
132
  });
@@ -130,7 +135,7 @@ describe("authplaneProvider", () => {
130
135
  const config = await authplaneProvider({
131
136
  issuer: ISSUER,
132
137
  resource: "https://coffee.example.com/mcp",
133
- });
138
+ }).resolve();
134
139
  expect(config.oauthMetadata.issuer).toBe(ISSUER);
135
140
  expect(config.oauthMetadata.registration_endpoint).toBe(`${ISSUER}/register`);
136
141
  });