@learncard/sss-key-manager 0.1.24 → 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.
Files changed (75) hide show
  1. package/README.md +25 -95
  2. package/dist/api-client.d.ts +44 -7
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/atomic-operations.d.ts +10 -2
  5. package/dist/atomic-operations.d.ts.map +1 -1
  6. package/dist/email-relay-crypto.d.ts +49 -0
  7. package/dist/email-relay-crypto.d.ts.map +1 -0
  8. package/dist/escrow-attestation.d.ts +12 -0
  9. package/dist/escrow-attestation.d.ts.map +1 -0
  10. package/dist/escrow-crypto.d.ts +52 -0
  11. package/dist/escrow-crypto.d.ts.map +1 -0
  12. package/dist/escrow-nitro-attestation.d.ts +32 -0
  13. package/dist/escrow-nitro-attestation.d.ts.map +1 -0
  14. package/dist/escrow-pin.d.ts +22 -0
  15. package/dist/escrow-pin.d.ts.map +1 -0
  16. package/dist/index.d.ts +15 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/key-manager.d.ts +2 -0
  19. package/dist/key-manager.d.ts.map +1 -1
  20. package/dist/qr-login.d.ts.map +1 -1
  21. package/dist/recovery-phrase.d.ts +6 -0
  22. package/dist/recovery-phrase.d.ts.map +1 -1
  23. package/dist/sss-key-manager.cjs.development.js +5564 -353
  24. package/dist/sss-key-manager.cjs.development.js.map +4 -4
  25. package/dist/sss-key-manager.cjs.production.min.js +33 -33
  26. package/dist/sss-key-manager.cjs.production.min.js.map +4 -4
  27. package/dist/sss-key-manager.esm.js +3494 -354
  28. package/dist/sss-key-manager.esm.js.map +4 -4
  29. package/dist/sss-strategy.d.ts +74 -4
  30. package/dist/sss-strategy.d.ts.map +1 -1
  31. package/dist/storage.d.ts +1 -0
  32. package/dist/storage.d.ts.map +1 -1
  33. package/dist/types.d.ts +98 -4
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +7 -3
  36. package/src/__fixtures__/escrow-vectors-rust.json +82 -0
  37. package/src/__fixtures__/escrow-vectors.json +103 -0
  38. package/src/__fixtures__/nitro-attestation/bad-signature.cbor.b64 +1 -0
  39. package/src/__fixtures__/nitro-attestation/debug-zero-pcrs.cbor.b64 +1 -0
  40. package/src/__fixtures__/nitro-attestation/legacy-user-data.cbor.b64 +1 -0
  41. package/src/__fixtures__/nitro-attestation/manifest.json +60 -0
  42. package/src/__fixtures__/nitro-attestation/nonce-mismatch.cbor.b64 +1 -0
  43. package/src/__fixtures__/nitro-attestation/root.pem +12 -0
  44. package/src/__fixtures__/nitro-attestation/stale-timestamp.cbor.b64 +1 -0
  45. package/src/__fixtures__/nitro-attestation/tampered-previous-key-ids.cbor.b64 +1 -0
  46. package/src/__fixtures__/nitro-attestation/truncated-chain.cbor.b64 +1 -0
  47. package/src/__fixtures__/nitro-attestation/valid.cbor.b64 +1 -0
  48. package/src/__fixtures__/nitro-attestation/wrong-pcr0.cbor.b64 +1 -0
  49. package/src/__fixtures__/nitro-attestation/wrong-root.cbor.b64 +1 -0
  50. package/src/api-client.test.ts +122 -0
  51. package/src/api-client.ts +107 -116
  52. package/src/atomic-operations.test.ts +56 -16
  53. package/src/atomic-operations.ts +55 -19
  54. package/src/critical-paths.test.ts +51 -1
  55. package/src/email-relay-crypto.test.ts +87 -0
  56. package/src/email-relay-crypto.ts +293 -0
  57. package/src/escrow-attestation.test.ts +90 -0
  58. package/src/escrow-attestation.ts +125 -0
  59. package/src/escrow-crypto.test.ts +381 -0
  60. package/src/escrow-crypto.ts +319 -0
  61. package/src/escrow-nitro-attestation.test.ts +204 -0
  62. package/src/escrow-nitro-attestation.ts +286 -0
  63. package/src/escrow-pin.test.ts +119 -0
  64. package/src/escrow-pin.ts +83 -0
  65. package/src/escrow-vectors.test.ts +198 -0
  66. package/src/index.ts +70 -1
  67. package/src/key-manager.ts +4 -9
  68. package/src/qr-login.ts +22 -15
  69. package/src/recovery-phrase.ts +40 -0
  70. package/src/sss-session-writes.test.ts +109 -0
  71. package/src/sss-strategy.test.ts +2736 -111
  72. package/src/sss-strategy.ts +2226 -224
  73. package/src/storage.test.ts +66 -8
  74. package/src/storage.ts +41 -29
  75. package/src/types.ts +118 -6
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import 'fake-indexeddb/auto';
16
- import { describe, it, expect, beforeEach } from 'vitest';
16
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
17
17
 
