@uploadista/server 0.0.3

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 (91) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-check.log +34 -0
  3. package/LICENSE +21 -0
  4. package/README.md +503 -0
  5. package/dist/auth/cache.d.ts +87 -0
  6. package/dist/auth/cache.d.ts.map +1 -0
  7. package/dist/auth/cache.js +121 -0
  8. package/dist/auth/cache.test.d.ts +2 -0
  9. package/dist/auth/cache.test.d.ts.map +1 -0
  10. package/dist/auth/cache.test.js +209 -0
  11. package/dist/auth/get-auth-credentials.d.ts +73 -0
  12. package/dist/auth/get-auth-credentials.d.ts.map +1 -0
  13. package/dist/auth/get-auth-credentials.js +55 -0
  14. package/dist/auth/index.d.ts +2 -0
  15. package/dist/auth/index.d.ts.map +1 -0
  16. package/dist/auth/index.js +1 -0
  17. package/dist/auth/jwt/index.d.ts +38 -0
  18. package/dist/auth/jwt/index.d.ts.map +1 -0
  19. package/dist/auth/jwt/index.js +36 -0
  20. package/dist/auth/jwt/types.d.ts +77 -0
  21. package/dist/auth/jwt/types.d.ts.map +1 -0
  22. package/dist/auth/jwt/types.js +1 -0
  23. package/dist/auth/jwt/validate.d.ts +58 -0
  24. package/dist/auth/jwt/validate.d.ts.map +1 -0
  25. package/dist/auth/jwt/validate.js +226 -0
  26. package/dist/auth/jwt/validate.test.d.ts +2 -0
  27. package/dist/auth/jwt/validate.test.d.ts.map +1 -0
  28. package/dist/auth/jwt/validate.test.js +492 -0
  29. package/dist/auth/service.d.ts +63 -0
  30. package/dist/auth/service.d.ts.map +1 -0
  31. package/dist/auth/service.js +43 -0
  32. package/dist/auth/service.test.d.ts +2 -0
  33. package/dist/auth/service.test.d.ts.map +1 -0
  34. package/dist/auth/service.test.js +195 -0
  35. package/dist/auth/types.d.ts +38 -0
  36. package/dist/auth/types.d.ts.map +1 -0
  37. package/dist/auth/types.js +1 -0
  38. package/dist/cache.d.ts +87 -0
  39. package/dist/cache.d.ts.map +1 -0
  40. package/dist/cache.js +121 -0
  41. package/dist/cache.test.d.ts +2 -0
  42. package/dist/cache.test.d.ts.map +1 -0
  43. package/dist/cache.test.js +209 -0
  44. package/dist/cloudflare-config.d.ts +72 -0
  45. package/dist/cloudflare-config.d.ts.map +1 -0
  46. package/dist/cloudflare-config.js +67 -0
  47. package/dist/error-types.d.ts +138 -0
  48. package/dist/error-types.d.ts.map +1 -0
  49. package/dist/error-types.js +155 -0
  50. package/dist/hono-adapter.d.ts +48 -0
  51. package/dist/hono-adapter.d.ts.map +1 -0
  52. package/dist/hono-adapter.js +58 -0
  53. package/dist/http-utils.d.ts +148 -0
  54. package/dist/http-utils.d.ts.map +1 -0
  55. package/dist/http-utils.js +233 -0
  56. package/dist/index.d.ts +9 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +8 -0
  59. package/dist/layer-utils.d.ts +121 -0
  60. package/dist/layer-utils.d.ts.map +1 -0
  61. package/dist/layer-utils.js +80 -0
  62. package/dist/metrics/service.d.ts +26 -0
  63. package/dist/metrics/service.d.ts.map +1 -0
  64. package/dist/metrics/service.js +20 -0
  65. package/dist/plugins-typing.d.ts +11 -0
  66. package/dist/plugins-typing.d.ts.map +1 -0
  67. package/dist/plugins-typing.js +1 -0
  68. package/dist/service.d.ts +63 -0
  69. package/dist/service.d.ts.map +1 -0
  70. package/dist/service.js +43 -0
  71. package/dist/service.test.d.ts +2 -0
  72. package/dist/service.test.d.ts.map +1 -0
  73. package/dist/service.test.js +195 -0
  74. package/dist/types.d.ts +38 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +1 -0
  77. package/package.json +47 -0
  78. package/src/auth/get-auth-credentials.ts +97 -0
  79. package/src/auth/index.ts +1 -0
  80. package/src/cache.test.ts +306 -0
  81. package/src/cache.ts +204 -0
  82. package/src/error-types.ts +172 -0
  83. package/src/http-utils.ts +264 -0
  84. package/src/index.ts +8 -0
  85. package/src/layer-utils.ts +184 -0
  86. package/src/plugins-typing.ts +57 -0
  87. package/src/service.test.ts +275 -0
  88. package/src/service.ts +78 -0
  89. package/src/types.ts +40 -0
  90. package/tsconfig.json +13 -0
  91. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,492 @@
