@fireproof/core-test 0.24.2-dev-cloud-api → 0.24.3-dev-ensure-cloud-token

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,12 +1,13 @@
1
- import { runtimeFn, URI } from "@adviser/cement";
1
+ import { Result, runtimeFn, URI } from "@adviser/cement";
2
2
  import { getFileName } from "@fireproof/core-gateways-base";
3
3
  import { ensureSuperThis, ensureSuperLog, getStore, inplaceFilter, makePartial, mimeBlockParser, sts, } from "@fireproof/core-runtime";
4
+ import { importJWK } from "jose";
4
5
  import { SignJWT } from "jose/jwt/sign";
5
- import { exportJWK } from "jose/key/export";
6
- import { JWTPayloadSchema } from "use-fireproof";
6
+ import { exportJWK, exportSPKI } from "jose/key/export";
7
+ import { JWKPrivateSchema, JWKPublicSchema, JWTPayloadSchema } from "use-fireproof";
7
8
  import { UUID } from "uuidv7";
8
9
  import { describe, beforeAll, it, expect, assert, vi } from "vitest";
9
- import { z } from "zod";
10
+ import { z } from "zod/v4";
10
11
  describe("utils", () => {
11
12
  const sthis = ensureSuperThis();
12
13
  const logger = ensureSuperLog(sthis, "getfilename");
@@ -158,6 +159,15 @@ describe("verifyToken", () => {
158
159
  test: z.string(),
159
160
  test1: z.number(),
160
161
  });
162
+ function parseSchema(payload) {
163
+ const r = claimSchema.safeParse(payload);
164
+ if (r.success) {
165
+ return Result.Ok(r.data);
166
+ }
167
+ else {
168
+ return Result.Err(r.error);
169
+ }
170
+ }
161
171
  beforeAll(async () => {
162
172
  pair = await sts.SessionTokenService.generateKeyPair();
163
173
  token = await new SignJWT({
@@ -172,30 +182,13 @@ describe("verifyToken", () => {
172
182
  .sign(pair.material.privateKey);
173
183
  });
174
184
  it("test coerceJWKPublic", async () => {
175
- const pemKey = await exportJWK(pair.material.publicKey);
176
- function encloseInPemBlock(jwkString) {
177
- const lines = [];
178
- const type = "PUBLIC KEY";
179
- lines.push(`-----BEGIN ${type}-----`);
180
- if (jwkString.startsWith("{")) {
181
- lines.push(JSON.stringify(JSON.parse(jwkString), null, 2));
182
- }
183
- else {
184
- for (let i = 0; i < jwkString.length; i += 64) {
185
- lines.push(jwkString.slice(i, i + 64));
186
- }
187
- }
188
- lines.push(`-----END ${type}-----`);
189
- return lines.join("\n");
190
- }
191
- const jsonString = JSON.stringify(pemKey);
185
+ const jwkKey = await exportJWK(pair.material.publicKey);
186
+ const jsonString = JSON.stringify(jwkKey);
192
187
  for (const input of [
193
- pemKey,
194
- ...[jsonString, sthis.txt.base64.encode(jsonString), sthis.txt.base58.encode(jsonString)]
195
- .map((input) => [input, encloseInPemBlock(input)])
196
- .flat(),
188
+ jwkKey,
189
+ ...[jsonString, sthis.txt.base64.encode(jsonString), sthis.txt.base58.encode(jsonString)].map((input) => [input]).flat(),
197
190
  ]) {
198
- const result = await sts.verifyToken(token, [input], [], claimSchema);
191
+ const result = await sts.verifyToken(token, [input], [], { parseSchema });
199
192
  expect(result.isOk()).toBe(true);
200
193
  }
201
194
  });
@@ -203,7 +196,7 @@ describe("verifyToken", () => {
203
196
  const presetKeys = [await exportJWK(pair.material.publicKey)];
204
197
  const wellKnownUrl = ["https://example.com/.well-known/jwks.json"];
205
198
  const mockFetch = mockFetchFactory(presetKeys);
206
- const result = await sts.verifyToken(token, presetKeys, wellKnownUrl, claimSchema, { fetch: mockFetch });
199
+ const result = await sts.verifyToken(token, presetKeys, wellKnownUrl, { fetch: mockFetch, parseSchema });
207
200
  expect(mockFetch).toHaveBeenCalledTimes(0);
208
201
  expect(result.isOk()).toBe(true);
209
202
  expect(result.Ok()).toEqual({
@@ -219,7 +212,7 @@ describe("verifyToken", () => {
219
212
  const presetKeys = [];
220
213
  const wellKnownUrl = ["https://example.com/.well-known/jwks.json"];
221
214
  const mockFetch = mockFetchFactory([await exportJWK(pair.material.publicKey)]);
222
- const result = await sts.verifyToken(token, presetKeys, wellKnownUrl, claimSchema, { fetch: mockFetch });
215
+ const result = await sts.verifyToken(token, presetKeys, wellKnownUrl, { fetch: mockFetch, parseSchema });
223
216
  expect(mockFetch).toHaveBeenCalledTimes(1);
224
217
  expect(result.isOk()).toBe(true);
225
218
  expect(result.Ok()).toEqual({
@@ -243,13 +236,13 @@ describe("verifyToken", () => {
243
236
  .setAudience("http://test.com/")
244
237
  .setExpirationTime(~~((Date.now() + 60000) / 1000))
245
238
  .sign(pair.material.privateKey);
246
- const result = await sts.verifyToken(token, presetKeys, [], claimSchema);
239
+ const result = await sts.verifyToken(token, presetKeys, [], { parseSchema });
247
240
  expect(result.isErr()).toBe(true);
248
241
  });
249
242
  it("invalid key", async () => {
250
243
  const defectKey = await sts.SessionTokenService.generateKeyPair();
251
244
  const presetKeys = [await exportJWK(defectKey.material.publicKey)];
252
- const result = await sts.verifyToken(token, presetKeys, [], claimSchema);
245
+ const result = await sts.verifyToken(token, presetKeys, [], { parseSchema });
253
246
  expect(result.isErr()).toBe(true);
254
247
  });
255
248
  });
@@ -416,4 +409,732 @@ describe("mimeBlockParser", () => {
416
409
  expect(blocks[0].content).toBe("content");
417
410
  });
418
411
  });
412
+ describe("coerceJWKPublic", () => {
413
+ const sthis = ensureSuperThis();
414
+ const jwk = {
415
+ kty: "RSA",
416
+ e: "AQAB",
417
+ n: "zuYkKADAu6UJeBq-G6MUerFLKEQVRUrQ8eJCFMWbh-JlCfr9uoEoxutWO3lpVAaFhq2vhRaYif8xoxCmvM74gCoNqE7DDUUrUTBt5kYSJyxAoLUpmVVg7pecJngcaqtPUekE_UGGJ2E2jHMRQ9thzF64BTaJFpddM45M4VEQs-PNEMnmo0NKWggikFXKCBWhakMsuygyBDtY7q73VLdG-jAG6YsVxDt_27DZ6Lv-iH2SMqM0-Xf4aYvCV_JxS75Emf1srsMC2C6_IJcIYSkxyfS6j_DE_dIzXPJ-quXhNrUgpzo2zvvMF44tDXrkeMQ5CQd06kTWe9_BxqkrVT3wLw",
418
+ alg: "RS256",
419
+ };
420
+ const jwkpub = {
421
+ ...jwk,
422
+ use: "sig",
423
+ kid: "ins_2olzJ4rRwcQ5lPbKNCYdwJ4GrFQ",
424
+ };
425
+ it("accepts JWK object", async () => {
426
+ expect(await sts.coerceJWKPublic(sthis, jwkpub)).toEqual([jwkpub]);
427
+ });
428
+ it("accepts JSON string", async () => {
429
+ expect(await sts.coerceJWKPublic(sthis, JSON.stringify(jwkpub))).toEqual([jwkpub]);
430
+ });
431
+ it("accepts base64 encoded JSON string", async () => {
432
+ expect(await sts.coerceJWKPublic(sthis, sthis.txt.base64.encode(JSON.stringify(jwkpub)))).toEqual([jwkpub]);
433
+ });
434
+ it("accepts base58 encoded JSON string", async () => {
435
+ expect(await sts.coerceJWKPublic(sthis, sthis.txt.base58.encode(JSON.stringify(jwkpub)))).toEqual([jwkpub]);
436
+ });
437
+ it("accepts PEM enclosed JSON string", async () => {
438
+ const pemWrapped = await exportSPKI((await importJWK(jwkpub, "RS256")));
439
+ expect(await sts.coerceJWKPublic(sthis, pemWrapped)).toEqual([jwk]);
440
+ });
441
+ });
442
+ describe("coerceJWK", () => {
443
+ const sthis = ensureSuperThis();
444
+ let jwkPublic;
445
+ let jwkPrivate;
446
+ let pair;
447
+ beforeAll(async () => {
448
+ pair = await sts.SessionTokenService.generateKeyPair("ES256", { extractable: true });
449
+ jwkPrivate = await exportJWK(pair.material.privateKey);
450
+ jwkPublic = await exportJWK(pair.material.publicKey);
451
+ });
452
+ describe("with public key", () => {
453
+ it("accepts public JWK object", async () => {
454
+ const result = await sts.coerceJWK(sthis, jwkPublic);
455
+ expect(result).toHaveLength(1);
456
+ expect(result[0]).toMatchObject({
457
+ kty: jwkPublic.kty,
458
+ crv: jwkPublic.crv,
459
+ x: jwkPublic.x,
460
+ });
461
+ expect(result[0]).not.toHaveProperty("d");
462
+ });
463
+ it("accepts public JWK as JSON string", async () => {
464
+ const result = await sts.coerceJWK(sthis, JSON.stringify(jwkPublic));
465
+ expect(result).toHaveLength(1);
466
+ expect(result[0]).not.toHaveProperty("d");
467
+ });
468
+ it("accepts public JWK as base64 encoded JSON", async () => {
469
+ const result = await sts.coerceJWK(sthis, sthis.txt.base64.encode(JSON.stringify(jwkPublic)));
470
+ expect(result).toHaveLength(1);
471
+ expect(result[0]).not.toHaveProperty("d");
472
+ });
473
+ it("accepts public JWK as base58 encoded JSON", async () => {
474
+ const result = await sts.coerceJWK(sthis, sthis.txt.base58.encode(JSON.stringify(jwkPublic)));
475
+ expect(result).toHaveLength(1);
476
+ expect(result[0]).not.toHaveProperty("d");
477
+ });
478
+ it("accepts public JWK via jwk2env", async () => {
479
+ const encoded = await sts.jwk2env(pair.material.publicKey, sthis);
480
+ const result = await sts.coerceJWK(sthis, encoded);
481
+ expect(result).toHaveLength(1);
482
+ expect(result[0]).not.toHaveProperty("d");
483
+ });
484
+ });
485
+ describe("with private key", () => {
486
+ it("accepts private JWK object", async () => {
487
+ const result = await sts.coerceJWK(sthis, jwkPrivate);
488
+ expect(result).toHaveLength(1);
489
+ expect(result[0]).toMatchObject({
490
+ kty: jwkPrivate.kty,
491
+ crv: jwkPrivate.crv,
492
+ x: jwkPrivate.x,
493
+ d: jwkPrivate.d,
494
+ });
495
+ });
496
+ it("accepts private JWK as JSON string", async () => {
497
+ const result = await sts.coerceJWK(sthis, JSON.stringify(jwkPrivate));
498
+ expect(result).toHaveLength(1);
499
+ expect(result[0]).toHaveProperty("d");
500
+ });
501
+ it("accepts private JWK as base64 encoded JSON", async () => {
502
+ const result = await sts.coerceJWK(sthis, sthis.txt.base64.encode(JSON.stringify(jwkPrivate)));
503
+ expect(result).toHaveLength(1);
504
+ expect(result[0]).toHaveProperty("d");
505
+ });
506
+ it("accepts private JWK as base58 encoded JSON", async () => {
507
+ const result = await sts.coerceJWK(sthis, sthis.txt.base58.encode(JSON.stringify(jwkPrivate)));
508
+ expect(result).toHaveLength(1);
509
+ expect(result[0]).toHaveProperty("d");
510
+ });
511
+ it("accepts private JWK via jwk2env", async () => {
512
+ const encoded = await sts.jwk2env(pair.material.privateKey, sthis);
513
+ const result = await sts.coerceJWK(sthis, encoded);
514
+ expect(result).toHaveLength(1);
515
+ expect(result[0]).toHaveProperty("d");
516
+ });
517
+ it("preserves private key through env2jwk", async () => {
518
+ const encoded = await sts.jwk2env(pair.material.privateKey, sthis);
519
+ const cryptoKeys = await sts.env2jwk(encoded, undefined, sthis);
520
+ expect(cryptoKeys).toHaveLength(1);
521
+ const exported = await exportJWK(cryptoKeys[0]);
522
+ expect(exported).toHaveProperty("d");
523
+ });
524
+ });
525
+ describe("with multiple keys", () => {
526
+ it("accepts array of mixed public and private keys", async () => {
527
+ const result = await sts.coerceJWK(sthis, [jwkPrivate, jwkPublic]);
528
+ expect(result).toHaveLength(2);
529
+ expect(result[0]).toHaveProperty("d");
530
+ expect(result[1]).not.toHaveProperty("d");
531
+ });
532
+ });
533
+ });
534
+ describe("coerceJWKPrivate", () => {
535
+ const sthis = ensureSuperThis();
536
+ let jwkPrivate;
537
+ let jwkPublic;
538
+ let pair;
539
+ beforeAll(async () => {
540
+ pair = await sts.SessionTokenService.generateKeyPair("ES256", { extractable: true });
541
+ jwkPrivate = await exportJWK(pair.material.privateKey);
542
+ jwkPublic = await exportJWK(pair.material.publicKey);
543
+ });
544
+ it("accepts private JWK object", async () => {
545
+ const result = await sts.coerceJWKPrivate(sthis, jwkPrivate);
546
+ expect(result).toHaveLength(1);
547
+ expect(result[0]).toMatchObject({
548
+ kty: jwkPrivate.kty,
549
+ crv: jwkPrivate.crv,
550
+ d: jwkPrivate.d,
551
+ });
552
+ });
553
+ it("accepts private JWK as JSON string", async () => {
554
+ const result = await sts.coerceJWKPrivate(sthis, JSON.stringify(jwkPrivate));
555
+ expect(result).toHaveLength(1);
556
+ expect(result[0]).toHaveProperty("d");
557
+ });
558
+ it("accepts private JWK as base64 encoded JSON", async () => {
559
+ const result = await sts.coerceJWKPrivate(sthis, sthis.txt.base64.encode(JSON.stringify(jwkPrivate)));
560
+ expect(result).toHaveLength(1);
561
+ expect(result[0]).toHaveProperty("d");
562
+ });
563
+ it("accepts private JWK as base58 encoded JSON", async () => {
564
+ const result = await sts.coerceJWKPrivate(sthis, sthis.txt.base58.encode(JSON.stringify(jwkPrivate)));
565
+ expect(result).toHaveLength(1);
566
+ expect(result[0]).toHaveProperty("d");
567
+ });
568
+ it("accepts private JWK via jwk2env", async () => {
569
+ const encoded = await sts.jwk2env(pair.material.privateKey, sthis);
570
+ const result = await sts.coerceJWKPrivate(sthis, encoded);
571
+ expect(result).toHaveLength(1);
572
+ expect(result[0]).toHaveProperty("d");
573
+ });
574
+ it("rejects public JWK (missing private key component)", async () => {
575
+ const result = await sts.coerceJWKPrivate(sthis, jwkPublic);
576
+ expect(result).toHaveLength(0);
577
+ });
578
+ it("rejects public JWK as JSON string", async () => {
579
+ const result = await sts.coerceJWKPrivate(sthis, JSON.stringify(jwkPublic));
580
+ expect(result).toHaveLength(0);
581
+ });
582
+ it("rejects public JWK via jwk2env", async () => {
583
+ const encoded = await sts.jwk2env(pair.material.publicKey, sthis);
584
+ const result = await sts.coerceJWKPrivate(sthis, encoded);
585
+ expect(result).toHaveLength(0);
586
+ });
587
+ it("filters out public keys from mixed array", async () => {
588
+ const result = await sts.coerceJWKPrivate(sthis, [jwkPrivate, jwkPublic]);
589
+ expect(result).toHaveLength(1);
590
+ expect(result[0]).toHaveProperty("d");
591
+ });
592
+ });
593
+ describe("coerceJWKWithSchema - mixed Private/Public keys in { keys: [...] } arrays", () => {
594
+ const sthis = ensureSuperThis();
595
+ let rsaPrivateKey;
596
+ let rsaPublicKey;
597
+ let ecPrivateKey;
598
+ let ecPublicKey;
599
+ let rsaPair;
600
+ let ecPair;
601
+ beforeAll(async () => {
602
+ rsaPair = await sts.SessionTokenService.generateKeyPair("RS256", { extractable: true });
603
+ rsaPrivateKey = await exportJWK(rsaPair.material.privateKey);
604
+ rsaPublicKey = await exportJWK(rsaPair.material.publicKey);
605
+ ecPair = await sts.SessionTokenService.generateKeyPair("ES256", { extractable: true });
606
+ ecPrivateKey = await exportJWK(ecPair.material.privateKey);
607
+ ecPublicKey = await exportJWK(ecPair.material.publicKey);
608
+ });
609
+ const encodings = [
610
+ {
611
+ name: "plain object",
612
+ encode: (obj) => obj,
613
+ },
614
+ {
615
+ name: "JSON string",
616
+ encode: (obj) => JSON.stringify(obj),
617
+ },
618
+ {
619
+ name: "base64",
620
+ encode: (obj) => sthis.txt.base64.encode(JSON.stringify(obj)),
621
+ },
622
+ {
623
+ name: "base58",
624
+ encode: (obj) => sthis.txt.base58.encode(JSON.stringify(obj)),
625
+ },
626
+ ];
627
+ describe.each(encodings)("with encoding: $name", ({ encode }) => {
628
+ describe("JWKPrivateSchema validation", () => {
629
+ it("accepts { keys: [private] }", async () => {
630
+ const input = encode({ keys: [rsaPrivateKey] });
631
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
632
+ expect(results).toHaveLength(1);
633
+ expect(results[0].isOk()).toBe(true);
634
+ if (results[0].isOk()) {
635
+ expect(results[0].Ok()).toHaveProperty("d");
636
+ expect(results[0].Ok().kty).toBe("RSA");
637
+ }
638
+ });
639
+ it("rejects { keys: [public] }", async () => {
640
+ const input = encode({ keys: [rsaPublicKey] });
641
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
642
+ expect(results).toHaveLength(1);
643
+ expect(results[0].isErr()).toBe(true);
644
+ });
645
+ it("handles { keys: [private, public] } - accepts only private", async () => {
646
+ const input = encode({ keys: [rsaPrivateKey, rsaPublicKey] });
647
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
648
+ expect(results).toHaveLength(2);
649
+ expect(results[0].isOk()).toBe(true);
650
+ expect(results[1].isErr()).toBe(true);
651
+ if (results[0].isOk()) {
652
+ expect(results[0].Ok()).toHaveProperty("d");
653
+ }
654
+ });
655
+ it("handles { keys: [public, private] } - reversed order", async () => {
656
+ const input = encode({ keys: [ecPublicKey, ecPrivateKey] });
657
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
658
+ expect(results).toHaveLength(2);
659
+ expect(results[0].isErr()).toBe(true);
660
+ expect(results[1].isOk()).toBe(true);
661
+ if (results[1].isOk()) {
662
+ expect(results[1].Ok()).toHaveProperty("d");
663
+ expect(results[1].Ok().kty).toBe("EC");
664
+ }
665
+ });
666
+ it("handles { keys: [rsaPrivate, ecPrivate] } - multiple private keys", async () => {
667
+ const input = encode({ keys: [rsaPrivateKey, ecPrivateKey] });
668
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
669
+ expect(results).toHaveLength(2);
670
+ expect(results[0].isOk()).toBe(true);
671
+ expect(results[1].isOk()).toBe(true);
672
+ if (results[0].isOk() && results[1].isOk()) {
673
+ expect(results[0].Ok()).toHaveProperty("d");
674
+ expect(results[0].Ok().kty).toBe("RSA");
675
+ expect(results[1].Ok()).toHaveProperty("d");
676
+ expect(results[1].Ok().kty).toBe("EC");
677
+ }
678
+ });
679
+ it("handles { keys: [rsaPrivate, ecPublic, ecPrivate, rsaPublic] } - complex mix", async () => {
680
+ const input = encode({ keys: [rsaPrivateKey, ecPublicKey, ecPrivateKey, rsaPublicKey] });
681
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
682
+ expect(results).toHaveLength(4);
683
+ expect(results[0].isOk()).toBe(true);
684
+ expect(results[1].isErr()).toBe(true);
685
+ expect(results[2].isOk()).toBe(true);
686
+ expect(results[3].isErr()).toBe(true);
687
+ });
688
+ });
689
+ describe("JWKPublicSchema validation", () => {
690
+ it("accepts { keys: [public] }", async () => {
691
+ const input = encode({ keys: [rsaPublicKey] });
692
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
693
+ expect(results).toHaveLength(1);
694
+ expect(results[0].isOk()).toBe(true);
695
+ if (results[0].isOk()) {
696
+ expect(results[0].Ok()).not.toHaveProperty("d");
697
+ expect(results[0].Ok().kty).toBe("RSA");
698
+ }
699
+ });
700
+ it("strips private component from { keys: [private] }", async () => {
701
+ const input = encode({ keys: [rsaPrivateKey] });
702
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
703
+ expect(results).toHaveLength(1);
704
+ expect(results[0].isOk()).toBe(true);
705
+ if (results[0].isOk()) {
706
+ expect(results[0].Ok()).not.toHaveProperty("d");
707
+ expect(results[0].Ok().kty).toBe("RSA");
708
+ }
709
+ });
710
+ it("handles { keys: [private, public] } - accepts both, strips private from first", async () => {
711
+ const input = encode({ keys: [rsaPrivateKey, rsaPublicKey] });
712
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
713
+ expect(results).toHaveLength(2);
714
+ expect(results[0].isOk()).toBe(true);
715
+ expect(results[1].isOk()).toBe(true);
716
+ if (results[0].isOk() && results[1].isOk()) {
717
+ expect(results[0].Ok()).not.toHaveProperty("d");
718
+ expect(results[1].Ok()).not.toHaveProperty("d");
719
+ }
720
+ });
721
+ it("handles { keys: [public, private, public] }", async () => {
722
+ const input = encode({ keys: [ecPublicKey, ecPrivateKey, rsaPublicKey] });
723
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
724
+ expect(results).toHaveLength(3);
725
+ expect(results[0].isOk()).toBe(true);
726
+ expect(results[1].isOk()).toBe(true);
727
+ expect(results[2].isOk()).toBe(true);
728
+ if (results[0].isOk() && results[1].isOk() && results[2].isOk()) {
729
+ expect(results[0].Ok()).not.toHaveProperty("d");
730
+ expect(results[1].Ok()).not.toHaveProperty("d");
731
+ expect(results[2].Ok()).not.toHaveProperty("d");
732
+ }
733
+ });
734
+ it("handles { keys: [rsaPrivate, ecPublic, rsaPublic, ecPrivate] } - complex mix", async () => {
735
+ const input = encode({ keys: [rsaPrivateKey, ecPublicKey, rsaPublicKey, ecPrivateKey] });
736
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
737
+ expect(results).toHaveLength(4);
738
+ results.forEach((result) => {
739
+ expect(result.isOk()).toBe(true);
740
+ if (result.isOk()) {
741
+ expect(result.Ok()).not.toHaveProperty("d");
742
+ }
743
+ });
744
+ });
745
+ });
746
+ describe("edge cases", () => {
747
+ it("handles empty { keys: [] }", async () => {
748
+ const input = encode({ keys: [] });
749
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
750
+ expect(results).toHaveLength(0);
751
+ });
752
+ it("handles many mixed keys", async () => {
753
+ const input = encode({
754
+ keys: [
755
+ rsaPrivateKey,
756
+ rsaPublicKey,
757
+ ecPrivateKey,
758
+ ecPublicKey,
759
+ rsaPrivateKey, // duplicate
760
+ ecPublicKey,
761
+ ],
762
+ });
763
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
764
+ expect(results).toHaveLength(6);
765
+ results.forEach((result) => {
766
+ expect(result.isOk()).toBe(true);
767
+ if (result.isOk()) {
768
+ expect(result.Ok()).not.toHaveProperty("d");
769
+ }
770
+ });
771
+ });
772
+ });
773
+ });
774
+ describe("multiple inputs (not encoding-specific)", () => {
775
+ it("handles multiple { keys: [...] } objects as separate arguments", async () => {
776
+ const input1 = { keys: [rsaPrivateKey, rsaPublicKey] };
777
+ const input2 = { keys: [ecPrivateKey] };
778
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input1, input2);
779
+ expect(results).toHaveLength(3);
780
+ expect(results[0].isOk()).toBe(true);
781
+ expect(results[1].isErr()).toBe(true);
782
+ expect(results[2].isOk()).toBe(true);
783
+ });
784
+ it("handles array of { keys: [...] } objects", async () => {
785
+ const inputs = [{ keys: [rsaPrivateKey, rsaPublicKey] }, { keys: [ecPrivateKey, ecPublicKey] }];
786
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, inputs);
787
+ expect(results).toHaveLength(4);
788
+ results.forEach((result) => {
789
+ expect(result.isOk()).toBe(true);
790
+ });
791
+ });
792
+ it("handles mix of plain keys and { keys: [...] }", async () => {
793
+ const inputs = [rsaPrivateKey, { keys: [ecPrivateKey, ecPublicKey] }, rsaPublicKey];
794
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, inputs);
795
+ expect(results).toHaveLength(4);
796
+ expect(results[0].isOk()).toBe(true);
797
+ expect(results[1].isOk()).toBe(true);
798
+ expect(results[2].isErr()).toBe(true);
799
+ expect(results[3].isErr()).toBe(true);
800
+ });
801
+ });
802
+ });
803
+ describe("coerceJWKWithSchema - single key objects (not { keys: [...] } wrapped)", () => {
804
+ const sthis = ensureSuperThis();
805
+ let rsaPrivateKey;
806
+ let rsaPublicKey;
807
+ let ecPrivateKey;
808
+ let ecPublicKey;
809
+ let rsaPair;
810
+ let ecPair;
811
+ beforeAll(async () => {
812
+ rsaPair = await sts.SessionTokenService.generateKeyPair("RS256", { extractable: true });
813
+ rsaPrivateKey = await exportJWK(rsaPair.material.privateKey);
814
+ rsaPublicKey = await exportJWK(rsaPair.material.publicKey);
815
+ ecPair = await sts.SessionTokenService.generateKeyPair("ES256", { extractable: true });
816
+ ecPrivateKey = await exportJWK(ecPair.material.privateKey);
817
+ ecPublicKey = await exportJWK(ecPair.material.publicKey);
818
+ });
819
+ const encodings = [
820
+ {
821
+ name: "plain JWK object",
822
+ encode: (jwk) => jwk,
823
+ },
824
+ {
825
+ name: "JSON string",
826
+ encode: (jwk) => JSON.stringify(jwk),
827
+ },
828
+ {
829
+ name: "base64",
830
+ encode: (jwk) => sthis.txt.base64.encode(JSON.stringify(jwk)),
831
+ },
832
+ {
833
+ name: "base58",
834
+ encode: (jwk) => sthis.txt.base58.encode(JSON.stringify(jwk)),
835
+ },
836
+ ];
837
+ describe.each(encodings)("with encoding: $name", ({ encode }) => {
838
+ describe("JWKPrivateSchema validation", () => {
839
+ it("accepts private key", async () => {
840
+ const input = encode(rsaPrivateKey);
841
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
842
+ expect(results).toHaveLength(1);
843
+ expect(results[0].isOk()).toBe(true);
844
+ if (results[0].isOk()) {
845
+ expect(results[0].Ok()).toHaveProperty("d");
846
+ expect(results[0].Ok().kty).toBe("RSA");
847
+ }
848
+ });
849
+ it("rejects public key", async () => {
850
+ const input = encode(rsaPublicKey);
851
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
852
+ expect(results).toHaveLength(1);
853
+ expect(results[0].isErr()).toBe(true);
854
+ });
855
+ it("accepts EC private key", async () => {
856
+ const input = encode(ecPrivateKey);
857
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, input);
858
+ expect(results).toHaveLength(1);
859
+ expect(results[0].isOk()).toBe(true);
860
+ if (results[0].isOk()) {
861
+ expect(results[0].Ok()).toHaveProperty("d");
862
+ expect(results[0].Ok().kty).toBe("EC");
863
+ }
864
+ });
865
+ });
866
+ describe("JWKPublicSchema validation", () => {
867
+ it("accepts public key", async () => {
868
+ const input = encode(rsaPublicKey);
869
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
870
+ expect(results).toHaveLength(1);
871
+ expect(results[0].isOk()).toBe(true);
872
+ if (results[0].isOk()) {
873
+ expect(results[0].Ok()).not.toHaveProperty("d");
874
+ expect(results[0].Ok().kty).toBe("RSA");
875
+ }
876
+ });
877
+ it("strips private component from private key", async () => {
878
+ const input = encode(rsaPrivateKey);
879
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
880
+ expect(results).toHaveLength(1);
881
+ expect(results[0].isOk()).toBe(true);
882
+ if (results[0].isOk()) {
883
+ expect(results[0].Ok()).not.toHaveProperty("d");
884
+ expect(results[0].Ok().kty).toBe("RSA");
885
+ }
886
+ });
887
+ it("accepts and processes EC public key", async () => {
888
+ const input = encode(ecPublicKey);
889
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, input);
890
+ expect(results).toHaveLength(1);
891
+ expect(results[0].isOk()).toBe(true);
892
+ if (results[0].isOk()) {
893
+ expect(results[0].Ok()).not.toHaveProperty("d");
894
+ expect(results[0].Ok().kty).toBe("EC");
895
+ }
896
+ });
897
+ });
898
+ });
899
+ describe("multiple single key inputs", () => {
900
+ it("handles array of plain JWK objects with private schema", async () => {
901
+ const inputs = [rsaPrivateKey, ecPrivateKey];
902
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, inputs);
903
+ expect(results).toHaveLength(2);
904
+ expect(results[0].isOk()).toBe(true);
905
+ expect(results[1].isOk()).toBe(true);
906
+ });
907
+ it("handles array of mixed plain JWK objects with private schema", async () => {
908
+ const inputs = [rsaPrivateKey, rsaPublicKey, ecPrivateKey];
909
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, inputs);
910
+ expect(results).toHaveLength(3);
911
+ expect(results[0].isOk()).toBe(true);
912
+ expect(results[1].isErr()).toBe(true);
913
+ expect(results[2].isOk()).toBe(true);
914
+ });
915
+ it("handles array of mixed plain JWK objects with public schema", async () => {
916
+ const inputs = [rsaPrivateKey, rsaPublicKey, ecPublicKey];
917
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, inputs);
918
+ expect(results).toHaveLength(3);
919
+ results.forEach((result) => {
920
+ expect(result.isOk()).toBe(true);
921
+ if (result.isOk()) {
922
+ expect(result.Ok()).not.toHaveProperty("d");
923
+ }
924
+ });
925
+ });
926
+ it("handles separate arguments (not array)", async () => {
927
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPrivateSchema, rsaPrivateKey, ecPrivateKey);
928
+ expect(results).toHaveLength(2);
929
+ expect(results[0].isOk()).toBe(true);
930
+ expect(results[1].isOk()).toBe(true);
931
+ });
932
+ });
933
+ });
934
+ describe("coerceJWKWithSchema - error cases and invalid inputs", () => {
935
+ const sthis = ensureSuperThis();
936
+ let rsaPrivateKey;
937
+ let rsaPublicKey;
938
+ beforeAll(async () => {
939
+ const rsaPair = await sts.SessionTokenService.generateKeyPair("RS256", { extractable: true });
940
+ rsaPrivateKey = await exportJWK(rsaPair.material.privateKey);
941
+ rsaPublicKey = await exportJWK(rsaPair.material.publicKey);
942
+ });
943
+ describe("invalid JSON strings", () => {
944
+ it("handles invalid JSON string", async () => {
945
+ const invalidJson = "{ this is not valid json }";
946
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalidJson);
947
+ expect(results).toHaveLength(1);
948
+ expect(results[0].isErr()).toBe(true);
949
+ });
950
+ it("handles base64 encoded invalid JSON", async () => {
951
+ const invalidJson = "{ not valid json either }";
952
+ const encoded = sthis.txt.base64.encode(invalidJson);
953
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, encoded);
954
+ expect(results).toHaveLength(1);
955
+ expect(results[0].isErr()).toBe(true);
956
+ });
957
+ it("handles base58 encoded invalid JSON", async () => {
958
+ const invalidJson = "{ malformed: json }";
959
+ const encoded = sthis.txt.base58.encode(invalidJson);
960
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, encoded);
961
+ expect(results).toHaveLength(1);
962
+ expect(results[0].isErr()).toBe(true);
963
+ });
964
+ it("handles truncated JSON string", async () => {
965
+ const truncated = JSON.stringify(rsaPublicKey).slice(0, -10);
966
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, truncated);
967
+ expect(results).toHaveLength(1);
968
+ expect(results[0].isErr()).toBe(true);
969
+ });
970
+ });
971
+ describe("invalid JWK structures", () => {
972
+ it("rejects JWK missing required 'kty' field", async () => {
973
+ const invalidJwk = { e: "AQAB", n: "somevalue" };
974
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalidJwk);
975
+ expect(results).toHaveLength(1);
976
+ expect(results[0].isErr()).toBe(true);
977
+ });
978
+ it("rejects JWK with invalid 'kty' value", async () => {
979
+ const invalidJwk = { kty: "INVALID", e: "AQAB", n: "somevalue" };
980
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalidJwk);
981
+ expect(results).toHaveLength(1);
982
+ expect(results[0].isErr()).toBe(true);
983
+ });
984
+ it("rejects RSA key missing required 'n' field", async () => {
985
+ const invalidJwk = { kty: "RSA", e: "AQAB" };
986
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalidJwk);
987
+ expect(results).toHaveLength(1);
988
+ expect(results[0].isErr()).toBe(true);
989
+ });
990
+ it("rejects RSA key missing required 'e' field", async () => {
991
+ const invalidJwk = { kty: "RSA", n: "somevalue" };
992
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalidJwk);
993
+ expect(results).toHaveLength(1);
994
+ expect(results[0].isErr()).toBe(true);
995
+ });
996
+ it("rejects EC key missing required 'x' field", async () => {
997
+ const invalidJwk = { kty: "EC", crv: "P-256", y: "somevalue" };
998
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalidJwk);
999
+ expect(results).toHaveLength(1);
1000
+ expect(results[0].isErr()).toBe(true);
1001
+ });
1002
+ it("rejects EC key missing required 'crv' field", async () => {
1003
+ const invalidJwk = { kty: "EC", x: "somevalue", y: "anothervalue" };
1004
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalidJwk);
1005
+ expect(results).toHaveLength(1);
1006
+ expect(results[0].isErr()).toBe(true);
1007
+ });
1008
+ it("rejects EC key with invalid curve", async () => {
1009
+ const invalidJwk = { kty: "EC", crv: "INVALID-CURVE", x: "somevalue", y: "anothervalue" };
1010
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalidJwk);
1011
+ expect(results).toHaveLength(1);
1012
+ expect(results[0].isErr()).toBe(true);
1013
+ });
1014
+ });
1015
+ describe("invalid { keys: [...] } structures", () => {
1016
+ it("rejects { keys: 'not-an-array' }", async () => {
1017
+ const invalid = { keys: "not an array" };
1018
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalid);
1019
+ expect(results).toHaveLength(1);
1020
+ expect(results[0].isErr()).toBe(true);
1021
+ });
1022
+ it("rejects { keys: null }", async () => {
1023
+ const invalid = { keys: null };
1024
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalid);
1025
+ expect(results).toHaveLength(1);
1026
+ expect(results[0].isErr()).toBe(true);
1027
+ });
1028
+ it("rejects { keys: {} }", async () => {
1029
+ const invalid = { keys: {} };
1030
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, invalid);
1031
+ expect(results).toHaveLength(1);
1032
+ expect(results[0].isErr()).toBe(true);
1033
+ });
1034
+ it("handles { keys: [...] } with mix of valid and invalid JWKs", async () => {
1035
+ const mixed = {
1036
+ keys: [
1037
+ rsaPublicKey, // valid
1038
+ { kty: "INVALID" }, // invalid
1039
+ rsaPrivateKey,
1040
+ ],
1041
+ };
1042
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, mixed);
1043
+ expect(results).toHaveLength(3);
1044
+ expect(results[0].isOk()).toBe(true);
1045
+ expect(results[1].isErr()).toBe(true);
1046
+ expect(results[2].isOk()).toBe(true);
1047
+ });
1048
+ it("handles JSON stringified { keys: [...] } with invalid entries", async () => {
1049
+ const mixed = JSON.stringify({
1050
+ keys: [rsaPublicKey, { invalid: "structure" }],
1051
+ });
1052
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, mixed);
1053
+ expect(results).toHaveLength(2);
1054
+ expect(results[0].isOk()).toBe(true);
1055
+ expect(results[1].isErr()).toBe(true);
1056
+ });
1057
+ });
1058
+ describe("completely invalid data types", () => {
1059
+ it("rejects number input", async () => {
1060
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, 12345);
1061
+ expect(results).toHaveLength(1);
1062
+ expect(results[0].isErr()).toBe(true);
1063
+ });
1064
+ it("rejects boolean input", async () => {
1065
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, true);
1066
+ expect(results).toHaveLength(1);
1067
+ expect(results[0].isErr()).toBe(true);
1068
+ });
1069
+ it("rejects array of invalid types", async () => {
1070
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, [123, true, "invalid"]);
1071
+ expect(results).toHaveLength(3);
1072
+ expect(results[0].isErr()).toBe(true);
1073
+ expect(results[1].isErr()).toBe(true);
1074
+ expect(results[2].isErr()).toBe(true);
1075
+ });
1076
+ it("rejects empty object", async () => {
1077
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, {});
1078
+ expect(results).toHaveLength(1);
1079
+ expect(results[0].isErr()).toBe(true);
1080
+ });
1081
+ it("rejects null", async () => {
1082
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, null);
1083
+ expect(results).toHaveLength(1);
1084
+ expect(results[0].isErr()).toBe(true);
1085
+ });
1086
+ it("rejects undefined", async () => {
1087
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, undefined);
1088
+ expect(results).toHaveLength(1);
1089
+ expect(results[0].isErr()).toBe(true);
1090
+ });
1091
+ });
1092
+ describe("edge cases with encoding", () => {
1093
+ it("handles corrupted base64 string", async () => {
1094
+ const corrupted = "!!!invalid-base64!!!";
1095
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, corrupted);
1096
+ expect(results).toHaveLength(1);
1097
+ expect(results[0].isErr()).toBe(true);
1098
+ });
1099
+ it("handles empty string", async () => {
1100
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, "");
1101
+ expect(results).toHaveLength(1);
1102
+ expect(results[0].isErr()).toBe(true);
1103
+ });
1104
+ it("handles whitespace-only string", async () => {
1105
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, " \n\t ");
1106
+ expect(results).toHaveLength(1);
1107
+ expect(results[0].isErr()).toBe(true);
1108
+ });
1109
+ it("handles string that looks like JSON but isn't", async () => {
1110
+ const fakeJson = "{looks like json but missing quotes and commas}";
1111
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, fakeJson);
1112
+ expect(results).toHaveLength(1);
1113
+ expect(results[0].isErr()).toBe(true);
1114
+ });
1115
+ });
1116
+ describe("mixed valid and invalid inputs", () => {
1117
+ it("processes array with mix of valid keys and invalid data", async () => {
1118
+ const mixed = [
1119
+ rsaPublicKey, // valid
1120
+ "invalid string", // invalid
1121
+ rsaPrivateKey, // valid
1122
+ { invalid: "object" },
1123
+ ];
1124
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, mixed);
1125
+ expect(results).toHaveLength(4);
1126
+ expect(results[0].isOk()).toBe(true);
1127
+ expect(results[1].isErr()).toBe(true);
1128
+ expect(results[2].isOk()).toBe(true);
1129
+ expect(results[3].isErr()).toBe(true);
1130
+ });
1131
+ it("processes multiple arguments with some invalid", async () => {
1132
+ const results = await sts.coerceJWKWithSchema(sthis, JWKPublicSchema, rsaPublicKey, "invalid", rsaPrivateKey);
1133
+ expect(results).toHaveLength(3);
1134
+ expect(results[0].isOk()).toBe(true);
1135
+ expect(results[1].isErr()).toBe(true);
1136
+ expect(results[2].isOk()).toBe(true);
1137
+ });
1138
+ });
1139
+ });
419
1140
  //# sourceMappingURL=utils.test.js.map