18
18
  import {
19
19
  storeDeviceShare,
@@ -25,6 +25,11 @@ import {
25
25
  storeShareVersion,
26
26
  getShareVersion,
27
27
  } from './storage';
28
+ import {
29
+ createSSSStrategy,
30
+ readPendingShareCandidates,
31
+ writePendingShareCandidates,
32
+ } from './sss-strategy';
28
33
 
29
34
  // ---------------------------------------------------------------------------
30
35
  // Helpers
@@ -249,7 +254,7 @@ describe('SSS Storage', () => {
249
254
  expect(await getShareVersion(id)).toBeNull();
250
255
  });
251
256
 
252
- it('deleting one user does not affect another user\'s version', async () => {
257
+ it("deleting one user does not affect another user's version", async () => {
253
258
  const idA = 'sss-device-share:user-a';
254
259
  const idB = 'sss-device-share:user-b';
255
260
 
@@ -380,7 +385,10 @@ describe('SSS Storage', () => {
380
385
  const t = db.transaction('shares', 'readwrite');
381
386
  const s = t.objectStore('shares');
382
387
  // Store garbage data that will fail AES-GCM decryption
383
- const req = s.put({ version: 1, iv: 'bad', cipher: 'bad', keyVersion: 1 }, orphanId);
388
+ const req = s.put(
389
+ { version: 1, iv: 'bad', cipher: 'bad', keyVersion: 1 },
390
+ orphanId
391
+ );
384
392
  req.onsuccess = () => resolve();
385
393
  req.onerror = () => reject(req.error);
386
394
  });
@@ -427,11 +435,7 @@ describe('SSS Storage', () => {
427
435
  // Session-only storage (public computer mode)
428
436
  // ---------------------------------------------------------------------------
429
437
 
430
- import {
431
- isPublicComputerMode,
432
- setPublicComputerMode,
433
- createAdaptiveStorage,
434
- } from './storage';
438
+ import { isPublicComputerMode, setPublicComputerMode, createAdaptiveStorage } from './storage';
435
439
 
436
440
  describe('Session-only storage (public computer mode)', () => {
437
441
  beforeEach(() => {
@@ -528,3 +532,57 @@ describe('Session-only storage (public computer mode)', () => {
528
532
  });
529
533
  });
530
534
  });
535
+
536
+ describe.each([false, true])(
537
+ 'SSS automatic cleanup with adaptive storage (public mode=%s)',
538
+ publicMode => {
539
+ beforeEach(async () => {
540
+ sessionStorage.clear();
541
+ await clearAllShares();
542
+ setPublicComputerMode(publicMode);
543
+ });
544
+
545
+ afterEach(async () => {
546
+ sessionStorage.clear();
547
+ await clearAllShares();
548
+ });
549
+
550
+ it('preserves scoped and unscoped pending writes while clearing stale main shares', async () => {
551
+ const storage = createAdaptiveStorage();
552
+ const scoped = createSSSStrategy({ serverUrl: 'https://unused.test', storage });
553
+ const accountId = 'sss-device-share:account-a';
554
+ const otherId = 'sss-device-share:account-b';
555
+ const scopedPending = [{ share: 'pending-a', createdAt: Date.now() }];
556
+ const legacyPending = [{ share: 'pending-legacy', createdAt: Date.now() }];
557
+ const otherPending = [{ share: 'pending-b', createdAt: Date.now() }];
558
+
559
+ scoped.setActiveUser!('account-a');
560
+ await scoped.storeLocalKey('stale-a');
561
+ await scoped.storeLocalShareVersion!(3);
562
+ await writePendingShareCandidates(storage, scopedPending, accountId);
563
+ await storage.storeDeviceShare('share-b', otherId);
564
+ await storage.storeShareVersion(7, otherId);
565
+ await writePendingShareCandidates(storage, otherPending, otherId);
566
+
567
+ await scoped.clearLocalKeys({ preservePending: true });
568
+ expect(await storage.getDeviceShare(accountId)).toBeNull();
569
+ expect(await storage.getShareVersion(accountId)).toBeNull();
570
+ expect(await readPendingShareCandidates(storage, accountId)).toEqual(scopedPending);
571
+ expect(await scoped.getLocalKey()).toBe('pending-a');
572
+
573
+ const legacy = createSSSStrategy({ serverUrl: 'https://unused.test', storage });
574
+ await legacy.storeLocalKey('stale-legacy');
575
+ await legacy.storeLocalShareVersion!(2);
576
+ await writePendingShareCandidates(storage, legacyPending);
577
+ await legacy.clearLocalKeys({ preservePending: true });
578
+ expect(await storage.getDeviceShare()).toBeNull();
579
+ expect(await storage.getShareVersion()).toBeNull();
580
+ expect(await readPendingShareCandidates(storage)).toEqual(legacyPending);
581
+ expect(await legacy.getLocalKey()).toBe('pending-legacy');
582
+
583
+ expect(await storage.getDeviceShare(otherId)).toBe('share-b');
584
+ expect(await storage.getShareVersion(otherId)).toBe(7);
585
+ expect(await readPendingShareCandidates(storage, otherId)).toEqual(otherPending);
586
+ });
587
+ }
588
+ );
package/src/storage.ts CHANGED
@@ -70,11 +70,10 @@ async function getOrCreateMasterKey(): Promise<CryptoKey> {
70
70
 
71
71
  if (existing) return existing;
72
72
 
73
- const key = await crypto.subtle.generateKey(
74
- { name: 'AES-GCM', length: 256 },
75
- false,
76
- ['encrypt', 'decrypt']
77
- );
73
+ const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, false, [
74
+ 'encrypt',
75
+ 'decrypt',
76
+ ]);
78
77
 
79
78
  await tx(db, KEYS_STORE, 'readwrite', s => s.put(key, 'master-key'));
80
79
 
@@ -145,7 +144,10 @@ async function decryptShare(payload: EncryptedPayload, id: string): Promise<stri
145
144
  return new TextDecoder().decode(plainBuffer);
146
145
  }
147
146
 
148
- export async function storeDeviceShare(share: string, id: string = DEFAULT_DEVICE_SHARE_ID): Promise<void> {
147
+ export async function storeDeviceShare(
148
+ share: string,
149
+ id: string = DEFAULT_DEVICE_SHARE_ID
150
+ ): Promise<void> {
149
151
  const db = await openDB();
150
152
 
151
153
  try {
@@ -160,7 +162,10 @@ export async function storeDeviceShare(share: string, id: string = DEFAULT_DEVIC
160
162
  * Store the share version alongside a device share.
161
163
  * Stored as a separate key `{id}:version` to avoid changing the encryption format.
162
164
  */
163
- export async function storeShareVersion(version: number, id: string = DEFAULT_DEVICE_SHARE_ID): Promise<void> {
165
+ export async function storeShareVersion(
166
+ version: number,
167
+ id: string = DEFAULT_DEVICE_SHARE_ID
168
+ ): Promise<void> {
164
169
  const db = await openDB();
165
170
 
166
171
  try {
@@ -174,7 +179,9 @@ export async function storeShareVersion(version: number, id: string = DEFAULT_DE
174
179
  * Retrieve the share version for a device share.
175
180
  * Returns null if no version is stored (legacy shares).
176
181
  */
177
- export async function getShareVersion(id: string = DEFAULT_DEVICE_SHARE_ID): Promise<number | null> {
182
+ export async function getShareVersion(
183
+ id: string = DEFAULT_DEVICE_SHARE_ID
184
+ ): Promise<number | null> {
178
185
  const db = await openDB();
179
186
 
180
187
  try {
@@ -192,9 +199,7 @@ export async function getDeviceShare(id: string = DEFAULT_DEVICE_SHARE_ID): Prom
192
199
  const db = await openDB();
193
200
 
194
201
  try {
195
- const raw = await tx<unknown>(db, SHARES_STORE, 'readonly', s =>
196
- s.get(id)
197
- );
202
+ const raw = await tx<unknown>(db, SHARES_STORE, 'readonly', s => s.get(id));
198
203
 
199
204
  if (raw == null) {
200
205
  return null;
@@ -288,7 +293,9 @@ export async function listAllDeviceShares(): Promise<DeviceShareEntry[]> {
288
293
  // Orphaned entry — value exists but can't be decrypted.
289
294
  // Auto-clean it (and its companion :version key) to prevent
290
295
  // phantom "(decrypt failed)" entries from accumulating.
291
- console.warn(`SSS Storage: removing orphaned entry that failed to decrypt: ${id}`);
296
+ console.warn(
297
+ `SSS Storage: removing orphaned entry that failed to decrypt: ${id}`
298
+ );
292
299
  await deleteDeviceShare(id);
293
300
  continue;
294
301
  }
@@ -321,8 +328,10 @@ const SESSION_PREFIX = 'sss:';
321
328
 
322
329
  export function isPublicComputerMode(): boolean {
323
330
  try {
324
- return typeof sessionStorage !== 'undefined' &&
325
- sessionStorage.getItem('lc-session-mode') === 'public';
331
+ return (
332
+ typeof sessionStorage !== 'undefined' &&
333
+ sessionStorage.getItem('lc-session-mode') === 'public'
334
+ );
326
335
  } catch {
327
336
  return false;
328
337
  }
@@ -340,7 +349,10 @@ export function setPublicComputerMode(enabled: boolean): void {
340
349
  }
341
350
  }
342
351
 
343
- async function sessionStoreDeviceShare(share: string, id: string = DEFAULT_DEVICE_SHARE_ID): Promise<void> {
352
+ async function sessionStoreDeviceShare(
353
+ share: string,
354
+ id: string = DEFAULT_DEVICE_SHARE_ID
355
+ ): Promise<void> {
344
356
  sessionStorage.setItem(`${SESSION_PREFIX}${id}`, share);
345
357
  }
346
358
 
@@ -357,11 +369,16 @@ async function sessionDeleteDeviceShare(id: string = DEFAULT_DEVICE_SHARE_ID): P
357
369
  sessionStorage.removeItem(`${SESSION_PREFIX}${id}:version`);
358
370
  }
359
371
 
360
- async function sessionStoreShareVersion(version: number, id: string = DEFAULT_DEVICE_SHARE_ID): Promise<void> {
372
+ async function sessionStoreShareVersion(
373
+ version: number,
374
+ id: string = DEFAULT_DEVICE_SHARE_ID
375
+ ): Promise<void> {
361
376
  sessionStorage.setItem(`${SESSION_PREFIX}${id}:version`, String(version));
362
377
  }
363
378
 
364
- async function sessionGetShareVersion(id: string = DEFAULT_DEVICE_SHARE_ID): Promise<number | null> {
379
+ async function sessionGetShareVersion(
380
+ id: string = DEFAULT_DEVICE_SHARE_ID
381
+ ): Promise<number | null> {
365
382
  const raw = sessionStorage.getItem(`${SESSION_PREFIX}${id}:version`);
366
383
 
367
384
  return raw !== null ? Number(raw) : null;
@@ -400,19 +417,16 @@ export function createAdaptiveStorage() {
400
417
  : storeDeviceShare(share, id),
401
418
 
402
419
  getDeviceShare: (id?: string) =>
403
- isPublicComputerMode()
404
- ? sessionGetDeviceShare(id)
405
- : getDeviceShare(id),
420
+ isPublicComputerMode() ? sessionGetDeviceShare(id) : getDeviceShare(id),
406
421
 
407
422
  hasDeviceShare: (id?: string) =>
408
- isPublicComputerMode()
409
- ? sessionHasDeviceShare(id)
410
- : hasDeviceShare(id),
423
+ isPublicComputerMode() ? sessionHasDeviceShare(id) : hasDeviceShare(id),
411
424
 
412
425
  clearAllShares: (id?: string) =>
413
- isPublicComputerMode()
414
- ? sessionClearAllShares(id)
415
- : clearAllShares(id),
426
+ isPublicComputerMode() ? sessionClearAllShares(id) : clearAllShares(id),
427
+
428
+ deleteDeviceShare: (id?: string) =>
429
+ isPublicComputerMode() ? sessionDeleteDeviceShare(id) : deleteDeviceShare(id),
416
430
 
417
431
  storeShareVersion: (version: number, id?: string) =>
418
432
  isPublicComputerMode()
@@ -420,9 +434,7 @@ export function createAdaptiveStorage() {
420
434
  : storeShareVersion(version, id),
421
435
 
422
436
  getShareVersion: (id?: string) =>
423
- isPublicComputerMode()
424
- ? sessionGetShareVersion(id)
425
- : getShareVersion(id),
437
+ isPublicComputerMode() ? sessionGetShareVersion(id) : getShareVersion(id),
426
438
  };
427
439
  }
428
440
 
package/src/types.ts CHANGED
@@ -16,11 +16,24 @@ export {
16
16
  type AuthProvider,
17
17
  type RecoveryMethodInfo,
18
18
  type RecoveryResult,
19
+ type IdentityRecoverySession,
19
20
  type ServerKeyStatus,
21
+ type SssActivationState,
20
22
  type KeyDerivationStrategy,
23
+ type DidAuthVpSigner,
24
+ type EscrowEnrollmentOptions,
25
+ type EscrowEnrollmentState,
26
+ type EscrowBlobStaleReason,
27
+ type EscrowPinStatus,
21
28
  } from '@learncard/types';
22
29
 
23
- import type { AuthProvider, AuthProviderType, KeyDerivationStrategy, RecoveryMethodInfo } from '@learncard/types';
30
+ import type {
31
+ AuthProvider,
32
+ AuthProviderType,
33
+ KeyDerivationStrategy,
34
+ RecoveryMethodInfo,
35
+ SssActivationState,
36
+ } from '@learncard/types';
24
37
 
25
38
  // ---------------------------------------------------------------------------
26
39
  // SSS-specific: Contact & auth provider mapping
@@ -54,7 +67,80 @@ export const SecurityLevels: readonly SecurityLevel[] = ['basic', 'enhanced', 'a
54
67
  * SSS recovery method type identifiers.
55
68
  * These are the specific recovery methods supported by the SSS strategy.
56
69
  */
57
- export type RecoveryMethodType = 'passkey' | 'backup' | 'phrase' | 'email';
70
+ export type RecoveryMethodType = 'passkey' | 'backup' | 'phrase' | 'email' | 'escrow';
71
+
72
+ /** Trust policy for the enclave that receives recovery material. */
73
+ export type EscrowAttestationPolicy =
74
+ | { mode: 'software'; pinnedPublicKeys: string[] }
75
+ | {
76
+ mode: 'nitro';
77
+ /** Legacy image-only pins remain assignable but never match an attestation. */
78
+ pinnedMeasurements: (
79
+ | { pcr0: string; pcr1: string; pcr2: string; imageSha384?: string }
80
+ | { imageSha384: string; pcr0?: string; pcr1?: string; pcr2?: string }
81
+ )[];
82
+ rootCertificateSha256?: string;
83
+ /** Maximum age in milliseconds; defaults to five minutes. */
84
+ maxAgeMs?: number;
85
+ };
86
+
87
+ /** Public status of an escrow recovery waiting period. */
88
+ export interface EscrowHoldStatus {
89
+ holdId: string;
90
+ releasePolicy: 'hold' | 'pin';
91
+ status: 'pending' | 'cancelled' | 'completed' | 'expired';
92
+ requestedAt: string;
93
+ releaseAfter: string;
94
+ cancelledAt?: string;
95
+ completedAt?: string;
96
+ }
97
+
98
+ /** Persist these secrets securely until completion; a null token means an existing hold. */
99
+ export type EscrowRecoveryStart = EscrowHoldStatus & {
100
+ resumeToken: string | null;
101
+ clientEphemeralPrivateKey: string;
102
+ pinSalt?: string;
103
+ };
104
+
105
+ /** The account has no available PIN release policy. */
106
+ export class EscrowPinUnavailableError extends Error {
107
+ constructor() {
108
+ super('PIN recovery is not available for this account.');
109
+ this.name = 'EscrowPinUnavailableError';
110
+ }
111
+ }
112
+
113
+ /** A failed PIN attempt consumes its hold; another attempt must start a fresh hold. */
114
+ export class EscrowPinMismatchError extends Error {
115
+ constructor(public readonly attemptsRemaining: number) {
116
+ super('Incorrect PIN.');
117
+ this.name = 'EscrowPinMismatchError';
118
+ }
119
+ }
120
+
121
+ /** Temporary throttle: retry after cooldown with a fresh hold, never retry /complete. */
122
+ export class EscrowPinThrottledError extends Error {
123
+ constructor() {
124
+ super('Too many tries right now. Wait a minute and try again.');
125
+ this.name = 'EscrowPinThrottledError';
126
+ }
127
+ }
128
+
129
+ /** A pending waiting-period request is too recent to restart. */
130
+ export class EscrowHoldRestartThrottledError extends Error {
131
+ constructor(public readonly retryAfter?: string) {
132
+ super('A recovery request was started recently. Please wait before restarting.');
133
+ this.name = 'EscrowHoldRestartThrottledError';
134
+ }
135
+ }
136
+
137
+ /** PIN recovery is exhausted; delayed escrow recovery remains available. */
138
+ export class EscrowPinLockedError extends Error {
139
+ constructor() {
140
+ super('Too many incorrect PIN attempts. You can still recover by waiting.');
141
+ this.name = 'EscrowPinLockedError';
142
+ }
143
+ }
58
144
 
59
145
  export interface PasskeyRecoveryMethod {
60
146
  type: 'passkey';
@@ -73,12 +159,15 @@ export interface RecoveryPhraseRecoveryMethod {
73
159
  }
74
160
 
75
161
  /** @deprecated Use RecoveryInput instead. Kept for legacy SSSKeyManager class. */
76
- export type RecoveryMethod = PasskeyRecoveryMethod | BackupFileRecoveryMethod | RecoveryPhraseRecoveryMethod;
162
+ export type RecoveryMethod =
163
+ PasskeyRecoveryMethod | BackupFileRecoveryMethod | RecoveryPhraseRecoveryMethod;
77
164
 
78
165
  /**
79
166
  * SSS-specific recovery input — what the user provides to recover their key.
80
167
  */
81
168
  export type RecoveryInput =
169
+ | { method: 'escrow-pin'; pin: string }
170
+ | { method: 'escrow'; holdId: string; resumeToken: string; clientEphemeralPrivateKey: string }
82
171
  | { method: 'passkey'; credentialId: string }
83
172
  | { method: 'phrase'; phrase: string }
84
173
  | { method: 'backup'; fileContents: string; password: string }
@@ -88,20 +177,34 @@ export type RecoveryInput =
88
177
  * SSS-specific recovery setup input — what the user provides to set up a method.
89
178
  */
90
179
  export type RecoverySetupInput =
180
+ | { method: 'escrow'; pin?: string }
91
181
  | { method: 'passkey' }
92
182
  | { method: 'phrase' }
93
183
  | { method: 'backup'; password: string; did: string }
94
- | { method: 'email' };
184
+ | { method: 'email'; email: string };
95
185
 
96
186
  /**
97
187
  * SSS-specific recovery setup result.
98
188
  */
99
189
  export type RecoverySetupResult =
190
+ | { method: 'escrow'; shareVersion: number }
100
191
  | { method: 'passkey'; credentialId: string }
101
- | { method: 'phrase'; phrase: string }
192
+ | {
193
+ method: 'phrase';
194
+ phrase: string;
195
+ challengeWordIndices: number[];
196
+ /** Shuffled choices per challenged word (the answer plus decoys not in the phrase). */
197
+ challengeWordOptions: string[][];
198
+ }
102
199
  | { method: 'backup'; backupFile: BackupFile }
103
200
  | { method: 'email' };
104
201
 
202
+ /** Proof supplied after a pending recovery method has been created. */
203
+ export type RecoveryConfirmationInput =
204
+ | { method: 'phrase'; challengeWords: string[] }
205
+ | { method: 'backup'; fileContents: string; password: string }
206
+ | { method: 'email'; code: string };
207
+
105
208
  // ---------------------------------------------------------------------------
106
209
  // SSS-specific: Share & encryption types
107
210
  // ---------------------------------------------------------------------------
@@ -133,6 +236,8 @@ export interface UserKeyRecord {
133
236
  recoveryMethods: RecoveryMethodInfo[];
134
237
  migratedFromWeb3Auth: boolean;
135
238
  migratedAt?: Date;
239
+ sssActivationState?: SssActivationState;
240
+ provisionalCreatedAt?: Date;
136
241
  createdAt: Date;
137
242
  updatedAt: Date;
138
243
  }
@@ -142,6 +247,8 @@ export interface BackupFile {
142
247
  createdAt: string;
143
248
  primaryDid: string;
144
249
  shareVersion?: number;
250
+ /** SHA-256 checksum of the decrypted share, used to verify a just-written file. */
251
+ shareChecksum?: string;
145
252
  encryptedShare: {
146
253
  ciphertext: string;
147
254
  iv: string;
@@ -173,7 +280,12 @@ export interface SSSKeyManagerConfig {
173
280
  * The SSS key derivation strategy — a KeyDerivationStrategy narrowed
174
281
  * with SSS-specific recovery input/output types.
175
282
  */
176
- export type SSSKeyDerivationStrategy = KeyDerivationStrategy<RecoveryInput, RecoverySetupInput, RecoverySetupResult>;
283
+ export type SSSKeyDerivationStrategy = KeyDerivationStrategy<
284
+ RecoveryInput,
285
+ RecoverySetupInput,
286
+ RecoverySetupResult,
287
+ RecoveryConfirmationInput
288
+ >;
177
289
 
178
290
  // ---------------------------------------------------------------------------
179
291
  // Legacy types (deprecated)