@codefox-inc/oauth-provider 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +572 -0
  3. package/dist/client/_generated/_ignore.d.ts +1 -0
  4. package/dist/client/_generated/_ignore.d.ts.map +1 -0
  5. package/dist/client/_generated/_ignore.js +3 -0
  6. package/dist/client/_generated/_ignore.js.map +1 -0
  7. package/dist/client/auth-config.d.ts +85 -0
  8. package/dist/client/auth-config.d.ts.map +1 -0
  9. package/dist/client/auth-config.js +81 -0
  10. package/dist/client/auth-config.js.map +1 -0
  11. package/dist/client/auth-helper.d.ts +81 -0
  12. package/dist/client/auth-helper.d.ts.map +1 -0
  13. package/dist/client/auth-helper.js +97 -0
  14. package/dist/client/auth-helper.js.map +1 -0
  15. package/dist/client/index.d.ts +189 -0
  16. package/dist/client/index.d.ts.map +1 -0
  17. package/dist/client/index.js +230 -0
  18. package/dist/client/index.js.map +1 -0
  19. package/dist/client/routes.d.ts +94 -0
  20. package/dist/client/routes.d.ts.map +1 -0
  21. package/dist/client/routes.js +113 -0
  22. package/dist/client/routes.js.map +1 -0
  23. package/dist/component/_generated/api.d.ts +44 -0
  24. package/dist/component/_generated/api.d.ts.map +1 -0
  25. package/dist/component/_generated/api.js +31 -0
  26. package/dist/component/_generated/api.js.map +1 -0
  27. package/dist/component/_generated/component.d.ts +123 -0
  28. package/dist/component/_generated/component.d.ts.map +1 -0
  29. package/dist/component/_generated/component.js +11 -0
  30. package/dist/component/_generated/component.js.map +1 -0
  31. package/dist/component/_generated/dataModel.d.ts +46 -0
  32. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  33. package/dist/component/_generated/dataModel.js +11 -0
  34. package/dist/component/_generated/dataModel.js.map +1 -0
  35. package/dist/component/_generated/server.d.ts +121 -0
  36. package/dist/component/_generated/server.d.ts.map +1 -0
  37. package/dist/component/_generated/server.js +78 -0
  38. package/dist/component/_generated/server.js.map +1 -0
  39. package/dist/component/clientManagement.d.ts +39 -0
  40. package/dist/component/clientManagement.d.ts.map +1 -0
  41. package/dist/component/clientManagement.js +169 -0
  42. package/dist/component/clientManagement.js.map +1 -0
  43. package/dist/component/constants.d.ts +31 -0
  44. package/dist/component/constants.d.ts.map +1 -0
  45. package/dist/component/constants.js +36 -0
  46. package/dist/component/constants.js.map +1 -0
  47. package/dist/component/convex.config.d.ts +3 -0
  48. package/dist/component/convex.config.d.ts.map +1 -0
  49. package/dist/component/convex.config.js +3 -0
  50. package/dist/component/convex.config.js.map +1 -0
  51. package/dist/component/handlers.d.ts +143 -0
  52. package/dist/component/handlers.d.ts.map +1 -0
  53. package/dist/component/handlers.js +624 -0
  54. package/dist/component/handlers.js.map +1 -0
  55. package/dist/component/mutations.d.ts +111 -0
  56. package/dist/component/mutations.d.ts.map +1 -0
  57. package/dist/component/mutations.js +459 -0
  58. package/dist/component/mutations.js.map +1 -0
  59. package/dist/component/queries.d.ts +127 -0
  60. package/dist/component/queries.d.ts.map +1 -0
  61. package/dist/component/queries.js +145 -0
  62. package/dist/component/queries.js.map +1 -0
  63. package/dist/component/schema.d.ts +116 -0
  64. package/dist/component/schema.d.ts.map +1 -0
  65. package/dist/component/schema.js +77 -0
  66. package/dist/component/schema.js.map +1 -0
  67. package/dist/component/token_security.d.ts +53 -0
  68. package/dist/component/token_security.d.ts.map +1 -0
  69. package/dist/component/token_security.js +91 -0
  70. package/dist/component/token_security.js.map +1 -0
  71. package/dist/lib/convex-types.d.ts +21 -0
  72. package/dist/lib/convex-types.d.ts.map +1 -0
  73. package/dist/lib/convex-types.js +2 -0
  74. package/dist/lib/convex-types.js.map +1 -0
  75. package/dist/lib/oauth.d.ts +123 -0
  76. package/dist/lib/oauth.d.ts.map +1 -0
  77. package/dist/lib/oauth.js +295 -0
  78. package/dist/lib/oauth.js.map +1 -0
  79. package/dist/react/index.d.ts +2 -0
  80. package/dist/react/index.d.ts.map +1 -0
  81. package/dist/react/index.js +6 -0
  82. package/dist/react/index.js.map +1 -0
  83. package/package.json +121 -0
  84. package/src/client/__tests__/auth-config.test.ts +244 -0
  85. package/src/client/__tests__/auth-helper.test.ts +273 -0
  86. package/src/client/__tests__/oauth-provider.test.ts +418 -0
  87. package/src/client/__tests__/routes.test.ts +428 -0
  88. package/src/client/_generated/_ignore.ts +1 -0
  89. package/src/client/auth-config.ts +157 -0
  90. package/src/client/auth-helper.ts +201 -0
  91. package/src/client/index.ts +326 -0
  92. package/src/client/routes.ts +251 -0
  93. package/src/component/__tests__/oauth.test.ts +3310 -0
  94. package/src/component/__tests__/rfc-compliance.test.ts +788 -0
  95. package/src/component/__tests__/token-security.test.ts +133 -0
  96. package/src/component/_generated/api.ts +60 -0
  97. package/src/component/_generated/component.ts +201 -0
  98. package/src/component/_generated/dataModel.ts +60 -0
  99. package/src/component/_generated/server.ts +156 -0
  100. package/src/component/clientManagement.ts +189 -0
  101. package/src/component/constants.ts +40 -0
  102. package/src/component/convex.config.ts +3 -0
  103. package/src/component/handlers.ts +964 -0
  104. package/src/component/mutations.ts +531 -0
  105. package/src/component/queries.ts +165 -0
  106. package/src/component/schema.ts +92 -0
  107. package/src/component/token_security.ts +102 -0
  108. package/src/lib/__tests__/oauth-helpers.test.ts +143 -0
  109. package/src/lib/__tests__/oauth-jwt.test.ts +405 -0
  110. package/src/lib/convex-types.ts +37 -0
  111. package/src/lib/oauth.ts +412 -0
  112. package/src/react/index.ts +7 -0
  113. package/src/test.ts +21 -0
