@learncard/sss-key-manager 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-client.d.ts +49 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/atomic-operations.d.ts +101 -0
- package/dist/atomic-operations.d.ts.map +1 -0
- package/dist/auth-coordinator.d.ts +10 -0
- package/dist/auth-coordinator.d.ts.map +1 -0
- package/dist/crypto.d.ts +31 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/key-manager.d.ts +30 -0
- package/dist/key-manager.d.ts.map +1 -0
- package/dist/passkey.d.ts +23 -0
- package/dist/passkey.d.ts.map +1 -0
- package/dist/qr-crypto.d.ts +56 -0
- package/dist/qr-crypto.d.ts.map +1 -0
- package/dist/qr-login.d.ts +122 -0
- package/dist/qr-login.d.ts.map +1 -0
- package/dist/recovery-phrase.d.ts +14 -0
- package/dist/recovery-phrase.d.ts.map +1 -0
- package/dist/sss-key-manager.cjs.development.js +39 -3
- package/dist/sss-key-manager.cjs.development.js.map +2 -2
- package/dist/sss-key-manager.cjs.production.min.js +20 -20
- package/dist/sss-key-manager.cjs.production.min.js.map +3 -3
- package/dist/sss-key-manager.esm.js +39 -3
- package/dist/sss-key-manager.esm.js.map +2 -2
- package/dist/sss-strategy.d.ts +82 -0
- package/dist/sss-strategy.d.ts.map +1 -0
- package/dist/sss.d.ts +15 -0
- package/dist/sss.d.ts.map +1 -0
- package/dist/storage.d.ts +46 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +8 -8
- package/src/crypto.ts +9 -14
- package/src/recovery-phrase.ts +7 -4
- package/src/sss-strategy.test.ts +404 -246
- package/dist/sss-key-manager.d.ts +0 -898
package/src/sss-strategy.test.ts
CHANGED
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
19
19
|
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createSSSStrategy,
|
|
22
|
+
formatVersionedEmailShare,
|
|
23
|
+
parseVersionedEmailShare,
|
|
24
|
+
} from './sss-strategy';
|
|
21
25
|
import { reconstructFromShares } from './sss';
|
|
22
26
|
import { splitAndVerify } from './atomic-operations';
|
|
23
27
|
import { shareToRecoveryPhrase, recoveryPhraseToShare } from './recovery-phrase';
|
|
@@ -31,7 +35,10 @@ import type { SSSKeyDerivationStrategy } from './types';
|
|
|
31
35
|
|
|
32
36
|
const DEFAULT_KEY = 'device';
|
|
33
37
|
|
|
34
|
-
const createMemoryStorage = (): SSSStorageFunctions & {
|
|
38
|
+
const createMemoryStorage = (): SSSStorageFunctions & {
|
|
39
|
+
_store: Map<string, string>;
|
|
40
|
+
_versions: Map<string, number>;
|
|
41
|
+
} => {
|
|
35
42
|
const store = new Map<string, string>();
|
|
36
43
|
const versions = new Map<string, number>();
|
|
37
44
|
|
|
@@ -322,13 +329,16 @@ describe('createSSSStrategy', () => {
|
|
|
322
329
|
describe('fetchServerKeyStatus shareVersion', () => {
|
|
323
330
|
it('returns shareVersion when server includes it', async () => {
|
|
324
331
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
325
|
-
new Response(
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
+
new Response(
|
|
333
|
+
JSON.stringify({
|
|
334
|
+
authShare: 'share-data',
|
|
335
|
+
keyProvider: 'sss',
|
|
336
|
+
primaryDid: 'did:key:z123',
|
|
337
|
+
recoveryMethods: [],
|
|
338
|
+
shareVersion: 5,
|
|
339
|
+
}),
|
|
340
|
+
{ status: 200 }
|
|
341
|
+
)
|
|
332
342
|
);
|
|
333
343
|
|
|
334
344
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -338,12 +348,15 @@ describe('createSSSStrategy', () => {
|
|
|
338
348
|
|
|
339
349
|
it('returns null shareVersion when server omits it', async () => {
|
|
340
350
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
341
|
-
new Response(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
351
|
+
new Response(
|
|
352
|
+
JSON.stringify({
|
|
353
|
+
authShare: 'share-data',
|
|
354
|
+
keyProvider: 'sss',
|
|
355
|
+
primaryDid: 'did:key:z123',
|
|
356
|
+
recoveryMethods: [],
|
|
357
|
+
}),
|
|
358
|
+
{ status: 200 }
|
|
359
|
+
)
|
|
347
360
|
);
|
|
348
361
|
|
|
349
362
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -358,13 +371,16 @@ describe('createSSSStrategy', () => {
|
|
|
358
371
|
expect(await strategy.getLocalShareVersion!()).toBeNull();
|
|
359
372
|
|
|
360
373
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
361
|
-
new Response(
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
374
|
+
new Response(
|
|
375
|
+
JSON.stringify({
|
|
376
|
+
authShare: 'share-data',
|
|
377
|
+
keyProvider: 'sss',
|
|
378
|
+
primaryDid: 'did:key:z123',
|
|
379
|
+
recoveryMethods: [],
|
|
380
|
+
shareVersion: 3,
|
|
381
|
+
}),
|
|
382
|
+
{ status: 200 }
|
|
383
|
+
)
|
|
368
384
|
);
|
|
369
385
|
|
|
370
386
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -374,7 +390,10 @@ describe('createSSSStrategy', () => {
|
|
|
374
390
|
// Wait for the fire-and-forget backfill to complete
|
|
375
391
|
await new Promise(r => setTimeout(r, 10));
|
|
376
392
|
|
|
377
|
-
expect(storage.storeShareVersion).toHaveBeenCalledWith(
|
|
393
|
+
expect(storage.storeShareVersion).toHaveBeenCalledWith(
|
|
394
|
+
3,
|
|
395
|
+
'sss-device-share:legacy-user'
|
|
396
|
+
);
|
|
378
397
|
expect(await strategy.getLocalShareVersion!()).toBe(3);
|
|
379
398
|
});
|
|
380
399
|
|
|
@@ -385,13 +404,16 @@ describe('createSSSStrategy', () => {
|
|
|
385
404
|
storage.storeShareVersion.mockClear();
|
|
386
405
|
|
|
387
406
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
388
|
-
new Response(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
407
|
+
new Response(
|
|
408
|
+
JSON.stringify({
|
|
409
|
+
authShare: 'share-data',
|
|
410
|
+
keyProvider: 'sss',
|
|
411
|
+
primaryDid: 'did:key:z123',
|
|
412
|
+
recoveryMethods: [],
|
|
413
|
+
shareVersion: 5,
|
|
414
|
+
}),
|
|
415
|
+
{ status: 200 }
|
|
416
|
+
)
|
|
395
417
|
);
|
|
396
418
|
|
|
397
419
|
await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -459,13 +481,16 @@ describe('createSSSStrategy', () => {
|
|
|
459
481
|
const method = (init?.method ?? 'GET').toUpperCase();
|
|
460
482
|
|
|
461
483
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
462
|
-
return new Response(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
484
|
+
return new Response(
|
|
485
|
+
JSON.stringify({
|
|
486
|
+
authShare: { encryptedData: remoteKey, encryptedDek: '', iv: '' },
|
|
487
|
+
primaryDid: 'did:key:zCorrect',
|
|
488
|
+
recoveryMethods: [],
|
|
489
|
+
keyProvider: 'sss',
|
|
490
|
+
shareVersion: 42,
|
|
491
|
+
}),
|
|
492
|
+
{ status: 200 }
|
|
493
|
+
);
|
|
469
494
|
}
|
|
470
495
|
|
|
471
496
|
return new Response(null, { status: 200 });
|
|
@@ -492,13 +517,16 @@ describe('createSSSStrategy', () => {
|
|
|
492
517
|
const method = (init?.method ?? 'GET').toUpperCase();
|
|
493
518
|
|
|
494
519
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
495
|
-
return new Response(
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
520
|
+
return new Response(
|
|
521
|
+
JSON.stringify({
|
|
522
|
+
authShare: { encryptedData: remoteKey, encryptedDek: '', iv: '' },
|
|
523
|
+
primaryDid: 'did:key:zCorrect',
|
|
524
|
+
recoveryMethods: [],
|
|
525
|
+
keyProvider: 'sss',
|
|
526
|
+
// no shareVersion field
|
|
527
|
+
}),
|
|
528
|
+
{ status: 200 }
|
|
529
|
+
);
|
|
502
530
|
}
|
|
503
531
|
|
|
504
532
|
return new Response(null, { status: 200 });
|
|
@@ -564,12 +592,15 @@ describe('createSSSStrategy', () => {
|
|
|
564
592
|
|
|
565
593
|
it('parses server response with string authShare', async () => {
|
|
566
594
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
567
|
-
new Response(
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
595
|
+
new Response(
|
|
596
|
+
JSON.stringify({
|
|
597
|
+
authShare: 'raw-auth-share-string',
|
|
598
|
+
keyProvider: 'sss',
|
|
599
|
+
primaryDid: 'did:key:z123',
|
|
600
|
+
recoveryMethods: [{ type: 'passkey', createdAt: '2024-01-01' }],
|
|
601
|
+
}),
|
|
602
|
+
{ status: 200 }
|
|
603
|
+
)
|
|
573
604
|
);
|
|
574
605
|
|
|
575
606
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -584,12 +615,19 @@ describe('createSSSStrategy', () => {
|
|
|
584
615
|
|
|
585
616
|
it('parses server response with object authShare (encrypted envelope)', async () => {
|
|
586
617
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
587
|
-
new Response(
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
618
|
+
new Response(
|
|
619
|
+
JSON.stringify({
|
|
620
|
+
authShare: {
|
|
621
|
+
encryptedData: 'encrypted-share',
|
|
622
|
+
iv: 'iv-value',
|
|
623
|
+
encryptedDek: 'dek',
|
|
624
|
+
},
|
|
625
|
+
keyProvider: 'sss',
|
|
626
|
+
primaryDid: 'did:key:z456',
|
|
627
|
+
recoveryMethods: [],
|
|
628
|
+
}),
|
|
629
|
+
{ status: 200 }
|
|
630
|
+
)
|
|
593
631
|
);
|
|
594
632
|
|
|
595
633
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -600,12 +638,15 @@ describe('createSSSStrategy', () => {
|
|
|
600
638
|
|
|
601
639
|
it('detects web3auth migration', async () => {
|
|
602
640
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
603
|
-
new Response(
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
641
|
+
new Response(
|
|
642
|
+
JSON.stringify({
|
|
643
|
+
authShare: 'share',
|
|
644
|
+
keyProvider: 'web3auth',
|
|
645
|
+
primaryDid: 'did:key:zOld',
|
|
646
|
+
recoveryMethods: [],
|
|
647
|
+
}),
|
|
648
|
+
{ status: 200 }
|
|
649
|
+
)
|
|
609
650
|
);
|
|
610
651
|
|
|
611
652
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -618,8 +659,9 @@ describe('createSSSStrategy', () => {
|
|
|
618
659
|
new Response(null, { status: 500, statusText: 'Internal Server Error' })
|
|
619
660
|
);
|
|
620
661
|
|
|
621
|
-
await expect(strategy.fetchServerKeyStatus('token', 'firebase'))
|
|
622
|
-
|
|
662
|
+
await expect(strategy.fetchServerKeyStatus('token', 'firebase')).rejects.toThrow(
|
|
663
|
+
'Failed to fetch key status'
|
|
664
|
+
);
|
|
623
665
|
});
|
|
624
666
|
});
|
|
625
667
|
|
|
@@ -630,7 +672,9 @@ describe('createSSSStrategy', () => {
|
|
|
630
672
|
describe('storeAuthShare', () => {
|
|
631
673
|
it('sends PUT request to the server and stores returned shareVersion', async () => {
|
|
632
674
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
633
|
-
new Response(JSON.stringify({ success: true, shareVersion: 3 }), {
|
|
675
|
+
new Response(JSON.stringify({ success: true, shareVersion: 3 }), {
|
|
676
|
+
status: 200,
|
|
677
|
+
})
|
|
634
678
|
);
|
|
635
679
|
|
|
636
680
|
await strategy.storeAuthShare('token', 'firebase', 'share-data', 'did:key:z1');
|
|
@@ -652,8 +696,9 @@ describe('createSSSStrategy', () => {
|
|
|
652
696
|
new Response(null, { status: 500, statusText: 'Server Error' })
|
|
653
697
|
);
|
|
654
698
|
|
|
655
|
-
await expect(
|
|
656
|
-
.
|
|
699
|
+
await expect(
|
|
700
|
+
strategy.storeAuthShare('token', 'firebase', 'share', 'did')
|
|
701
|
+
).rejects.toThrow('Failed to store auth share');
|
|
657
702
|
});
|
|
658
703
|
});
|
|
659
704
|
|
|
@@ -663,9 +708,9 @@ describe('createSSSStrategy', () => {
|
|
|
663
708
|
|
|
664
709
|
describe('markMigrated', () => {
|
|
665
710
|
it('sends POST to /keys/migrate', async () => {
|
|
666
|
-
const fetchSpy = vi
|
|
667
|
-
|
|
668
|
-
|
|
711
|
+
const fetchSpy = vi
|
|
712
|
+
.spyOn(globalThis, 'fetch')
|
|
713
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
669
714
|
|
|
670
715
|
await strategy.markMigrated!('token', 'firebase');
|
|
671
716
|
|
|
@@ -682,8 +727,9 @@ describe('createSSSStrategy', () => {
|
|
|
682
727
|
new Response(null, { status: 500, statusText: 'Fail' })
|
|
683
728
|
);
|
|
684
729
|
|
|
685
|
-
await expect(strategy.markMigrated!('token', 'firebase'))
|
|
686
|
-
|
|
730
|
+
await expect(strategy.markMigrated!('token', 'firebase')).rejects.toThrow(
|
|
731
|
+
'Failed to mark migrated'
|
|
732
|
+
);
|
|
687
733
|
});
|
|
688
734
|
});
|
|
689
735
|
|
|
@@ -728,17 +774,20 @@ describe('createSSSStrategy', () => {
|
|
|
728
774
|
fetchCalls.push({
|
|
729
775
|
url: urlStr,
|
|
730
776
|
method,
|
|
731
|
-
body: init?.body as string ?? '',
|
|
777
|
+
body: (init?.body as string) ?? '',
|
|
732
778
|
});
|
|
733
779
|
|
|
734
780
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
735
|
-
return new Response(
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
781
|
+
return new Response(
|
|
782
|
+
JSON.stringify({
|
|
783
|
+
authShare: { encryptedData: remoteKey, encryptedDek: '', iv: '' },
|
|
784
|
+
primaryDid: 'did:key:zCorrect',
|
|
785
|
+
recoveryMethods: [],
|
|
786
|
+
keyProvider: 'sss',
|
|
787
|
+
shareVersion: 1,
|
|
788
|
+
}),
|
|
789
|
+
{ status: 200 }
|
|
790
|
+
);
|
|
742
791
|
}
|
|
743
792
|
|
|
744
793
|
return new Response(null, { status: 200 });
|
|
@@ -854,13 +903,16 @@ describe('createSSSStrategy', () => {
|
|
|
854
903
|
await strategy.storeLocalShareVersion!(2);
|
|
855
904
|
|
|
856
905
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
857
|
-
new Response(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
906
|
+
new Response(
|
|
907
|
+
JSON.stringify({
|
|
908
|
+
authShare: 'v2-auth-share',
|
|
909
|
+
keyProvider: 'sss',
|
|
910
|
+
primaryDid: 'did:key:z123',
|
|
911
|
+
recoveryMethods: [],
|
|
912
|
+
shareVersion: 3,
|
|
913
|
+
}),
|
|
914
|
+
{ status: 200 }
|
|
915
|
+
)
|
|
864
916
|
);
|
|
865
917
|
|
|
866
918
|
await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -874,13 +926,16 @@ describe('createSSSStrategy', () => {
|
|
|
874
926
|
strategy.setActiveUser!('new-device');
|
|
875
927
|
|
|
876
928
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
877
|
-
new Response(
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
929
|
+
new Response(
|
|
930
|
+
JSON.stringify({
|
|
931
|
+
authShare: 'current-share',
|
|
932
|
+
keyProvider: 'sss',
|
|
933
|
+
primaryDid: 'did:key:z123',
|
|
934
|
+
recoveryMethods: [],
|
|
935
|
+
shareVersion: 3,
|
|
936
|
+
}),
|
|
937
|
+
{ status: 200 }
|
|
938
|
+
)
|
|
884
939
|
);
|
|
885
940
|
|
|
886
941
|
await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -893,7 +948,9 @@ describe('createSSSStrategy', () => {
|
|
|
893
948
|
it('full flow: v2 device share from another device reconstructs with v2 auth share from server', async () => {
|
|
894
949
|
// --- Setup: split a key to get a real v2 device/auth pair ---
|
|
895
950
|
const originalKey = 'deadbeef1234'.padEnd(64, '0');
|
|
896
|
-
const { localKey: v2DeviceShare, remoteKey: v2AuthShare } = await strategy.splitKey(
|
|
951
|
+
const { localKey: v2DeviceShare, remoteKey: v2AuthShare } = await strategy.splitKey(
|
|
952
|
+
originalKey
|
|
953
|
+
);
|
|
897
954
|
|
|
898
955
|
// --- Simulate: Device A receives v2 share via QR recovery ---
|
|
899
956
|
strategy.setActiveUser!('recovering-user');
|
|
@@ -902,13 +959,16 @@ describe('createSSSStrategy', () => {
|
|
|
902
959
|
|
|
903
960
|
// --- Mock server: returns the v2 auth share when version 2 is requested ---
|
|
904
961
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
905
|
-
new Response(
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
962
|
+
new Response(
|
|
963
|
+
JSON.stringify({
|
|
964
|
+
authShare: { encryptedData: v2AuthShare, encryptedDek: '', iv: '' },
|
|
965
|
+
keyProvider: 'sss',
|
|
966
|
+
primaryDid: 'did:key:zRecoveredUser',
|
|
967
|
+
recoveryMethods: [{ type: 'passkey', createdAt: '2024-01-01' }],
|
|
968
|
+
shareVersion: 3, // server is at v3 but returns v2 auth share
|
|
969
|
+
}),
|
|
970
|
+
{ status: 200 }
|
|
971
|
+
)
|
|
912
972
|
);
|
|
913
973
|
|
|
914
974
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -927,14 +987,17 @@ describe('createSSSStrategy', () => {
|
|
|
927
987
|
strategy.setActiveUser!('overwrite-user');
|
|
928
988
|
|
|
929
989
|
// Step 1: First initialize — no local key, server backfills v3
|
|
930
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
931
|
-
new Response(
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
990
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
991
|
+
new Response(
|
|
992
|
+
JSON.stringify({
|
|
993
|
+
authShare: 'v3-auth-share',
|
|
994
|
+
keyProvider: 'sss',
|
|
995
|
+
primaryDid: 'did:key:z123',
|
|
996
|
+
recoveryMethods: [],
|
|
997
|
+
shareVersion: 3,
|
|
998
|
+
}),
|
|
999
|
+
{ status: 200 }
|
|
1000
|
+
)
|
|
938
1001
|
);
|
|
939
1002
|
|
|
940
1003
|
await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -950,14 +1013,17 @@ describe('createSSSStrategy', () => {
|
|
|
950
1013
|
expect(await strategy.getLocalShareVersion!()).toBe(2);
|
|
951
1014
|
|
|
952
1015
|
// Step 3: Second fetchServerKeyStatus — should send version 2
|
|
953
|
-
|
|
954
|
-
new Response(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1016
|
+
fetchSpy.mockClear().mockResolvedValueOnce(
|
|
1017
|
+
new Response(
|
|
1018
|
+
JSON.stringify({
|
|
1019
|
+
authShare: 'v2-auth-share-from-previous',
|
|
1020
|
+
keyProvider: 'sss',
|
|
1021
|
+
primaryDid: 'did:key:z123',
|
|
1022
|
+
recoveryMethods: [],
|
|
1023
|
+
shareVersion: 3,
|
|
1024
|
+
}),
|
|
1025
|
+
{ status: 200 }
|
|
1026
|
+
)
|
|
961
1027
|
);
|
|
962
1028
|
|
|
963
1029
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -975,20 +1041,25 @@ describe('createSSSStrategy', () => {
|
|
|
975
1041
|
// 3. coordinator.initialize() → fetchServerKeyStatus sends v2
|
|
976
1042
|
|
|
977
1043
|
const originalKey = 'cafe0123babe'.padEnd(64, '0');
|
|
978
|
-
const { localKey: v2DeviceShare, remoteKey: v2AuthShare } = await strategy.splitKey(
|
|
1044
|
+
const { localKey: v2DeviceShare, remoteKey: v2AuthShare } = await strategy.splitKey(
|
|
1045
|
+
originalKey
|
|
1046
|
+
);
|
|
979
1047
|
|
|
980
1048
|
strategy.setActiveUser!('device-recovery-user');
|
|
981
1049
|
await strategy.storeLocalKey(v2DeviceShare);
|
|
982
1050
|
await strategy.storeLocalShareVersion!(2);
|
|
983
1051
|
|
|
984
1052
|
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
985
|
-
new Response(
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1053
|
+
new Response(
|
|
1054
|
+
JSON.stringify({
|
|
1055
|
+
authShare: { encryptedData: v2AuthShare, encryptedDek: '', iv: '' },
|
|
1056
|
+
keyProvider: 'sss',
|
|
1057
|
+
primaryDid: 'did:key:zUser',
|
|
1058
|
+
recoveryMethods: [],
|
|
1059
|
+
shareVersion: 3,
|
|
1060
|
+
}),
|
|
1061
|
+
{ status: 200 }
|
|
1062
|
+
)
|
|
992
1063
|
);
|
|
993
1064
|
|
|
994
1065
|
const status = await strategy.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -1038,8 +1109,11 @@ describe('createSSSStrategy', () => {
|
|
|
1038
1109
|
const { remoteKey } = await emailStrategy.splitKey(originalKey);
|
|
1039
1110
|
|
|
1040
1111
|
// Step 1b: storeAuthShare so the version is cached for email send
|
|
1041
|
-
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1042
|
-
|
|
1112
|
+
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1113
|
+
async () =>
|
|
1114
|
+
new Response(JSON.stringify({ success: true, shareVersion: 5 }), {
|
|
1115
|
+
status: 200,
|
|
1116
|
+
})
|
|
1043
1117
|
);
|
|
1044
1118
|
|
|
1045
1119
|
await emailStrategy.storeAuthShare('token', 'firebase', remoteKey, 'did:key:z1');
|
|
@@ -1055,7 +1129,10 @@ describe('createSSSStrategy', () => {
|
|
|
1055
1129
|
});
|
|
1056
1130
|
|
|
1057
1131
|
await emailStrategy.sendEmailBackupShare!(
|
|
1058
|
-
'token',
|
|
1132
|
+
'token',
|
|
1133
|
+
'firebase',
|
|
1134
|
+
originalKey,
|
|
1135
|
+
'user@test.com'
|
|
1059
1136
|
);
|
|
1060
1137
|
|
|
1061
1138
|
expect(capturedPayload).toBeDefined();
|
|
@@ -1078,8 +1155,11 @@ describe('createSSSStrategy', () => {
|
|
|
1078
1155
|
const { localKey, remoteKey } = await emailStrategy.splitKey(originalKey);
|
|
1079
1156
|
|
|
1080
1157
|
// storeAuthShare so version is cached
|
|
1081
|
-
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1082
|
-
|
|
1158
|
+
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1159
|
+
async () =>
|
|
1160
|
+
new Response(JSON.stringify({ success: true, shareVersion: 1 }), {
|
|
1161
|
+
status: 200,
|
|
1162
|
+
})
|
|
1083
1163
|
);
|
|
1084
1164
|
|
|
1085
1165
|
await emailStrategy.storeAuthShare('token', 'firebase', remoteKey, 'did:key:z1');
|
|
@@ -1095,7 +1175,10 @@ describe('createSSSStrategy', () => {
|
|
|
1095
1175
|
});
|
|
1096
1176
|
|
|
1097
1177
|
await emailStrategy.sendEmailBackupShare!(
|
|
1098
|
-
'token',
|
|
1178
|
+
'token',
|
|
1179
|
+
'firebase',
|
|
1180
|
+
originalKey,
|
|
1181
|
+
'user@test.com'
|
|
1099
1182
|
);
|
|
1100
1183
|
|
|
1101
1184
|
// Strip 4-char hex version prefix
|
|
@@ -1117,7 +1200,10 @@ describe('createSSSStrategy', () => {
|
|
|
1117
1200
|
|
|
1118
1201
|
// Call sendEmailBackupShare without calling splitKey first
|
|
1119
1202
|
await emailStrategy.sendEmailBackupShare!(
|
|
1120
|
-
'token',
|
|
1203
|
+
'token',
|
|
1204
|
+
'firebase',
|
|
1205
|
+
'some-key',
|
|
1206
|
+
'user@test.com'
|
|
1121
1207
|
);
|
|
1122
1208
|
|
|
1123
1209
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
@@ -1133,8 +1219,11 @@ describe('createSSSStrategy', () => {
|
|
|
1133
1219
|
const { remoteKey } = await emailStrategy.splitKey(originalKey);
|
|
1134
1220
|
|
|
1135
1221
|
// storeAuthShare to cache version
|
|
1136
|
-
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1137
|
-
|
|
1222
|
+
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1223
|
+
async () =>
|
|
1224
|
+
new Response(JSON.stringify({ success: true, shareVersion: 2 }), {
|
|
1225
|
+
status: 200,
|
|
1226
|
+
})
|
|
1138
1227
|
);
|
|
1139
1228
|
|
|
1140
1229
|
await emailStrategy.storeAuthShare('token', 'firebase', remoteKey, 'did:key:z1');
|
|
@@ -1144,14 +1233,17 @@ describe('createSSSStrategy', () => {
|
|
|
1144
1233
|
vi.spyOn(globalThis, 'fetch').mockImplementation(async (url, init) => {
|
|
1145
1234
|
fetchCalls.push({
|
|
1146
1235
|
url: typeof url === 'string' ? url : url.toString(),
|
|
1147
|
-
body: init?.body as string ?? '',
|
|
1236
|
+
body: (init?.body as string) ?? '',
|
|
1148
1237
|
});
|
|
1149
1238
|
|
|
1150
1239
|
return new Response(null, { status: 200 });
|
|
1151
1240
|
});
|
|
1152
1241
|
|
|
1153
1242
|
await emailStrategy.sendEmailBackupShare!(
|
|
1154
|
-
'token',
|
|
1243
|
+
'token',
|
|
1244
|
+
'firebase',
|
|
1245
|
+
originalKey,
|
|
1246
|
+
'user@test.com'
|
|
1155
1247
|
);
|
|
1156
1248
|
|
|
1157
1249
|
// Only one fetch call should have been made — to the email relay
|
|
@@ -1194,22 +1286,27 @@ describe('createSSSStrategy', () => {
|
|
|
1194
1286
|
|
|
1195
1287
|
fetchCalls.push({
|
|
1196
1288
|
url: urlStr,
|
|
1197
|
-
body: init?.body as string ?? '',
|
|
1289
|
+
body: (init?.body as string) ?? '',
|
|
1198
1290
|
});
|
|
1199
1291
|
|
|
1200
1292
|
// fetchAuthShareRaw needs to return server data
|
|
1201
1293
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
1202
|
-
return new Response(
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1294
|
+
return new Response(
|
|
1295
|
+
JSON.stringify({
|
|
1296
|
+
authShare: 'existing-auth-share',
|
|
1297
|
+
primaryDid: 'did:key:z123',
|
|
1298
|
+
recoveryMethods: [],
|
|
1299
|
+
shareVersion: 2,
|
|
1300
|
+
}),
|
|
1301
|
+
{ status: 200 }
|
|
1302
|
+
);
|
|
1208
1303
|
}
|
|
1209
1304
|
|
|
1210
1305
|
// putAuthShare returns shareVersion
|
|
1211
1306
|
if (urlStr.includes('/keys/auth-share') && method === 'PUT') {
|
|
1212
|
-
return new Response(JSON.stringify({ success: true, shareVersion: 3 }), {
|
|
1307
|
+
return new Response(JSON.stringify({ success: true, shareVersion: 3 }), {
|
|
1308
|
+
status: 200,
|
|
1309
|
+
});
|
|
1213
1310
|
}
|
|
1214
1311
|
|
|
1215
1312
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1250,8 +1347,11 @@ describe('createSSSStrategy', () => {
|
|
|
1250
1347
|
await emailStrategy.splitKey(originalKey);
|
|
1251
1348
|
|
|
1252
1349
|
// storeAuthShare returns version 7
|
|
1253
|
-
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1254
|
-
|
|
1350
|
+
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1351
|
+
async () =>
|
|
1352
|
+
new Response(JSON.stringify({ success: true, shareVersion: 7 }), {
|
|
1353
|
+
status: 200,
|
|
1354
|
+
})
|
|
1255
1355
|
);
|
|
1256
1356
|
|
|
1257
1357
|
await emailStrategy.storeAuthShare('token', 'firebase', 'auth-share', 'did:key:z1');
|
|
@@ -1267,7 +1367,10 @@ describe('createSSSStrategy', () => {
|
|
|
1267
1367
|
});
|
|
1268
1368
|
|
|
1269
1369
|
await emailStrategy.sendEmailBackupShare!(
|
|
1270
|
-
'token',
|
|
1370
|
+
'token',
|
|
1371
|
+
'firebase',
|
|
1372
|
+
originalKey,
|
|
1373
|
+
'user@test.com'
|
|
1271
1374
|
);
|
|
1272
1375
|
|
|
1273
1376
|
// New format: 4-char hex prefix (version 7 = "0007")
|
|
@@ -1287,20 +1390,25 @@ describe('createSSSStrategy', () => {
|
|
|
1287
1390
|
|
|
1288
1391
|
fetchCalls.push({
|
|
1289
1392
|
url: urlStr,
|
|
1290
|
-
body: init?.body as string ?? '',
|
|
1393
|
+
body: (init?.body as string) ?? '',
|
|
1291
1394
|
});
|
|
1292
1395
|
|
|
1293
1396
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
1294
|
-
return new Response(
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1397
|
+
return new Response(
|
|
1398
|
+
JSON.stringify({
|
|
1399
|
+
authShare: 'existing',
|
|
1400
|
+
primaryDid: 'did:key:z1',
|
|
1401
|
+
recoveryMethods: [],
|
|
1402
|
+
shareVersion: 4,
|
|
1403
|
+
}),
|
|
1404
|
+
{ status: 200 }
|
|
1405
|
+
);
|
|
1300
1406
|
}
|
|
1301
1407
|
|
|
1302
1408
|
if (urlStr.includes('/keys/auth-share') && method === 'PUT') {
|
|
1303
|
-
return new Response(JSON.stringify({ success: true, shareVersion: 5 }), {
|
|
1409
|
+
return new Response(JSON.stringify({ success: true, shareVersion: 5 }), {
|
|
1410
|
+
status: 200,
|
|
1411
|
+
});
|
|
1304
1412
|
}
|
|
1305
1413
|
|
|
1306
1414
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1345,12 +1453,15 @@ describe('createSSSStrategy', () => {
|
|
|
1345
1453
|
const body = JSON.parse(init?.body as string);
|
|
1346
1454
|
capturedVersion = body.shareVersion;
|
|
1347
1455
|
|
|
1348
|
-
return new Response(
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1456
|
+
return new Response(
|
|
1457
|
+
JSON.stringify({
|
|
1458
|
+
authShare: shares.authShare,
|
|
1459
|
+
primaryDid: 'did:key:z1',
|
|
1460
|
+
recoveryMethods: [],
|
|
1461
|
+
shareVersion: 2,
|
|
1462
|
+
}),
|
|
1463
|
+
{ status: 200 }
|
|
1464
|
+
);
|
|
1354
1465
|
}
|
|
1355
1466
|
|
|
1356
1467
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1383,12 +1494,15 @@ describe('createSSSStrategy', () => {
|
|
|
1383
1494
|
const body = JSON.parse(init?.body as string);
|
|
1384
1495
|
capturedVersion = body.shareVersion;
|
|
1385
1496
|
|
|
1386
|
-
return new Response(
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1497
|
+
return new Response(
|
|
1498
|
+
JSON.stringify({
|
|
1499
|
+
authShare: shares.authShare,
|
|
1500
|
+
primaryDid: 'did:key:z1',
|
|
1501
|
+
recoveryMethods: [],
|
|
1502
|
+
shareVersion: 255,
|
|
1503
|
+
}),
|
|
1504
|
+
{ status: 200 }
|
|
1505
|
+
);
|
|
1392
1506
|
}
|
|
1393
1507
|
|
|
1394
1508
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1418,22 +1532,27 @@ describe('createSSSStrategy', () => {
|
|
|
1418
1532
|
fetchCalls.push({
|
|
1419
1533
|
url: urlStr,
|
|
1420
1534
|
method,
|
|
1421
|
-
body: init?.body as string ?? '',
|
|
1535
|
+
body: (init?.body as string) ?? '',
|
|
1422
1536
|
});
|
|
1423
1537
|
|
|
1424
1538
|
// fetchAuthShareRaw
|
|
1425
1539
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
1426
|
-
return new Response(
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1540
|
+
return new Response(
|
|
1541
|
+
JSON.stringify({
|
|
1542
|
+
authShare: 'existing',
|
|
1543
|
+
primaryDid: 'did:key:z1',
|
|
1544
|
+
recoveryMethods: [],
|
|
1545
|
+
shareVersion: 5,
|
|
1546
|
+
}),
|
|
1547
|
+
{ status: 200 }
|
|
1548
|
+
);
|
|
1432
1549
|
}
|
|
1433
1550
|
|
|
1434
1551
|
// putAuthShare
|
|
1435
1552
|
if (urlStr.includes('/keys/auth-share') && method === 'PUT') {
|
|
1436
|
-
return new Response(JSON.stringify({ success: true, shareVersion: 6 }), {
|
|
1553
|
+
return new Response(JSON.stringify({ success: true, shareVersion: 6 }), {
|
|
1554
|
+
status: 200,
|
|
1555
|
+
});
|
|
1437
1556
|
}
|
|
1438
1557
|
|
|
1439
1558
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1489,9 +1608,12 @@ describe('createSSSStrategy', () => {
|
|
|
1489
1608
|
|
|
1490
1609
|
// getRecoveryShare for phrase — returns shareVersion only (no encryptedShare)
|
|
1491
1610
|
if (urlStr.includes('/keys/recovery') && method === 'GET') {
|
|
1492
|
-
return new Response(
|
|
1493
|
-
|
|
1494
|
-
|
|
1611
|
+
return new Response(
|
|
1612
|
+
JSON.stringify({
|
|
1613
|
+
shareVersion: 3,
|
|
1614
|
+
}),
|
|
1615
|
+
{ status: 200 }
|
|
1616
|
+
);
|
|
1495
1617
|
}
|
|
1496
1618
|
|
|
1497
1619
|
// fetchAuthShareRaw — capture requested shareVersion
|
|
@@ -1499,12 +1621,17 @@ describe('createSSSStrategy', () => {
|
|
|
1499
1621
|
const body = JSON.parse(init?.body as string);
|
|
1500
1622
|
authShareRequestVersion = body.shareVersion;
|
|
1501
1623
|
|
|
1502
|
-
return new Response(
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1624
|
+
return new Response(
|
|
1625
|
+
JSON.stringify({
|
|
1626
|
+
authShare: shares.authShare,
|
|
1627
|
+
primaryDid: 'did:key:z1',
|
|
1628
|
+
recoveryMethods: [
|
|
1629
|
+
{ type: 'phrase', createdAt: new Date().toISOString() },
|
|
1630
|
+
],
|
|
1631
|
+
shareVersion: 3,
|
|
1632
|
+
}),
|
|
1633
|
+
{ status: 200 }
|
|
1634
|
+
);
|
|
1508
1635
|
}
|
|
1509
1636
|
|
|
1510
1637
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1536,22 +1663,27 @@ describe('createSSSStrategy', () => {
|
|
|
1536
1663
|
fetchCalls.push({
|
|
1537
1664
|
url: urlStr,
|
|
1538
1665
|
method,
|
|
1539
|
-
body: init?.body as string ?? '',
|
|
1666
|
+
body: (init?.body as string) ?? '',
|
|
1540
1667
|
});
|
|
1541
1668
|
|
|
1542
1669
|
// fetchAuthShareRaw
|
|
1543
1670
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
1544
|
-
return new Response(
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1671
|
+
return new Response(
|
|
1672
|
+
JSON.stringify({
|
|
1673
|
+
authShare: 'existing',
|
|
1674
|
+
primaryDid: 'did:key:z1',
|
|
1675
|
+
recoveryMethods: [],
|
|
1676
|
+
shareVersion: 7,
|
|
1677
|
+
}),
|
|
1678
|
+
{ status: 200 }
|
|
1679
|
+
);
|
|
1550
1680
|
}
|
|
1551
1681
|
|
|
1552
1682
|
// putAuthShare
|
|
1553
1683
|
if (urlStr.includes('/keys/auth-share') && method === 'PUT') {
|
|
1554
|
-
return new Response(JSON.stringify({ success: true, shareVersion: 8 }), {
|
|
1684
|
+
return new Response(JSON.stringify({ success: true, shareVersion: 8 }), {
|
|
1685
|
+
status: 200,
|
|
1686
|
+
});
|
|
1555
1687
|
}
|
|
1556
1688
|
|
|
1557
1689
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1594,7 +1726,6 @@ describe('createSSSStrategy', () => {
|
|
|
1594
1726
|
// -----------------------------------------------------------------------
|
|
1595
1727
|
|
|
1596
1728
|
describe('email routing — recovery email vs primary', () => {
|
|
1597
|
-
|
|
1598
1729
|
it('setupRecoveryMethod("email") sends ONLY to recovery email, never to primary', async () => {
|
|
1599
1730
|
const strat = createSSSStrategy({
|
|
1600
1731
|
serverUrl: 'http://test-server:5100/api',
|
|
@@ -1612,21 +1743,26 @@ describe('createSSSStrategy', () => {
|
|
|
1612
1743
|
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
1613
1744
|
const method = (init?.method ?? 'GET').toUpperCase();
|
|
1614
1745
|
|
|
1615
|
-
fetchCalls.push({ url: urlStr, method, body: init?.body as string ?? '' });
|
|
1746
|
+
fetchCalls.push({ url: urlStr, method, body: (init?.body as string) ?? '' });
|
|
1616
1747
|
|
|
1617
1748
|
// fetchAuthShareRaw
|
|
1618
1749
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
1619
|
-
return new Response(
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1750
|
+
return new Response(
|
|
1751
|
+
JSON.stringify({
|
|
1752
|
+
authShare: 'existing',
|
|
1753
|
+
primaryDid: 'did:key:z1',
|
|
1754
|
+
recoveryMethods: [],
|
|
1755
|
+
shareVersion: 10,
|
|
1756
|
+
}),
|
|
1757
|
+
{ status: 200 }
|
|
1758
|
+
);
|
|
1625
1759
|
}
|
|
1626
1760
|
|
|
1627
1761
|
// putAuthShare
|
|
1628
1762
|
if (urlStr.includes('/keys/auth-share') && method === 'PUT') {
|
|
1629
|
-
return new Response(JSON.stringify({ success: true, shareVersion: 11 }), {
|
|
1763
|
+
return new Response(JSON.stringify({ success: true, shareVersion: 11 }), {
|
|
1764
|
+
status: 200,
|
|
1765
|
+
});
|
|
1630
1766
|
}
|
|
1631
1767
|
|
|
1632
1768
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1663,15 +1799,19 @@ describe('createSSSStrategy', () => {
|
|
|
1663
1799
|
const originalKey = 'f1f2f3f4f5f6'.padEnd(64, '0');
|
|
1664
1800
|
|
|
1665
1801
|
// First: fetchServerKeyStatus to set hasRecoveryEmail = true
|
|
1666
|
-
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1802
|
+
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1803
|
+
async () =>
|
|
1804
|
+
new Response(
|
|
1805
|
+
JSON.stringify({
|
|
1806
|
+
authShare: 'existing-share',
|
|
1807
|
+
primaryDid: 'did:key:z1',
|
|
1808
|
+
recoveryMethods: [],
|
|
1809
|
+
shareVersion: 1,
|
|
1810
|
+
keyProvider: 'sss',
|
|
1811
|
+
maskedRecoveryEmail: 'r****@personal.com',
|
|
1812
|
+
}),
|
|
1813
|
+
{ status: 200 }
|
|
1814
|
+
)
|
|
1675
1815
|
);
|
|
1676
1816
|
|
|
1677
1817
|
await stratWithRecovery.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -1687,20 +1827,25 @@ describe('createSSSStrategy', () => {
|
|
|
1687
1827
|
const urlStr = typeof url === 'string' ? url : url.toString();
|
|
1688
1828
|
const method = (init?.method ?? 'GET').toUpperCase();
|
|
1689
1829
|
|
|
1690
|
-
fetchCalls.push({ url: urlStr, method, body: init?.body as string ?? '' });
|
|
1830
|
+
fetchCalls.push({ url: urlStr, method, body: (init?.body as string) ?? '' });
|
|
1691
1831
|
|
|
1692
1832
|
if (urlStr.includes('/keys/auth-share') && method === 'POST') {
|
|
1693
|
-
return new Response(
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1833
|
+
return new Response(
|
|
1834
|
+
JSON.stringify({
|
|
1835
|
+
authShare: 'existing',
|
|
1836
|
+
primaryDid: 'did:key:z1',
|
|
1837
|
+
recoveryMethods: [],
|
|
1838
|
+
shareVersion: 1,
|
|
1839
|
+
maskedRecoveryEmail: 'r****@personal.com',
|
|
1840
|
+
}),
|
|
1841
|
+
{ status: 200 }
|
|
1842
|
+
);
|
|
1700
1843
|
}
|
|
1701
1844
|
|
|
1702
1845
|
if (urlStr.includes('/keys/auth-share') && method === 'PUT') {
|
|
1703
|
-
return new Response(JSON.stringify({ success: true, shareVersion: 2 }), {
|
|
1846
|
+
return new Response(JSON.stringify({ success: true, shareVersion: 2 }), {
|
|
1847
|
+
status: 200,
|
|
1848
|
+
});
|
|
1704
1849
|
}
|
|
1705
1850
|
|
|
1706
1851
|
return new Response(JSON.stringify({ success: true }), { status: 200 });
|
|
@@ -1735,15 +1880,19 @@ describe('createSSSStrategy', () => {
|
|
|
1735
1880
|
const originalKey = 'd1d2d3d4d5d6'.padEnd(64, '0');
|
|
1736
1881
|
|
|
1737
1882
|
// Prime hasRecoveryEmail via fetchServerKeyStatus
|
|
1738
|
-
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1883
|
+
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1884
|
+
async () =>
|
|
1885
|
+
new Response(
|
|
1886
|
+
JSON.stringify({
|
|
1887
|
+
authShare: 'existing-share',
|
|
1888
|
+
primaryDid: 'did:key:z1',
|
|
1889
|
+
recoveryMethods: [],
|
|
1890
|
+
shareVersion: 3,
|
|
1891
|
+
keyProvider: 'sss',
|
|
1892
|
+
maskedRecoveryEmail: 'r****@personal.com',
|
|
1893
|
+
}),
|
|
1894
|
+
{ status: 200 }
|
|
1895
|
+
)
|
|
1747
1896
|
);
|
|
1748
1897
|
|
|
1749
1898
|
await stratWithRecovery.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -1754,8 +1903,11 @@ describe('createSSSStrategy', () => {
|
|
|
1754
1903
|
const { remoteKey } = await stratWithRecovery.splitKey(originalKey);
|
|
1755
1904
|
|
|
1756
1905
|
// storeAuthShare to cache version
|
|
1757
|
-
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1758
|
-
|
|
1906
|
+
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1907
|
+
async () =>
|
|
1908
|
+
new Response(JSON.stringify({ success: true, shareVersion: 4 }), {
|
|
1909
|
+
status: 200,
|
|
1910
|
+
})
|
|
1759
1911
|
);
|
|
1760
1912
|
|
|
1761
1913
|
await stratWithRecovery.storeAuthShare('token', 'firebase', remoteKey, 'did:key:z1');
|
|
@@ -1769,7 +1921,10 @@ describe('createSSSStrategy', () => {
|
|
|
1769
1921
|
});
|
|
1770
1922
|
|
|
1771
1923
|
await stratWithRecovery.sendEmailBackupShare!(
|
|
1772
|
-
'token',
|
|
1924
|
+
'token',
|
|
1925
|
+
'firebase',
|
|
1926
|
+
originalKey,
|
|
1927
|
+
'primary@test.com'
|
|
1773
1928
|
);
|
|
1774
1929
|
|
|
1775
1930
|
// Should route to recovery email, not primary
|
|
@@ -1790,8 +1945,11 @@ describe('createSSSStrategy', () => {
|
|
|
1790
1945
|
const { remoteKey } = await strat.splitKey(originalKey);
|
|
1791
1946
|
|
|
1792
1947
|
// storeAuthShare to cache version
|
|
1793
|
-
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1794
|
-
|
|
1948
|
+
vi.spyOn(globalThis, 'fetch').mockImplementationOnce(
|
|
1949
|
+
async () =>
|
|
1950
|
+
new Response(JSON.stringify({ success: true, shareVersion: 1 }), {
|
|
1951
|
+
status: 200,
|
|
1952
|
+
})
|
|
1795
1953
|
);
|
|
1796
1954
|
|
|
1797
1955
|
await strat.storeAuthShare('token', 'firebase', remoteKey, 'did:key:z1');
|
|
@@ -1803,9 +1961,7 @@ describe('createSSSStrategy', () => {
|
|
|
1803
1961
|
return new Response(null, { status: 200 });
|
|
1804
1962
|
});
|
|
1805
1963
|
|
|
1806
|
-
await strat.sendEmailBackupShare!(
|
|
1807
|
-
'token', 'firebase', originalKey, 'primary@test.com'
|
|
1808
|
-
);
|
|
1964
|
+
await strat.sendEmailBackupShare!('token', 'firebase', originalKey, 'primary@test.com');
|
|
1809
1965
|
|
|
1810
1966
|
// Should send to the explicit primary email
|
|
1811
1967
|
expect(capturedBody).toBeDefined();
|
|
@@ -1830,14 +1986,17 @@ describe('createSSSStrategy', () => {
|
|
|
1830
1986
|
|
|
1831
1987
|
// Step 1: fetchServerKeyStatus returns a user WITH a recovery email
|
|
1832
1988
|
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
|
|
1833
|
-
new Response(
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1989
|
+
new Response(
|
|
1990
|
+
JSON.stringify({
|
|
1991
|
+
authShare: 'share',
|
|
1992
|
+
keyProvider: 'sss',
|
|
1993
|
+
primaryDid: 'did:key:z1',
|
|
1994
|
+
recoveryMethods: [{ type: 'email', createdAt: new Date().toISOString() }],
|
|
1995
|
+
maskedRecoveryEmail: 'r***@test.com',
|
|
1996
|
+
shareVersion: 1,
|
|
1997
|
+
}),
|
|
1998
|
+
{ status: 200 }
|
|
1999
|
+
)
|
|
1841
2000
|
);
|
|
1842
2001
|
|
|
1843
2002
|
await strat.fetchServerKeyStatus('token', 'firebase');
|
|
@@ -1885,7 +2044,6 @@ describe('createSSSStrategy', () => {
|
|
|
1885
2044
|
// -----------------------------------------------------------------------
|
|
1886
2045
|
|
|
1887
2046
|
describe('formatVersionedEmailShare / parseVersionedEmailShare', () => {
|
|
1888
|
-
|
|
1889
2047
|
it('round-trip: format then parse recovers the original share and version', () => {
|
|
1890
2048
|
const share = 'abcdef1234567890';
|
|
1891
2049
|
const version = 5;
|