@learncard/sss-key-manager 0.2.5 → 0.2.6

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.
@@ -24031,7 +24031,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
24031
24031
  state: external_exports.enum(["none", "enabled", "locked", "stale"]),
24032
24032
  enabled: external_exports.boolean(),
24033
24033
  attemptsRemaining: external_exports.number().int().nonnegative(),
24034
- salt: external_exports.string().optional()
24034
+ salt: external_exports.string().optional(),
24035
+ /**
24036
+ * Verified wrong PIN entries against the current PIN (successful releases and
24037
+ * unresolved reservations are not counted). Omitted when the server cannot tell,
24038
+ * e.g. legacy records that predate the verified counter.
24039
+ */
24040
+ wrongAttempts: external_exports.number().int().nonnegative().optional()
24035
24041
  });
24036
24042
  var BITSTRING_STATUS_PURPOSES = ["revocation", "suspension"];
24037
24043
  var DEFAULT_BITSTRING_STATUS_LIST_SIZE = 131072;
@@ -25098,6 +25104,7 @@ __export(index_exports, {
25098
25104
  NitroAttestationError: () => NitroAttestationError,
25099
25105
  PIN_MAX_LENGTH: () => PIN_MAX_LENGTH,
25100
25106
  PIN_MIN_LENGTH: () => PIN_MIN_LENGTH,
25107
+ PinSignInStaleError: () => PinSignInStaleError,
25101
25108
  SSSApiClient: () => SSSApiClient,
25102
25109
  SSSKeyManager: () => SSSKeyManager,
25103
25110
  SSS_THRESHOLD: () => SSS_THRESHOLD,
@@ -30154,6 +30161,15 @@ var EscrowPinLockedError = class extends Error {
30154
30161
  this.name = "EscrowPinLockedError";
30155
30162
  }
30156
30163
  };
30164
+ var PinSignInStaleError = class extends Error {
30165
+ static {
30166
+ __name(this, "PinSignInStaleError");
30167
+ }
30168
+ constructor() {
30169
+ super("Your account changed while signing in. Try again.");
30170
+ this.name = "PinSignInStaleError";
30171
+ }
30172
+ };
30157
30173
 
30158
30174
  // src/escrow-pin.ts
30159
30175
  var PIN_MIN_LENGTH = 6;
@@ -31016,7 +31032,7 @@ function createSSSStrategy(config) {
31016
31032
  assertCurrent();
31017
31033
  return result;
31018
31034
  }, "fetchAuthorityState");
31019
- const enrollEscrow = /* @__PURE__ */ __name(async (token, providerType, privateKey, primaryDid, shares, shareVersion, signDidAuthVp, verifiedKey, pinMaterial, clearPin = false, assertCurrent = guardedStorage().assertCurrent, recoveryEmail = "", recoveryLanguage = "") => {
31035
+ const enrollEscrow = /* @__PURE__ */ __name(async (token, providerType, privateKey, primaryDid, shares, shareVersion, signDidAuthVp, verifiedKey, pinMaterial, clearPin = false, assertCurrent = guardedStorage().assertCurrent, recoveryEmail = "", recoveryLanguage = "", expectedPinCharges) => {
31020
31036
  if (!config.escrow?.enabled) throw new Error("Escrow enrollment is disabled");
31021
31037
  if (!signDidAuthVp) throw new Error("DID proof signing is required for escrow enrollment");
31022
31038
  const identity = verifiedKey ?? await fetchVerifiedEnclaveKey();
@@ -31075,7 +31091,8 @@ function createSSSStrategy(config) {
31075
31091
  enclaveKeyId: keyId,
31076
31092
  ...pinMaterial ? { pinSalt: pinMaterial.pinSalt } : {},
31077
31093
  ...(signedIntent ? signedIntent.intent.pinAction === "clear" : clearPin) ? { clearPin: true } : {},
31078
- ...signedIntent ? { signedIntent, sourceEnvelope: authority?.envelope ?? null } : {}
31094
+ ...signedIntent ? { signedIntent, sourceEnvelope: authority?.envelope ?? null } : {},
31095
+ ...expectedPinCharges ? { expectedPinCharges } : {}
31079
31096
  })
31080
31097
  });
31081
31098
  if (signedIntent) {
@@ -31088,11 +31105,11 @@ function createSSSStrategy(config) {
31088
31105
  verifiedEscrowEnvelope = committed.envelope ?? void 0;
31089
31106
  }
31090
31107
  }, "enrollEscrow");