1
+ import { SignJWT } from "jose";
2
+ import { describe, expect, it } from "vitest";
3
+ import { extractAuthContextFromJwt, validateJwtToken } from "./validate";
4
+ // Helper to create test JWTs
5
+ async function createTestJWT(payload, secret, algorithm = "HS256", expiresIn = "1h") {
6
+ const secretKey = new TextEncoder().encode(secret);
7
+ const jwt = new SignJWT(payload)
8
+ .setProtectedHeader({ alg: algorithm })
9
+ .setIssuedAt();
10
+ if (expiresIn !== "none") {
11
+ if (expiresIn.endsWith("s")) {
12
+ const seconds = Number.parseInt(expiresIn.slice(0, -1));
13
+ jwt.setExpirationTime(Math.floor(Date.now() / 1000) + seconds);
14
+ }
15
+ else if (expiresIn.endsWith("m")) {
16
+ const minutes = Number.parseInt(expiresIn.slice(0, -1));
17
+ jwt.setExpirationTime(Math.floor(Date.now() / 1000) + minutes * 60);
18
+ }
19
+ else if (expiresIn.endsWith("h")) {
20
+ const hours = Number.parseInt(expiresIn.slice(0, -1));
21
+ jwt.setExpirationTime(Math.floor(Date.now() / 1000) + hours * 3600);
22
+ }
23
+ }
24
+ return await jwt.sign(secretKey);
25
+ }
26
+ // Helper to create expired JWT
27
+ async function createExpiredJWT(payload, secret) {
28
+ const secretKey = new TextEncoder().encode(secret);
29
+ const jwt = new SignJWT(payload)
30
+ .setProtectedHeader({ alg: "HS256" })
31
+ .setIssuedAt()
32
+ .setExpirationTime(Math.floor(Date.now() / 1000) - 3600); // Expired 1 hour ago
33
+ return await jwt.sign(secretKey);
34
+ }
35
+ describe("validateJwtToken", () => {
36
+ const testSecret = "test-secret-key-min-32-chars-long";
37
+ const wrongSecret = "wrong-secret-key-min-32-chars-long";
38
+ describe("Valid token scenarios", () => {
39
+ it("should successfully validate a valid token with minimal claims", async () => {
40
+ const token = await createTestJWT({ sub: "user-123" }, testSecret, "HS256", "1h");
41
+ const result = await validateJwtToken(token, { secret: testSecret });
42
+ expect(result.success).toBe(true);
43
+ if (result.success) {
44
+ expect(result.userId).toBe("user-123");
45
+ expect(result.claims.sub).toBe("user-123");
46
+ }
47
+ });
48
+ it("should successfully validate token with custom claims", async () => {
49
+ const token = await createTestJWT({
50
+ sub: "user-456",
51
+ role: "admin",
52
+ department: "engineering",
53
+ permissions: ["upload:create", "flow:execute"],
54
+ }, testSecret, "HS256", "1h");
55
+ const result = await validateJwtToken(token, { secret: testSecret });
56
+ expect(result.success).toBe(true);
57
+ if (result.success) {
58
+ expect(result.userId).toBe("user-456");
59
+ expect(result.claims.role).toBe("admin");
60
+ expect(result.claims.department).toBe("engineering");
61
+ expect(result.claims.permissions).toEqual([
62
+ "upload:create",
63
+ "flow:execute",
64
+ ]);
65
+ }
66
+ });
67
+ it("should validate token with issuer when issuer matches", async () => {
68
+ const token = await createTestJWT({ sub: "user-789" }, testSecret, "HS256", "1h");
69
+ const jwt = new SignJWT({ sub: "user-789" })
70
+ .setProtectedHeader({ alg: "HS256" })
71
+ .setIssuer("https://auth.example.com")
72
+ .setIssuedAt()
73
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600);
74
+ const tokenWithIssuer = await jwt.sign(new TextEncoder().encode(testSecret));
75
+ const result = await validateJwtToken(tokenWithIssuer, {
76
+ secret: testSecret,
77
+ issuer: "https://auth.example.com",
78
+ });
79
+ expect(result.success).toBe(true);
80
+ if (result.success) {
81
+ expect(result.userId).toBe("user-789");
82
+ }
83
+ });
84
+ it("should validate token with audience when audience matches", async () => {
85
+ const jwt = new SignJWT({ sub: "user-101" })
86
+ .setProtectedHeader({ alg: "HS256" })
87
+ .setAudience("uploadista-api")
88
+ .setIssuedAt()
89
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600);
90
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
91
+ const result = await validateJwtToken(token, {
92
+ secret: testSecret,
93
+ audience: "uploadista-api",
94
+ });
95
+ expect(result.success).toBe(true);
96
+ if (result.success) {
97
+ expect(result.userId).toBe("user-101");
98
+ }
99
+ });
100
+ it("should validate token with HS256 algorithm when explicitly specified", async () => {
101
+ const token = await createTestJWT({ sub: "user-111" }, testSecret, "HS256", "1h");
102
+ const result = await validateJwtToken(token, {
103
+ secret: testSecret,
104
+ algorithms: ["HS256"],
105
+ });
106
+ expect(result.success).toBe(true);
107
+ });
108
+ it("should validate token with custom clock tolerance", async () => {
109
+ // Create a token that expires in 30 seconds
110
+ const jwt = new SignJWT({ sub: "user-222" })
111
+ .setProtectedHeader({ alg: "HS256" })
112
+ .setIssuedAt()
113
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 30);
114
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
115
+ const result = await validateJwtToken(token, {
116
+ secret: testSecret,
117
+ clockTolerance: 120, // 2 minutes tolerance
118
+ });
119
+ expect(result.success).toBe(true);
120
+ });
121
+ });
122
+ describe("Expired token scenarios", () => {
123
+ it("should reject expired token", async () => {
124
+ const token = await createExpiredJWT({ sub: "user-expired" }, testSecret);
125
+ const result = await validateJwtToken(token, { secret: testSecret });
126
+ expect(result.success).toBe(false);
127
+ if (!result.success) {
128
+ expect(result.error.type).toBe("EXPIRED");
129
+ expect(result.error.message).toContain("expired");
130
+ }
131
+ });
132
+ it("should reject token expired beyond clock tolerance", async () => {
133
+ const token = await createExpiredJWT({ sub: "user-very-expired" }, testSecret);
134
+ const result = await validateJwtToken(token, {
135
+ secret: testSecret,
136
+ clockTolerance: 30, // 30 seconds tolerance, but token expired 1 hour ago
137
+ });
138
+ expect(result.success).toBe(false);
139
+ if (!result.success) {
140
+ expect(result.error.type).toBe("EXPIRED");
141
+ }
142
+ });
143
+ });
144
+ describe("Invalid signature scenarios", () => {
145
+ it("should reject token with invalid signature", async () => {
146
+ const token = await createTestJWT({ sub: "user-wrong-sig" }, testSecret, "HS256", "1h");
147
+ const result = await validateJwtToken(token, { secret: wrongSecret });
148
+ expect(result.success).toBe(false);
149
+ if (!result.success) {
150
+ expect(result.error.type).toBe("INVALID_SIGNATURE");
151
+ expect(result.error.message).toContain("signature");
152
+ }
153
+ });
154
+ it("should reject malformed token", async () => {
155
+ const result = await validateJwtToken("not.a.jwt", {
156
+ secret: testSecret,
157
+ });
158
+ expect(result.success).toBe(false);
159
+ if (!result.success) {
160
+ expect(result.error.type).toBe("INVALID_TOKEN");
161
+ }
162
+ });
163
+ it("should reject completely invalid token string", async () => {
164
+ const result = await validateJwtToken("invalid-token-string", {
165
+ secret: testSecret,
166
+ });
167
+ expect(result.success).toBe(false);
168
+ if (!result.success) {
169
+ expect(result.error.type).toBe("INVALID_TOKEN");
170
+ }
171
+ });
172
+ });
173
+ describe("Algorithm validation scenarios", () => {
174
+ it("should reject token with wrong algorithm when algorithms specified", async () => {
175
+ const token = await createTestJWT({ sub: "user-wrong-alg" }, testSecret, "HS256", "1h");
176
+ const result = await validateJwtToken(token, {
177
+ secret: testSecret,
178
+ algorithms: ["HS384", "HS512"], // Expecting HS384 or HS512, but token is HS256
179
+ });
180
+ expect(result.success).toBe(false);
181
+ if (!result.success) {
182
+ // jose's jwtVerify rejects with INVALID_TOKEN before algorithm check
183
+ expect(["INVALID_TOKEN", "INVALID_ALGORITHM"]).toContain(result.error.type);
184
+ }
185
+ });
186
+ it("should accept token with correct algorithm from allowed list", async () => {
187
+ const token = await createTestJWT({ sub: "user-correct-alg" }, testSecret, "HS256", "1h");
188
+ const result = await validateJwtToken(token, {
189
+ secret: testSecret,
190
+ algorithms: ["HS256", "HS384"],
191
+ });
192
+ expect(result.success).toBe(true);
193
+ });
194
+ });
195
+ describe("Issuer validation scenarios", () => {
196
+ it("should reject token with wrong issuer", async () => {
197
+ const jwt = new SignJWT({ sub: "user-wrong-iss" })
198
+ .setProtectedHeader({ alg: "HS256" })
199
+ .setIssuer("https://wrong-issuer.com")
200
+ .setIssuedAt()
201
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600); // 1 hour from now
202
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
203
+ const result = await validateJwtToken(token, {
204
+ secret: testSecret,
205
+ issuer: "https://expected-issuer.com",
206
+ });
207
+ expect(result.success).toBe(false);
208
+ if (!result.success) {
209
+ expect(result.error.type).toBe("INVALID_ISSUER");
210
+ expect(result.error.message).toContain("issuer");
211
+ }
212
+ });
213
+ });
214
+ describe("Audience validation scenarios", () => {
215
+ it("should reject token with wrong audience", async () => {
216
+ const jwt = new SignJWT({ sub: "user-wrong-aud" })
217
+ .setProtectedHeader({ alg: "HS256" })
218
+ .setAudience("wrong-audience")
219
+ .setIssuedAt()
220
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600); // 1 hour from now
221
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
222
+ const result = await validateJwtToken(token, {
223
+ secret: testSecret,
224
+ audience: "expected-audience",
225
+ });
226
+ expect(result.success).toBe(false);
227
+ if (!result.success) {
228
+ expect(result.error.type).toBe("INVALID_AUDIENCE");
229
+ expect(result.error.message).toContain("audience");
230
+ }
231
+ });
232
+ });
233
+ describe("Missing subject scenarios", () => {
234
+ it("should reject token without subject claim", async () => {
235
+ const jwt = new SignJWT({ role: "admin" }) // No 'sub' claim
236
+ .setProtectedHeader({ alg: "HS256" })
237
+ .setIssuedAt()
238
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600);
239
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
240
+ const result = await validateJwtToken(token, { secret: testSecret });
241
+ expect(result.success).toBe(false);
242
+ if (!result.success) {
243
+ expect(result.error.type).toBe("MISSING_SUBJECT");
244
+ expect(result.error.message).toContain("sub");
245
+ }
246
+ });
247
+ });
248
+ describe("Edge cases", () => {
249
+ it("should handle Uint8Array secret", async () => {
250
+ const secretBytes = new TextEncoder().encode(testSecret);
251
+ const token = await createTestJWT({ sub: "user-bytes" }, testSecret, "HS256", "1h");
252
+ const result = await validateJwtToken(token, { secret: secretBytes });
253
+ expect(result.success).toBe(true);
254
+ if (result.success) {
255
+ expect(result.userId).toBe("user-bytes");
256
+ }
257
+ });
258
+ it("should handle empty metadata gracefully", async () => {
259
+ const token = await createTestJWT({ sub: "user-empty" }, testSecret, "HS256", "1h");
260
+ const result = await validateJwtToken(token, { secret: testSecret });
261
+ expect(result.success).toBe(true);
262
+ if (result.success) {
263
+ expect(result.userId).toBe("user-empty");
264
+ }
265
+ });
266
+ it("should use default clock tolerance when not specified", async () => {
267
+ const token = await createTestJWT({ sub: "user-default" }, testSecret, "HS256", "1h");
268
+ const result = await validateJwtToken(token, { secret: testSecret });
269
+ expect(result.success).toBe(true);
270
+ });
271
+ });
272
+ });
273
+ describe("extractAuthContextFromJwt", () => {
274
+ const testSecret = "test-secret-key-min-32-chars-long";
275
+ describe("Successful extraction scenarios", () => {
276
+ it("should extract minimal AuthContext from valid token", async () => {
277
+ const token = await createTestJWT({ sub: "user-123" }, testSecret, "HS256", "1h");
278
+ const authContext = await extractAuthContextFromJwt(token, {
279
+ secret: testSecret,
280
+ });
281
+ expect(authContext).not.toBeNull();
282
+ expect(authContext?.userId).toBe("user-123");
283
+ expect(authContext?.metadata).toBeUndefined();
284
+ expect(authContext?.permissions).toBeUndefined();
285
+ });
286
+ it("should extract AuthContext with permissions from token", async () => {
287
+ const token = await createTestJWT({
288
+ sub: "user-456",
289
+ permissions: ["upload:create", "flow:execute", "admin:read"],
290
+ }, testSecret, "HS256", "1h");
291
+ const authContext = await extractAuthContextFromJwt(token, {
292
+ secret: testSecret,
293
+ });
294
+ expect(authContext).not.toBeNull();
295
+ expect(authContext?.userId).toBe("user-456");
296
+ expect(authContext?.permissions).toEqual([
297
+ "upload:create",
298
+ "flow:execute",
299
+ "admin:read",
300
+ ]);
301
+ });
302
+ it("should extract AuthContext with custom metadata", async () => {
303
+ const token = await createTestJWT({
304
+ sub: "user-789",
305
+ role: "admin",
306
+ department: "engineering",
307
+ tier: "premium",
308
+ customField: "custom-value",
309
+ }, testSecret, "HS256", "1h");
310
+ const authContext = await extractAuthContextFromJwt(token, {
311
+ secret: testSecret,
312
+ });
313
+ expect(authContext).not.toBeNull();
314
+ expect(authContext?.userId).toBe("user-789");
315
+ expect(authContext?.metadata).toEqual({
316
+ role: "admin",
317
+ department: "engineering",
318
+ tier: "premium",
319
+ customField: "custom-value",
320
+ });
321
+ });
322
+ it("should extract full AuthContext with both permissions and metadata", async () => {
323
+ const token = await createTestJWT({
324
+ sub: "user-full",
325
+ permissions: ["upload:create", "flow:execute"],
326
+ role: "admin",
327
+ department: "engineering",
328
+ }, testSecret, "HS256", "1h");
329
+ const authContext = await extractAuthContextFromJwt(token, {
330
+ secret: testSecret,
331
+ });
332
+ expect(authContext).not.toBeNull();
333
+ const ctx = authContext;
334
+ expect(ctx.userId).toBe("user-full");
335
+ expect(ctx.permissions).toEqual(["upload:create", "flow:execute"]);
336
+ expect(ctx.metadata).toEqual({
337
+ role: "admin",
338
+ department: "engineering",
339
+ });
340
+ });
341
+ it("should exclude standard JWT claims from metadata", async () => {
342
+ const jwt = new SignJWT({
343
+ sub: "user-standard",
344
+ role: "admin",
345
+ customClaim: "custom-value",
346
+ })
347
+ .setProtectedHeader({ alg: "HS256" })
348
+ .setIssuer("https://auth.example.com")
349
+ .setAudience("uploadista-api")
350
+ .setIssuedAt()
351
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600)
352
+ .setJti("jwt-id-123");
353
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
354
+ const authContext = await extractAuthContextFromJwt(token, {
355
+ secret: testSecret,
356
+ issuer: "https://auth.example.com",
357
+ audience: "uploadista-api",
358
+ });
359
+ expect(authContext).not.toBeNull();
360
+ const ctx = authContext;
361
+ expect(ctx.userId).toBe("user-standard");
362
+ expect(ctx.metadata).toEqual({
363
+ role: "admin",
364
+ customClaim: "custom-value",
365
+ });
366
+ // Standard claims should not be in metadata
367
+ expect(ctx.metadata).not.toHaveProperty("iss");
368
+ expect(ctx.metadata).not.toHaveProperty("sub");
369
+ expect(ctx.metadata).not.toHaveProperty("aud");
370
+ expect(ctx.metadata).not.toHaveProperty("exp");
371
+ expect(ctx.metadata).not.toHaveProperty("iat");
372
+ expect(ctx.metadata).not.toHaveProperty("jti");
373
+ });
374
+ it("should handle empty metadata when only standard claims present", async () => {
375
+ const jwt = new SignJWT({ sub: "user-only-standard" })
376
+ .setProtectedHeader({ alg: "HS256" })
377
+ .setIssuedAt()
378
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600);
379
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
380
+ const authContext = await extractAuthContextFromJwt(token, {
381
+ secret: testSecret,
382
+ });
383
+ expect(authContext).not.toBeNull();
384
+ const ctx = authContext;
385
+ expect(ctx.userId).toBe("user-only-standard");
386
+ expect(ctx.metadata).toBeUndefined();
387
+ });
388
+ });
389
+ describe("Failed extraction scenarios", () => {
390
+ it("should return null for invalid token", async () => {
391
+ const authContext = await extractAuthContextFromJwt("invalid.jwt.token", {
392
+ secret: testSecret,
393
+ });
394
+ expect(authContext).toBeNull();
395
+ });
396
+ it("should return null for expired token", async () => {
397
+ const token = await createExpiredJWT({ sub: "user-expired" }, testSecret);
398
+ const authContext = await extractAuthContextFromJwt(token, {
399
+ secret: testSecret,
400
+ });
401
+ expect(authContext).toBeNull();
402
+ });
403
+ it("should return null for token with wrong signature", async () => {
404
+ const token = await createTestJWT({ sub: "user-wrong" }, testSecret, "HS256", "1h");
405
+ const authContext = await extractAuthContextFromJwt(token, {
406
+ secret: "wrong-secret-key-min-32-chars-long",
407
+ });
408
+ expect(authContext).toBeNull();
409
+ });
410
+ it("should return null for token missing subject", async () => {
411
+ const jwt = new SignJWT({ role: "admin" })
412
+ .setProtectedHeader({ alg: "HS256" })
413
+ .setIssuedAt()
414
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600);
415
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
416
+ const authContext = await extractAuthContextFromJwt(token, {
417
+ secret: testSecret,
418
+ });
419
+ expect(authContext).toBeNull();
420
+ });
421
+ it("should return null for token with wrong issuer", async () => {
422
+ const jwt = new SignJWT({ sub: "user-iss" })
423
+ .setProtectedHeader({ alg: "HS256" })
424
+ .setIssuer("https://wrong.com")
425
+ .setIssuedAt()
426
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600);
427
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
428
+ const authContext = await extractAuthContextFromJwt(token, {
429
+ secret: testSecret,
430
+ issuer: "https://expected.com",
431
+ });
432
+ expect(authContext).toBeNull();
433
+ });
434
+ it("should return null for token with wrong audience", async () => {
435
+ const jwt = new SignJWT({ sub: "user-aud" })
436
+ .setProtectedHeader({ alg: "HS256" })
437
+ .setAudience("wrong-audience")
438
+ .setIssuedAt()
439
+ .setExpirationTime(Math.floor(Date.now() / 1000) + 3600);
440
+ const token = await jwt.sign(new TextEncoder().encode(testSecret));
441
+ const authContext = await extractAuthContextFromJwt(token, {
442
+ secret: testSecret,
443
+ audience: "expected-audience",
444
+ });
445
+ expect(authContext).toBeNull();
446
+ });
447
+ });
448
+ describe("Edge cases", () => {
449
+ it("should handle non-array permissions claim", async () => {
450
+ const token = await createTestJWT({
451
+ sub: "user-bad-perms",
452
+ permissions: "not-an-array", // Invalid permissions format
453
+ }, testSecret, "HS256", "1h");
454
+ const authContext = await extractAuthContextFromJwt(token, {
455
+ secret: testSecret,
456
+ });
457
+ expect(authContext).not.toBeNull();
458
+ const ctx = authContext;
459
+ expect(ctx.userId).toBe("user-bad-perms");
460
+ expect(ctx.permissions).toBeUndefined(); // Should be undefined since it's not an array
461
+ });
462
+ it("should handle complex metadata types", async () => {
463
+ const token = await createTestJWT({
464
+ sub: "user-complex",
465
+ nested: {
466
+ level1: {
467
+ level2: "deep-value",
468
+ },
469
+ },
470
+ arrayField: [1, 2, 3],
471
+ boolField: true,
472
+ numberField: 42,
473
+ }, testSecret, "HS256", "1h");
474
+ const authContext = await extractAuthContextFromJwt(token, {
475
+ secret: testSecret,
476
+ });
477
+ expect(authContext).not.toBeNull();
478
+ const ctx = authContext;
479
+ expect(ctx.userId).toBe("user-complex");
480
+ expect(ctx.metadata).toEqual({
481
+ nested: {
482
+ level1: {
483
+ level2: "deep-value",
484
+ },
485
+ },
486
+ arrayField: [1, 2, 3],
487
+ boolField: true,
488
+ numberField: 42,
489
+ });
490
+ });
491
+ });
492
+ });
@@ -0,0 +1,63 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ import type { AuthContext } from "./types";
3
+ declare const AuthContextService_base: Context.TagClass<AuthContextService, "AuthContextService", {
4
+ /**
5
+ * Get the current client ID from auth context.
6
+ * Returns null if no authentication context is available.
7
+ */
8
+ readonly getClientId: () => Effect.Effect<string | null>;
9
+ /**
10
+ * Get the current auth metadata.
11
+ * Returns empty object if no authentication context or no metadata.
12
+ */
13
+ readonly getMetadata: () => Effect.Effect<Record<string, unknown>>;
14
+ /**
15
+ * Check if the current client has a specific permission.
16
+ * Returns false if no authentication context or permission not found.
17
+ */
18
+ readonly hasPermission: (permission: string) => Effect.Effect<boolean>;
19
+ /**
20
+ * Get the full authentication context if available.
21
+ * Returns null if no authentication context is available.
22
+ */
23
+ readonly getAuthContext: () => Effect.Effect<AuthContext | null>;
24
+ }>;
25
+ /**
26
+ * Authentication Context Service
27
+ *
28
+ * Provides access to the current authentication context throughout
29
+ * the upload and flow processing pipeline. The service is provided
30
+ * via Effect Layer and can be accessed using Effect.service().
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * import { Effect } from "effect";
35
+ * import { AuthContextService } from "@uploadista/server";
36
+ *
37
+ * const uploadHandler = Effect.gen(function* () {
38
+ * const authService = yield* AuthContextService;
39
+ * const clientId = yield* authService.getClientId();
40
+ * if (clientId) {
41
+ * console.log(`Processing upload for client: ${clientId}`);
42
+ * }
43
+ * });
44
+ * ```
45
+ */
46
+ export declare class AuthContextService extends AuthContextService_base {
47
+ }
48
+ /**
49
+ * Creates an AuthContextService Layer from an AuthContext.
50
+ * This is typically called by adapters after successful authentication.
51
+ *
52
+ * @param authContext - The authentication context from middleware
53
+ * @returns Effect Layer providing AuthContextService
54
+ */
55
+ export declare const AuthContextServiceLive: (authContext: AuthContext | null) => Layer.Layer<AuthContextService>;
56
+ /**
57
+ * No-auth implementation of AuthContextService.
58
+ * Returns null/empty values for all operations.
59
+ * Used when no authentication middleware is configured (backward compatibility).
60
+ */
61
+ export declare const NoAuthContextServiceLive: Layer.Layer<AuthContextService>;
62
+ export {};
63
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/auth/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;;IA0BvC;;;OAGG;0BACmB,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC;IAExD;;;OAGG;0BACmB,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAElE;;;OAGG;4BACqB,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;IAEtE;;;OAGG;6BACsB,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,GAAG,IAAI,CAAC;;AA9CpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,kBAAmB,SAAQ,uBA2BrC;CAAG;AAEN;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GACjC,aAAa,WAAW,GAAG,IAAI,KAC9B,KAAK,CAAC,KAAK,CAAC,kBAAkB,CAO7B,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,KAAK,CAAC,KAAK,CAAC,kBAAkB,CACvC,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+ /**
3
+ * Authentication Context Service
4
+ *
5
+ * Provides access to the current authentication context throughout
6
+ * the upload and flow processing pipeline. The service is provided
7
+ * via Effect Layer and can be accessed using Effect.service().
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { Effect } from "effect";
12
+ * import { AuthContextService } from "@uploadista/server";
13
+ *
14
+ * const uploadHandler = Effect.gen(function* () {
15
+ * const authService = yield* AuthContextService;
16
+ * const clientId = yield* authService.getClientId();
17
+ * if (clientId) {
18
+ * console.log(`Processing upload for client: ${clientId}`);
19
+ * }
20
+ * });
21
+ * ```
22
+ */
23
+ export class AuthContextService extends Context.Tag("AuthContextService")() {
24
+ }
25
+ /**
26
+ * Creates an AuthContextService Layer from an AuthContext.
27
+ * This is typically called by adapters after successful authentication.
28
+ *
29
+ * @param authContext - The authentication context from middleware
30
+ * @returns Effect Layer providing AuthContextService
31
+ */
32
+ export const AuthContextServiceLive = (authContext) => Layer.succeed(AuthContextService, {
33
+ getClientId: () => Effect.succeed(authContext?.clientId ?? null),
34
+ getMetadata: () => Effect.succeed(authContext?.metadata ?? {}),
35
+ hasPermission: (permission) => Effect.succeed(authContext?.permissions?.includes(permission) ?? false),
36
+ getAuthContext: () => Effect.succeed(authContext),
37
+ });
38
+ /**
39
+ * No-auth implementation of AuthContextService.
40
+ * Returns null/empty values for all operations.
41
+ * Used when no authentication middleware is configured (backward compatibility).
42
+ */
43
+ export const NoAuthContextServiceLive = AuthContextServiceLive(null);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../../src/auth/service.test.ts"],"names":[],"mappings":""}