@learncard/sss-key-manager 0.2.3 → 0.2.4
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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/sss-key-manager.cjs.development.js +8178 -2070
- package/dist/sss-key-manager.cjs.development.js.map +3 -3
- package/dist/sss-key-manager.cjs.production.min.js +46 -49
- package/dist/sss-key-manager.cjs.production.min.js.map +3 -3
- package/dist/sss-key-manager.esm.js +8179 -2071
- package/dist/sss-key-manager.esm.js.map +3 -3
- package/dist/sss-strategy.d.ts +31 -0
- package/dist/sss-strategy.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +5 -1
- package/src/sss-authority.test.ts +378 -2
- package/src/sss-strategy.test.ts +41 -2
- package/src/sss-strategy.ts +195 -25
package/src/sss-strategy.ts
CHANGED
|
@@ -108,6 +108,16 @@ const SSS_DB_NAME = 'lcb-sss-keys';
|
|
|
108
108
|
const DEFAULT_DEVICE_SHARE_ID = 'sss-device-share';
|
|
109
109
|
const MAX_RECONCILIATION_HISTORY = 5;
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* OFF by default. Every input to the uncarryable-PIN inference (recovery methods,
|
|
113
|
+
* share version, PIN state) comes from lca-api, so a compromised API could forge it
|
|
114
|
+
* for a healthy PIN account and have the client sign away the PIN path (Custard7's
|
|
115
|
+
* PR #1815 review). With it off the strategy returns `pin-reset-required` and the
|
|
116
|
+
* app asks the user to set a new PIN. Enabling it needs an independently trustworthy
|
|
117
|
+
* recovery signal and security sign-off: POST_RECOVERY_ESCROW_REPAIR_DESIGN.md, Q1.
|
|
118
|
+
*/
|
|
119
|
+
export const DEFAULT_AUTO_CLEAR_UNCARRYABLE_PIN = false;
|
|
120
|
+
|
|
111
121
|
export interface SSSStorageFunctions {
|
|
112
122
|
storeDeviceShare: (share: string, id?: string) => Promise<void>;
|
|
113
123
|
getDeviceShare: (id?: string) => Promise<string | null>;
|
|
@@ -125,6 +135,22 @@ export interface SSSStrategyConfig {
|
|
|
125
135
|
/** Report enrollment failures without logging recovery material. */
|
|
126
136
|
onEscrowError?: (err: unknown) => void;
|
|
127
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Nitro repair only: when the signed authority still has a PIN but lca-api holds
|
|
140
|
+
* no PIN to carry (an identity-recovery rebind deleted it), sign a PIN `clear`
|
|
141
|
+
* instead of a `carry` that lca-api would reject with PRECONDITION_FAILED.
|
|
142
|
+
* Defaults to {@link DEFAULT_AUTO_CLEAR_UNCARRYABLE_PIN} (off). Do not enable
|
|
143
|
+
* without security sign-off: POST_RECOVERY_ESCROW_REPAIR_DESIGN.md, open question Q1.
|
|
144
|
+
*/
|
|
145
|
+
autoClearUncarryablePin?: boolean;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Called once after every committed automatic PIN clear
|
|
149
|
+
* ({@link SSSStrategyConfig.autoClearUncarryablePin}). Must not throw; defaults
|
|
150
|
+
* to a console warning. Never receives the DID, email or PIN material.
|
|
151
|
+
*/
|
|
152
|
+
onEscrowPinAutoCleared?: (event: EscrowPinAutoClearedEvent) => void;
|
|
153
|
+
|
|
128
154
|
/** Server URL for key share operations */
|
|
129
155
|
serverUrl: string;
|
|
130
156
|
|
|
@@ -193,9 +219,18 @@ const describeHttpStatus = (response: Response): string =>
|
|
|
193
219
|
response.statusText ? `${response.status} ${response.statusText}` : `${response.status}`;
|
|
194
220
|
|
|
195
221
|
type EscrowEnrollmentResult =
|
|
196
|
-
| { enrolled: false; reason: 'disabled' | 'opted-out' }
|
|
222
|
+
| { enrolled: false; reason: 'disabled' | 'opted-out' | 'not-enrolled' | 'pin-reset-required' }
|
|
197
223
|
| { enrolled: true; changed: false }
|
|
198
|
-
| { enrolled: true; changed: true; shareVersion: number };
|
|
224
|
+
| { enrolled: true; changed: true; shareVersion: number; pinCleared?: 'automatic' };
|
|
225
|
+
|
|
226
|
+
/** Telemetry for an automatic post-rebind PIN clear. Carries no DID, email or PIN data. */
|
|
227
|
+
export interface EscrowPinAutoClearedEvent {
|
|
228
|
+
type: 'escrow.pin.auto_cleared';
|
|
229
|
+
/** Share version the PIN-free enrollment was committed at. */
|
|
230
|
+
shareVersion: number;
|
|
231
|
+
/** Share version the signed authority was still active at (pre-rebind). */
|
|
232
|
+
authorityShareVersion: number | null;
|
|
233
|
+
}
|
|
199
234
|
|
|
200
235
|
/** HTTP failure metadata without potentially sensitive server response text. */
|
|
201
236
|
export class EscrowRequestError extends Error {
|
|
@@ -1065,6 +1100,8 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1065
1100
|
recoveryShare: string;
|
|
1066
1101
|
authShare: string;
|
|
1067
1102
|
rebindSessionToken: string;
|
|
1103
|
+
/** Recovered from an existing escrow enrollment (hold or PIN), not another method. */
|
|
1104
|
+
fromEscrow: boolean;
|
|
1068
1105
|
rebind?: { shares: SSSShares; storageId: string | undefined; attempted: boolean };
|
|
1069
1106
|
}
|
|
1070
1107
|
| undefined;
|
|
@@ -1421,10 +1458,11 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1421
1458
|
}
|
|
1422
1459
|
};
|
|
1423
1460
|
|
|
1424
|
-
const tryEnrollEscrow = async (...args: Parameters<typeof enrollEscrow>): Promise<
|
|
1425
|
-
if (!config.escrow?.enabled) return;
|
|
1461
|
+
const tryEnrollEscrow = async (...args: Parameters<typeof enrollEscrow>): Promise<boolean> => {
|
|
1462
|
+
if (!config.escrow?.enabled) return false;
|
|
1426
1463
|
try {
|
|
1427
1464
|
await enrollEscrow(...args);
|
|
1465
|
+
return true;
|
|
1428
1466
|
} catch (err) {
|
|
1429
1467
|
// A reporting callback must not turn a successful rotation into a failure.
|
|
1430
1468
|
try {
|
|
@@ -1433,9 +1471,30 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1433
1471
|
} catch {
|
|
1434
1472
|
console.warn('SSS: escrow error reporting failed');
|
|
1435
1473
|
}
|
|
1474
|
+
return false;
|
|
1436
1475
|
}
|
|
1437
1476
|
};
|
|
1438
1477
|
|
|
1478
|
+
/** Report a committed automatic PIN clear so the caller can prompt for a new PIN. */
|
|
1479
|
+
const withAutoClear = (
|
|
1480
|
+
shareVersion: number,
|
|
1481
|
+
autoClearedFrom: { authorityShareVersion: number | null } | undefined
|
|
1482
|
+
): EscrowEnrollmentResult => {
|
|
1483
|
+
if (!autoClearedFrom) return { enrolled: true, changed: true, shareVersion };
|
|
1484
|
+
try {
|
|
1485
|
+
const event: EscrowPinAutoClearedEvent = {
|
|
1486
|
+
type: 'escrow.pin.auto_cleared',
|
|
1487
|
+
shareVersion,
|
|
1488
|
+
authorityShareVersion: autoClearedFrom.authorityShareVersion,
|
|
1489
|
+
};
|
|
1490
|
+
if (config.onEscrowPinAutoCleared) config.onEscrowPinAutoCleared(event);
|
|
1491
|
+
else console.warn('SSS: escrow PIN cleared automatically', event);
|
|
1492
|
+
} catch {
|
|
1493
|
+
console.warn('SSS: escrow event reporting failed');
|
|
1494
|
+
}
|
|
1495
|
+
return { enrolled: true, changed: true, shareVersion, pinCleared: 'automatic' };
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1439
1498
|
const ensureEscrowEnrollmentUnguarded = async (
|
|
1440
1499
|
self: SSSKeyDerivationStrategy,
|
|
1441
1500
|
params: Parameters<NonNullable<SSSKeyDerivationStrategy['ensureEscrowEnrollment']>>[0],
|
|
@@ -1443,7 +1502,8 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1443
1502
|
forceRotate = false,
|
|
1444
1503
|
allowDisabledEnrollment = false
|
|
1445
1504
|
): Promise<EscrowEnrollmentResult> => {
|
|
1446
|
-
let clearPinForEnrollment = forceRotate;
|
|
1505
|
+
let clearPinForEnrollment = forceRotate || params.options?.clearPin === true;
|
|
1506
|
+
let autoClearedFrom: { authorityShareVersion: number | null } | undefined;
|
|
1447
1507
|
if (!config.escrow?.enabled) return { enrolled: false, reason: 'disabled' };
|
|
1448
1508
|
const pin = params.options?.pin;
|
|
1449
1509
|
const recoveryEmail = params.options?.recoveryEmail ?? '';
|
|
@@ -1470,7 +1530,15 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1470
1530
|
('confirmationStatus' in method &&
|
|
1471
1531
|
method.confirmationStatus === 'confirmed'))
|
|
1472
1532
|
);
|
|
1533
|
+
if (
|
|
1534
|
+
params.options?.requirePriorEnrollment &&
|
|
1535
|
+
config.escrow.attestation.mode !== 'nitro' &&
|
|
1536
|
+
!status.recoveryMethods.some(method => method.type === 'escrow')
|
|
1537
|
+
)
|
|
1538
|
+
return { enrolled: false, reason: 'not-enrolled' };
|
|
1473
1539
|
let verifiedKey: VerifiedEnclaveIdentity | undefined;
|
|
1540
|
+
// Locked, stale or unknown PIN state never satisfies a clear request.
|
|
1541
|
+
let hasPin = status.escrowPin?.state !== 'none';
|
|
1474
1542
|
if (config.escrow.attestation.mode === 'nitro') {
|
|
1475
1543
|
if (!status.primaryDid || !params.signDidAuthVp)
|
|
1476
1544
|
throw new Error('Owner proof signing is required for escrow enrollment.');
|
|
@@ -1484,6 +1552,9 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1484
1552
|
verifiedKey,
|
|
1485
1553
|
storage.assertCurrent
|
|
1486
1554
|
);
|
|
1555
|
+
// The signed authority survives the identity rebind that drops Mongo's blob.
|
|
1556
|
+
if (params.options?.requirePriorEnrollment && current.status === 'empty')
|
|
1557
|
+
return { enrolled: false, reason: 'not-enrolled' };
|
|
1487
1558
|
if (current.status === 'disabled') {
|
|
1488
1559
|
verifiedEscrowEnvelope = undefined;
|
|
1489
1560
|
if (!allowDisabledEnrollment) return { enrolled: false, reason: 'opted-out' };
|
|
@@ -1496,7 +1567,51 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1496
1567
|
current.shareVersion === status.shareVersion &&
|
|
1497
1568
|
current.envelope?.keyId === verifiedKey.keyId;
|
|
1498
1569
|
if (currentlyEnrolled) verifiedEscrowEnvelope = current.envelope ?? undefined;
|
|
1570
|
+
hasPin = current.hasPin;
|
|
1571
|
+
// After an identity-recovery rebind lca-api has no PIN left to carry, so an
|
|
1572
|
+
// automatic carry/keep can only fail.
|
|
1573
|
+
const uncarryablePin =
|
|
1574
|
+
!clearPinForEnrollment &&
|
|
1575
|
+
current.status === 'active' &&
|
|
1576
|
+
current.hasPin &&
|
|
1577
|
+
pin === undefined &&
|
|
1578
|
+
!currentlyEnrolled &&
|
|
1579
|
+
status.escrowPin?.state === 'none';
|
|
1580
|
+
// Opt-in only (design doc Q1): sign a clear instead. Rebind evidence: the
|
|
1581
|
+
// signed authority is still active at an older share version, and lca-api
|
|
1582
|
+
// lists no confirmed escrow method at any version. All but the authority
|
|
1583
|
+
// are lca-api claims, so this is forgeable by a compromised API.
|
|
1584
|
+
if (
|
|
1585
|
+
uncarryablePin &&
|
|
1586
|
+
(config.autoClearUncarryablePin ?? DEFAULT_AUTO_CLEAR_UNCARRYABLE_PIN) &&
|
|
1587
|
+
current.shareVersion !== null &&
|
|
1588
|
+
status.shareVersion !== null &&
|
|
1589
|
+
current.shareVersion < status.shareVersion &&
|
|
1590
|
+
!status.recoveryMethods.some(
|
|
1591
|
+
method =>
|
|
1592
|
+
method.type === 'escrow' &&
|
|
1593
|
+
(Boolean(method.confirmedAt) ||
|
|
1594
|
+
('confirmationStatus' in method &&
|
|
1595
|
+
method.confirmationStatus === 'confirmed'))
|
|
1596
|
+
)
|
|
1597
|
+
) {
|
|
1598
|
+
clearPinForEnrollment = true;
|
|
1599
|
+
autoClearedFrom = { authorityShareVersion: current.shareVersion };
|
|
1600
|
+
} else if (uncarryablePin) {
|
|
1601
|
+
// Never rotate or sign here: only a user-chosen PIN (`set`) resolves it.
|
|
1602
|
+
return { enrolled: false, reason: 'pin-reset-required' };
|
|
1603
|
+
}
|
|
1499
1604
|
}
|
|
1605
|
+
// A current, PIN-free enrollment already satisfies a clear request
|
|
1606
|
+
// (e.g. post-recovery repair after direct enrollment succeeded).
|
|
1607
|
+
if (
|
|
1608
|
+
currentlyEnrolled &&
|
|
1609
|
+
!forceRotate &&
|
|
1610
|
+
pin === undefined &&
|
|
1611
|
+
!hasPin &&
|
|
1612
|
+
!status.escrowStale
|
|
1613
|
+
)
|
|
1614
|
+
clearPinForEnrollment = false;
|
|
1500
1615
|
// A confirmed enrollment whose sealed blob no longer matches the active
|
|
1501
1616
|
// enclave (P6 mode migration/rollback, or a rotated enclave key) still
|
|
1502
1617
|
// needs the same rotation a missing enrollment would get — but only one
|
|
@@ -1519,7 +1634,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1519
1634
|
}
|
|
1520
1635
|
try {
|
|
1521
1636
|
if (
|
|
1522
|
-
!
|
|
1637
|
+
!forceRotate &&
|
|
1523
1638
|
pin === undefined &&
|
|
1524
1639
|
unenrolledRotation &&
|
|
1525
1640
|
unenrolledRotation.shareVersion === status.shareVersion &&
|
|
@@ -1536,13 +1651,13 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1536
1651
|
params.signDidAuthVp,
|
|
1537
1652
|
undefined,
|
|
1538
1653
|
undefined,
|
|
1539
|
-
retry.clearPin,
|
|
1654
|
+
clearPinForEnrollment || retry.clearPin,
|
|
1540
1655
|
storage.assertCurrent,
|
|
1541
1656
|
recoveryEmail,
|
|
1542
1657
|
recoveryLanguage
|
|
1543
1658
|
);
|
|
1544
1659
|
unenrolledRotation = undefined;
|
|
1545
|
-
return
|
|
1660
|
+
return withAutoClear(retry.shareVersion, autoClearedFrom);
|
|
1546
1661
|
}
|
|
1547
1662
|
// Verify the enclave before rotating: a bad attestation must not burn
|
|
1548
1663
|
// a share version (and, repeated, the retained auth-share history).
|
|
@@ -1601,7 +1716,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
1601
1716
|
recoveryLanguage
|
|
1602
1717
|
);
|
|
1603
1718
|
unenrolledRotation = undefined;
|
|
1604
|
-
return
|
|
1719
|
+
return withAutoClear(shareVersion, autoClearedFrom);
|
|
1605
1720
|
} catch (err) {
|
|
1606
1721
|
// A background repair the caller never asked for must not surface as
|
|
1607
1722
|
// a hard failure (mirrors tryEnrollEscrow's existing swallow-and-report
|
|
@@ -2036,6 +2151,9 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
2036
2151
|
escrowPin: data.escrowPin,
|
|
2037
2152
|
escrowStale: data.escrowStale,
|
|
2038
2153
|
sssActivationState: data.sssActivationState ?? 'active',
|
|
2154
|
+
...(typeof data.priorEscrowEnrollment === 'boolean'
|
|
2155
|
+
? { priorEscrowEnrollment: data.priorEscrowEnrollment }
|
|
2156
|
+
: {}),
|
|
2039
2157
|
};
|
|
2040
2158
|
},
|
|
2041
2159
|
|
|
@@ -2123,7 +2241,19 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
2123
2241
|
escrowPin: status.escrowPin,
|
|
2124
2242
|
staleReason: status.escrowStale,
|
|
2125
2243
|
};
|
|
2126
|
-
return { state:
|
|
2244
|
+
if (enrolled) return { state: 'enrolled', escrowPin: status.escrowPin };
|
|
2245
|
+
// A rebind leaves escrow methods `pending`, which /keys/auth-share omits but
|
|
2246
|
+
// reports as priorEscrowEnrollment; flag them so the caller can request a
|
|
2247
|
+
// prior-enrollment repair instead of a first-time enrollment. Older servers
|
|
2248
|
+
// lack the field, so fall back to the (confirmed-only) method list.
|
|
2249
|
+
const priorEscrowMethod =
|
|
2250
|
+
status.priorEscrowEnrollment ??
|
|
2251
|
+
status.recoveryMethods.some(method => method.type === 'escrow');
|
|
2252
|
+
return {
|
|
2253
|
+
state: 'not-enrolled',
|
|
2254
|
+
escrowPin: status.escrowPin,
|
|
2255
|
+
...(priorEscrowMethod ? { priorEscrowMethod: true } : {}),
|
|
2256
|
+
};
|
|
2127
2257
|
},
|
|
2128
2258
|
|
|
2129
2259
|
async disableEscrowRecovery(params): Promise<void> {
|
|
@@ -2220,7 +2350,20 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
2220
2350
|
providerType: params.providerType,
|
|
2221
2351
|
}),
|
|
2222
2352
|
});
|
|
2223
|
-
|
|
2353
|
+
const result = await ensureEscrowEnrollmentUnguarded(
|
|
2354
|
+
this,
|
|
2355
|
+
params,
|
|
2356
|
+
storage,
|
|
2357
|
+
false,
|
|
2358
|
+
true
|
|
2359
|
+
);
|
|
2360
|
+
// Explicit enable never requests prior-enrollment-only repair.
|
|
2361
|
+
if (result.enrolled) return result;
|
|
2362
|
+
if (result.reason === 'not-enrolled')
|
|
2363
|
+
throw new Error('Escrow enrollment did not complete');
|
|
2364
|
+
if (result.reason === 'pin-reset-required')
|
|
2365
|
+
throw new Error('Set a new recovery PIN to turn on automatic recovery.');
|
|
2366
|
+
return { enrolled: false as const, reason: result.reason };
|
|
2224
2367
|
});
|
|
2225
2368
|
},
|
|
2226
2369
|
|
|
@@ -2478,6 +2621,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
2478
2621
|
input: RecoveryInput;
|
|
2479
2622
|
didFromPrivateKey?: (privateKey: string) => Promise<string>;
|
|
2480
2623
|
signDidAuthVp?: DidAuthVpSigner;
|
|
2624
|
+
options?: { recoveryEmail?: string; recoveryLanguage?: string };
|
|
2481
2625
|
}): Promise<RecoveryResult> {
|
|
2482
2626
|
const storage = guardedStorage();
|
|
2483
2627
|
const { token, providerType, input, didFromPrivateKey, signDidAuthVp } = params;
|
|
@@ -2565,11 +2709,17 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
2565
2709
|
recoveryShare: plaintext.recoveryShare,
|
|
2566
2710
|
authShare,
|
|
2567
2711
|
rebindSessionToken: material.rebindSessionToken,
|
|
2712
|
+
fromEscrow: true,
|
|
2568
2713
|
};
|
|
2569
2714
|
// The OTP path uses exactly this pending state and atomic rebind operation.
|
|
2570
2715
|
// Without a provider session the caller completes it after signing in.
|
|
2571
2716
|
if (!token) return { privateKey, did: material.primaryDid };
|
|
2572
|
-
return this.completeIdentityRecovery!({
|
|
2717
|
+
return this.completeIdentityRecovery!({
|
|
2718
|
+
token,
|
|
2719
|
+
providerType,
|
|
2720
|
+
signDidAuthVp,
|
|
2721
|
+
options: params.options,
|
|
2722
|
+
});
|
|
2573
2723
|
}
|
|
2574
2724
|
|
|
2575
2725
|
let recoveryShare: string;
|
|
@@ -3295,6 +3445,7 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
3295
3445
|
recoveryShare,
|
|
3296
3446
|
authShare,
|
|
3297
3447
|
rebindSessionToken: material.rebindSessionToken,
|
|
3448
|
+
fromEscrow: false,
|
|
3298
3449
|
};
|
|
3299
3450
|
|
|
3300
3451
|
return { privateKey, did: material.primaryDid };
|
|
@@ -3465,22 +3616,41 @@ export function createSSSStrategy(config: SSSStrategyConfig): SSSKeyDerivationSt
|
|
|
3465
3616
|
})
|
|
3466
3617
|
);
|
|
3467
3618
|
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3619
|
+
// Other recovery methods leave enrollment to the caller's rollout gate.
|
|
3620
|
+
const enrolled =
|
|
3621
|
+
pending.fromEscrow &&
|
|
3622
|
+
(await tryEnrollEscrow(
|
|
3623
|
+
params.token,
|
|
3624
|
+
params.providerType,
|
|
3625
|
+
pending.privateKey,
|
|
3626
|
+
pending.primaryDid,
|
|
3627
|
+
shares,
|
|
3628
|
+
shareVersion,
|
|
3629
|
+
signDidAuthVp,
|
|
3630
|
+
undefined,
|
|
3631
|
+
undefined,
|
|
3632
|
+
true,
|
|
3633
|
+
storage.assertCurrent,
|
|
3634
|
+
params.options?.recoveryEmail,
|
|
3635
|
+
params.options?.recoveryLanguage
|
|
3636
|
+
));
|
|
3480
3637
|
if (activeStorageId !== storageId || storageGeneration !== generation) {
|
|
3481
3638
|
throw new Error('This recovery request was cancelled.');
|
|
3482
3639
|
}
|
|
3483
|
-
|
|
3640
|
+
if (!enrolled) {
|
|
3641
|
+
unenrolledRotation = {
|
|
3642
|
+
shares,
|
|
3643
|
+
shareVersion,
|
|
3644
|
+
primaryDid: pending.primaryDid,
|
|
3645
|
+
clearPin: true,
|
|
3646
|
+
};
|
|
3647
|
+
}
|
|
3648
|
+
return {
|
|
3649
|
+
privateKey: pending.privateKey,
|
|
3650
|
+
did: pending.primaryDid,
|
|
3651
|
+
escrowEnrolled: enrolled,
|
|
3652
|
+
escrowRecovery: pending.fromEscrow,
|
|
3653
|
+
};
|
|
3484
3654
|
},
|
|
3485
3655
|
|
|
3486
3656
|
// --- Contact method management ---
|