@japikey/cloudflare 0.1.0 → 0.2.1

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.
@@ -1,1120 +0,0 @@
1
- import {
2
- describe,
3
- test,
4
- expect,
5
- beforeAll,
6
- afterAll,
7
- vi,
8
- Mock,
9
- beforeEach,
10
- } from 'vitest';
11
- import type {
12
- ExportedHandler,
13
- ExecutionContext,
14
- Request as CloudflareRequest,
15
- IncomingRequestCfProperties,
16
- } from '@cloudflare/workers-types';
17
- import { v4 as uuidv4 } from 'uuid';
18
- import * as jose from 'jose';
19
- import {
20
- createJWKSRouter,
21
- createApiKeyRouter,
22
- isJWKSPath,
23
- type CreateApiKeyRouterOptions,
24
- } from '../src/router.ts';
25
-
26
- import { createApiKey, DatabaseDriver, errorType } from '@japikey/japikey';
27
- import {
28
- baseIssuer,
29
- userClaims,
30
- apiKeyOptions,
31
- } from '../../japikey/test/testHelpers.ts';
32
- import { appendPathToUrl } from '../../shared/src/util.ts';
33
- import SqliteDriver from '../../sqlite/src/index.ts';
34
- import { UnauthorizedError } from '@japikey/shared';
35
-
36
- function castRequest(
37
- request: Request
38
- ): CloudflareRequest<unknown, IncomingRequestCfProperties<unknown>> {
39
- return request as unknown as CloudflareRequest<
40
- unknown,
41
- IncomingRequestCfProperties<unknown>
42
- >;
43
- }
44
-
45
- describe('createJWKSRouter', () => {
46
- let db: DatabaseDriver;
47
- let app: ExportedHandler;
48
- const ctx: ExecutionContext = {
49
- waitUntil: vi.fn(),
50
- passThroughOnException: vi.fn(),
51
- props: {},
52
- };
53
- beforeAll(async () => {
54
- db = new SqliteDriver(':memory:');
55
- await db.ensureTable();
56
- app = createJWKSRouter(baseIssuer, db);
57
- });
58
- afterAll(async () => {
59
- await db.close();
60
- });
61
-
62
- test('returns the JWKS for a valid kid', async () => {
63
- const { jwk, kid } = await createApiKey(userClaims(), apiKeyOptions());
64
- await db.insertApiKey({
65
- user_id: apiKeyOptions().sub,
66
- revoked: false,
67
- metadata: {},
68
- jwk,
69
- kid,
70
- });
71
- const response = await app.fetch!(
72
- castRequest(
73
- new Request(appendPathToUrl(baseIssuer, `${kid}/.well-known/jwks.json`))
74
- ),
75
- {},
76
- ctx
77
- );
78
- expect(response.status).toBe(200);
79
- await expect(response.json()).resolves.toEqual({ keys: [jwk] });
80
- });
81
-
82
- test('returns a 404 for a revoked kid', async () => {
83
- const { jwk, kid } = await createApiKey(userClaims(), apiKeyOptions());
84
- await db.insertApiKey({
85
- user_id: apiKeyOptions().sub,
86
- revoked: false,
87
- metadata: {},
88
- jwk,
89
- kid,
90
- });
91
- await db.revokeApiKey({ user_id: apiKeyOptions().sub, kid });
92
- const response = await app.fetch!(
93
- castRequest(
94
- new Request(appendPathToUrl(baseIssuer, `${kid}/.well-known/jwks.json`))
95
- ),
96
- {},
97
- ctx
98
- );
99
- expect(response.status).toBe(404);
100
- await expect(response.json()).resolves.toEqual({
101
- error: {
102
- message: 'API key not found',
103
- type: errorType.NOT_FOUND,
104
- },
105
- });
106
- });
107
-
108
- test('returns a 404 for a non-existent kid', async () => {
109
- const missingKid = uuidv4();
110
- const response = await app.fetch!(
111
- castRequest(
112
- new Request(
113
- appendPathToUrl(baseIssuer, `${missingKid}/.well-known/jwks.json`)
114
- )
115
- ),
116
- {},
117
- ctx
118
- );
119
- expect(response.status).toBe(404);
120
- await expect(response.json()).resolves.toEqual({
121
- error: {
122
- message: 'API key not found',
123
- type: errorType.NOT_FOUND,
124
- },
125
- });
126
- });
127
-
128
- test('returns a 404 for a malformed kid', async () => {
129
- const response = await app.fetch!(
130
- castRequest(
131
- new Request(
132
- appendPathToUrl(baseIssuer, `not-a-uuid/.well-known/jwks.json`)
133
- )
134
- ),
135
- {},
136
- ctx
137
- );
138
- expect(response.status).toBe(404);
139
- await expect(response.json()).resolves.toEqual({
140
- error: {
141
- message: 'Invalid JWKS request',
142
- type: errorType.NOT_FOUND,
143
- },
144
- });
145
- });
146
-
147
- test('returns a 404 for the wrong subpath', async () => {
148
- const response = await app.fetch!(
149
- castRequest(
150
- new Request(
151
- appendPathToUrl(
152
- baseIssuer,
153
- `another-subpath/${uuidv4()}/.well-known/jwks.json`
154
- )
155
- )
156
- ),
157
- {},
158
- ctx
159
- );
160
- expect(response.status).toBe(404);
161
- await expect(response.json()).resolves.toEqual({
162
- error: {
163
- message: 'Invalid JWKS request',
164
- type: errorType.NOT_FOUND,
165
- },
166
- });
167
- });
168
-
169
- test('returns a 404 for the wrong issuer', async () => {
170
- const response = await app.fetch!(
171
- castRequest(
172
- new Request(
173
- appendPathToUrl(
174
- new URL('https://susu.dev'),
175
- `${uuidv4()}/.well-known/jwks.json`
176
- )
177
- )
178
- ),
179
- {},
180
- ctx
181
- );
182
- expect(response.status).toBe(404);
183
- await expect(response.json()).resolves.toEqual({
184
- error: {
185
- message: 'Invalid JWKS request',
186
- type: errorType.NOT_FOUND,
187
- },
188
- });
189
- });
190
- });
191
-
192
- describe('createApiKeyRouter', () => {
193
- let db: DatabaseDriver;
194
- let getUserId: Mock;
195
- let parseCreateApiKeyRequest: Mock;
196
- let userId: string;
197
- const ctx: ExecutionContext = {
198
- waitUntil: vi.fn(),
199
- passThroughOnException: vi.fn(),
200
- props: {},
201
- };
202
-
203
- beforeAll(async () => {
204
- db = new SqliteDriver(':memory:');
205
- await db.ensureTable();
206
- });
207
-
208
- afterAll(async () => {
209
- await db.close();
210
- });
211
-
212
- beforeEach(() => {
213
- getUserId = vi.fn();
214
- parseCreateApiKeyRequest = vi.fn();
215
-
216
- getUserId.mockReset();
217
- userId = uuidv4();
218
- getUserId.mockResolvedValue(userId);
219
-
220
- parseCreateApiKeyRequest.mockReset();
221
- parseCreateApiKeyRequest.mockResolvedValue({
222
- expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
223
- claims: {},
224
- databaseMetadata: {},
225
- });
226
- });
227
-
228
- const routePrefixTestData = [
229
- { routePrefix: '', normalizedPath: '' },
230
- { routePrefix: '/', normalizedPath: '/' },
231
- { routePrefix: 'api-keys', normalizedPath: '/api-keys' },
232
- { routePrefix: 'api-keys/', normalizedPath: '/api-keys/' },
233
- { routePrefix: '/api-keys', normalizedPath: '/api-keys' },
234
- { routePrefix: '/api-keys/', normalizedPath: '/api-keys/' },
235
- { routePrefix: '/subpath/api-keys', normalizedPath: '/subpath/api-keys' },
236
- ];
237
-
238
- test.each(routePrefixTestData)(
239
- 'creates and gets an api key with routePrefix: $routePrefix',
240
- async ({ routePrefix, normalizedPath }) => {
241
- const options: CreateApiKeyRouterOptions<unknown> = {
242
- getUserId,
243
- parseCreateApiKeyRequest,
244
- issuer: new URL('https://example.com'),
245
- aud: 'api-key',
246
- db,
247
- routePrefix,
248
- };
249
-
250
- const app = createApiKeyRouter(options);
251
-
252
- const response = await app.fetch!(
253
- castRequest(
254
- new Request(`https://example.com${normalizedPath}`, {
255
- method: 'POST',
256
- })
257
- ),
258
- {},
259
- ctx
260
- );
261
- expect(response.status).toBe(200);
262
- const body = (await response.json()) as { api_key: string; kid: string };
263
- expect(body).toEqual({
264
- api_key: expect.any(String),
265
- kid: expect.any(String),
266
- });
267
-
268
- const { kid } = body;
269
- const getResponse = await app.fetch!(
270
- castRequest(
271
- new Request(`https://example.com${normalizedPath}/${kid}`, {
272
- method: 'GET',
273
- })
274
- ),
275
- {},
276
- ctx
277
- );
278
- expect(getResponse.status).toBe(200);
279
- const getBody = (await getResponse.json()) as any;
280
- expect(getBody).toEqual(
281
- expect.objectContaining({
282
- kid,
283
- user_id: userId,
284
- revoked: false,
285
- })
286
- );
287
- }
288
- );
289
-
290
- test.each(routePrefixTestData)(
291
- 'creates and gets an api key with claims from request body with routePrefix: $routePrefix',
292
- async ({ routePrefix, normalizedPath }) => {
293
- parseCreateApiKeyRequest.mockImplementation(async (request, env) => {
294
- const body = await request.json();
295
- return {
296
- expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365),
297
- claims: {
298
- scopes: ['read', 'write'],
299
- os: 'linux',
300
- },
301
- databaseMetadata: {
302
- tags: ['red'],
303
- },
304
- };
305
- });
306
-
307
- const options: CreateApiKeyRouterOptions<unknown> = {
308
- getUserId,
309
- parseCreateApiKeyRequest,
310
- issuer: new URL('https://example.com'),
311
- aud: 'api-key',
312
- db,
313
- routePrefix,
314
- };
315
-
316
- const app = createApiKeyRouter(options);
317
-
318
- const response = await app.fetch!(
319
- castRequest(
320
- new Request(`https://example.com${normalizedPath}`, {
321
- method: 'POST',
322
- headers: { 'Content-Type': 'application/json' },
323
- body: JSON.stringify({
324
- scopes: ['read', 'write'],
325
- os: 'linux',
326
- }),
327
- })
328
- ),
329
- {},
330
- ctx
331
- );
332
- expect(response.status).toBe(200);
333
- const body = (await response.json()) as { api_key: string; kid: string };
334
- expect(body).toEqual({
335
- api_key: expect.any(String),
336
- kid: expect.any(String),
337
- });
338
-
339
- const { kid, api_key } = body;
340
- const decoded = await jose.decodeJwt(api_key);
341
- expect(decoded.scopes).toEqual(['read', 'write']);
342
- expect(decoded.os).toBe('linux');
343
-
344
- const getResponse = await app.fetch!(
345
- castRequest(
346
- new Request(`https://example.com${normalizedPath}/${kid}`, {
347
- method: 'GET',
348
- })
349
- ),
350
- {},
351
- ctx
352
- );
353
- expect(getResponse.status).toBe(200);
354
- const getBody = await getResponse.json();
355
- expect(getBody).toEqual(
356
- expect.objectContaining({
357
- kid,
358
- user_id: userId,
359
- revoked: false,
360
- metadata: {
361
- tags: ['red'],
362
- },
363
- })
364
- );
365
- }
366
- );
367
-
368
- test.each(routePrefixTestData)(
369
- '/my with no api keys with routePrefix: $routePrefix',
370
- async ({ routePrefix, normalizedPath }) => {
371
- const options: CreateApiKeyRouterOptions<unknown> = {
372
- getUserId,
373
- parseCreateApiKeyRequest,
374
- issuer: new URL('https://example.com'),
375
- aud: 'api-key',
376
- db,
377
- routePrefix,
378
- };
379
-
380
- const app = createApiKeyRouter(options);
381
-
382
- const response = await app.fetch!(
383
- castRequest(
384
- new Request(`https://example.com${normalizedPath}/my`, {
385
- method: 'GET',
386
- })
387
- ),
388
- {},
389
- ctx
390
- );
391
- expect(response.status).toBe(200);
392
- const body = await response.json();
393
- expect(body).toEqual([]);
394
- }
395
- );
396
-
397
- test.each(routePrefixTestData)(
398
- '/my with one api key with routePrefix: $routePrefix',
399
- async ({ routePrefix, normalizedPath }) => {
400
- const options: CreateApiKeyRouterOptions<unknown> = {
401
- getUserId,
402
- parseCreateApiKeyRequest,
403
- issuer: new URL('https://example.com'),
404
- aud: 'api-key',
405
- db,
406
- routePrefix,
407
- };
408
-
409
- const app = createApiKeyRouter(options);
410
-
411
- const createResponse = await app.fetch!(
412
- castRequest(
413
- new Request(`https://example.com${normalizedPath}`, {
414
- method: 'POST',
415
- })
416
- ),
417
- {},
418
- ctx
419
- );
420
- expect(createResponse.status).toBe(200);
421
- const createBody = (await createResponse.json()) as {
422
- api_key: string;
423
- kid: string;
424
- };
425
- const { kid } = createBody;
426
-
427
- const response = await app.fetch!(
428
- castRequest(
429
- new Request(`https://example.com${normalizedPath}/my`, {
430
- method: 'GET',
431
- })
432
- ),
433
- {},
434
- ctx
435
- );
436
- expect(response.status).toBe(200);
437
- const body = (await response.json()) as any[];
438
- expect(body).toEqual([
439
- expect.objectContaining({
440
- kid,
441
- user_id: userId,
442
- revoked: false,
443
- }),
444
- ]);
445
- }
446
- );
447
-
448
- test.each(routePrefixTestData)(
449
- '/my with two keys, one revoked with routePrefix: $routePrefix',
450
- async ({ routePrefix, normalizedPath }) => {
451
- const options: CreateApiKeyRouterOptions<unknown> = {
452
- getUserId,
453
- parseCreateApiKeyRequest,
454
- issuer: new URL('https://example.com'),
455
- aud: 'api-key',
456
- db,
457
- routePrefix,
458
- };
459
-
460
- const app = createApiKeyRouter(options);
461
-
462
- const createResponse = await app.fetch!(
463
- castRequest(
464
- new Request(`https://example.com${normalizedPath}`, {
465
- method: 'POST',
466
- })
467
- ),
468
- {},
469
- ctx
470
- );
471
- expect(createResponse.status).toBe(200);
472
- const createBody = (await createResponse.json()) as {
473
- api_key: string;
474
- kid: string;
475
- };
476
- const { kid } = createBody;
477
-
478
- const revokeResponse = await app.fetch!(
479
- castRequest(
480
- new Request(`https://example.com${normalizedPath}/${kid}`, {
481
- method: 'DELETE',
482
- })
483
- ),
484
- {},
485
- ctx
486
- );
487
- expect(revokeResponse.status).toBe(200);
488
-
489
- const createResponse2 = await app.fetch!(
490
- castRequest(
491
- new Request(`https://example.com${normalizedPath}`, {
492
- method: 'POST',
493
- })
494
- ),
495
- {},
496
- ctx
497
- );
498
- expect(createResponse2.status).toBe(200);
499
- const createBody2 = (await createResponse2.json()) as {
500
- api_key: string;
501
- kid: string;
502
- };
503
- const { kid: kid2 } = createBody2;
504
-
505
- const response = await app.fetch!(
506
- castRequest(
507
- new Request(`https://example.com${normalizedPath}/my`, {
508
- method: 'GET',
509
- })
510
- ),
511
- {},
512
- ctx
513
- );
514
- expect(response.status).toBe(200);
515
- const body = (await response.json()) as any[];
516
- expect(body).toEqual(
517
- expect.arrayContaining([
518
- expect.objectContaining({
519
- kid,
520
- user_id: userId,
521
- revoked: true,
522
- }),
523
- expect.objectContaining({
524
- kid: kid2,
525
- user_id: userId,
526
- revoked: false,
527
- }),
528
- ])
529
- );
530
- }
531
- );
532
-
533
- test.each(routePrefixTestData)(
534
- '/my rejects if auth fails with routePrefix: $routePrefix',
535
- async ({ routePrefix, normalizedPath }) => {
536
- getUserId.mockRejectedValue(new UnauthorizedError('auth failed'));
537
-
538
- const options: CreateApiKeyRouterOptions<unknown> = {
539
- getUserId,
540
- parseCreateApiKeyRequest,
541
- issuer: new URL('https://example.com'),
542
- aud: 'api-key',
543
- db,
544
- routePrefix,
545
- };
546
-
547
- const app = createApiKeyRouter(options);
548
-
549
- const response = await app.fetch!(
550
- castRequest(
551
- new Request(`https://example.com${normalizedPath}/my`, {
552
- method: 'GET',
553
- })
554
- ),
555
- {},
556
- ctx
557
- );
558
- expect(response.status).toBe(403);
559
- }
560
- );
561
-
562
- test.each(routePrefixTestData)(
563
- 'get :id fails if auth fails with routePrefix: $routePrefix',
564
- async ({ routePrefix, normalizedPath }) => {
565
- const options: CreateApiKeyRouterOptions<unknown> = {
566
- getUserId,
567
- parseCreateApiKeyRequest,
568
- issuer: new URL('https://example.com'),
569
- aud: 'api-key',
570
- db,
571
- routePrefix,
572
- };
573
-
574
- const app = createApiKeyRouter(options);
575
-
576
- const createResponse = await app.fetch!(
577
- castRequest(
578
- new Request(`https://example.com${normalizedPath}`, {
579
- method: 'POST',
580
- })
581
- ),
582
- {},
583
- ctx
584
- );
585
- expect(createResponse.status).toBe(200);
586
- const createBody = (await createResponse.json()) as {
587
- api_key: string;
588
- kid: string;
589
- };
590
- const { kid } = createBody;
591
-
592
- getUserId.mockRejectedValue(new UnauthorizedError('auth failed'));
593
- const response = await app.fetch!(
594
- castRequest(
595
- new Request(`https://example.com${normalizedPath}/${kid}`, {
596
- method: 'GET',
597
- })
598
- ),
599
- {},
600
- ctx
601
- );
602
- expect(response.status).toBe(403);
603
- }
604
- );
605
-
606
- test.each(routePrefixTestData)(
607
- 'get :id fails if the key does not belong to the user with routePrefix: $routePrefix',
608
- async ({ routePrefix, normalizedPath }) => {
609
- const options: CreateApiKeyRouterOptions<unknown> = {
610
- getUserId,
611
- parseCreateApiKeyRequest,
612
- issuer: new URL('https://example.com'),
613
- aud: 'api-key',
614
- db,
615
- routePrefix,
616
- };
617
-
618
- const app = createApiKeyRouter(options);
619
-
620
- const createResponse = await app.fetch!(
621
- castRequest(
622
- new Request(`https://example.com${normalizedPath}`, {
623
- method: 'POST',
624
- })
625
- ),
626
- {},
627
- ctx
628
- );
629
- expect(createResponse.status).toBe(200);
630
- const createBody = (await createResponse.json()) as {
631
- api_key: string;
632
- kid: string;
633
- };
634
- const { kid } = createBody;
635
-
636
- getUserId.mockResolvedValue(uuidv4());
637
- const response = await app.fetch!(
638
- castRequest(
639
- new Request(`https://example.com${normalizedPath}/${kid}`, {
640
- method: 'GET',
641
- })
642
- ),
643
- {},
644
- ctx
645
- );
646
- expect(response.status).toBe(404);
647
- const body = await response.json();
648
- expect(body).toEqual({
649
- error: {
650
- message: 'API key not found',
651
- type: errorType.NOT_FOUND,
652
- },
653
- });
654
- }
655
- );
656
-
657
- test.each(routePrefixTestData)(
658
- 'get :id fails if the key does not exist with routePrefix: $routePrefix',
659
- async ({ routePrefix, normalizedPath }) => {
660
- const options: CreateApiKeyRouterOptions<unknown> = {
661
- getUserId,
662
- parseCreateApiKeyRequest,
663
- issuer: new URL('https://example.com'),
664
- aud: 'api-key',
665
- db,
666
- routePrefix,
667
- };
668
-
669
- const app = createApiKeyRouter(options);
670
-
671
- const response = await app.fetch!(
672
- castRequest(
673
- new Request(`https://example.com${normalizedPath}/${uuidv4()}`, {
674
- method: 'GET',
675
- })
676
- ),
677
- {},
678
- ctx
679
- );
680
- expect(response.status).toBe(404);
681
- const body = await response.json();
682
- expect(body).toEqual({
683
- error: {
684
- message: 'API key not found',
685
- type: errorType.NOT_FOUND,
686
- },
687
- });
688
- }
689
- );
690
-
691
- test.each(routePrefixTestData)(
692
- 'get :id fails if the key is not a uuid with routePrefix: $routePrefix',
693
- async ({ routePrefix, normalizedPath }) => {
694
- const options: CreateApiKeyRouterOptions<unknown> = {
695
- getUserId,
696
- parseCreateApiKeyRequest,
697
- issuer: new URL('https://example.com'),
698
- aud: 'api-key',
699
- db,
700
- routePrefix,
701
- };
702
-
703
- const app = createApiKeyRouter(options);
704
-
705
- const response = await app.fetch!(
706
- castRequest(
707
- new Request(`https://example.com${normalizedPath}/not-a-uuid`, {
708
- method: 'GET',
709
- })
710
- ),
711
- {},
712
- ctx
713
- );
714
- expect(response.status).toBe(404);
715
- const body = await response.json();
716
- expect(body).toEqual({
717
- error: {
718
- message: 'Invalid API key ID',
719
- type: errorType.NOT_FOUND,
720
- },
721
- });
722
- }
723
- );
724
-
725
- test.each(routePrefixTestData)(
726
- 'creates and deletes an api key with routePrefix: $routePrefix',
727
- async ({ routePrefix, normalizedPath }) => {
728
- const options: CreateApiKeyRouterOptions<unknown> = {
729
- getUserId,
730
- parseCreateApiKeyRequest,
731
- issuer: new URL('https://example.com'),
732
- aud: 'api-key',
733
- db,
734
- routePrefix,
735
- };
736
-
737
- const app = createApiKeyRouter(options);
738
-
739
- const createResponse = await app.fetch!(
740
- castRequest(
741
- new Request(`https://example.com${normalizedPath}`, {
742
- method: 'POST',
743
- })
744
- ),
745
- {},
746
- ctx
747
- );
748
- expect(createResponse.status).toBe(200);
749
- const createBody = (await createResponse.json()) as {
750
- api_key: string;
751
- kid: string;
752
- };
753
- const { kid } = createBody;
754
-
755
- const deleteResponse = await app.fetch!(
756
- castRequest(
757
- new Request(`https://example.com${normalizedPath}/${kid}`, {
758
- method: 'DELETE',
759
- })
760
- ),
761
- {},
762
- ctx
763
- );
764
- expect(deleteResponse.status).toBe(200);
765
-
766
- const getResponse = await app.fetch!(
767
- castRequest(
768
- new Request(`https://example.com${normalizedPath}/${kid}`, {
769
- method: 'GET',
770
- })
771
- ),
772
- {},
773
- ctx
774
- );
775
- expect(getResponse.status).toBe(200);
776
- const getBody = (await getResponse.json()) as any;
777
- expect(getBody.revoked).toBe(true);
778
- }
779
- );
780
-
781
- test.each(routePrefixTestData)(
782
- 'delete :id fails if not authenticated with routePrefix: $routePrefix',
783
- async ({ routePrefix, normalizedPath }) => {
784
- const options: CreateApiKeyRouterOptions<unknown> = {
785
- getUserId,
786
- parseCreateApiKeyRequest,
787
- issuer: new URL('https://example.com'),
788
- aud: 'api-key',
789
- db,
790
- routePrefix,
791
- };
792
-
793
- const app = createApiKeyRouter(options);
794
-
795
- const createResponse = await app.fetch!(
796
- castRequest(
797
- new Request(`https://example.com${normalizedPath}`, {
798
- method: 'POST',
799
- })
800
- ),
801
- {},
802
- ctx
803
- );
804
- expect(createResponse.status).toBe(200);
805
- const createBody = (await createResponse.json()) as {
806
- api_key: string;
807
- kid: string;
808
- };
809
- const { kid } = createBody;
810
-
811
- getUserId.mockRejectedValue(new UnauthorizedError('auth failed'));
812
- const response = await app.fetch!(
813
- castRequest(
814
- new Request(`https://example.com${normalizedPath}/${kid}`, {
815
- method: 'DELETE',
816
- })
817
- ),
818
- {},
819
- ctx
820
- );
821
- expect(response.status).toBe(403);
822
- }
823
- );
824
-
825
- test.each(routePrefixTestData)(
826
- 'delete :id fails if the key does not belong to the user with routePrefix: $routePrefix',
827
- async ({ routePrefix, normalizedPath }) => {
828
- const options: CreateApiKeyRouterOptions<unknown> = {
829
- getUserId,
830
- parseCreateApiKeyRequest,
831
- issuer: new URL('https://example.com'),
832
- aud: 'api-key',
833
- db,
834
- routePrefix,
835
- };
836
-
837
- const app = createApiKeyRouter(options);
838
-
839
- const createResponse = await app.fetch!(
840
- castRequest(
841
- new Request(`https://example.com${normalizedPath}`, {
842
- method: 'POST',
843
- })
844
- ),
845
- {},
846
- ctx
847
- );
848
- expect(createResponse.status).toBe(200);
849
- const createBody = (await createResponse.json()) as {
850
- api_key: string;
851
- kid: string;
852
- };
853
- const { kid } = createBody;
854
-
855
- getUserId.mockResolvedValue(uuidv4());
856
- const response = await app.fetch!(
857
- castRequest(
858
- new Request(`https://example.com${normalizedPath}/${kid}`, {
859
- method: 'DELETE',
860
- })
861
- ),
862
- {},
863
- ctx
864
- );
865
- expect(response.status).toBe(404);
866
- }
867
- );
868
-
869
- test.each(routePrefixTestData)(
870
- 'delete :id fails if the key does not exist with routePrefix: $routePrefix',
871
- async ({ routePrefix, normalizedPath }) => {
872
- const options: CreateApiKeyRouterOptions<unknown> = {
873
- getUserId,
874
- parseCreateApiKeyRequest,
875
- issuer: new URL('https://example.com'),
876
- aud: 'api-key',
877
- db,
878
- routePrefix,
879
- };
880
-
881
- const app = createApiKeyRouter(options);
882
-
883
- const response = await app.fetch!(
884
- castRequest(
885
- new Request(`https://example.com${normalizedPath}/non-existent-kid`, {
886
- method: 'DELETE',
887
- })
888
- ),
889
- {},
890
- ctx
891
- );
892
- expect(response.status).toBe(404);
893
- }
894
- );
895
-
896
- test.each(routePrefixTestData)(
897
- 'returns the user-defined error when authentication fails with routePrefix: $routePrefix',
898
- async ({ routePrefix, normalizedPath }) => {
899
- const expressError: any = new Error('tempest in a teapot');
900
- expressError.statusCode = 418;
901
- expressError.statusMessage = 'I am a teapot';
902
- getUserId.mockRejectedValue(expressError);
903
-
904
- const options: CreateApiKeyRouterOptions<unknown> = {
905
- getUserId,
906
- parseCreateApiKeyRequest,
907
- issuer: new URL('https://example.com'),
908
- aud: 'api-key',
909
- db,
910
- routePrefix,
911
- };
912
-
913
- const app = createApiKeyRouter(options);
914
-
915
- const promise = app.fetch!(
916
- castRequest(
917
- new Request(`https://example.com${normalizedPath}/my`, {
918
- method: 'GET',
919
- })
920
- ),
921
- {},
922
- ctx
923
- );
924
- await expect(promise).rejects.toThrow('tempest in a teapot');
925
- }
926
- );
927
-
928
- test.each(routePrefixTestData)(
929
- 'returns a japikeyerror when authentication fails with routePrefix: $routePrefix',
930
- async ({ routePrefix, normalizedPath }) => {
931
- const underlying = new Error('bad header');
932
- getUserId.mockRejectedValue(
933
- new UnauthorizedError('my custom error', { cause: underlying })
934
- );
935
-
936
- const options: CreateApiKeyRouterOptions<unknown> = {
937
- getUserId,
938
- parseCreateApiKeyRequest,
939
- issuer: new URL('https://example.com'),
940
- aud: 'api-key',
941
- db,
942
- routePrefix,
943
- };
944
-
945
- const app = createApiKeyRouter(options);
946
-
947
- const response = await app.fetch!(
948
- castRequest(
949
- new Request(`https://example.com${normalizedPath}/my`, {
950
- method: 'GET',
951
- })
952
- ),
953
- {},
954
- ctx
955
- );
956
- expect(response.status).toBe(403);
957
- const body = await response.json();
958
- expect(body).toEqual({
959
- error: {
960
- message: 'my custom error',
961
- type: errorType.UNAUTHORIZED,
962
- },
963
- });
964
- }
965
- );
966
-
967
- test.each(routePrefixTestData)(
968
- 'throws an error if a non-japikey error is thrown with routePrefix: $routePrefix',
969
- async ({ routePrefix, normalizedPath }) => {
970
- getUserId.mockImplementation(() => {
971
- throw new Error('not a japikey error');
972
- });
973
-
974
- const options: CreateApiKeyRouterOptions<unknown> = {
975
- getUserId,
976
- parseCreateApiKeyRequest,
977
- issuer: new URL('https://example.com'),
978
- aud: 'api-key',
979
- db,
980
- routePrefix,
981
- };
982
-
983
- const app = createApiKeyRouter(options);
984
-
985
- const promise = app.fetch!(
986
- castRequest(
987
- new Request(`https://example.com${normalizedPath}/my`, {
988
- method: 'GET',
989
- })
990
- ),
991
- {},
992
- ctx
993
- );
994
- await expect(promise).rejects.toThrow('not a japikey error');
995
- }
996
- );
997
-
998
- test.each(routePrefixTestData)(
999
- 'returns a 404 for a non-existent route with routePrefix: $routePrefix',
1000
- async ({ routePrefix, normalizedPath }) => {
1001
- const options: CreateApiKeyRouterOptions<unknown> = {
1002
- getUserId,
1003
- parseCreateApiKeyRequest,
1004
- issuer: new URL('https://example.com'),
1005
- aud: 'api-key',
1006
- db,
1007
- routePrefix,
1008
- };
1009
-
1010
- const app = createApiKeyRouter(options);
1011
-
1012
- const response = await app.fetch!(
1013
- castRequest(
1014
- new Request('https://example.com/non-existent-route', {
1015
- method: 'GET',
1016
- })
1017
- ),
1018
- {},
1019
- ctx
1020
- );
1021
- expect(response.status).toBe(404);
1022
- }
1023
- );
1024
- });
1025
-
1026
- describe('isJWKSPath', () => {
1027
- const baseIssuer = new URL('https://example.com');
1028
-
1029
- test('returns true for valid JWKS path with UUID', () => {
1030
- const request = new Request(
1031
- 'https://example.com/123e4567-e89b-12d3-a456-426614174000/.well-known/jwks.json'
1032
- );
1033
- expect(isJWKSPath(request, baseIssuer)).toBe(true);
1034
- });
1035
-
1036
- test('returns false for valid JWKS path with trailing slash', () => {
1037
- const request = new Request(
1038
- 'https://example.com/123e4567-e89b-12d3-a456-426614174000/.well-known/jwks.json/'
1039
- );
1040
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1041
- });
1042
-
1043
- test('returns false for invalid UUID', () => {
1044
- const request = new Request(
1045
- 'https://example.com/not-a-uuid/.well-known/jwks.json'
1046
- );
1047
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1048
- });
1049
-
1050
- test('returns false for wrong path structure', () => {
1051
- const request = new Request(
1052
- 'https://example.com/123e4567-e89b-12d3-a456-426614174000/wrong-path/jwks.json'
1053
- );
1054
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1055
- });
1056
-
1057
- test('returns false for wrong issuer', () => {
1058
- const request = new Request(
1059
- 'https://different.com/123e4567-e89b-12d3-a456-426614174000/.well-known/jwks.json'
1060
- );
1061
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1062
- });
1063
-
1064
- test('returns false for missing .well-known', () => {
1065
- const request = new Request(
1066
- 'https://example.com/123e4567-e89b-12d3-a456-426614174000/jwks.json'
1067
- );
1068
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1069
- });
1070
-
1071
- test('returns false for wrong filename', () => {
1072
- const request = new Request(
1073
- 'https://example.com/123e4567-e89b-12d3-a456-426614174000/.well-known/keys.json'
1074
- );
1075
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1076
- });
1077
-
1078
- test('returns false for extra path segments', () => {
1079
- const request = new Request(
1080
- 'https://example.com/extra/123e4567-e89b-12d3-a456-426614174000/.well-known/jwks.json'
1081
- );
1082
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1083
- });
1084
-
1085
- test('returns false for empty kid', () => {
1086
- const request = new Request('https://example.com//.well-known/jwks.json');
1087
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1088
- });
1089
-
1090
- test('returns false for non-JWKS path', () => {
1091
- const request = new Request('https://example.com/api/users');
1092
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1093
- });
1094
-
1095
- test('returns false for root path', () => {
1096
- const request = new Request('https://example.com/');
1097
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1098
- });
1099
-
1100
- test('returns false for empty path', () => {
1101
- const request = new Request('https://example.com');
1102
- expect(isJWKSPath(request, baseIssuer)).toBe(false);
1103
- });
1104
-
1105
- test('returns true for valid JWKS path with different base issuer', () => {
1106
- const customIssuer = new URL('https://api.custom.com');
1107
- const request = new Request(
1108
- 'https://api.custom.com/123e4567-e89b-12d3-a456-426614174000/.well-known/jwks.json'
1109
- );
1110
- expect(isJWKSPath(request, customIssuer)).toBe(true);
1111
- });
1112
-
1113
- test('returns false for valid JWKS path with wrong base issuer', () => {
1114
- const customIssuer = new URL('https://api.custom.com');
1115
- const request = new Request(
1116
- 'https://example.com/123e4567-e89b-12d3-a456-426614174000/.well-known/jwks.json'
1117
- );
1118
- expect(isJWKSPath(request, customIssuer)).toBe(false);
1119
- });
1120
- });