@learncard/sss-key-manager 0.2.5 → 0.2.7

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;
@@ -30412,6 +30428,7 @@ var EscrowRequestError = class extends Error {
30412
30428
  __name(this, "EscrowRequestError");
30413
30429
  }
30414
30430
  };
30431
+ var isConflictRefusal = /* @__PURE__ */ __name((error) => error instanceof EscrowRequestError && error.status === 409, "isConflictRefusal");
30415
30432
  var DuplicateShareCoordinateError = class extends Error {
30416
30433
  static {
30417
30434
  __name(this, "DuplicateShareCoordinateError");
@@ -31016,7 +31033,7 @@ function createSSSStrategy(config) {
31016
31033
  assertCurrent();
31017
31034
  return result;
31018
31035
  }, "fetchAuthorityState");
31019
- const enrollEscrow = /* @__PURE__ */ __name(async (token, providerType, privateKey, primaryDid, shares, shareVersion, signDidAuthVp, verifiedKey, pinMaterial, clearPin = false, assertCurrent = guardedStorage().assertCurrent, recoveryEmail = "", recoveryLanguage = "") => {
31036
+ const enrollEscrow = /* @__PURE__ */ __name(async (token, providerType, privateKey, primaryDid, shares, shareVersion, signDidAuthVp, verifiedKey, pinMaterial, clearPin = false, assertCurrent = guardedStorage().assertCurrent, recoveryEmail = "", recoveryLanguage = "", expectedPinCharges) => {
31020
31037
  if (!config.escrow?.enabled) throw new Error("Escrow enrollment is disabled");
31021
31038
  if (!signDidAuthVp) throw new Error("DID proof signing is required for escrow enrollment");
31022
31039
  const identity = verifiedKey ?? await fetchVerifiedEnclaveKey();
@@ -31075,7 +31092,8 @@ function createSSSStrategy(config) {
31075
31092
  enclaveKeyId: keyId,
31076
31093
  ...pinMaterial ? { pinSalt: pinMaterial.pinSalt } : {},
31077
31094
  ...(signedIntent ? signedIntent.intent.pinAction === "clear" : clearPin) ? { clearPin: true } : {},
31078
- ...signedIntent ? { signedIntent, sourceEnvelope: authority?.envelope ?? null } : {}
31095
+ ...signedIntent ? { signedIntent, sourceEnvelope: authority?.envelope ?? null } : {},
31096
+ ...expectedPinCharges ? { expectedPinCharges } : {}
31079
31097
  })
31080
31098
  });
31081
31099
  if (signedIntent) {
@@ -31088,11 +31106,11 @@ function createSSSStrategy(config) {
31088
31106
  verifiedEscrowEnvelope = committed.envelope ?? void 0;
31089
31107
  }
31090
31108
  }, "enrollEscrow");
31091
- const tryEnrollEscrow = /* @__PURE__ */ __name(async (...args) => {
31092
- if (!config.escrow?.enabled) return false;
31109
+ const attemptEnrollEscrow = /* @__PURE__ */ __name(async (...args) => {
31110
+ if (!config.escrow?.enabled) return { enrolled: false };
31093
31111
  try {
31094
31112
  await enrollEscrow(...args);
31095
- return true;
31113
+ return { enrolled: true };
31096
31114
  } catch (err2) {
31097
31115
  try {
31098
31116
  if (config.onEscrowError) config.onEscrowError(err2);
@@ -31100,9 +31118,10 @@ function createSSSStrategy(config) {
31100
31118
  } catch {
31101
31119
  console.warn("SSS: escrow error reporting failed");
31102
31120
  }
31103
- return false;
31121
+ return { enrolled: false, error: err2 };
31104
31122
  }
31105
- }, "tryEnrollEscrow");
31123
+ }, "attemptEnrollEscrow");
31124
+ const tryEnrollEscrow = /* @__PURE__ */ __name(async (...args) => (await attemptEnrollEscrow(...args)).enrolled, "tryEnrollEscrow");
31106
31125
  const withAutoClear = /* @__PURE__ */ __name((shareVersion, autoClearedFrom) => {
31107
31126
  if (!autoClearedFrom) return { enrolled: true, changed: true, shareVersion };
31108
31127
  try {
@@ -31163,7 +31182,22 @@ function createSSSStrategy(config) {
31163
31182
  clearPinForEnrollment ||= current.hasPin && pin === void 0;
31164
31183
  }
31165
31184
  currentlyEnrolled = currentlyEnrolled && current.status === "active" && current.shareVersion === status.shareVersion && current.envelope?.keyId === verifiedKey.keyId;
31185
+ let identityMismatch = false;
31186
+ if (currentlyEnrolled && current.envelope && status.escrowBlobIdentity !== void 0) {
31187
+ const authorityIdentity = await getEscrowBlobIdentity(current.envelope);
31188
+ if (authorityIdentity !== status.escrowBlobIdentity) {
31189
+ const reread = await self.fetchServerKeyStatus(
31190
+ params.token,
31191
+ params.providerType
31192
+ );
31193
+ storage2.assertCurrent();
31194
+ identityMismatch = reread.escrowBlobIdentity !== authorityIdentity && reread.escrowUpdating !== true;
31195
+ }
31196
+ }
31197
+ if (identityMismatch) currentlyEnrolled = false;
31166
31198
  if (currentlyEnrolled) verifiedEscrowEnvelope = current.envelope ?? void 0;
31199
+ if (identityMismatch && current.hasPin && pin === void 0 && !clearPinForEnrollment)
31200
+ return { enrolled: false, reason: "pin-reset-required" };
31167
31201
  hasPin = current.hasPin;
31168
31202
  const uncarryablePin = !clearPinForEnrollment && current.status === "active" && current.hasPin && pin === void 0 && !currentlyEnrolled && status.escrowPin?.state === "none";
31169
31203
  if (uncarryablePin && (config.autoClearUncarryablePin ?? DEFAULT_AUTO_CLEAR_UNCARRYABLE_PIN) && current.shareVersion !== null && status.shareVersion !== null && current.shareVersion < status.shareVersion && !status.recoveryMethods.some(
@@ -31291,6 +31325,411 @@ function createSSSStrategy(config) {
31291
31325
  }
31292
31326
  };
31293
31327
  }, "captureActiveUserGuard");
31328
+ const recordedWrongPinAttempts = /* @__PURE__ */ __name((escrowPin) => {
31329
+ const status = escrowPin;
31330
+ if (status?.state === "none") return 0;
31331
+ const count = status?.wrongAttempts;
31332
+ return typeof count === "number" && Number.isSafeInteger(count) && count >= 0 ? count : void 0;
31333
+ }, "recordedWrongPinAttempts");
31334
+ const attemptCount = /* @__PURE__ */ __name((value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0, "attemptCount");
31335
+ const pinSignInClean = /* @__PURE__ */ __name((observed, flowAttempts) => {
31336
+ const flowSalt = flowAttempts?.pinSalt;
31337
+ const sameGeneration = typeof flowSalt !== "string" || observed.every((status) => {
31338
+ const salt = status?.salt;
31339
+ return typeof salt !== "string" || salt === flowSalt;
31340
+ });
31341
+ const attempts = sameGeneration ? flowAttempts : void 0;
31342
+ const own = attempts ? attemptCount(attempts.ownWrongAttempts) : 0;
31343
+ if (own === void 0) return { clean: false, ownWrongAttempts: 0 };
31344
+ const prior = attempts ? attemptCount(attempts.priorWrongAttempts) : recordedWrongPinAttempts(observed[0]);
31345
+ const clean = prior === 0 && observed.length > 0 && observed.every((status) => recordedWrongPinAttempts(status) === own);
31346
+ return { clean, ownWrongAttempts: own };
31347
+ }, "pinSignInClean");
31348
+ const refreshablePinCharges = /* @__PURE__ */ __name((before, after, ownWrongAttempts) => {
31349
+ const pre = before;
31350
+ const post = after;
31351
+ if (pre?.state !== "enabled" || post?.state !== "enabled") return void 0;
31352
+ if (typeof pre.attemptsRemaining !== "number" || typeof post.attemptsRemaining !== "number")
31353
+ return void 0;
31354
+ if (post.attemptsRemaining !== pre.attemptsRemaining - 1) return void 0;
31355
+ return { attemptsRemaining: post.attemptsRemaining, wrongAttempts: ownWrongAttempts };
31356
+ }, "refreshablePinCharges");
31357
+ const freshPinMaterial = /* @__PURE__ */ __name(async (pin) => {
31358
+ if (!validatePin(pin).ok) return void 0;
31359
+ try {
31360
+ const pinSalt = generatePinSalt();
31361
+ return { pinSalt, pinVerifier: await derivePinProof(pin, pinSalt) };
31362
+ } catch {
31363
+ return void 0;
31364
+ }
31365
+ }, "freshPinMaterial");
31366
+ const adoptReleasedShare = /* @__PURE__ */ __name(async (storage2, storageId, released, shareVersion, currentVersion) => {
31367
+ await storage2.storeDeviceShare(released.recoveryShare, storageId);
31368
+ await storage2.storeShareVersion(shareVersion, storageId);
31369
+ lastEmailShare = void 0;
31370
+ lastShareVersion = shareVersion;
31371
+ lastServerSnapshot = {
31372
+ currentVersion,
31373
+ resolvedVersion: shareVersion,
31374
+ authShare: released.authShare,
31375
+ primaryDid: released.primaryDid
31376
+ };
31377
+ }, "adoptReleasedShare");
31378
+ const completePinSignInKeep = /* @__PURE__ */ __name(async (params, released, pin, pinBefore, pinAttempts) => {
31379
+ const storage2 = guardedStorage();
31380
+ const generation = storageGeneration;
31381
+ const storageId = activeStorageId;
31382
+ const assertActive = /* @__PURE__ */ __name(() => {
31383
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
31384
+ throw new Error("This recovery request was cancelled.");
31385
+ }
31386
+ }, "assertActive");
31387
+ const {
31388
+ mode,
31389
+ newPinRequired: newPinRequiredAtRelease,
31390
+ refreshCharges
31391
+ } = await withShareUpdateLock(
31392
+ storage2,
31393
+ storageId,
31394
+ () => withRotationLock(async () => {
31395
+ assertActive();
31396
+ const current = await fetchAuthShareRaw(
31397
+ serverUrl,
31398
+ params.token,
31399
+ params.providerType,
31400
+ void 0,
31401
+ tenantId
31402
+ );
31403
+ assertActive();
31404
+ if (!current || current.primaryDid !== released.primaryDid || typeof current.shareVersion !== "number") {
31405
+ throw new PinSignInStaleError();
31406
+ }
31407
+ let mode2;
31408
+ if (current.shareVersion === released.shareVersion) {
31409
+ if (authShareToString(current.authShare) !== released.authShare) {
31410
+ throw new PinSignInStaleError();
31411
+ }
31412
+ mode2 = "kept";
31413
+ } else {
31414
+ const historical = current.shareVersion > released.shareVersion ? await fetchAuthShareRaw(
31415
+ serverUrl,
31416
+ params.token,
31417
+ params.providerType,
31418
+ released.shareVersion,
31419
+ tenantId
31420
+ ) : null;
31421
+ if (authShareToString(historical?.authShare) !== released.authShare) {
31422
+ throw new PinSignInStaleError();
31423
+ }
31424
+ mode2 = "deferred-rotation";
31425
+ }
31426
+ assertActive();
31427
+ await adoptReleasedShare(
31428
+ storage2,
31429
+ storageId,
31430
+ released,
31431
+ released.shareVersion,
31432
+ current.shareVersion
31433
+ );
31434
+ pendingIdentityRecovery = void 0;
31435
+ const { clean, ownWrongAttempts } = pinSignInClean(
31436
+ [pinBefore, current.escrowPin],
31437
+ pinAttempts
31438
+ );
31439
+ return {
31440
+ mode: mode2,
31441
+ newPinRequired: !clean,
31442
+ refreshCharges: clean ? refreshablePinCharges(pinBefore, current.escrowPin, ownWrongAttempts) : void 0
31443
+ };
31444
+ })
31445
+ );
31446
+ let pinRefreshed = false;
31447
+ let pinRefreshFailed = false;
31448
+ let newPinRequired = newPinRequiredAtRelease;
31449
+ if (mode === "kept" && !newPinRequired && refreshCharges && params.signDidAuthVp) {
31450
+ const pinMaterial = await freshPinMaterial(pin);
31451
+ const refresh = pinMaterial === void 0 ? void 0 : await attemptEnrollEscrow(
31452
+ params.token,
31453
+ params.providerType,
31454
+ released.privateKey,
31455
+ released.primaryDid,
31456
+ { recoveryShare: released.recoveryShare },
31457
+ released.shareVersion,
31458
+ params.signDidAuthVp,
31459
+ void 0,
31460
+ pinMaterial,
31461
+ false,
31462
+ storage2.assertCurrent,
31463
+ params.options?.recoveryEmail,
31464
+ params.options?.recoveryLanguage,
31465
+ refreshCharges
31466
+ );
31467
+ pinRefreshed = refresh?.enrolled === true;
31468
+ pinRefreshFailed = pinMaterial !== void 0 && !pinRefreshed;
31469
+ if (pinRefreshFailed) {
31470
+ const latest = await fetchAuthShareRaw(
31471
+ serverUrl,
31472
+ params.token,
31473
+ params.providerType,
31474
+ void 0,
31475
+ tenantId
31476
+ ).catch(() => void 0);
31477
+ newPinRequired = latest ? !pinSignInClean([pinBefore, latest.escrowPin], pinAttempts).clean : isConflictRefusal(refresh && !refresh.enrolled ? refresh.error : void 0);
31478
+ }
31479
+ }
31480
+ assertActive();
31481
+ return {
31482
+ privateKey: released.privateKey,
31483
+ did: released.primaryDid,
31484
+ escrowEnrolled: true,
31485
+ escrowRecovery: true,
31486
+ pinSignIn: {
31487
+ mode,
31488
+ pinRefreshed,
31489
+ newPinRequired,
31490
+ ...pinRefreshFailed ? { pinRefreshFailed: true } : {}
31491
+ }
31492
+ };
31493
+ }, "completePinSignInKeep");
31494
+ const completePinSignInRelink = /* @__PURE__ */ __name(async (params, pending, signDidAuthVp) => {
31495
+ const storage2 = guardedStorage();
31496
+ const generation = storageGeneration;
31497
+ const storageId = activeStorageId;
31498
+ const shareVersion = pending.pinKeep.shareVersion;
31499
+ const assertActive = /* @__PURE__ */ __name(() => {
31500
+ if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
31501
+ throw new Error("This recovery request was cancelled.");
31502
+ }
31503
+ }, "assertActive");
31504
+ await withShareUpdateLock(
31505
+ storage2,
31506
+ storageId,
31507
+ () => withRotationLock(async () => {
31508
+ assertActive();
31509
+ const didAuthVp = await requestFreshDidAuthVp(
31510
+ serverUrl,
31511
+ pending.privateKey,
31512
+ pending.primaryDid,
31513
+ signDidAuthVp,
31514
+ tenantId
31515
+ );
31516
+ assertActive();
31517
+ try {
31518
+ const response = await postJson(
31519
+ `${serverUrl}/keys/recovery-session/pin-sign-in`,
31520
+ {
31521
+ recoverySessionToken: pending.rebindSessionToken,
31522
+ providerType: params.providerType,
31523
+ primaryDid: pending.primaryDid
31524
+ },
31525
+ {
31526
+ ...buildHeaders("", didAuthVp, tenantId),
31527
+ "X-Auth-Token": params.token
31528
+ }
31529
+ );
31530
+ if (response.shareVersion !== shareVersion) throw new PinSignInStaleError();
31531
+ } catch (error) {
31532
+ const current = await fetchAuthShareRaw(
31533
+ serverUrl,
31534
+ params.token,
31535
+ params.providerType,
31536
+ void 0,
31537
+ tenantId
31538
+ ).catch(() => null);
31539
+ if (!current || current.primaryDid !== pending.primaryDid || current.shareVersion !== shareVersion || authShareToString(current.authShare) !== pending.authShare) {
31540
+ throw error;
31541
+ }
31542
+ }
31543
+ assertActive();
31544
+ await adoptReleasedShare(storage2, storageId, pending, shareVersion, shareVersion);
31545
+ pendingIdentityRecovery = void 0;
31546
+ })
31547
+ );
31548
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
31549
+ throw new Error("This recovery request was cancelled.");
31550
+ }
31551
+ const status = await fetchAuthShareRaw(
31552
+ serverUrl,
31553
+ params.token,
31554
+ params.providerType,
31555
+ void 0,
31556
+ tenantId
31557
+ ).catch(() => null);
31558
+ return {
31559
+ privateKey: pending.privateKey,
31560
+ did: pending.primaryDid,
31561
+ escrowEnrolled: true,
31562
+ escrowRecovery: true,
31563
+ pinSignIn: {
31564
+ mode: "relinked",
31565
+ pinRefreshed: false,
31566
+ newPinRequired: !pinSignInClean([status?.escrowPin], pending.pinKeep?.pinAttempts).clean
31567
+ }
31568
+ };
31569
+ }, "completePinSignInRelink");
31570
+ const completeIdentityRecoveryWith = /* @__PURE__ */ __name(async (params) => {
31571
+ const storage2 = guardedStorage();
31572
+ const generation = storageGeneration;
31573
+ const pending = pendingIdentityRecovery;
31574
+ const signDidAuthVp = params.signDidAuthVp;
31575
+ if (!pending) throw new Error("No identity recovery is waiting for a new sign-in");
31576
+ if (!signDidAuthVp) {
31577
+ throw new Error("DID proof signing is required to bind a new sign-in");
31578
+ }
31579
+ if (pending.pinKeep) return completePinSignInRelink(params, pending, signDidAuthVp);
31580
+ const storageId = activeStorageId;
31581
+ const { shares, shareVersion } = await withShareUpdateLock(
31582
+ storage2,
31583
+ storageId,
31584
+ () => withRotationLock(async () => {
31585
+ const assertActive = /* @__PURE__ */ __name(() => {
31586
+ if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
31587
+ throw new Error("This recovery request was cancelled.");
31588
+ }
31589
+ }, "assertActive");
31590
+ assertActive();
31591
+ const candidates = await readPendingShareCandidates(storage2, storageId);
31592
+ if (pending.rebind && pending.rebind.storageId !== storageId) {
31593
+ throw new Error("Active account changed during identity recovery");
31594
+ }
31595
+ if (candidates.some(
31596
+ (candidate) => candidate.share !== pending.rebind?.shares.deviceShare
31597
+ )) {
31598
+ throw new Error("Reconcile the pending share update before binding a sign-in");
31599
+ }
31600
+ let shareVersion2;
31601
+ const retry = pending.rebind?.attempted ?? false;
31602
+ if (retry && pending.rebind) {
31603
+ const current = await fetchAuthShareRaw(
31604
+ serverUrl,
31605
+ params.token,
31606
+ params.providerType,
31607
+ void 0,
31608
+ tenantId
31609
+ );
31610
+ const authShare = authShareToString(current?.authShare);
31611
+ if (authShare && current?.shareVersion != null) {
31612
+ const health = await verifyStoredShares(
31613
+ {
31614
+ getDevice: /* @__PURE__ */ __name(async () => pending.rebind.shares.deviceShare, "getDevice"),
31615
+ getAuth: /* @__PURE__ */ __name(async () => authShare, "getAuth")
31616
+ },
31617
+ pending.primaryDid,
31618
+ async (key) => key === pending.privateKey ? pending.primaryDid : ""
31619
+ );
31620
+ if (health.healthy) shareVersion2 = current.shareVersion;
31621
+ }
31622
+ }
31623
+ assertActive();
31624
+ if (shareVersion2 === void 0) {
31625
+ const didAuthVp = await requestFreshDidAuthVp(
31626
+ serverUrl,
31627
+ pending.privateKey,
31628
+ pending.primaryDid,
31629
+ signDidAuthVp,
31630
+ tenantId
31631
+ );
31632
+ assertActive();
31633
+ pending.rebind ??= {
31634
+ shares: (await splitAndVerify(pending.privateKey)).shares,
31635
+ storageId,
31636
+ attempted: false
31637
+ };
31638
+ const rebind = pending.rebind;
31639
+ if (!candidates.some((candidate) => candidate.share === rebind.shares.deviceShare)) {
31640
+ await addPendingShareCandidate(
31641
+ storage2,
31642
+ { share: rebind.shares.deviceShare, createdAt: Date.now() },
31643
+ storageId
31644
+ );
31645
+ }
31646
+ assertActive();
31647
+ rebind.attempted = true;
31648
+ try {
31649
+ const response = await postJson(
31650
+ `${serverUrl}/keys/recovery-session/rebind`,
31651
+ {
31652
+ recoverySessionToken: pending.rebindSessionToken,
31653
+ providerType: params.providerType,
31654
+ primaryDid: pending.primaryDid,
31655
+ authShare: {
31656
+ encryptedData: rebind.shares.authShare,
31657
+ encryptedDek: "",
31658
+ iv: ""
31659
+ }
31660
+ },
31661
+ {
31662
+ ...buildHeaders("", didAuthVp, tenantId),
31663
+ "X-Auth-Token": params.token
31664
+ }
31665
+ );
31666
+ shareVersion2 = response.shareVersion;
31667
+ } catch (error) {
31668
+ if (error instanceof ShareWriteRejectedError && !retry) {
31669
+ await removePendingShareCandidate(
31670
+ storage2,
31671
+ rebind.shares.deviceShare,
31672
+ storageId
31673
+ );
31674
+ pending.rebind = void 0;
31675
+ }
31676
+ throw error;
31677
+ }
31678
+ }
31679
+ if (shareVersion2 === void 0 || !pending.rebind) {
31680
+ throw new Error("Server did not confirm the new share");
31681
+ }
31682
+ assertActive();
31683
+ const shares2 = pending.rebind.shares;
31684
+ await storage2.storeDeviceShare(shares2.deviceShare, storageId);
31685
+ await storage2.storeShareVersion(shareVersion2, storageId);
31686
+ await clearPendingShareCandidates(storage2, storageId);
31687
+ assertActive();
31688
+ lastEmailShare = shares2.emailShare;
31689
+ lastShareVersion = shareVersion2;
31690
+ lastServerSnapshot = {
31691
+ currentVersion: shareVersion2,
31692
+ resolvedVersion: shareVersion2,
31693
+ authShare: shares2.authShare,
31694
+ primaryDid: pending.primaryDid
31695
+ };
31696
+ pendingIdentityRecovery = void 0;
31697
+ return { shares: shares2, shareVersion: shareVersion2 };
31698
+ })
31699
+ );
31700
+ const enrolled = pending.fromEscrow && await tryEnrollEscrow(
31701
+ params.token,
31702
+ params.providerType,
31703
+ pending.privateKey,
31704
+ pending.primaryDid,
31705
+ shares,
31706
+ shareVersion,
31707
+ signDidAuthVp,
31708
+ void 0,
31709
+ void 0,
31710
+ true,
31711
+ storage2.assertCurrent,
31712
+ params.options?.recoveryEmail,
31713
+ params.options?.recoveryLanguage
31714
+ );
31715
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
31716
+ throw new Error("This recovery request was cancelled.");
31717
+ }
31718
+ if (!enrolled) {
31719
+ unenrolledRotation = {
31720
+ shares,
31721
+ shareVersion,
31722
+ primaryDid: pending.primaryDid,
31723
+ clearPin: true
31724
+ };
31725
+ }
31726
+ return {
31727
+ privateKey: pending.privateKey,
31728
+ did: pending.primaryDid,
31729
+ escrowEnrolled: enrolled,
31730
+ escrowRecovery: pending.fromEscrow
31731
+ };
31732
+ }, "completeIdentityRecoveryWith");
31294
31733
  return {
31295
31734
  name: "sss",
31296
31735
  capabilities: {
@@ -31592,6 +32031,8 @@ function createSSSStrategy(config) {
31592
32031
  escrowOptedOut: data.escrowOptedOut === true,
31593
32032
  escrowPin: data.escrowPin,
31594
32033
  escrowStale: data.escrowStale,
32034
+ ...typeof data.escrowBlobIdentity === "string" ? { escrowBlobIdentity: data.escrowBlobIdentity } : {},
32035
+ ...data.escrowUpdating === true ? { escrowUpdating: true } : {},
31595
32036
  sssActivationState: data.sssActivationState ?? "active",
31596
32037
  ...typeof data.priorEscrowEnrollment === "boolean" ? { priorEscrowEnrollment: data.priorEscrowEnrollment } : {}
31597
32038
  };
@@ -31966,9 +32407,20 @@ function createSSSStrategy(config) {
31966
32407
  if (input.method === "escrow" || input.method === "escrow-pin") {
31967
32408
  if (!didFromPrivateKey)
31968
32409
  throw new Error("DID verification is required for escrow recovery");
31969
- const start = input.method === "escrow-pin" ? await this.startEscrowRecovery({
32410
+ if (input.method === "escrow-pin" && !token && !input.recoverySessionToken)
32411
+ throw new Error("PIN recovery requires signing in first.");
32412
+ const pinBefore = input.method === "escrow-pin" && token ? await fetchAuthShareRaw(
32413
+ serverUrl,
31970
32414
  token,
31971
32415
  providerType,
32416
+ void 0,
32417
+ tenantId
32418
+ ).then(
32419
+ (current) => current?.escrowPin,
32420
+ () => void 0
32421
+ ) : void 0;
32422
+ const start = input.method === "escrow-pin" ? await this.startEscrowRecovery({
32423
+ ...token ? { token, providerType } : { recoverySessionToken: input.recoverySessionToken },
31972
32424
  expectedDid: input.expectedDid,
31973
32425
  options: { releasePolicy: "pin" }
31974
32426
  }) : input;
@@ -32020,16 +32472,38 @@ function createSSSStrategy(config) {
32020
32472
  "Recovery produced an incorrect key. Try another recovery method."
32021
32473
  );
32022
32474
  }
32475
+ if (input.method === "escrow-pin" && token) {
32476
+ return completePinSignInKeep(
32477
+ { token, providerType, signDidAuthVp, options: params.options },
32478
+ {
32479
+ privateKey: privateKey2,
32480
+ primaryDid: material.primaryDid,
32481
+ recoveryShare: plaintext.recoveryShare,
32482
+ authShare,
32483
+ shareVersion: material.shareVersion
32484
+ },
32485
+ input.pin,
32486
+ pinBefore,
32487
+ input.pinAttempts
32488
+ );
32489
+ }
32023
32490
  pendingIdentityRecovery = {
32024
32491
  privateKey: privateKey2,
32025
32492
  primaryDid: material.primaryDid,
32026
32493
  recoveryShare: plaintext.recoveryShare,
32027
32494
  authShare,
32028
32495
  rebindSessionToken: material.rebindSessionToken,
32029
- fromEscrow: true
32496
+ fromEscrow: true,
32497
+ // The PIN itself is not kept across the new-login step.
32498
+ ...input.method === "escrow-pin" ? {
32499
+ pinKeep: {
32500
+ shareVersion: material.shareVersion,
32501
+ ...input.pinAttempts ? { pinAttempts: { ...input.pinAttempts } } : {}
32502
+ }
32503
+ } : {}
32030
32504
  };
32031
32505
  if (!token) return { privateKey: privateKey2, did: material.primaryDid };
32032
- return this.completeIdentityRecovery({
32506
+ return completeIdentityRecoveryWith({
32033
32507
  token,
32034
32508
  providerType,
32035
32509
  signDidAuthVp,
@@ -32604,125 +33078,128 @@ function createSSSStrategy(config) {
32604
33078
  pendingIdentityRecovery = void 0;
32605
33079
  },
32606
33080
  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");
33081
+ return completeIdentityRecoveryWith(params);
33082
+ },
33083
+ /**
33084
+ * "Secure my account": one atomic server update re-splits the key and drops all
33085
+ * older auth share versions, signing out other devices and turning off the old
33086
+ * phrase/backup/passkey/email key. Staged through the pending-candidate slot, so
33087
+ * a lost acknowledgement is promoted by the usual reconciliation. Escrow is then
33088
+ * re-enrolled with `pin` (fresh salt) or, for `null`, without a PIN. The PIN is
33089
+ * used in this call only and never kept in retry state.
33090
+ */
33091
+ async revokeOtherDevices(params) {
33092
+ const { token, providerType, privateKey, did, signDidAuthVp, pin } = params;
33093
+ if (!signDidAuthVp) throw new Error("DID proof signing is required");
33094
+ if (pin !== null) {
33095
+ const validation = validatePin(pin);
33096
+ if (!validation.ok)
33097
+ throw new Error(
33098
+ validation.reason === "length" ? "PIN must contain 6\u201312 digits." : "Choose a PIN without trivial or sequential patterns."
33099
+ );
32614
33100
  }
33101
+ const storage2 = guardedStorage();
32615
33102
  const storageId = activeStorageId;
33103
+ const assertUser = captureActiveUserGuard();
33104
+ const assertActive = /* @__PURE__ */ __name(() => {
33105
+ assertUser();
33106
+ storage2.assertCurrent();
33107
+ }, "assertActive");
33108
+ const before = await fetchAuthShareRaw(
33109
+ serverUrl,
33110
+ token,
33111
+ providerType,
33112
+ void 0,
33113
+ tenantId
33114
+ );
33115
+ if (!before || before.primaryDid !== did) {
33116
+ throw new Error("This account could not be found. Sign in again.");
33117
+ }
33118
+ const hadEscrow = !before.escrowOptedOut && (before.priorEscrowEnrollment === true || Array.isArray(before.recoveryMethods) && before.recoveryMethods.some(
33119
+ (method) => method?.type === "escrow"
33120
+ ));
33121
+ const didFromKey = /* @__PURE__ */ __name(async (key) => key === privateKey ? did : `${did}:mismatch`, "didFromKey");
32616
33122
  const { shares, shareVersion } = await withShareUpdateLock(
32617
33123
  storage2,
32618
33124
  storageId,
32619
33125
  () => 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 : ""
33126
+ const { snapshot } = await resolvePendingShareCandidates(
33127
+ storage2,
33128
+ storageId,
33129
+ async () => {
33130
+ const data = await fetchAuthShareRaw(
33131
+ serverUrl,
33132
+ token,
33133
+ providerType,
33134
+ void 0,
33135
+ tenantId
32656
33136
  );
32657
- if (health.healthy) shareVersion2 = current.shareVersion;
32658
- }
33137
+ return {
33138
+ currentVersion: data?.shareVersion ?? null,
33139
+ authShare: authShareToString(data?.authShare)
33140
+ };
33141
+ },
33142
+ did,
33143
+ didFromKey,
33144
+ assertActive,
33145
+ recordPromotion(did),
33146
+ async (version) => authShareToString(
33147
+ (await fetchAuthShareRaw(
33148
+ serverUrl,
33149
+ token,
33150
+ providerType,
33151
+ version,
33152
+ tenantId
33153
+ ))?.authShare
33154
+ )
33155
+ );
33156
+ if (!snapshot.currentVersion) {
33157
+ throw new Error("This account could not be found. Sign in again.");
32659
33158
  }
33159
+ const expectedShareVersion = snapshot.currentVersion;
33160
+ const didAuthVp = await requestFreshDidAuthVp(
33161
+ serverUrl,
33162
+ privateKey,
33163
+ did,
33164
+ signDidAuthVp,
33165
+ tenantId
33166
+ );
32660
33167
  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
- )) {
33168
+ let shareVersion2;
33169
+ let stagedShare = "";
33170
+ const shares2 = await atomicShareUpdate(privateKey, {
33171
+ storeDevice: /* @__PURE__ */ __name(async (share) => {
33172
+ assertActive();
33173
+ stagedShare = share;
32679
33174
  await addPendingShareCandidate(
32680
33175
  storage2,
32681
- { share: rebind.shares.deviceShare, createdAt: Date.now() },
33176
+ { share, createdAt: Date.now() },
32682
33177
  storageId
32683
33178
  );
32684
- }
32685
- assertActive();
32686
- rebind.attempted = true;
32687
- try {
33179
+ }, "storeDevice"),
33180
+ clearDevice: /* @__PURE__ */ __name(() => removePendingShareCandidate(storage2, stagedShare, storageId), "clearDevice"),
33181
+ storeAuth: /* @__PURE__ */ __name(async (share) => {
33182
+ assertActive();
32688
33183
  const response = await postJson(
32689
- `${serverUrl}/keys/recovery-session/rebind`,
33184
+ `${serverUrl}/keys/revoke-other-devices`,
32690
33185
  {
32691
- recoverySessionToken: pending.rebindSessionToken,
32692
- providerType: params.providerType,
32693
- primaryDid: pending.primaryDid,
32694
- authShare: {
32695
- encryptedData: rebind.shares.authShare,
32696
- encryptedDek: "",
32697
- iv: ""
32698
- }
33186
+ authToken: token,
33187
+ providerType,
33188
+ primaryDid: did,
33189
+ authShare: { encryptedData: share, encryptedDek: "", iv: "" },
33190
+ expectedShareVersion
32699
33191
  },
32700
- {
32701
- ...buildHeaders("", didAuthVp, tenantId),
32702
- "X-Auth-Token": params.token
32703
- }
33192
+ buildHeaders(token, didAuthVp, tenantId)
32704
33193
  );
32705
33194
  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");
33195
+ }, "storeAuth")
33196
+ });
33197
+ if (shareVersion2 === void 0) {
33198
+ throw new Error("Server did not return a share version");
32720
33199
  }
32721
- assertActive();
32722
- const shares2 = pending.rebind.shares;
32723
33200
  await storage2.storeDeviceShare(shares2.deviceShare, storageId);
32724
33201
  await storage2.storeShareVersion(shareVersion2, storageId);
32725
- await clearPendingShareCandidates(storage2, storageId);
33202
+ await removePendingShareCandidate(storage2, shares2.deviceShare, storageId);
32726
33203
  assertActive();
32727
33204
  lastEmailShare = shares2.emailShare;
32728
33205
  lastShareVersion = shareVersion2;
@@ -32730,44 +33207,36 @@ function createSSSStrategy(config) {
32730
33207
  currentVersion: shareVersion2,
32731
33208
  resolvedVersion: shareVersion2,
32732
33209
  authShare: shares2.authShare,
32733
- primaryDid: pending.primaryDid
33210
+ primaryDid: did
32734
33211
  };
32735
- pendingIdentityRecovery = void 0;
32736
33212
  return { shares: shares2, shareVersion: shareVersion2 };
32737
33213
  })
32738
33214
  );
32739
- const enrolled = pending.fromEscrow && await tryEnrollEscrow(
32740
- params.token,
32741
- params.providerType,
32742
- pending.privateKey,
32743
- pending.primaryDid,
33215
+ if (!hadEscrow || !config.escrow?.enabled) {
33216
+ return { shareVersion, escrowEnrolled: false, pinKept: false };
33217
+ }
33218
+ const pinMaterial = pin === null ? void 0 : await freshPinMaterial(pin);
33219
+ const enrollArgs = [
33220
+ token,
33221
+ providerType,
33222
+ privateKey,
33223
+ did,
32744
33224
  shares,
32745
33225
  shareVersion,
32746
33226
  signDidAuthVp,
32747
33227
  void 0,
32748
- void 0,
32749
- true,
33228
+ pinMaterial,
33229
+ !pinMaterial,
32750
33230
  storage2.assertCurrent,
32751
33231
  params.options?.recoveryEmail,
32752
33232
  params.options?.recoveryLanguage
32753
- );
32754
- if (activeStorageId !== storageId || storageGeneration !== generation) {
32755
- throw new Error("This recovery request was cancelled.");
32756
- }
33233
+ ];
33234
+ const attempt = await attemptEnrollEscrow(...enrollArgs);
33235
+ const enrolled = attempt.enrolled || attempt.error instanceof EscrowRequestError && attempt.error.status === 403 && await tryEnrollEscrow(...enrollArgs);
32757
33236
  if (!enrolled) {
32758
- unenrolledRotation = {
32759
- shares,
32760
- shareVersion,
32761
- primaryDid: pending.primaryDid,
32762
- clearPin: true
32763
- };
33237
+ unenrolledRotation = { shares, shareVersion, primaryDid: did, clearPin: true };
32764
33238
  }
32765
- return {
32766
- privateKey: pending.privateKey,
32767
- did: pending.primaryDid,
32768
- escrowEnrolled: enrolled,
32769
- escrowRecovery: pending.fromEscrow
32770
- };
33239
+ return { shareVersion, escrowEnrolled: enrolled, pinKept: enrolled && !!pinMaterial };
32771
33240
  },
32772
33241
  // --- Contact method management ---
32773
33242
  async upgradeContactMethod(token, providerType, previousPhone, email, code) {