@@ -0,0 +1,428 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { registerOAuthRoutes } from "../routes";
3
+
4
+ describe("Route Registration", () => {
5
+ // Mock HTTP router
6
+ const createMockRouter = () => {
7
+ const routes: Array<{ path: string; method: string; handler: any }> = [];
8
+ return {
9
+ router: {
10
+ route: vi.fn((config: { path: string; method: string; handler: any }) => {
11
+ routes.push(config);
12
+ }),
13
+ },
14
+ routes,
15
+ };
16
+ };
17
+
18
+ // Mock httpAction creator
19
+ const mockHttpAction = (handler: any) => handler;
20
+
21
+ // Mock OAuth provider
22
+ const createMockProvider = (config?: { prefix?: string }) => {
23
+ const mockHandlers = {
24
+ openIdConfiguration: vi.fn(async () => new Response("{}")),
25
+ jwks: vi.fn(async () => new Response("{}")),
26
+ protectedResource: vi.fn(async () => new Response("{}")),
27
+ authorize: vi.fn(async () => new Response(null, { status: 302 })),
28
+ token: vi.fn(async () => new Response("{}")),
29
+ userInfo: vi.fn(async () => new Response("{}")),
30
+ register: vi.fn(async () => new Response("{}")),
31
+ };
32
+
33
+ return {
34
+ provider: {
35
+ handlers: mockHandlers,
36
+ getConfig: () => config,
37
+ },
38
+ mockHandlers,
39
+ };
40
+ };
41
+
42
+ test("should register all OAuth endpoints with default prefix", () => {
43
+ const { router, routes } = createMockRouter();
44
+ const { provider } = createMockProvider();
45
+
46
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
47
+
48
+ // Expected routes (GET + OPTIONS for most, POST + OPTIONS for token/register/userinfo, GET + POST + OPTIONS for userinfo)
49
+ const expectedPaths = [
50
+ // Prefixed routes
51
+ "/oauth/.well-known/openid-configuration",
52
+ "/oauth/.well-known/oauth-authorization-server",
53
+ "/oauth/.well-known/jwks.json",
54
+ "/oauth/.well-known/oauth-protected-resource",
55
+ "/oauth/authorize",
56
+ "/oauth/token",
57
+ "/oauth/userinfo",
58
+ "/oauth/register",
59
+ // Root well-known routes
60
+ "/.well-known/oauth-authorization-server",
61
+ "/.well-known/oauth-authorization-server/oauth",
62
+ "/.well-known/oauth-protected-resource",
63
+ ];
64
+
65
+ // Check all expected paths are registered
66
+ const registeredPaths = routes.map(r => r.path);
67
+ for (const path of expectedPaths) {
68
+ expect(registeredPaths).toContain(path);
69
+ }
70
+
71
+ // Total routes: 11 paths × methods
72
+ // - 7 GET endpoints (openid-config, oauth-auth-server, jwks, protected-resource, authorize, userinfo, root×3) × 2 (GET + OPTIONS) = 14
73
+ // - 2 POST endpoints (token, register) × 2 (POST + OPTIONS) = 4
74
+ // - 1 userinfo (GET + POST + OPTIONS) = 3
75
+ expect(routes.length).toBeGreaterThan(20);
76
+ });
77
+
78
+ test("should register routes with custom prefix", () => {
79
+ const { router, routes } = createMockRouter();
80
+ const { provider } = createMockProvider();
81
+
82
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
83
+ prefix: "/api/auth",
84
+ });
85
+
86
+ const registeredPaths = routes.map(r => r.path);
87
+ expect(registeredPaths).toContain("/api/auth/.well-known/openid-configuration");
88
+ expect(registeredPaths).toContain("/api/auth/authorize");
89
+ expect(registeredPaths).toContain("/api/auth/token");
90
+ expect(registeredPaths).toContain("/api/auth/userinfo");
91
+ expect(registeredPaths).toContain("/api/auth/register");
92
+ expect(registeredPaths).toContain("/.well-known/oauth-authorization-server/api/auth");
93
+ });
94
+
95
+ test("should use prefix from provider config if not specified in options", () => {
96
+ const { router, routes } = createMockRouter();
97
+ const { provider } = createMockProvider({ prefix: "/custom" });
98
+
99
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
100
+
101
+ const registeredPaths = routes.map(r => r.path);
102
+ expect(registeredPaths).toContain("/custom/.well-known/openid-configuration");
103
+ expect(registeredPaths).toContain("/custom/authorize");
104
+ });
105
+
106
+ test("should handle root prefix", () => {
107
+ const { router, routes } = createMockRouter();
108
+ const { provider } = createMockProvider();
109
+
110
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
111
+ prefix: "/",
112
+ });
113
+
114
+ const registeredPaths = routes.map(r => r.path);
115
+ expect(registeredPaths).toContain("/.well-known/openid-configuration");
116
+ expect(registeredPaths).toContain("/authorize");
117
+ expect(registeredPaths).toContain("/token");
118
+ expect(registeredPaths).toContain("/.well-known/oauth-authorization-server");
119
+ });
120
+
121
+ test("should skip root well-known routes when disabled", () => {
122
+ const { router, routes } = createMockRouter();
123
+ const { provider } = createMockProvider();
124
+
125
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
126
+ registerRootWellKnown: false,
127
+ });
128
+
129
+ const registeredPaths = routes.map(r => r.path);
130
+ expect(registeredPaths).toContain("/oauth/.well-known/openid-configuration");
131
+ expect(registeredPaths).not.toContain("/.well-known/oauth-authorization-server");
132
+ expect(registeredPaths).not.toContain("/.well-known/oauth-protected-resource");
133
+ });
134
+
135
+ test("should call custom authorizeHandler when provided", async () => {
136
+ const { router } = createMockRouter();
137
+ const { provider, mockHandlers } = createMockProvider();
138
+ const customAuthorizeHandler = vi.fn(async (_ctx, _req, defaultFn) => {
139
+ return defaultFn();
140
+ });
141
+
142
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
143
+ authorizeHandler: customAuthorizeHandler,
144
+ });
145
+
146
+ // Find the authorize route handler
147
+ const authorizeRoute = router.route.mock.calls.find(
148
+ call => call[0].path === "/oauth/authorize" && call[0].method === "GET"
149
+ );
150
+ expect(authorizeRoute).toBeDefined();
151
+
152
+ // Invoke the handler
153
+ const handler = authorizeRoute![0].handler;
154
+ const mockCtx = { auth: { getUserIdentity: async () => null } };
155
+ const mockReq = new Request("http://localhost/oauth/authorize");
156
+ await handler(mockCtx, mockReq);
157
+
158
+ expect(customAuthorizeHandler).toHaveBeenCalled();
159
+ expect(mockHandlers.authorize).toHaveBeenCalled();
160
+ });
161
+
162
+ test("should use default getUserProfile when not provided", async () => {
163
+ const { router } = createMockRouter();
164
+ const { provider } = createMockProvider();
165
+
166
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
167
+
168
+ // Find userinfo route
169
+ const userInfoRoute = router.route.mock.calls.find(
170
+ call => call[0].path === "/oauth/userinfo" && call[0].method === "GET"
171
+ );
172
+ expect(userInfoRoute).toBeDefined();
173
+
174
+ // The handler should be registered
175
+ expect(userInfoRoute![0].handler).toBeDefined();
176
+ });
177
+
178
+ test("should use custom getUserProfile when provided", async () => {
179
+ const { router } = createMockRouter();
180
+ const { provider } = createMockProvider();
181
+ const customGetUserProfile = vi.fn(async (_ctx, userId) => ({
182
+ sub: userId,
183
+ name: "Test User",
184
+ email: "test@example.com",
185
+ }));
186
+
187
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
188
+ getUserProfile: customGetUserProfile,
189
+ });
190
+
191
+ // Find userinfo route
192
+ const userInfoRoute = router.route.mock.calls.find(
193
+ call => call[0].path === "/oauth/userinfo" && call[0].method === "GET"
194
+ );
195
+ expect(userInfoRoute).toBeDefined();
196
+
197
+ // The handler should be registered
198
+ expect(userInfoRoute![0].handler).toBeDefined();
199
+ });
200
+
201
+ test("should register GET and OPTIONS for discovery endpoints", () => {
202
+ const { router, routes } = createMockRouter();
203
+ const { provider } = createMockProvider();
204
+
205
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
206
+
207
+ const openidConfigRoutes = routes.filter(
208
+ r => r.path === "/oauth/.well-known/openid-configuration"
209
+ );
210
+ expect(openidConfigRoutes).toHaveLength(2);
211
+ expect(openidConfigRoutes.map(r => r.method)).toEqual(
212
+ expect.arrayContaining(["GET", "OPTIONS"])
213
+ );
214
+ });
215
+
216
+ test("should register POST and OPTIONS for token endpoint", () => {
217
+ const { router, routes } = createMockRouter();
218
+ const { provider } = createMockProvider();
219
+
220
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
221
+
222
+ const tokenRoutes = routes.filter(r => r.path === "/oauth/token");
223
+ expect(tokenRoutes).toHaveLength(2);
224
+ expect(tokenRoutes.map(r => r.method)).toEqual(
225
+ expect.arrayContaining(["POST", "OPTIONS"])
226
+ );
227
+ });
228
+
229
+ test("should register GET, POST, and OPTIONS for userinfo endpoint", () => {
230
+ const { router, routes } = createMockRouter();
231
+ const { provider } = createMockProvider();
232
+
233
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
234
+
235
+ const userInfoRoutes = routes.filter(r => r.path === "/oauth/userinfo");
236
+ expect(userInfoRoutes).toHaveLength(3);
237
+ expect(userInfoRoutes.map(r => r.method)).toEqual(
238
+ expect.arrayContaining(["GET", "POST", "OPTIONS"])
239
+ );
240
+ });
241
+
242
+ test("should register routes with custom siteUrl", () => {
243
+ const { router } = createMockRouter();
244
+ const { provider } = createMockProvider();
245
+
246
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
247
+ siteUrl: "https://example.com",
248
+ });
249
+
250
+ // siteUrl is used internally but doesn't affect route registration
251
+ expect(router.route).toHaveBeenCalled();
252
+ });
253
+
254
+ test("should handle all options combined", () => {
255
+ const { router, routes } = createMockRouter();
256
+ const { provider } = createMockProvider();
257
+ const customAuthorizeHandler = vi.fn(async (_ctx, _req, defaultFn) => defaultFn());
258
+ const customGetUserProfile = vi.fn(async (_ctx, userId) => ({ sub: userId }));
259
+
260
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
261
+ prefix: "/api/oauth",
262
+ getUserProfile: customGetUserProfile,
263
+ authorizeHandler: customAuthorizeHandler,
264
+ siteUrl: "https://example.com",
265
+ registerRootWellKnown: false,
266
+ });
267
+
268
+ const registeredPaths = routes.map(r => r.path);
269
+ expect(registeredPaths).toContain("/api/oauth/.well-known/openid-configuration");
270
+ expect(registeredPaths).toContain("/api/oauth/authorize");
271
+ expect(registeredPaths).toContain("/api/oauth/token");
272
+ expect(registeredPaths).toContain("/api/oauth/userinfo");
273
+ expect(registeredPaths).toContain("/api/oauth/register");
274
+ expect(registeredPaths).not.toContain("/.well-known/oauth-authorization-server");
275
+ });
276
+
277
+ describe("Handler Execution", () => {
278
+ test("should execute openIdConfiguration handler", async () => {
279
+ const { router, routes } = createMockRouter();
280
+ const { provider, mockHandlers } = createMockProvider();
281
+
282
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
283
+
284
+ const openidRoute = routes.find(
285
+ r => r.path === "/oauth/.well-known/openid-configuration" && r.method === "GET"
286
+ );
287
+ expect(openidRoute).toBeDefined();
288
+
289
+ const mockCtx = {};
290
+ const mockReq = new Request("http://localhost/oauth/.well-known/openid-configuration");
291
+ await openidRoute!.handler(mockCtx, mockReq);
292
+
293
+ expect(mockHandlers.openIdConfiguration).toHaveBeenCalledWith(mockCtx, mockReq);
294
+ });
295
+
296
+ test("should execute jwks handler", async () => {
297
+ const { router, routes } = createMockRouter();
298
+ const { provider, mockHandlers } = createMockProvider();
299
+
300
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
301
+
302
+ const jwksRoute = routes.find(
303
+ r => r.path === "/oauth/.well-known/jwks.json" && r.method === "GET"
304
+ );
305
+ expect(jwksRoute).toBeDefined();
306
+
307
+ const mockCtx = {};
308
+ const mockReq = new Request("http://localhost/oauth/.well-known/jwks.json");
309
+ await jwksRoute!.handler(mockCtx, mockReq);
310
+
311
+ expect(mockHandlers.jwks).toHaveBeenCalledWith(mockCtx, mockReq);
312
+ });
313
+
314
+ test("should execute protectedResource handler", async () => {
315
+ const { router, routes } = createMockRouter();
316
+ const { provider, mockHandlers } = createMockProvider();
317
+
318
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
319
+
320
+ const protectedRoute = routes.find(
321
+ r => r.path === "/oauth/.well-known/oauth-protected-resource" && r.method === "GET"
322
+ );
323
+ expect(protectedRoute).toBeDefined();
324
+
325
+ const mockCtx = {};
326
+ const mockReq = new Request("http://localhost/oauth/.well-known/oauth-protected-resource");
327
+ await protectedRoute!.handler(mockCtx, mockReq);
328
+
329
+ expect(mockHandlers.protectedResource).toHaveBeenCalledWith(mockCtx, mockReq);
330
+ });
331
+
332
+ test("should execute authorize handler without custom handler", async () => {
333
+ const { router, routes } = createMockRouter();
334
+ const { provider, mockHandlers } = createMockProvider();
335
+
336
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
337
+
338
+ const authorizeRoute = routes.find(
339
+ r => r.path === "/oauth/authorize" && r.method === "GET"
340
+ );
341
+ expect(authorizeRoute).toBeDefined();
342
+
343
+ const mockCtx = {};
344
+ const mockReq = new Request("http://localhost/oauth/authorize");
345
+ await authorizeRoute!.handler(mockCtx, mockReq);
346
+
347
+ expect(mockHandlers.authorize).toHaveBeenCalledWith(mockCtx, mockReq);
348
+ });
349
+
350
+ test("should execute token handler", async () => {
351
+ const { router, routes } = createMockRouter();
352
+ const { provider, mockHandlers } = createMockProvider();
353
+
354
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
355
+
356
+ const tokenRoute = routes.find(
357
+ r => r.path === "/oauth/token" && r.method === "POST"
358
+ );
359
+ expect(tokenRoute).toBeDefined();
360
+
361
+ const mockCtx = {};
362
+ const mockReq = new Request("http://localhost/oauth/token", { method: "POST" });
363
+ await tokenRoute!.handler(mockCtx, mockReq);
364
+
365
+ expect(mockHandlers.token).toHaveBeenCalledWith(mockCtx, mockReq);
366
+ });
367
+
368
+ test("should execute register handler", async () => {
369
+ const { router, routes } = createMockRouter();
370
+ const { provider, mockHandlers } = createMockProvider();
371
+
372
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
373
+
374
+ const registerRoute = routes.find(
375
+ r => r.path === "/oauth/register" && r.method === "POST"
376
+ );
377
+ expect(registerRoute).toBeDefined();
378
+
379
+ const mockCtx = {};
380
+ const mockReq = new Request("http://localhost/oauth/register", { method: "POST" });
381
+ await registerRoute!.handler(mockCtx, mockReq);
382
+
383
+ expect(mockHandlers.register).toHaveBeenCalledWith(mockCtx, mockReq);
384
+ });
385
+
386
+ test("should execute userInfo handler with default getUserProfile", async () => {
387
+ const { router, routes } = createMockRouter();
388
+ const { provider, mockHandlers } = createMockProvider();
389
+
390
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any);
391
+
392
+ const userInfoRoute = routes.find(
393
+ r => r.path === "/oauth/userinfo" && r.method === "GET"
394
+ );
395
+ expect(userInfoRoute).toBeDefined();
396
+
397
+ const mockCtx = {};
398
+ const mockReq = new Request("http://localhost/oauth/userinfo");
399
+ await userInfoRoute!.handler(mockCtx, mockReq);
400
+
401
+ expect(mockHandlers.userInfo).toHaveBeenCalled();
402
+ });
403
+
404
+ test("should execute userInfo handler with custom getUserProfile", async () => {
405
+ const { router, routes } = createMockRouter();
406
+ const { provider, mockHandlers } = createMockProvider();
407
+ const customGetUserProfile = vi.fn(async (_ctx, userId) => ({
408
+ sub: userId,
409
+ name: "Test User",
410
+ }));
411
+
412
+ registerOAuthRoutes(router as any, mockHttpAction as any, provider as any, {
413
+ getUserProfile: customGetUserProfile,
414
+ });
415
+
416
+ const userInfoRoute = routes.find(
417
+ r => r.path === "/oauth/userinfo" && r.method === "GET"
418
+ );
419
+ expect(userInfoRoute).toBeDefined();
420
+
421
+ const mockCtx = {};
422
+ const mockReq = new Request("http://localhost/oauth/userinfo");
423
+ await userInfoRoute!.handler(mockCtx, mockReq);
424
+
425
+ expect(mockHandlers.userInfo).toHaveBeenCalled();
426
+ });
427
+ });
428
+ });
@@ -0,0 +1 @@
1
+ // This is only here so convex-test can detect a _generated folder
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Auth Config Generator
3
+ *
4
+ * Generates auth.config.ts configuration for Convex Auth
5
+ * to trust JWTs from the OAuth Provider.
6
+ */
7
+
8
+ import { normalizePrefix } from "../lib/oauth.js";
9
+
10
+ /**
11
+ * Auth provider configuration for Convex
12
+ */
13
+ export interface AuthProvider {
14
+ domain: string;
15
+ applicationID: string;
16
+ }
17
+
18
+ /**
19
+ * Auth config structure (matches Convex Auth config)
20
+ */
21
+ export interface AuthConfig {
22
+ providers: AuthProvider[];
23
+ }
24
+
25
+ /**
26
+ * Options for generating auth config
27
+ */
28
+ export interface GenerateAuthConfigOptions {
29
+ /**
30
+ * CONVEX_SITE_URL - the deployed Convex site URL
31
+ * @example "https://your-app.convex.site"
32
+ */
33
+ convexSiteUrl?: string;
34
+
35
+ /**
36
+ * Local development port for OAuth provider
37
+ * @default 5173
38
+ */
39
+ localPort?: number;
40
+
41
+ /**
42
+ * OAuth endpoint prefix
43
+ * @default "/oauth"
44
+ */
45
+ prefix?: string;
46
+
47
+ /**
48
+ * Audience value for JWT validation
49
+ * @default "convex"
50
+ */
51
+ applicationID?: string;
52
+
53
+ /**
54
+ * Additional provider domains to trust
55
+ */
56
+ additionalProviders?: AuthProvider[];
57
+
58
+ /**
59
+ * Include the CONVEX_SITE_URL as a provider (for Convex Auth)
60
+ * @default true
61
+ */
62
+ includeConvexSiteUrl?: boolean;
63
+ }
64
+
65
+ /**
66
+ * Generate auth.config.ts configuration for OAuth Provider
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * // convex/auth.config.ts
71
+ * import { generateAuthConfig } from "@codefox-inc/oauth-provider";
72
+ *
73
+ * export default generateAuthConfig({
74
+ * convexSiteUrl: process.env.CONVEX_SITE_URL,
75
+ * localPort: 5173,
76
+ * });
77
+ * ```
78
+ *
79
+ * @example Output
80
+ * ```javascript
81
+ * {
82
+ * providers: [
83
+ * { domain: "https://your-app.convex.site", applicationID: "convex" },
84
+ * { domain: "http://localhost:5173/oauth", applicationID: "convex" },
85
+ * { domain: "https://your-app.convex.site/oauth", applicationID: "convex" },
86
+ * ]
87
+ * }
88
+ * ```
89
+ */
90
+ export function generateAuthConfig(options: GenerateAuthConfigOptions = {}): AuthConfig {
91
+ const {
92
+ convexSiteUrl,
93
+ localPort = 5173,
94
+ prefix: rawPrefix = "/oauth",
95
+ applicationID = "convex",
96
+ additionalProviders = [],
97
+ includeConvexSiteUrl = true,
98
+ } = options;
99
+ const prefix = normalizePrefix(rawPrefix);
100
+
101
+ const providers: AuthProvider[] = [];
102
+
103
+ // 1. CONVEX_SITE_URL for Convex Auth (session-based auth)
104
+ if (includeConvexSiteUrl && convexSiteUrl) {
105
+ providers.push({
106
+ domain: convexSiteUrl,
107
+ applicationID,
108
+ });
109
+ }
110
+
111
+ // 2. Local development OAuth issuer
112
+ providers.push({
113
+ domain: `http://localhost:${localPort}${prefix}`,
114
+ applicationID,
115
+ });
116
+
117
+ // 3. Production OAuth issuer (CONVEX_SITE_URL + prefix)
118
+ if (convexSiteUrl) {
119
+ providers.push({
120
+ domain: `${convexSiteUrl}${prefix}`,
121
+ applicationID,
122
+ });
123
+ }
124
+
125
+ // 4. Additional providers
126
+ providers.push(...additionalProviders);
127
+
128
+ return { providers };
129
+ }
130
+
131
+ /**
132
+ * Create auth config with validation
133
+ * Throws if required environment variables are missing
134
+ */
135
+ export function createAuthConfig(options: GenerateAuthConfigOptions = {}): AuthConfig {
136
+ const config = generateAuthConfig(options);
137
+
138
+ const prefix = normalizePrefix(options.prefix);
139
+ const localPort = options.localPort ?? 5173;
140
+
141
+ // Validate that we have at least one OAuth issuer (with the configured prefix/port)
142
+ const hasOAuthIssuer = config.providers.some(p => {
143
+ if (prefix) {
144
+ return p.domain.includes(prefix) || p.domain.includes(`:${localPort}`);
145
+ }
146
+ return p.domain.includes(`:${localPort}`) || (!!options.convexSiteUrl && p.domain === options.convexSiteUrl);
147
+ });
148
+
149
+ if (!hasOAuthIssuer) {
150
+ console.warn(
151
+ "[oauth-provider] Warning: No OAuth issuer found in auth config. " +
152
+ "MCP clients may not be able to authenticate."
153
+ );
154
+ }
155
+
156
+ return config;
157
+ }