31091
- const tryEnrollEscrow = /* @__PURE__ */ __name(async (...args) => {
31092
- if (!config.escrow?.enabled) return false;
31108
+ const attemptEnrollEscrow = /* @__PURE__ */ __name(async (...args) => {
31109
+ if (!config.escrow?.enabled) return { enrolled: false };
31093
31110
  try {
31094
31111
  await enrollEscrow(...args);
31095
- return true;
31112
+ return { enrolled: true };
31096
31113
  } catch (err2) {
31097
31114
  try {
31098
31115
  if (config.onEscrowError) config.onEscrowError(err2);
@@ -31100,9 +31117,10 @@ function createSSSStrategy(config) {
31100
31117
  } catch {
31101
31118
  console.warn("SSS: escrow error reporting failed");
31102
31119
  }
31103
- return false;
31120
+ return { enrolled: false, error: err2 };
31104
31121
  }
31105
- }, "tryEnrollEscrow");
31122
+ }, "attemptEnrollEscrow");
31123
+ const tryEnrollEscrow = /* @__PURE__ */ __name(async (...args) => (await attemptEnrollEscrow(...args)).enrolled, "tryEnrollEscrow");
31106
31124
  const withAutoClear = /* @__PURE__ */ __name((shareVersion, autoClearedFrom) => {
31107
31125
  if (!autoClearedFrom) return { enrolled: true, changed: true, shareVersion };
31108
31126
  try {
@@ -31163,7 +31181,22 @@ function createSSSStrategy(config) {
31163
31181
  clearPinForEnrollment ||= current.hasPin && pin === void 0;
31164
31182
  }
31165
31183
  currentlyEnrolled = currentlyEnrolled && current.status === "active" && current.shareVersion === status.shareVersion && current.envelope?.keyId === verifiedKey.keyId;
31184
+ let identityMismatch = false;
31185
+ if (currentlyEnrolled && current.envelope && status.escrowBlobIdentity !== void 0) {
31186
+ const authorityIdentity = await getEscrowBlobIdentity(current.envelope);
31187
+ if (authorityIdentity !== status.escrowBlobIdentity) {
31188
+ const reread = await self.fetchServerKeyStatus(
31189
+ params.token,
31190
+ params.providerType
31191
+ );
31192
+ storage2.assertCurrent();
31193
+ identityMismatch = reread.escrowBlobIdentity !== authorityIdentity && reread.escrowUpdating !== true;
31194
+ }
31195
+ }
31196
+ if (identityMismatch) currentlyEnrolled = false;
31166
31197
  if (currentlyEnrolled) verifiedEscrowEnvelope = current.envelope ?? void 0;
31198
+ if (identityMismatch && current.hasPin && pin === void 0 && !clearPinForEnrollment)
31199
+ return { enrolled: false, reason: "pin-reset-required" };
31167
31200
  hasPin = current.hasPin;
31168
31201
  const uncarryablePin = !clearPinForEnrollment && current.status === "active" && current.hasPin && pin === void 0 && !currentlyEnrolled && status.escrowPin?.state === "none";
31169
31202
  if (uncarryablePin && (config.autoClearUncarryablePin ?? DEFAULT_AUTO_CLEAR_UNCARRYABLE_PIN) && current.shareVersion !== null && status.shareVersion !== null && current.shareVersion < status.shareVersion && !status.recoveryMethods.some(
@@ -31291,6 +31324,378 @@ function createSSSStrategy(config) {
31291
31324
  }
31292
31325
  };
31293
31326
  }, "captureActiveUserGuard");
31327
+ const pinHasRecordedWrongAttempts = /* @__PURE__ */ __name((escrowPin) => {
31328
+ const status = escrowPin;
31329
+ if (status?.state === "none") return false;
31330
+ return typeof status?.wrongAttempts !== "number" || status.wrongAttempts > 0;
31331
+ }, "pinHasRecordedWrongAttempts");
31332
+ const refreshablePinCharges = /* @__PURE__ */ __name((before, after) => {
31333
+ const pre = before;
31334
+ const post = after;
31335
+ if (pinHasRecordedWrongAttempts(before) || pinHasRecordedWrongAttempts(after))
31336
+ return void 0;
31337
+ if (pre?.state !== "enabled" || post?.state !== "enabled") return void 0;
31338
+ if (typeof pre.attemptsRemaining !== "number" || typeof post.attemptsRemaining !== "number")
31339
+ return void 0;
31340
+ if (post.attemptsRemaining !== pre.attemptsRemaining - 1) return void 0;
31341
+ return { attemptsRemaining: post.attemptsRemaining, wrongAttempts: 0 };
31342
+ }, "refreshablePinCharges");
31343
+ const freshPinMaterial = /* @__PURE__ */ __name(async (pin) => {
31344
+ if (!validatePin(pin).ok) return void 0;
31345
+ try {
31346
+ const pinSalt = generatePinSalt();
31347
+ return { pinSalt, pinVerifier: await derivePinProof(pin, pinSalt) };
31348
+ } catch {
31349
+ return void 0;
31350
+ }
31351
+ }, "freshPinMaterial");
31352
+ const adoptReleasedShare = /* @__PURE__ */ __name(async (storage2, storageId, released, shareVersion, currentVersion) => {
31353
+ await storage2.storeDeviceShare(released.recoveryShare, storageId);
31354
+ await storage2.storeShareVersion(shareVersion, storageId);
31355
+ lastEmailShare = void 0;
31356
+ lastShareVersion = shareVersion;
31357
+ lastServerSnapshot = {
31358
+ currentVersion,
31359
+ resolvedVersion: shareVersion,
31360
+ authShare: released.authShare,
31361
+ primaryDid: released.primaryDid
31362
+ };
31363
+ }, "adoptReleasedShare");
31364
+ const completePinSignInKeep = /* @__PURE__ */ __name(async (params, released, pin, pinBefore) => {
31365
+ const storage2 = guardedStorage();
31366
+ const generation = storageGeneration;
31367
+ const storageId = activeStorageId;
31368
+ const assertActive = /* @__PURE__ */ __name(() => {
31369
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
31370
+ throw new Error("This recovery request was cancelled.");
31371
+ }
31372
+ }, "assertActive");
31373
+ const { mode, newPinRequired, refreshCharges } = await withShareUpdateLock(
31374
+ storage2,
31375
+ storageId,
31376
+ () => withRotationLock(async () => {
31377
+ assertActive();
31378
+ const current = await fetchAuthShareRaw(
31379
+ serverUrl,
31380
+ params.token,
31381
+ params.providerType,
31382
+ void 0,
31383
+ tenantId
31384
+ );
31385
+ assertActive();
31386
+ if (!current || current.primaryDid !== released.primaryDid || typeof current.shareVersion !== "number") {
31387
+ throw new PinSignInStaleError();
31388
+ }
31389
+ let mode2;
31390
+ if (current.shareVersion === released.shareVersion) {
31391
+ if (authShareToString(current.authShare) !== released.authShare) {
31392
+ throw new PinSignInStaleError();
31393
+ }
31394
+ mode2 = "kept";
31395
+ } else {
31396
+ const historical = current.shareVersion > released.shareVersion ? await fetchAuthShareRaw(
31397
+ serverUrl,
31398
+ params.token,
31399
+ params.providerType,
31400
+ released.shareVersion,
31401
+ tenantId
31402
+ ) : null;
31403
+ if (authShareToString(historical?.authShare) !== released.authShare) {
31404
+ throw new PinSignInStaleError();
31405
+ }
31406
+ mode2 = "deferred-rotation";
31407
+ }
31408
+ assertActive();
31409
+ await adoptReleasedShare(
31410
+ storage2,
31411
+ storageId,
31412
+ released,
31413
+ released.shareVersion,
31414
+ current.shareVersion
31415
+ );
31416
+ pendingIdentityRecovery = void 0;
31417
+ return {
31418
+ mode: mode2,
31419
+ newPinRequired: pinHasRecordedWrongAttempts(pinBefore) || pinHasRecordedWrongAttempts(current.escrowPin),
31420
+ refreshCharges: refreshablePinCharges(pinBefore, current.escrowPin)
31421
+ };
31422
+ })
31423
+ );
31424
+ let pinRefreshed = false;
31425
+ let pinRefreshFailed = false;
31426
+ if (mode === "kept" && !newPinRequired && refreshCharges && params.signDidAuthVp) {
31427
+ const pinMaterial = await freshPinMaterial(pin);
31428
+ pinRefreshed = pinMaterial !== void 0 && await tryEnrollEscrow(
31429
+ params.token,
31430
+ params.providerType,
31431
+ released.privateKey,
31432
+ released.primaryDid,
31433
+ { recoveryShare: released.recoveryShare },
31434
+ released.shareVersion,
31435
+ params.signDidAuthVp,
31436
+ void 0,
31437
+ pinMaterial,
31438
+ false,
31439
+ storage2.assertCurrent,
31440
+ params.options?.recoveryEmail,
31441
+ params.options?.recoveryLanguage,
31442
+ refreshCharges
31443
+ );
31444
+ pinRefreshFailed = pinMaterial !== void 0 && !pinRefreshed;
31445
+ }
31446
+ assertActive();
31447
+ return {
31448
+ privateKey: released.privateKey,
31449
+ did: released.primaryDid,
31450
+ escrowEnrolled: true,
31451
+ escrowRecovery: true,
31452
+ pinSignIn: {
31453
+ mode,
31454
+ pinRefreshed,
31455
+ newPinRequired,
31456
+ ...pinRefreshFailed ? { pinRefreshFailed: true } : {}
31457
+ }
31458
+ };
31459
+ }, "completePinSignInKeep");
31460
+ const completePinSignInRelink = /* @__PURE__ */ __name(async (params, pending, signDidAuthVp) => {
31461
+ const storage2 = guardedStorage();
31462
+ const generation = storageGeneration;
31463
+ const storageId = activeStorageId;
31464
+ const shareVersion = pending.pinKeep.shareVersion;
31465
+ const assertActive = /* @__PURE__ */ __name(() => {
31466
+ if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
31467
+ throw new Error("This recovery request was cancelled.");
31468
+ }
31469
+ }, "assertActive");
31470
+ await withShareUpdateLock(
31471
+ storage2,
31472
+ storageId,
31473
+ () => withRotationLock(async () => {
31474
+ assertActive();
31475
+ const didAuthVp = await requestFreshDidAuthVp(
31476
+ serverUrl,
31477
+ pending.privateKey,
31478
+ pending.primaryDid,
31479
+ signDidAuthVp,
31480
+ tenantId
31481
+ );
31482
+ assertActive();
31483
+ try {
31484
+ const response = await postJson(
31485
+ `${serverUrl}/keys/recovery-session/pin-sign-in`,
31486
+ {
31487
+ recoverySessionToken: pending.rebindSessionToken,
31488
+ providerType: params.providerType,
31489
+ primaryDid: pending.primaryDid
31490
+ },
31491
+ {
31492
+ ...buildHeaders("", didAuthVp, tenantId),
31493
+ "X-Auth-Token": params.token
31494
+ }
31495
+ );
31496
+ if (response.shareVersion !== shareVersion) throw new PinSignInStaleError();
31497
+ } catch (error) {
31498
+ const current = await fetchAuthShareRaw(
31499
+ serverUrl,
31500
+ params.token,
31501
+ params.providerType,
31502
+ void 0,
31503
+ tenantId
31504
+ ).catch(() => null);
31505
+ if (!current || current.primaryDid !== pending.primaryDid || current.shareVersion !== shareVersion || authShareToString(current.authShare) !== pending.authShare) {
31506
+ throw error;
31507
+ }
31508
+ }
31509
+ assertActive();
31510
+ await adoptReleasedShare(storage2, storageId, pending, shareVersion, shareVersion);
31511
+ pendingIdentityRecovery = void 0;
31512
+ })
31513
+ );
31514
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
31515
+ throw new Error("This recovery request was cancelled.");
31516
+ }
31517
+ const status = await fetchAuthShareRaw(
31518
+ serverUrl,
31519
+ params.token,
31520
+ params.providerType,
31521
+ void 0,
31522
+ tenantId
31523
+ ).catch(() => null);
31524
+ return {
31525
+ privateKey: pending.privateKey,
31526
+ did: pending.primaryDid,
31527
+ escrowEnrolled: true,
31528
+ escrowRecovery: true,
31529
+ pinSignIn: {
31530
+ mode: "relinked",
31531
+ pinRefreshed: false,
31532
+ newPinRequired: pinHasRecordedWrongAttempts(status?.escrowPin)
31533
+ }
31534
+ };
31535
+ }, "completePinSignInRelink");
31536
+ const completeIdentityRecoveryWith = /* @__PURE__ */ __name(async (params) => {
31537
+ const storage2 = guardedStorage();
31538
+ const generation = storageGeneration;
31539
+ const pending = pendingIdentityRecovery;
31540
+ const signDidAuthVp = params.signDidAuthVp;
31541
+ if (!pending) throw new Error("No identity recovery is waiting for a new sign-in");
31542
+ if (!signDidAuthVp) {
31543
+ throw new Error("DID proof signing is required to bind a new sign-in");
31544
+ }
31545
+ if (pending.pinKeep) return completePinSignInRelink(params, pending, signDidAuthVp);
31546
+ const storageId = activeStorageId;
31547
+ const { shares, shareVersion } = await withShareUpdateLock(
31548
+ storage2,
31549
+ storageId,
31550
+ () => withRotationLock(async () => {
31551
+ const assertActive = /* @__PURE__ */ __name(() => {
31552
+ if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
31553
+ throw new Error("This recovery request was cancelled.");
31554
+ }
31555
+ }, "assertActive");
31556
+ assertActive();
31557
+ const candidates = await readPendingShareCandidates(storage2, storageId);
31558
+ if (pending.rebind && pending.rebind.storageId !== storageId) {
31559
+ throw new Error("Active account changed during identity recovery");
31560
+ }
31561
+ if (candidates.some(
31562
+ (candidate) => candidate.share !== pending.rebind?.shares.deviceShare
31563
+ )) {
31564
+ throw new Error("Reconcile the pending share update before binding a sign-in");
31565
+ }
31566
+ let shareVersion2;
31567
+ const retry = pending.rebind?.attempted ?? false;
31568
+ if (retry && pending.rebind) {
31569
+ const current = await fetchAuthShareRaw(
31570
+ serverUrl,
31571
+ params.token,
31572
+ params.providerType,
31573
+ void 0,
31574
+ tenantId
31575
+ );
31576
+ const authShare = authShareToString(current?.authShare);
31577
+ if (authShare && current?.shareVersion != null) {
31578
+ const health = await verifyStoredShares(
31579
+ {
31580
+ getDevice: /* @__PURE__ */ __name(async () => pending.rebind.shares.deviceShare, "getDevice"),
31581
+ getAuth: /* @__PURE__ */ __name(async () => authShare, "getAuth")
31582
+ },
31583
+ pending.primaryDid,
31584
+ async (key) => key === pending.privateKey ? pending.primaryDid : ""
31585
+ );
31586
+ if (health.healthy) shareVersion2 = current.shareVersion;
31587
+ }
31588
+ }
31589
+ assertActive();
31590
+ if (shareVersion2 === void 0) {
31591
+ const didAuthVp = await requestFreshDidAuthVp(
31592
+ serverUrl,
31593
+ pending.privateKey,
31594
+ pending.primaryDid,
31595
+ signDidAuthVp,
31596
+ tenantId
31597
+ );
31598
+ assertActive();
31599
+ pending.rebind ??= {
31600
+ shares: (await splitAndVerify(pending.privateKey)).shares,
31601
+ storageId,
31602
+ attempted: false
31603
+ };
31604
+ const rebind = pending.rebind;
31605
+ if (!candidates.some((candidate) => candidate.share === rebind.shares.deviceShare)) {
31606
+ await addPendingShareCandidate(
31607
+ storage2,
31608
+ { share: rebind.shares.deviceShare, createdAt: Date.now() },
31609
+ storageId
31610
+ );
31611
+ }
31612
+ assertActive();
31613
+ rebind.attempted = true;
31614
+ try {
31615
+ const response = await postJson(
31616
+ `${serverUrl}/keys/recovery-session/rebind`,
31617
+ {
31618
+ recoverySessionToken: pending.rebindSessionToken,
31619
+ providerType: params.providerType,
31620
+ primaryDid: pending.primaryDid,
31621
+ authShare: {
31622
+ encryptedData: rebind.shares.authShare,
31623
+ encryptedDek: "",
31624
+ iv: ""
31625
+ }
31626
+ },
31627
+ {
31628
+ ...buildHeaders("", didAuthVp, tenantId),
31629
+ "X-Auth-Token": params.token
31630
+ }
31631
+ );
31632
+ shareVersion2 = response.shareVersion;
31633
+ } catch (error) {
31634
+ if (error instanceof ShareWriteRejectedError && !retry) {
31635
+ await removePendingShareCandidate(
31636
+ storage2,
31637
+ rebind.shares.deviceShare,
31638
+ storageId
31639
+ );
31640
+ pending.rebind = void 0;
31641
+ }
31642
+ throw error;
31643
+ }
31644
+ }
31645
+ if (shareVersion2 === void 0 || !pending.rebind) {
31646
+ throw new Error("Server did not confirm the new share");
31647
+ }
31648
+ assertActive();
31649
+ const shares2 = pending.rebind.shares;
31650
+ await storage2.storeDeviceShare(shares2.deviceShare, storageId);
31651
+ await storage2.storeShareVersion(shareVersion2, storageId);
31652
+ await clearPendingShareCandidates(storage2, storageId);
31653
+ assertActive();
31654
+ lastEmailShare = shares2.emailShare;
31655
+ lastShareVersion = shareVersion2;
31656
+ lastServerSnapshot = {
31657
+ currentVersion: shareVersion2,
31658
+ resolvedVersion: shareVersion2,
31659
+ authShare: shares2.authShare,
31660
+ primaryDid: pending.primaryDid
31661
+ };
31662
+ pendingIdentityRecovery = void 0;
31663
+ return { shares: shares2, shareVersion: shareVersion2 };
31664
+ })
31665
+ );
31666
+ const enrolled = pending.fromEscrow && await tryEnrollEscrow(
31667
+ params.token,
31668
+ params.providerType,
31669
+ pending.privateKey,
31670
+ pending.primaryDid,
31671
+ shares,
31672
+ shareVersion,
31673
+ signDidAuthVp,
31674
+ void 0,
31675
+ void 0,
31676
+ true,
31677
+ storage2.assertCurrent,
31678
+ params.options?.recoveryEmail,
31679
+ params.options?.recoveryLanguage
31680
+ );
31681
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
31682
+ throw new Error("This recovery request was cancelled.");
31683
+ }
31684
+ if (!enrolled) {
31685
+ unenrolledRotation = {
31686
+ shares,
31687
+ shareVersion,
31688
+ primaryDid: pending.primaryDid,
31689
+ clearPin: true
31690
+ };
31691
+ }
31692
+ return {
31693
+ privateKey: pending.privateKey,
31694
+ did: pending.primaryDid,
31695
+ escrowEnrolled: enrolled,
31696
+ escrowRecovery: pending.fromEscrow
31697
+ };
31698
+ }, "completeIdentityRecoveryWith");
31294
31699
  return {
31295
31700
  name: "sss",
31296
31701
  capabilities: {
@@ -31592,6 +31997,8 @@ function createSSSStrategy(config) {
31592
31997
  escrowOptedOut: data.escrowOptedOut === true,
31593
31998
  escrowPin: data.escrowPin,
31594
31999
  escrowStale: data.escrowStale,
32000
+ ...typeof data.escrowBlobIdentity === "string" ? { escrowBlobIdentity: data.escrowBlobIdentity } : {},
32001
+ ...data.escrowUpdating === true ? { escrowUpdating: true } : {},
31595
32002
  sssActivationState: data.sssActivationState ?? "active",
31596
32003
  ...typeof data.priorEscrowEnrollment === "boolean" ? { priorEscrowEnrollment: data.priorEscrowEnrollment } : {}
31597
32004
  };
@@ -31966,9 +32373,20 @@ function createSSSStrategy(config) {
31966
32373
  if (input.method === "escrow" || input.method === "escrow-pin") {
31967
32374
  if (!didFromPrivateKey)
31968
32375
  throw new Error("DID verification is required for escrow recovery");
31969
- const start = input.method === "escrow-pin" ? await this.startEscrowRecovery({
32376
+ if (input.method === "escrow-pin" && !token && !input.recoverySessionToken)
32377
+ throw new Error("PIN recovery requires signing in first.");
32378
+ const pinBefore = input.method === "escrow-pin" && token ? await fetchAuthShareRaw(
32379
+ serverUrl,
31970
32380
  token,
31971
32381
  providerType,
32382
+ void 0,
32383
+ tenantId
32384
+ ).then(
32385
+ (current) => current?.escrowPin,
32386
+ () => void 0
32387
+ ) : void 0;
32388
+ const start = input.method === "escrow-pin" ? await this.startEscrowRecovery({
32389
+ ...token ? { token, providerType } : { recoverySessionToken: input.recoverySessionToken },
31972
32390
  expectedDid: input.expectedDid,
31973
32391
  options: { releasePolicy: "pin" }
31974
32392
  }) : input;
@@ -32020,16 +32438,32 @@ function createSSSStrategy(config) {
32020
32438
  "Recovery produced an incorrect key. Try another recovery method."
32021
32439
  );
32022
32440
  }
32441
+ if (input.method === "escrow-pin" && token) {
32442
+ return completePinSignInKeep(
32443
+ { token, providerType, signDidAuthVp, options: params.options },
32444
+ {
32445
+ privateKey: privateKey2,
32446
+ primaryDid: material.primaryDid,
32447
+ recoveryShare: plaintext.recoveryShare,
32448
+ authShare,
32449
+ shareVersion: material.shareVersion
32450
+ },
32451
+ input.pin,
32452
+ pinBefore
32453
+ );
32454
+ }
32023
32455
  pendingIdentityRecovery = {
32024
32456
  privateKey: privateKey2,
32025
32457
  primaryDid: material.primaryDid,
32026
32458
  recoveryShare: plaintext.recoveryShare,
32027
32459
  authShare,
32028
32460
  rebindSessionToken: material.rebindSessionToken,
32029
- fromEscrow: true
32461
+ fromEscrow: true,
32462
+ // The PIN itself is not kept across the new-login step.
32463
+ ...input.method === "escrow-pin" ? { pinKeep: { shareVersion: material.shareVersion } } : {}
32030
32464
  };
32031
32465
  if (!token) return { privateKey: privateKey2, did: material.primaryDid };
32032
- return this.completeIdentityRecovery({
32466
+ return completeIdentityRecoveryWith({
32033
32467
  token,
32034
32468
  providerType,
32035
32469
  signDidAuthVp,
@@ -32604,125 +33038,128 @@ function createSSSStrategy(config) {
32604
33038
  pendingIdentityRecovery = void 0;
32605
33039
  },
32606
33040
  async completeIdentityRecovery(params) {
32607
- const storage2 = guardedStorage();
32608
- const generation = storageGeneration;
32609
- const pending = pendingIdentityRecovery;
32610
- const signDidAuthVp = params.signDidAuthVp;
32611
- if (!pending) throw new Error("No identity recovery is waiting for a new sign-in");
32612
- if (!signDidAuthVp) {
32613
- throw new Error("DID proof signing is required to bind a new sign-in");
33041
+ return completeIdentityRecoveryWith(params);
33042
+ },
33043
+ /**
33044
+ * "Secure my account": one atomic server update re-splits the key and drops all
33045
+ * older auth share versions, signing out other devices and turning off the old
33046
+ * phrase/backup/passkey/email key. Staged through the pending-candidate slot, so
33047
+ * a lost acknowledgement is promoted by the usual reconciliation. Escrow is then
33048
+ * re-enrolled with `pin` (fresh salt) or, for `null`, without a PIN. The PIN is
33049
+ * used in this call only and never kept in retry state.
33050
+ */
33051
+ async revokeOtherDevices(params) {
33052
+ const { token, providerType, privateKey, did, signDidAuthVp, pin } = params;
33053
+ if (!signDidAuthVp) throw new Error("DID proof signing is required");
33054
+ if (pin !== null) {
33055
+ const validation = validatePin(pin);
33056
+ if (!validation.ok)
33057
+ throw new Error(
33058
+ validation.reason === "length" ? "PIN must contain 6\u201312 digits." : "Choose a PIN without trivial or sequential patterns."
33059
+ );
32614
33060
  }
33061
+ const storage2 = guardedStorage();
32615
33062
  const storageId = activeStorageId;
33063
+ const assertUser = captureActiveUserGuard();
33064
+ const assertActive = /* @__PURE__ */ __name(() => {
33065
+ assertUser();
33066
+ storage2.assertCurrent();
33067
+ }, "assertActive");
33068
+ const before = await fetchAuthShareRaw(
33069
+ serverUrl,
33070
+ token,
33071
+ providerType,
33072
+ void 0,
33073
+ tenantId
33074
+ );
33075
+ if (!before || before.primaryDid !== did) {
33076
+ throw new Error("This account could not be found. Sign in again.");
33077
+ }
33078
+ const hadEscrow = !before.escrowOptedOut && (before.priorEscrowEnrollment === true || Array.isArray(before.recoveryMethods) && before.recoveryMethods.some(
33079
+ (method) => method?.type === "escrow"
33080
+ ));
33081
+ const didFromKey = /* @__PURE__ */ __name(async (key) => key === privateKey ? did : `${did}:mismatch`, "didFromKey");
32616
33082
  const { shares, shareVersion } = await withShareUpdateLock(
32617
33083
  storage2,
32618
33084
  storageId,
32619
33085
  () => withRotationLock(async () => {
32620
- const assertActive = /* @__PURE__ */ __name(() => {
32621
- if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
32622
- throw new Error("This recovery request was cancelled.");
32623
- }
32624
- }, "assertActive");
32625
- assertActive();
32626
- const candidates = await readPendingShareCandidates(storage2, storageId);
32627
- if (pending.rebind && pending.rebind.storageId !== storageId) {
32628
- throw new Error("Active account changed during identity recovery");
32629
- }
32630
- if (candidates.some(
32631
- (candidate) => candidate.share !== pending.rebind?.shares.deviceShare
32632
- )) {
32633
- throw new Error(
32634
- "Reconcile the pending share update before binding a sign-in"
32635
- );
32636
- }
32637
- let shareVersion2;
32638
- const retry = pending.rebind?.attempted ?? false;
32639
- if (retry && pending.rebind) {
32640
- const current = await fetchAuthShareRaw(
32641
- serverUrl,
32642
- params.token,
32643
- params.providerType,
32644
- void 0,
32645
- tenantId
32646
- );
32647
- const authShare = authShareToString(current?.authShare);
32648
- if (authShare && current?.shareVersion != null) {
32649
- const health = await verifyStoredShares(
32650
- {
32651
- getDevice: /* @__PURE__ */ __name(async () => pending.rebind.shares.deviceShare, "getDevice"),
32652
- getAuth: /* @__PURE__ */ __name(async () => authShare, "getAuth")
32653
- },
32654
- pending.primaryDid,
32655
- async (key) => key === pending.privateKey ? pending.primaryDid : ""
33086
+ const { snapshot } = await resolvePendingShareCandidates(
33087
+ storage2,
33088
+ storageId,
33089
+ async () => {
33090
+ const data = await fetchAuthShareRaw(
33091
+ serverUrl,
33092
+ token,
33093
+ providerType,
33094
+ void 0,
33095
+ tenantId
32656
33096
  );
32657
- if (health.healthy) shareVersion2 = current.shareVersion;
32658
- }
33097
+ return {
33098
+ currentVersion: data?.shareVersion ?? null,
33099
+ authShare: authShareToString(data?.authShare)
33100
+ };
33101
+ },
33102
+ did,
33103
+ didFromKey,
33104
+ assertActive,
33105
+ recordPromotion(did),
33106
+ async (version) => authShareToString(
33107
+ (await fetchAuthShareRaw(
33108
+ serverUrl,
33109
+ token,
33110
+ providerType,
33111
+ version,
33112
+ tenantId
33113
+ ))?.authShare
33114
+ )
33115
+ );
33116
+ if (!snapshot.currentVersion) {
33117
+ throw new Error("This account could not be found. Sign in again.");
32659
33118
  }
33119
+ const expectedShareVersion = snapshot.currentVersion;
33120
+ const didAuthVp = await requestFreshDidAuthVp(
33121
+ serverUrl,
33122
+ privateKey,
33123
+ did,
33124
+ signDidAuthVp,
33125
+ tenantId
33126
+ );
32660
33127
  assertActive();
32661
- if (shareVersion2 === void 0) {
32662
- const didAuthVp = await requestFreshDidAuthVp(
32663
- serverUrl,
32664
- pending.privateKey,
32665
- pending.primaryDid,
32666
- signDidAuthVp,
32667
- tenantId
32668
- );
32669
- assertActive();
32670
- pending.rebind ??= {
32671
- shares: (await splitAndVerify(pending.privateKey)).shares,
32672
- storageId,
32673
- attempted: false
32674
- };
32675
- const rebind = pending.rebind;
32676
- if (!candidates.some(
32677
- (candidate) => candidate.share === rebind.shares.deviceShare
32678
- )) {
33128
+ let shareVersion2;
33129
+ let stagedShare = "";
33130
+ const shares2 = await atomicShareUpdate(privateKey, {
33131
+ storeDevice: /* @__PURE__ */ __name(async (share) => {
33132
+ assertActive();
33133
+ stagedShare = share;
32679
33134
  await addPendingShareCandidate(
32680
33135
  storage2,
32681
- { share: rebind.shares.deviceShare, createdAt: Date.now() },
33136
+ { share, createdAt: Date.now() },
32682
33137
  storageId
32683
33138
  );
32684
- }
32685
- assertActive();
32686
- rebind.attempted = true;
32687
- try {
33139
+ }, "storeDevice"),
33140
+ clearDevice: /* @__PURE__ */ __name(() => removePendingShareCandidate(storage2, stagedShare, storageId), "clearDevice"),
33141
+ storeAuth: /* @__PURE__ */ __name(async (share) => {
33142
+ assertActive();
32688
33143
  const response = await postJson(
32689
- `${serverUrl}/keys/recovery-session/rebind`,
33144
+ `${serverUrl}/keys/revoke-other-devices`,
32690
33145
  {
32691
- recoverySessionToken: pending.rebindSessionToken,
32692
- providerType: params.providerType,
32693
- primaryDid: pending.primaryDid,
32694
- authShare: {
32695
- encryptedData: rebind.shares.authShare,
32696
- encryptedDek: "",
32697
- iv: ""
32698
- }
33146
+ authToken: token,
33147
+ providerType,
33148
+ primaryDid: did,
33149
+ authShare: { encryptedData: share, encryptedDek: "", iv: "" },
33150
+ expectedShareVersion
32699
33151
  },
32700
- {
32701
- ...buildHeaders("", didAuthVp, tenantId),
32702
- "X-Auth-Token": params.token
32703
- }
33152
+ buildHeaders(token, didAuthVp, tenantId)
32704
33153
  );
32705
33154
  shareVersion2 = response.shareVersion;
32706
- } catch (error) {
32707
- if (error instanceof ShareWriteRejectedError && !retry) {
32708
- await removePendingShareCandidate(
32709
- storage2,
32710
- rebind.shares.deviceShare,
32711
- storageId
32712
- );
32713
- pending.rebind = void 0;
32714
- }
32715
- throw error;
32716
- }
32717
- }
32718
- if (shareVersion2 === void 0 || !pending.rebind) {
32719
- throw new Error("Server did not confirm the new share");
33155
+ }, "storeAuth")
33156
+ });
33157
+ if (shareVersion2 === void 0) {
33158
+ throw new Error("Server did not return a share version");
32720
33159
  }
32721
- assertActive();
32722
- const shares2 = pending.rebind.shares;
32723
33160
  await storage2.storeDeviceShare(shares2.deviceShare, storageId);
32724
33161
  await storage2.storeShareVersion(shareVersion2, storageId);
32725
- await clearPendingShareCandidates(storage2, storageId);
33162
+ await removePendingShareCandidate(storage2, shares2.deviceShare, storageId);
32726
33163
  assertActive();
32727
33164
  lastEmailShare = shares2.emailShare;
32728
33165
  lastShareVersion = shareVersion2;
@@ -32730,44 +33167,36 @@ function createSSSStrategy(config) {
32730
33167
  currentVersion: shareVersion2,
32731
33168
  resolvedVersion: shareVersion2,
32732
33169
  authShare: shares2.authShare,
32733
- primaryDid: pending.primaryDid
33170
+ primaryDid: did
32734
33171
  };
32735
- pendingIdentityRecovery = void 0;
32736
33172
  return { shares: shares2, shareVersion: shareVersion2 };
32737
33173
  })
32738
33174
  );
32739
- const enrolled = pending.fromEscrow && await tryEnrollEscrow(
32740
- params.token,
32741
- params.providerType,
32742
- pending.privateKey,
32743
- pending.primaryDid,
33175
+ if (!hadEscrow || !config.escrow?.enabled) {
33176
+ return { shareVersion, escrowEnrolled: false, pinKept: false };
33177
+ }
33178
+ const pinMaterial = pin === null ? void 0 : await freshPinMaterial(pin);
33179
+ const enrollArgs = [
33180
+ token,
33181
+ providerType,
33182
+ privateKey,
33183
+ did,
32744
33184
  shares,
32745
33185
  shareVersion,
32746
33186
  signDidAuthVp,
32747
33187
  void 0,
32748
- void 0,
32749
- true,
33188
+ pinMaterial,
33189
+ !pinMaterial,
32750
33190
  storage2.assertCurrent,
32751
33191
  params.options?.recoveryEmail,
32752
33192
  params.options?.recoveryLanguage
32753
- );
32754
- if (activeStorageId !== storageId || storageGeneration !== generation) {
32755
- throw new Error("This recovery request was cancelled.");
32756
- }
33193
+ ];
33194
+ const attempt = await attemptEnrollEscrow(...enrollArgs);
33195
+ const enrolled = attempt.enrolled || attempt.error instanceof EscrowRequestError && attempt.error.status === 403 && await tryEnrollEscrow(...enrollArgs);
32757
33196
  if (!enrolled) {
32758
- unenrolledRotation = {
32759
- shares,
32760
- shareVersion,
32761
- primaryDid: pending.primaryDid,
32762
- clearPin: true
32763
- };
33197
+ unenrolledRotation = { shares, shareVersion, primaryDid: did, clearPin: true };
32764
33198
  }
32765
- return {
32766
- privateKey: pending.privateKey,
32767
- did: pending.primaryDid,
32768
- escrowEnrolled: enrolled,
32769
- escrowRecovery: pending.fromEscrow
32770
- };
33199
+ return { shareVersion, escrowEnrolled: enrolled, pinKept: enrolled && !!pinMaterial };
32771
33200
  },
32772
33201
  // --- Contact method management ---
32773
33202
  async upgradeContactMethod(token, providerType, previousPhone, email, code) {