@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,405 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import {
3
+ sign,
4
+ verifyAccessToken,
5
+ getJWKS,
6
+ getPublicJWK,
7
+ resetKeysForTest,
8
+ getIssuerUrl,
9
+ getAllowedOrigin,
10
+ createCorsHeaders,
11
+ handleCorsOptions,
12
+ OAuthError,
13
+ getSigningKeyId,
14
+ } from "../oauth";
15
+ import type { OAuthConfig } from "../oauth";
16
+
17
+ // Test keys generated with OpenSSL
18
+ const TEST_PRIVATE_KEY = `-----BEGIN PRIVATE KEY-----
19
+ MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDUpgMiz+zobsK9
20
+ kFcnKhPBilSSIdkm0+/B/Af/Cy2qgQKdU5KvjBEM3N22Ie3PgcyQ1Qk9x6KnyHpS
21
+ CWMhPDd+76Ite1Ae8jx+q/N6NeLaaWb2wTx4c9QnKPxS4dBsf0L3eiiLGC8fHLfC
22
+ nro97I/87Lef1aiL+Dk9Le8ZOD82dckYSUxuI9Ds0yp1fxhfMy2GixKr1z2BSPSc
23
+ EPgcLFs8urNaQAQTXR9OQnTyMXPCuGhrGzn3pXLqUCDguNEH1Id3NdMazJ1CmLhQ
24
+ u1R4QEXO8+NkfivNVqa2vGfQpFDQJdTQCD1ue21ZsF1W9fIcmXQU4M05IbtaildD
25
+ /PsrSIK9AgMBAAECggEAJSuqtypYy01XIZsqPNiUUPus6klb47devM4hGLIbxqbb
26
+ 7ePGq4Rkk5bE85oNL31NJJD0l1W+5yy6Qv5Mk2nq+neJZgFc4TfvHqZQfk+Oiqar
27
+ fp0LBLQchMbbimJaFCkPq+Iw1ZWB4SKcNXsY64ufJLM9KsWGe4cFfF374jDsjchp
28
+ 50AIL4RrimLaWKp1yWgRcWToBWjaoAEdjMiGKOQkite8JwkZZYRSMqAWX7LnOV4q
29
+ gRG8sGLtyWSGpXWZYvTf5kPqZ4qYWicKro7BorYeSCcZuJG7AWZBrx9TpD7L+LFc
30
+ R49UZAdDt/pdipvRrryCG/NIpAKK3WBGOD3C203TuQKBgQDv96VLRwKez1S28VUu
31
+ aL8P72gPnSEn9O0sC1B6EKCRDoxP2o6qvUKvycsYcJuaBEmNoqqsEBozwNJK6qL7
32
+ QO63ctj2KU1JAn1WgZmAl+pqOqZ3mX8PdLTsw/9aTxMmBN/LMw2dcs3l7tWjm+ju
33
+ vhqSJ9iTQTcqLwt79KPmKIWnaQKBgQDi2xwey3ucHzHXTNZVB4vsh1izwKl9rqT3
34
+ 2/bV6jKiBJCucbFC13VxqIn0Nm07NY+cxVEfjvsPmczDlj7M4EW2NAs8xINe5KY0
35
+ VizyS/PBU62N8kLTW8Vt3vvO2XmyuBH5v6uI8OuCD1YobpauF5+4FoKiiLKNSIsY
36
+ U+PxeOTKNQKBgGmh2OhfNN8Vo1P4vid0wo5QM72TzJGbNoAJ5v4krZnNDqTkL6Mn
37
+ NuDM8pMqlsRgmMQ5U+n0GKSpf6isytvRRIQKkUki+ztlVikrWZgKx4zFjpvdPNpf
38
+ 5HjI+nIVlvdIc/8t1RN3Av3xeafQrOPTWTz3P1XrAk6WcPa6xR8+vT7pAoGAY25w
39
+ O9sqWbqeiOSnyOse3FRSf68BWxISQoVKAma9PKBNnfg9HrP7SQ77MGwuolYOlUMz
40
+ FGcCCct6oXuYGQpv47WZ+0+S2SPU6XmgB69crq7zkhTOT3+Y4Fhs/DP8EGZ3koT9
41
+ NW+Leh0owV3/c1ztZ62OIplR0XUrakVS0oMPnMUCgYBph0Dx9paH59ZkdNE0ZSTF
42
+ PXPCPi93VdlvHrMzULUNYiFSE/o8PMpV3D7UTlqiBwd4vPGVjawrPZRtuqEuZJcV
43
+ VtHxjpq0V41wXi/Dn5gSJwJjEUGaI5ftADIZFwOGy+DIOrC1XYvWMQlYp2ML6Q7w
44
+ xVl8tka0TkDpXl5tvvqy9A==
45
+ -----END PRIVATE KEY-----`;
46
+
47
+ const TEST_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
48
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1KYDIs/s6G7CvZBXJyoT
49
+ wYpUkiHZJtPvwfwH/wstqoECnVOSr4wRDNzdtiHtz4HMkNUJPceip8h6UgljITw3
50
+ fu+iLXtQHvI8fqvzejXi2mlm9sE8eHPUJyj8UuHQbH9C93ooixgvHxy3wp66PeyP
51
+ /Oy3n9Woi/g5PS3vGTg/NnXJGElMbiPQ7NMqdX8YXzMthosSq9c9gUj0nBD4HCxb
52
+ PLqzWkAEE10fTkJ08jFzwrhoaxs596Vy6lAg4LjRB9SHdzXTGsydQpi4ULtUeEBF
53
+ zvPjZH4rzVamtrxn0KRQ0CXU0Ag9bnttWbBdVvXyHJl0FODNOSG7WopXQ/z7K0iC
54
+ vQIDAQAB
55
+ -----END PUBLIC KEY-----`;
56
+
57
+ const TEST_JWKS = JSON.stringify({
58
+ keys: [
59
+ {
60
+ kty: "RSA",
61
+ n: "1KYDIs_s6G7CvZBXJyoTwYpUkiHZJtPvwfwH_wstqoECnVOSr4wRDNzdtiHtz4HMkNUJPceip8h6UgljITw3fu-iLXtQHvI8fqvzejXi2mlm9sE8eHPUJyj8UuHQbH9C93ooixgvHxy3wp66PeyP_Oy3n9Woi_g5PS3vGTg_NnXJGElMbiPQ7NMqdX8YXzMthosSq9c9gUj0nBD4HCxbPLqzWkAEE10fTkJ08jFzwrhoaxs596Vy6lAg4LjRB9SHdzXTGsydQpi4ULtUeEBFzvPjZH4rzVamtrxn0KRQ0CXU0Ag9bnttWbBdVvXyHJl0FODNOSG7WopXQ_z7K0iCvQ",
62
+ e: "AQAB",
63
+ use: "sig",
64
+ alg: "RS256",
65
+ kid: "test-key-1"
66
+ }
67
+ ]
68
+ });
69
+ const TEST_JWKS_NO_KID = JSON.stringify({
70
+ keys: [
71
+ {
72
+ kty: "RSA",
73
+ n: "1KYDIs_s6G7CvZBXJyoTwYpUkiHZJtPvwfwH_wstqoECnVOSr4wRDNzdtiHtz4HMkNUJPceip8h6UgljITw3fu-iLXtQHvI8fqvzejXi2mlm9sE8eHPUJyj8UuHQbH9C93ooixgvHxy3wp66PeyP_Oy3n9Woi_g5PS3vGTg_NnXJGElMbiPQ7NMqdX8YXzMthosSq9c9gUj0nBD4HCxbPLqzWkAEE10fTkJ08jFzwrhoaxs596Vy6lAg4LjRB9SHdzXTGsydQpi4ULtUeEBFzvPjZH4rzVamtrxn0KRQ0CXU0Ag9bnttWbBdVvXyHJl0FODNOSG7WopXQ_z7K0iCvQ",
74
+ e: "AQAB",
75
+ use: "sig",
76
+ alg: "RS256",
77
+ }
78
+ ]
79
+ });
80
+
81
+ describe("OAuth JWT and Utilities", () => {
82
+ beforeEach(() => {
83
+ resetKeysForTest();
84
+ });
85
+
86
+ describe("JWT Signing and Verification", () => {
87
+ it("should sign a JWT", async () => {
88
+ const token = await sign(
89
+ { custom: "claim" },
90
+ "user123",
91
+ "test-audience",
92
+ "1h",
93
+ TEST_PRIVATE_KEY,
94
+ "https://example.com"
95
+ );
96
+
97
+ expect(token).toBeDefined();
98
+ expect(typeof token).toBe("string");
99
+ expect(token.split(".")).toHaveLength(3); // JWT has 3 parts
100
+ });
101
+
102
+ it("should verify a signed JWT", async () => {
103
+ const token = await sign(
104
+ { custom: "claim" },
105
+ "user123",
106
+ "test-audience",
107
+ "1h",
108
+ TEST_PRIVATE_KEY,
109
+ "https://example.com"
110
+ );
111
+
112
+ const payload = await verifyAccessToken(
113
+ token,
114
+ TEST_PUBLIC_KEY,
115
+ "https://example.com",
116
+ "test-audience"
117
+ );
118
+
119
+ expect(payload.sub).toBe("user123");
120
+ expect(payload.aud).toBe("test-audience");
121
+ expect(payload.iss).toBe("https://example.com");
122
+ });
123
+
124
+ it("should verify with JWKS missing kid", async () => {
125
+ const token = await sign(
126
+ { custom: "claim" },
127
+ "user123",
128
+ "test-audience",
129
+ "1h",
130
+ TEST_PRIVATE_KEY,
131
+ "https://example.com"
132
+ );
133
+ const config: OAuthConfig = {
134
+ siteUrl: "https://example.com",
135
+ jwks: TEST_JWKS_NO_KID,
136
+ privateKey: TEST_PRIVATE_KEY,
137
+ };
138
+
139
+ const payload = await verifyAccessToken(
140
+ token,
141
+ config,
142
+ "https://example.com",
143
+ "test-audience"
144
+ );
145
+
146
+ expect(payload.sub).toBe("user123");
147
+ });
148
+
149
+ it("should fail verification with wrong audience", async () => {
150
+ const token = await sign(
151
+ {},
152
+ "user123",
153
+ "correct-audience",
154
+ "1h",
155
+ TEST_PRIVATE_KEY
156
+ );
157
+
158
+ await expect(
159
+ verifyAccessToken(token, TEST_PUBLIC_KEY, "", "wrong-audience")
160
+ ).rejects.toThrow();
161
+ });
162
+ });
163
+
164
+ describe("JWKS Functions", () => {
165
+ it("should get JWKS from config", async () => {
166
+ const config: OAuthConfig = {
167
+ siteUrl: "https://example.com",
168
+ jwks: TEST_JWKS,
169
+ privateKey: TEST_PRIVATE_KEY
170
+ };
171
+
172
+ const jwks = await getJWKS(config);
173
+ expect(jwks.keys).toHaveLength(1);
174
+ expect(jwks.keys[0].kty).toBe("RSA");
175
+ expect(jwks.keys[0].kid).toBe("test-key-1"); // Preserves existing kid
176
+ });
177
+
178
+ it("should add default kid when missing", async () => {
179
+ const jwksWithoutKid = JSON.stringify({
180
+ keys: [{
181
+ kty: "RSA",
182
+ n: "test",
183
+ e: "AQAB"
184
+ }]
185
+ });
186
+ const config: OAuthConfig = {
187
+ siteUrl: "https://example.com",
188
+ jwks: jwksWithoutKid,
189
+ privateKey: TEST_PRIVATE_KEY
190
+ };
191
+
192
+ const jwks = await getJWKS(config);
193
+ expect(jwks.keys[0].kid).toBe("default-key"); // Should add default kid
194
+ });
195
+
196
+ it("should get public JWK from PEM", async () => {
197
+ const jwk = await getPublicJWK(TEST_PUBLIC_KEY);
198
+ expect(jwk.kty).toBe("RSA");
199
+ expect(jwk.use).toBe("sig");
200
+ expect(jwk.alg).toBe("RS256");
201
+ expect(jwk.kid).toBe("default-key");
202
+ });
203
+
204
+ it("should strip private JWK parameters", async () => {
205
+ const jwksWithPrivate = JSON.stringify({
206
+ keys: [{
207
+ kty: "RSA",
208
+ n: "test",
209
+ e: "AQAB",
210
+ d: "private",
211
+ p: "private",
212
+ q: "private",
213
+ }]
214
+ });
215
+ const config: OAuthConfig = {
216
+ siteUrl: "https://example.com",
217
+ jwks: jwksWithPrivate,
218
+ privateKey: TEST_PRIVATE_KEY
219
+ };
220
+
221
+ const jwks = await getJWKS(config);
222
+ expect(jwks.keys[0].d).toBeUndefined();
223
+ expect(jwks.keys[0].p).toBeUndefined();
224
+ expect(jwks.keys[0].q).toBeUndefined();
225
+ });
226
+
227
+ it("should cache JWK results", async () => {
228
+ const jwk1 = await getPublicJWK(TEST_PUBLIC_KEY);
229
+ const jwk2 = await getPublicJWK(TEST_PUBLIC_KEY);
230
+ expect(jwk1).toBe(jwk2); // Same reference = cached
231
+ });
232
+ });
233
+
234
+ describe("Utility Functions", () => {
235
+ it("should get issuer URL", () => {
236
+ const config: OAuthConfig = {
237
+ siteUrl: "https://example.com",
238
+ privateKey: TEST_PRIVATE_KEY,
239
+ jwks: TEST_JWKS
240
+ };
241
+ expect(getIssuerUrl(config)).toBe("https://example.com/oauth");
242
+ });
243
+
244
+ it("should prefer convexSiteUrl for issuer", () => {
245
+ const config: OAuthConfig = {
246
+ siteUrl: "https://wrong.com",
247
+ convexSiteUrl: "https://correct.convex.site",
248
+ privateKey: TEST_PRIVATE_KEY,
249
+ jwks: TEST_JWKS
250
+ };
251
+ expect(getIssuerUrl(config)).toBe("https://correct.convex.site/oauth");
252
+ });
253
+
254
+ it("should resolve signing key id from config", () => {
255
+ const configFromJwks: OAuthConfig = {
256
+ siteUrl: "https://example.com",
257
+ jwks: TEST_JWKS,
258
+ privateKey: TEST_PRIVATE_KEY
259
+ };
260
+ expect(getSigningKeyId(configFromJwks)).toBe("test-key-1");
261
+
262
+ const configWithOverride: OAuthConfig = {
263
+ siteUrl: "https://example.com",
264
+ jwks: TEST_JWKS,
265
+ privateKey: TEST_PRIVATE_KEY,
266
+ keyId: "override-key"
267
+ };
268
+ expect(getSigningKeyId(configWithOverride)).toBe("override-key");
269
+
270
+ const configWithMissingKid: OAuthConfig = {
271
+ siteUrl: "https://example.com",
272
+ jwks: TEST_JWKS_NO_KID,
273
+ privateKey: TEST_PRIVATE_KEY
274
+ };
275
+ expect(getSigningKeyId(configWithMissingKid)).toBe("default-key");
276
+ });
277
+ });
278
+
279
+ describe("CORS Functions", () => {
280
+ const config: OAuthConfig = {
281
+ siteUrl: "https://example.com",
282
+ allowedOrigins: "https://app1.com,https://app2.com",
283
+ privateKey: TEST_PRIVATE_KEY,
284
+ jwks: TEST_JWKS
285
+ };
286
+
287
+ it("should allow null origin (CLI clients)", () => {
288
+ expect(getAllowedOrigin(null, config)).toBeNull();
289
+ });
290
+
291
+ it("should allow explicitly listed origins", () => {
292
+ expect(getAllowedOrigin("https://app1.com", config)).toBe("https://app1.com");
293
+ expect(getAllowedOrigin("https://app2.com", config)).toBe("https://app2.com");
294
+ });
295
+
296
+ it("should allow siteUrl as origin", () => {
297
+ expect(getAllowedOrigin("https://example.com", config)).toBe("https://example.com");
298
+ });
299
+
300
+ it("should allow convexSiteUrl as origin", () => {
301
+ const configWithConvex: OAuthConfig = {
302
+ ...config,
303
+ convexSiteUrl: "https://test.convex.site"
304
+ };
305
+ expect(getAllowedOrigin("https://test.convex.site", configWithConvex))
306
+ .toBe("https://test.convex.site");
307
+ });
308
+
309
+ it("should allow localhost origins", () => {
310
+ expect(getAllowedOrigin("http://localhost:3000", config)).toBe("http://localhost:3000");
311
+ expect(getAllowedOrigin("http://localhost", config)).toBe("http://localhost");
312
+ expect(getAllowedOrigin("http://127.0.0.1:8080", config)).toBe("http://127.0.0.1:8080");
313
+ });
314
+
315
+ it("should reject unlisted origins", () => {
316
+ expect(getAllowedOrigin("https://evil.com", config)).toBeNull();
317
+ });
318
+
319
+ it("should create CORS headers", () => {
320
+ const headers = createCorsHeaders("https://app1.com", config);
321
+ expect(headers["Access-Control-Allow-Origin"]).toBe("https://app1.com");
322
+ expect(headers["Access-Control-Allow-Methods"]).toBe("GET, POST, OPTIONS");
323
+ expect(headers["Content-Type"]).toBe("application/json");
324
+ });
325
+
326
+ it("should create CORS headers with null origin", () => {
327
+ const headers = createCorsHeaders(null, config);
328
+ expect(headers["Access-Control-Allow-Origin"]).toBeUndefined();
329
+ });
330
+
331
+ it("should omit Access-Control-Allow-Origin for unlisted origins", () => {
332
+ const headers = createCorsHeaders("https://evil.com", config);
333
+ expect(headers["Access-Control-Allow-Origin"]).toBeUndefined();
334
+ });
335
+
336
+ it("should handle OPTIONS preflight", () => {
337
+ const request = new Request("https://example.com/test", {
338
+ method: "OPTIONS",
339
+ headers: { "Origin": "https://app1.com" }
340
+ });
341
+
342
+ const response = handleCorsOptions(request, config);
343
+ expect(response).not.toBeNull();
344
+ expect(response?.status).toBe(200);
345
+ expect(response?.headers.get("Access-Control-Allow-Origin")).toBe("https://app1.com");
346
+ });
347
+
348
+ it("should return null for non-OPTIONS requests", () => {
349
+ const request = new Request("https://example.com/test", {
350
+ method: "GET"
351
+ });
352
+
353
+ const response = handleCorsOptions(request, config);
354
+ expect(response).toBeNull();
355
+ });
356
+ });
357
+
358
+ describe("OAuthError", () => {
359
+ it("should create OAuth error", () => {
360
+ const error = new OAuthError("invalid_request", "Missing parameter");
361
+ expect(error.code).toBe("invalid_request");
362
+ expect(error.message).toBe("Missing parameter");
363
+ expect(error.statusCode).toBe(400);
364
+ expect(error.name).toBe("OAuthError");
365
+ });
366
+
367
+ it("should create error with custom status code", () => {
368
+ const error = new OAuthError("unauthorized", "Access denied", 401);
369
+ expect(error.statusCode).toBe(401);
370
+ });
371
+
372
+ it("should convert to Response", () => {
373
+ const error = new OAuthError("invalid_grant", "Token expired", 400);
374
+ const response = error.toResponse({ "X-Custom": "header" });
375
+
376
+ expect(response.status).toBe(400);
377
+ expect(response.headers.get("X-Custom")).toBe("header");
378
+ });
379
+
380
+ it("should format error response correctly", async () => {
381
+ const error = new OAuthError("invalid_client", "Client not found");
382
+ const response = error.toResponse({});
383
+ const body = await response.json();
384
+
385
+ expect(body).toEqual({
386
+ error: "invalid_client",
387
+ error_description: "Client not found"
388
+ });
389
+ });
390
+ });
391
+
392
+ describe("Key Caching", () => {
393
+ it("should reset key cache", async () => {
394
+ // First call - cache miss
395
+ await getPublicJWK(TEST_PUBLIC_KEY);
396
+
397
+ // Reset cache
398
+ resetKeysForTest();
399
+
400
+ // Should work after reset
401
+ const jwk = await getPublicJWK(TEST_PUBLIC_KEY);
402
+ expect(jwk).toBeDefined();
403
+ });
404
+ });
405
+ });
@@ -0,0 +1,37 @@
1
+ import type {
2
+ FunctionReference,
3
+ FunctionVisibility,
4
+ FunctionArgs,
5
+ FunctionReturnType,
6
+ } from "convex/server";
7
+
8
+ /**
9
+ * Convex component common Context types
10
+ *
11
+ * Unified RunQueryCtx, RunMutationCtx, RunActionCtx that were defined separately in each package.
12
+ *
13
+ * Using generics, infer return value types from FunctionReference.
14
+ * - FunctionVisibility: supports both internal/public functions
15
+ * - FunctionArgs<F>: extracts function argument types
16
+ * - FunctionReturnType<F>: extracts function return value types
17
+ */
18
+ export type RunQueryCtx = {
19
+ runQuery<F extends FunctionReference<"query", FunctionVisibility>>(
20
+ query: F,
21
+ args: FunctionArgs<F>,
22
+ ): Promise<FunctionReturnType<F>>;
23
+ };
24
+
25
+ export type RunMutationCtx = RunQueryCtx & {
26
+ runMutation<F extends FunctionReference<"mutation", FunctionVisibility>>(
27
+ mutation: F,
28
+ args: FunctionArgs<F>,
29
+ ): Promise<FunctionReturnType<F>>;
30
+ };
31
+
32
+ export type RunActionCtx = RunMutationCtx & {
33
+ runAction<F extends FunctionReference<"action", FunctionVisibility>>(
34
+ action: F,
35
+ args: FunctionArgs<F>,
36
+ ): Promise<FunctionReturnType<F>>;
37
+ };