@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.
- package/LICENSE +201 -0
- package/README.md +572 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/auth-config.d.ts +85 -0
- package/dist/client/auth-config.d.ts.map +1 -0
- package/dist/client/auth-config.js +81 -0
- package/dist/client/auth-config.js.map +1 -0
- package/dist/client/auth-helper.d.ts +81 -0
- package/dist/client/auth-helper.d.ts.map +1 -0
- package/dist/client/auth-helper.js +97 -0
- package/dist/client/auth-helper.js.map +1 -0
- package/dist/client/index.d.ts +189 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +230 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/routes.d.ts +94 -0
- package/dist/client/routes.d.ts.map +1 -0
- package/dist/client/routes.js +113 -0
- package/dist/client/routes.js.map +1 -0
- package/dist/component/_generated/api.d.ts +44 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +123 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/clientManagement.d.ts +39 -0
- package/dist/component/clientManagement.d.ts.map +1 -0
- package/dist/component/clientManagement.js +169 -0
- package/dist/component/clientManagement.js.map +1 -0
- package/dist/component/constants.d.ts +31 -0
- package/dist/component/constants.d.ts.map +1 -0
- package/dist/component/constants.js +36 -0
- package/dist/component/constants.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/handlers.d.ts +143 -0
- package/dist/component/handlers.d.ts.map +1 -0
- package/dist/component/handlers.js +624 -0
- package/dist/component/handlers.js.map +1 -0
- package/dist/component/mutations.d.ts +111 -0
- package/dist/component/mutations.d.ts.map +1 -0
- package/dist/component/mutations.js +459 -0
- package/dist/component/mutations.js.map +1 -0
- package/dist/component/queries.d.ts +127 -0
- package/dist/component/queries.d.ts.map +1 -0
- package/dist/component/queries.js +145 -0
- package/dist/component/queries.js.map +1 -0
- package/dist/component/schema.d.ts +116 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +77 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/token_security.d.ts +53 -0
- package/dist/component/token_security.d.ts.map +1 -0
- package/dist/component/token_security.js +91 -0
- package/dist/component/token_security.js.map +1 -0
- package/dist/lib/convex-types.d.ts +21 -0
- package/dist/lib/convex-types.d.ts.map +1 -0
- package/dist/lib/convex-types.js +2 -0
- package/dist/lib/convex-types.js.map +1 -0
- package/dist/lib/oauth.d.ts +123 -0
- package/dist/lib/oauth.d.ts.map +1 -0
- package/dist/lib/oauth.js +295 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +6 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +121 -0
- package/src/client/__tests__/auth-config.test.ts +244 -0
- package/src/client/__tests__/auth-helper.test.ts +273 -0
- package/src/client/__tests__/oauth-provider.test.ts +418 -0
- package/src/client/__tests__/routes.test.ts +428 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/auth-config.ts +157 -0
- package/src/client/auth-helper.ts +201 -0
- package/src/client/index.ts +326 -0
- package/src/client/routes.ts +251 -0
- package/src/component/__tests__/oauth.test.ts +3310 -0
- package/src/component/__tests__/rfc-compliance.test.ts +788 -0
- package/src/component/__tests__/token-security.test.ts +133 -0
- package/src/component/_generated/api.ts +60 -0
- package/src/component/_generated/component.ts +201 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/clientManagement.ts +189 -0
- package/src/component/constants.ts +40 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/handlers.ts +964 -0
- package/src/component/mutations.ts +531 -0
- package/src/component/queries.ts +165 -0
- package/src/component/schema.ts +92 -0
- package/src/component/token_security.ts +102 -0
- package/src/lib/__tests__/oauth-helpers.test.ts +143 -0
- package/src/lib/__tests__/oauth-jwt.test.ts +405 -0
- package/src/lib/convex-types.ts +37 -0
- package/src/lib/oauth.ts +412 -0
- package/src/react/index.ts +7 -0
- 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
|
+
};
|