@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.
@@ -24025,7 +24025,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
24025
24025
  state: external_exports.enum(["none", "enabled", "locked", "stale"]),
24026
24026
  enabled: external_exports.boolean(),
24027
24027
  attemptsRemaining: external_exports.number().int().nonnegative(),
24028
- salt: external_exports.string().optional()
24028
+ salt: external_exports.string().optional(),
24029
+ /**
24030
+ * Verified wrong PIN entries against the current PIN (successful releases and
24031
+ * unresolved reservations are not counted). Omitted when the server cannot tell,
24032
+ * e.g. legacy records that predate the verified counter.
24033
+ */
24034
+ wrongAttempts: external_exports.number().int().nonnegative().optional()
24029
24035
  });
24030
24036
  var BITSTRING_STATUS_PURPOSES = ["revocation", "suspension"];
24031
24037
  var DEFAULT_BITSTRING_STATUS_LIST_SIZE = 131072;
@@ -27532,6 +27538,15 @@ var EscrowPinLockedError = class extends Error {
27532
27538
  this.name = "EscrowPinLockedError";
27533
27539
  }
27534
27540
  };
27541
+ var PinSignInStaleError = class extends Error {
27542
+ static {
27543
+ __name(this, "PinSignInStaleError");
27544
+ }
27545
+ constructor() {
27546
+ super("Your account changed while signing in. Try again.");
27547
+ this.name = "PinSignInStaleError";
27548
+ }
27549
+ };
27535
27550
 
27536
27551
  // src/escrow-pin.ts
27537
27552
  var PIN_MIN_LENGTH = 6;
@@ -28394,7 +28409,7 @@ function createSSSStrategy(config) {
28394
28409
  assertCurrent();
28395
28410
  return result;
28396
28411
  }, "fetchAuthorityState");
28397
- const enrollEscrow = /* @__PURE__ */ __name(async (token, providerType, privateKey, primaryDid, shares, shareVersion, signDidAuthVp, verifiedKey, pinMaterial, clearPin = false, assertCurrent = guardedStorage().assertCurrent, recoveryEmail = "", recoveryLanguage = "") => {
28412
+ const enrollEscrow = /* @__PURE__ */ __name(async (token, providerType, privateKey, primaryDid, shares, shareVersion, signDidAuthVp, verifiedKey, pinMaterial, clearPin = false, assertCurrent = guardedStorage().assertCurrent, recoveryEmail = "", recoveryLanguage = "", expectedPinCharges) => {
28398
28413
  if (!config.escrow?.enabled) throw new Error("Escrow enrollment is disabled");
28399
28414
  if (!signDidAuthVp) throw new Error("DID proof signing is required for escrow enrollment");
28400
28415
  const identity = verifiedKey ?? await fetchVerifiedEnclaveKey();
@@ -28453,7 +28468,8 @@ function createSSSStrategy(config) {
28453
28468
  enclaveKeyId: keyId,
28454
28469
  ...pinMaterial ? { pinSalt: pinMaterial.pinSalt } : {},
28455
28470
  ...(signedIntent ? signedIntent.intent.pinAction === "clear" : clearPin) ? { clearPin: true } : {},
28456
- ...signedIntent ? { signedIntent, sourceEnvelope: authority?.envelope ?? null } : {}
28471
+ ...signedIntent ? { signedIntent, sourceEnvelope: authority?.envelope ?? null } : {},
28472
+ ...expectedPinCharges ? { expectedPinCharges } : {}
28457
28473
  })
28458
28474
  });
28459
28475
  if (signedIntent) {
@@ -28466,11 +28482,11 @@ function createSSSStrategy(config) {
28466
28482
  verifiedEscrowEnvelope = committed.envelope ?? void 0;
28467
28483
  }
28468
28484
  }, "enrollEscrow");
28469
- const tryEnrollEscrow = /* @__PURE__ */ __name(async (...args) => {
28470
- if (!config.escrow?.enabled) return false;
28485
+ const attemptEnrollEscrow = /* @__PURE__ */ __name(async (...args) => {
28486
+ if (!config.escrow?.enabled) return { enrolled: false };
28471
28487
  try {
28472
28488
  await enrollEscrow(...args);
28473
- return true;
28489
+ return { enrolled: true };
28474
28490
  } catch (err) {
28475
28491
  try {
28476
28492
  if (config.onEscrowError) config.onEscrowError(err);
@@ -28478,9 +28494,10 @@ function createSSSStrategy(config) {
28478
28494
  } catch {
28479
28495
  console.warn("SSS: escrow error reporting failed");
28480
28496
  }
28481
- return false;
28497
+ return { enrolled: false, error: err };
28482
28498
  }
28483
- }, "tryEnrollEscrow");
28499
+ }, "attemptEnrollEscrow");
28500
+ const tryEnrollEscrow = /* @__PURE__ */ __name(async (...args) => (await attemptEnrollEscrow(...args)).enrolled, "tryEnrollEscrow");
28484
28501
  const withAutoClear = /* @__PURE__ */ __name((shareVersion, autoClearedFrom) => {
28485
28502
  if (!autoClearedFrom) return { enrolled: true, changed: true, shareVersion };
28486
28503
  try {
@@ -28541,7 +28558,22 @@ function createSSSStrategy(config) {
28541
28558
  clearPinForEnrollment ||= current.hasPin && pin === void 0;
28542
28559
  }
28543
28560
  currentlyEnrolled = currentlyEnrolled && current.status === "active" && current.shareVersion === status.shareVersion && current.envelope?.keyId === verifiedKey.keyId;
28561
+ let identityMismatch = false;
28562
+ if (currentlyEnrolled && current.envelope && status.escrowBlobIdentity !== void 0) {
28563
+ const authorityIdentity = await getEscrowBlobIdentity(current.envelope);
28564
+ if (authorityIdentity !== status.escrowBlobIdentity) {
28565
+ const reread = await self.fetchServerKeyStatus(
28566
+ params.token,
28567
+ params.providerType
28568
+ );
28569
+ storage2.assertCurrent();
28570
+ identityMismatch = reread.escrowBlobIdentity !== authorityIdentity && reread.escrowUpdating !== true;
28571
+ }
28572
+ }
28573
+ if (identityMismatch) currentlyEnrolled = false;
28544
28574
  if (currentlyEnrolled) verifiedEscrowEnvelope = current.envelope ?? void 0;
28575
+ if (identityMismatch && current.hasPin && pin === void 0 && !clearPinForEnrollment)
28576
+ return { enrolled: false, reason: "pin-reset-required" };
28545
28577
  hasPin = current.hasPin;
28546
28578
  const uncarryablePin = !clearPinForEnrollment && current.status === "active" && current.hasPin && pin === void 0 && !currentlyEnrolled && status.escrowPin?.state === "none";
28547
28579
  if (uncarryablePin && (config.autoClearUncarryablePin ?? DEFAULT_AUTO_CLEAR_UNCARRYABLE_PIN) && current.shareVersion !== null && status.shareVersion !== null && current.shareVersion < status.shareVersion && !status.recoveryMethods.some(
@@ -28669,6 +28701,378 @@ function createSSSStrategy(config) {
28669
28701
  }
28670
28702
  };
28671
28703
  }, "captureActiveUserGuard");
28704
+ const pinHasRecordedWrongAttempts = /* @__PURE__ */ __name((escrowPin) => {
28705
+ const status = escrowPin;
28706
+ if (status?.state === "none") return false;
28707
+ return typeof status?.wrongAttempts !== "number" || status.wrongAttempts > 0;
28708
+ }, "pinHasRecordedWrongAttempts");
28709
+ const refreshablePinCharges = /* @__PURE__ */ __name((before, after) => {
28710
+ const pre = before;
28711
+ const post = after;
28712
+ if (pinHasRecordedWrongAttempts(before) || pinHasRecordedWrongAttempts(after))
28713
+ return void 0;
28714
+ if (pre?.state !== "enabled" || post?.state !== "enabled") return void 0;
28715
+ if (typeof pre.attemptsRemaining !== "number" || typeof post.attemptsRemaining !== "number")
28716
+ return void 0;
28717
+ if (post.attemptsRemaining !== pre.attemptsRemaining - 1) return void 0;
28718
+ return { attemptsRemaining: post.attemptsRemaining, wrongAttempts: 0 };
28719
+ }, "refreshablePinCharges");
28720
+ const freshPinMaterial = /* @__PURE__ */ __name(async (pin) => {
28721
+ if (!validatePin(pin).ok) return void 0;
28722
+ try {
28723
+ const pinSalt = generatePinSalt();
28724
+ return { pinSalt, pinVerifier: await derivePinProof(pin, pinSalt) };
28725
+ } catch {
28726
+ return void 0;
28727
+ }
28728
+ }, "freshPinMaterial");
28729
+ const adoptReleasedShare = /* @__PURE__ */ __name(async (storage2, storageId, released, shareVersion, currentVersion) => {
28730
+ await storage2.storeDeviceShare(released.recoveryShare, storageId);
28731
+ await storage2.storeShareVersion(shareVersion, storageId);
28732
+ lastEmailShare = void 0;
28733
+ lastShareVersion = shareVersion;
28734
+ lastServerSnapshot = {
28735
+ currentVersion,
28736
+ resolvedVersion: shareVersion,
28737
+ authShare: released.authShare,
28738
+ primaryDid: released.primaryDid
28739
+ };
28740
+ }, "adoptReleasedShare");
28741
+ const completePinSignInKeep = /* @__PURE__ */ __name(async (params, released, pin, pinBefore) => {
28742
+ const storage2 = guardedStorage();
28743
+ const generation = storageGeneration;
28744
+ const storageId = activeStorageId;
28745
+ const assertActive = /* @__PURE__ */ __name(() => {
28746
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
28747
+ throw new Error("This recovery request was cancelled.");
28748
+ }
28749
+ }, "assertActive");
28750
+ const { mode, newPinRequired, refreshCharges } = await withShareUpdateLock(
28751
+ storage2,
28752
+ storageId,
28753
+ () => withRotationLock(async () => {
28754
+ assertActive();
28755
+ const current = await fetchAuthShareRaw(
28756
+ serverUrl,
28757
+ params.token,
28758
+ params.providerType,
28759
+ void 0,
28760
+ tenantId
28761
+ );
28762
+ assertActive();
28763
+ if (!current || current.primaryDid !== released.primaryDid || typeof current.shareVersion !== "number") {
28764
+ throw new PinSignInStaleError();
28765
+ }
28766
+ let mode2;
28767
+ if (current.shareVersion === released.shareVersion) {
28768
+ if (authShareToString(current.authShare) !== released.authShare) {
28769
+ throw new PinSignInStaleError();
28770
+ }
28771
+ mode2 = "kept";
28772
+ } else {
28773
+ const historical = current.shareVersion > released.shareVersion ? await fetchAuthShareRaw(
28774
+ serverUrl,
28775
+ params.token,
28776
+ params.providerType,
28777
+ released.shareVersion,
28778
+ tenantId
28779
+ ) : null;
28780
+ if (authShareToString(historical?.authShare) !== released.authShare) {
28781
+ throw new PinSignInStaleError();
28782
+ }
28783
+ mode2 = "deferred-rotation";
28784
+ }
28785
+ assertActive();
28786
+ await adoptReleasedShare(
28787
+ storage2,
28788
+ storageId,
28789
+ released,
28790
+ released.shareVersion,
28791
+ current.shareVersion
28792
+ );
28793
+ pendingIdentityRecovery = void 0;
28794
+ return {
28795
+ mode: mode2,
28796
+ newPinRequired: pinHasRecordedWrongAttempts(pinBefore) || pinHasRecordedWrongAttempts(current.escrowPin),
28797
+ refreshCharges: refreshablePinCharges(pinBefore, current.escrowPin)
28798
+ };
28799
+ })
28800
+ );
28801
+ let pinRefreshed = false;
28802
+ let pinRefreshFailed = false;
28803
+ if (mode === "kept" && !newPinRequired && refreshCharges && params.signDidAuthVp) {
28804
+ const pinMaterial = await freshPinMaterial(pin);
28805
+ pinRefreshed = pinMaterial !== void 0 && await tryEnrollEscrow(
28806
+ params.token,
28807
+ params.providerType,
28808
+ released.privateKey,
28809
+ released.primaryDid,
28810
+ { recoveryShare: released.recoveryShare },
28811
+ released.shareVersion,
28812
+ params.signDidAuthVp,
28813
+ void 0,
28814
+ pinMaterial,
28815
+ false,
28816
+ storage2.assertCurrent,
28817
+ params.options?.recoveryEmail,
28818
+ params.options?.recoveryLanguage,
28819
+ refreshCharges
28820
+ );
28821
+ pinRefreshFailed = pinMaterial !== void 0 && !pinRefreshed;
28822
+ }
28823
+ assertActive();
28824
+ return {
28825
+ privateKey: released.privateKey,
28826
+ did: released.primaryDid,
28827
+ escrowEnrolled: true,
28828
+ escrowRecovery: true,
28829
+ pinSignIn: {
28830
+ mode,
28831
+ pinRefreshed,
28832
+ newPinRequired,
28833
+ ...pinRefreshFailed ? { pinRefreshFailed: true } : {}
28834
+ }
28835
+ };
28836
+ }, "completePinSignInKeep");
28837
+ const completePinSignInRelink = /* @__PURE__ */ __name(async (params, pending, signDidAuthVp) => {
28838
+ const storage2 = guardedStorage();
28839
+ const generation = storageGeneration;
28840
+ const storageId = activeStorageId;
28841
+ const shareVersion = pending.pinKeep.shareVersion;
28842
+ const assertActive = /* @__PURE__ */ __name(() => {
28843
+ if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
28844
+ throw new Error("This recovery request was cancelled.");
28845
+ }
28846
+ }, "assertActive");
28847
+ await withShareUpdateLock(
28848
+ storage2,
28849
+ storageId,
28850
+ () => withRotationLock(async () => {
28851
+ assertActive();
28852
+ const didAuthVp = await requestFreshDidAuthVp(
28853
+ serverUrl,
28854
+ pending.privateKey,
28855
+ pending.primaryDid,
28856
+ signDidAuthVp,
28857
+ tenantId
28858
+ );
28859
+ assertActive();
28860
+ try {
28861
+ const response = await postJson(
28862
+ `${serverUrl}/keys/recovery-session/pin-sign-in`,
28863
+ {
28864
+ recoverySessionToken: pending.rebindSessionToken,
28865
+ providerType: params.providerType,
28866
+ primaryDid: pending.primaryDid
28867
+ },
28868
+ {
28869
+ ...buildHeaders("", didAuthVp, tenantId),
28870
+ "X-Auth-Token": params.token
28871
+ }
28872
+ );
28873
+ if (response.shareVersion !== shareVersion) throw new PinSignInStaleError();
28874
+ } catch (error) {
28875
+ const current = await fetchAuthShareRaw(
28876
+ serverUrl,
28877
+ params.token,
28878
+ params.providerType,
28879
+ void 0,
28880
+ tenantId
28881
+ ).catch(() => null);
28882
+ if (!current || current.primaryDid !== pending.primaryDid || current.shareVersion !== shareVersion || authShareToString(current.authShare) !== pending.authShare) {
28883
+ throw error;
28884
+ }
28885
+ }
28886
+ assertActive();
28887
+ await adoptReleasedShare(storage2, storageId, pending, shareVersion, shareVersion);
28888
+ pendingIdentityRecovery = void 0;
28889
+ })
28890
+ );
28891
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
28892
+ throw new Error("This recovery request was cancelled.");
28893
+ }
28894
+ const status = await fetchAuthShareRaw(
28895
+ serverUrl,
28896
+ params.token,
28897
+ params.providerType,
28898
+ void 0,
28899
+ tenantId
28900
+ ).catch(() => null);
28901
+ return {
28902
+ privateKey: pending.privateKey,
28903
+ did: pending.primaryDid,
28904
+ escrowEnrolled: true,
28905
+ escrowRecovery: true,
28906
+ pinSignIn: {
28907
+ mode: "relinked",
28908
+ pinRefreshed: false,
28909
+ newPinRequired: pinHasRecordedWrongAttempts(status?.escrowPin)
28910
+ }
28911
+ };
28912
+ }, "completePinSignInRelink");
28913
+ const completeIdentityRecoveryWith = /* @__PURE__ */ __name(async (params) => {
28914
+ const storage2 = guardedStorage();
28915
+ const generation = storageGeneration;
28916
+ const pending = pendingIdentityRecovery;
28917
+ const signDidAuthVp = params.signDidAuthVp;
28918
+ if (!pending) throw new Error("No identity recovery is waiting for a new sign-in");
28919
+ if (!signDidAuthVp) {
28920
+ throw new Error("DID proof signing is required to bind a new sign-in");
28921
+ }
28922
+ if (pending.pinKeep) return completePinSignInRelink(params, pending, signDidAuthVp);
28923
+ const storageId = activeStorageId;
28924
+ const { shares, shareVersion } = await withShareUpdateLock(
28925
+ storage2,
28926
+ storageId,
28927
+ () => withRotationLock(async () => {
28928
+ const assertActive = /* @__PURE__ */ __name(() => {
28929
+ if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
28930
+ throw new Error("This recovery request was cancelled.");
28931
+ }
28932
+ }, "assertActive");
28933
+ assertActive();
28934
+ const candidates = await readPendingShareCandidates(storage2, storageId);
28935
+ if (pending.rebind && pending.rebind.storageId !== storageId) {
28936
+ throw new Error("Active account changed during identity recovery");
28937
+ }
28938
+ if (candidates.some(
28939
+ (candidate) => candidate.share !== pending.rebind?.shares.deviceShare
28940
+ )) {
28941
+ throw new Error("Reconcile the pending share update before binding a sign-in");
28942
+ }
28943
+ let shareVersion2;
28944
+ const retry = pending.rebind?.attempted ?? false;
28945
+ if (retry && pending.rebind) {
28946
+ const current = await fetchAuthShareRaw(
28947
+ serverUrl,
28948
+ params.token,
28949
+ params.providerType,
28950
+ void 0,
28951
+ tenantId
28952
+ );
28953
+ const authShare = authShareToString(current?.authShare);
28954
+ if (authShare && current?.shareVersion != null) {
28955
+ const health = await verifyStoredShares(
28956
+ {
28957
+ getDevice: /* @__PURE__ */ __name(async () => pending.rebind.shares.deviceShare, "getDevice"),
28958
+ getAuth: /* @__PURE__ */ __name(async () => authShare, "getAuth")
28959
+ },
28960
+ pending.primaryDid,
28961
+ async (key) => key === pending.privateKey ? pending.primaryDid : ""
28962
+ );
28963
+ if (health.healthy) shareVersion2 = current.shareVersion;
28964
+ }
28965
+ }
28966
+ assertActive();
28967
+ if (shareVersion2 === void 0) {
28968
+ const didAuthVp = await requestFreshDidAuthVp(
28969
+ serverUrl,
28970
+ pending.privateKey,
28971
+ pending.primaryDid,
28972
+ signDidAuthVp,
28973
+ tenantId
28974
+ );
28975
+ assertActive();
28976
+ pending.rebind ??= {
28977
+ shares: (await splitAndVerify(pending.privateKey)).shares,
28978
+ storageId,
28979
+ attempted: false
28980
+ };
28981
+ const rebind = pending.rebind;
28982
+ if (!candidates.some((candidate) => candidate.share === rebind.shares.deviceShare)) {
28983
+ await addPendingShareCandidate(
28984
+ storage2,
28985
+ { share: rebind.shares.deviceShare, createdAt: Date.now() },
28986
+ storageId
28987
+ );
28988
+ }
28989
+ assertActive();
28990
+ rebind.attempted = true;
28991
+ try {
28992
+ const response = await postJson(
28993
+ `${serverUrl}/keys/recovery-session/rebind`,
28994
+ {
28995
+ recoverySessionToken: pending.rebindSessionToken,
28996
+ providerType: params.providerType,
28997
+ primaryDid: pending.primaryDid,
28998
+ authShare: {
28999
+ encryptedData: rebind.shares.authShare,
29000
+ encryptedDek: "",
29001
+ iv: ""
29002
+ }
29003
+ },
29004
+ {
29005
+ ...buildHeaders("", didAuthVp, tenantId),
29006
+ "X-Auth-Token": params.token
29007
+ }
29008
+ );
29009
+ shareVersion2 = response.shareVersion;
29010
+ } catch (error) {
29011
+ if (error instanceof ShareWriteRejectedError && !retry) {
29012
+ await removePendingShareCandidate(
29013
+ storage2,
29014
+ rebind.shares.deviceShare,
29015
+ storageId
29016
+ );
29017
+ pending.rebind = void 0;
29018
+ }
29019
+ throw error;
29020
+ }
29021
+ }
29022
+ if (shareVersion2 === void 0 || !pending.rebind) {
29023
+ throw new Error("Server did not confirm the new share");
29024
+ }
29025
+ assertActive();
29026
+ const shares2 = pending.rebind.shares;
29027
+ await storage2.storeDeviceShare(shares2.deviceShare, storageId);
29028
+ await storage2.storeShareVersion(shareVersion2, storageId);
29029
+ await clearPendingShareCandidates(storage2, storageId);
29030
+ assertActive();
29031
+ lastEmailShare = shares2.emailShare;
29032
+ lastShareVersion = shareVersion2;
29033
+ lastServerSnapshot = {
29034
+ currentVersion: shareVersion2,
29035
+ resolvedVersion: shareVersion2,
29036
+ authShare: shares2.authShare,
29037
+ primaryDid: pending.primaryDid
29038
+ };
29039
+ pendingIdentityRecovery = void 0;
29040
+ return { shares: shares2, shareVersion: shareVersion2 };
29041
+ })
29042
+ );
29043
+ const enrolled = pending.fromEscrow && await tryEnrollEscrow(
29044
+ params.token,
29045
+ params.providerType,
29046
+ pending.privateKey,
29047
+ pending.primaryDid,
29048
+ shares,
29049
+ shareVersion,
29050
+ signDidAuthVp,
29051
+ void 0,
29052
+ void 0,
29053
+ true,
29054
+ storage2.assertCurrent,
29055
+ params.options?.recoveryEmail,
29056
+ params.options?.recoveryLanguage
29057
+ );
29058
+ if (activeStorageId !== storageId || storageGeneration !== generation) {
29059
+ throw new Error("This recovery request was cancelled.");
29060
+ }
29061
+ if (!enrolled) {
29062
+ unenrolledRotation = {
29063
+ shares,
29064
+ shareVersion,
29065
+ primaryDid: pending.primaryDid,
29066
+ clearPin: true
29067
+ };
29068
+ }
29069
+ return {
29070
+ privateKey: pending.privateKey,
29071
+ did: pending.primaryDid,
29072
+ escrowEnrolled: enrolled,
29073
+ escrowRecovery: pending.fromEscrow
29074
+ };
29075
+ }, "completeIdentityRecoveryWith");
28672
29076
  return {
28673
29077
  name: "sss",
28674
29078
  capabilities: {
@@ -28970,6 +29374,8 @@ function createSSSStrategy(config) {
28970
29374
  escrowOptedOut: data.escrowOptedOut === true,
28971
29375
  escrowPin: data.escrowPin,
28972
29376
  escrowStale: data.escrowStale,
29377
+ ...typeof data.escrowBlobIdentity === "string" ? { escrowBlobIdentity: data.escrowBlobIdentity } : {},
29378
+ ...data.escrowUpdating === true ? { escrowUpdating: true } : {},
28973
29379
  sssActivationState: data.sssActivationState ?? "active",
28974
29380
  ...typeof data.priorEscrowEnrollment === "boolean" ? { priorEscrowEnrollment: data.priorEscrowEnrollment } : {}
28975
29381
  };
@@ -29344,9 +29750,20 @@ function createSSSStrategy(config) {
29344
29750
  if (input.method === "escrow" || input.method === "escrow-pin") {
29345
29751
  if (!didFromPrivateKey)
29346
29752
  throw new Error("DID verification is required for escrow recovery");
29347
- const start = input.method === "escrow-pin" ? await this.startEscrowRecovery({
29753
+ if (input.method === "escrow-pin" && !token && !input.recoverySessionToken)
29754
+ throw new Error("PIN recovery requires signing in first.");
29755
+ const pinBefore = input.method === "escrow-pin" && token ? await fetchAuthShareRaw(
29756
+ serverUrl,
29348
29757
  token,
29349
29758
  providerType,
29759
+ void 0,
29760
+ tenantId
29761
+ ).then(
29762
+ (current) => current?.escrowPin,
29763
+ () => void 0
29764
+ ) : void 0;
29765
+ const start = input.method === "escrow-pin" ? await this.startEscrowRecovery({
29766
+ ...token ? { token, providerType } : { recoverySessionToken: input.recoverySessionToken },
29350
29767
  expectedDid: input.expectedDid,
29351
29768
  options: { releasePolicy: "pin" }
29352
29769
  }) : input;
@@ -29398,16 +29815,32 @@ function createSSSStrategy(config) {
29398
29815
  "Recovery produced an incorrect key. Try another recovery method."
29399
29816
  );
29400
29817
  }
29818
+ if (input.method === "escrow-pin" && token) {
29819
+ return completePinSignInKeep(
29820
+ { token, providerType, signDidAuthVp, options: params.options },
29821
+ {
29822
+ privateKey: privateKey2,
29823
+ primaryDid: material.primaryDid,
29824
+ recoveryShare: plaintext.recoveryShare,
29825
+ authShare,
29826
+ shareVersion: material.shareVersion
29827
+ },
29828
+ input.pin,
29829
+ pinBefore
29830
+ );
29831
+ }
29401
29832
  pendingIdentityRecovery = {
29402
29833
  privateKey: privateKey2,
29403
29834
  primaryDid: material.primaryDid,
29404
29835
  recoveryShare: plaintext.recoveryShare,
29405
29836
  authShare,
29406
29837
  rebindSessionToken: material.rebindSessionToken,
29407
- fromEscrow: true
29838
+ fromEscrow: true,
29839
+ // The PIN itself is not kept across the new-login step.
29840
+ ...input.method === "escrow-pin" ? { pinKeep: { shareVersion: material.shareVersion } } : {}
29408
29841
  };
29409
29842
  if (!token) return { privateKey: privateKey2, did: material.primaryDid };
29410
- return this.completeIdentityRecovery({
29843
+ return completeIdentityRecoveryWith({
29411
29844
  token,
29412
29845
  providerType,
29413
29846
  signDidAuthVp,
@@ -29982,125 +30415,128 @@ function createSSSStrategy(config) {
29982
30415
  pendingIdentityRecovery = void 0;
29983
30416
  },
29984
30417
  async completeIdentityRecovery(params) {
29985
- const storage2 = guardedStorage();
29986
- const generation = storageGeneration;
29987
- const pending = pendingIdentityRecovery;
29988
- const signDidAuthVp = params.signDidAuthVp;
29989
- if (!pending) throw new Error("No identity recovery is waiting for a new sign-in");
29990
- if (!signDidAuthVp) {
29991
- throw new Error("DID proof signing is required to bind a new sign-in");
30418
+ return completeIdentityRecoveryWith(params);
30419
+ },
30420
+ /**
30421
+ * "Secure my account": one atomic server update re-splits the key and drops all
30422
+ * older auth share versions, signing out other devices and turning off the old
30423
+ * phrase/backup/passkey/email key. Staged through the pending-candidate slot, so
30424
+ * a lost acknowledgement is promoted by the usual reconciliation. Escrow is then
30425
+ * re-enrolled with `pin` (fresh salt) or, for `null`, without a PIN. The PIN is
30426
+ * used in this call only and never kept in retry state.
30427
+ */
30428
+ async revokeOtherDevices(params) {
30429
+ const { token, providerType, privateKey, did, signDidAuthVp, pin } = params;
30430
+ if (!signDidAuthVp) throw new Error("DID proof signing is required");
30431
+ if (pin !== null) {
30432
+ const validation = validatePin(pin);
30433
+ if (!validation.ok)
30434
+ throw new Error(
30435
+ validation.reason === "length" ? "PIN must contain 6\u201312 digits." : "Choose a PIN without trivial or sequential patterns."
30436
+ );
29992
30437
  }
30438
+ const storage2 = guardedStorage();
29993
30439
  const storageId = activeStorageId;
30440
+ const assertUser = captureActiveUserGuard();
30441
+ const assertActive = /* @__PURE__ */ __name(() => {
30442
+ assertUser();
30443
+ storage2.assertCurrent();
30444
+ }, "assertActive");
30445
+ const before = await fetchAuthShareRaw(
30446
+ serverUrl,
30447
+ token,
30448
+ providerType,
30449
+ void 0,
30450
+ tenantId
30451
+ );
30452
+ if (!before || before.primaryDid !== did) {
30453
+ throw new Error("This account could not be found. Sign in again.");
30454
+ }
30455
+ const hadEscrow = !before.escrowOptedOut && (before.priorEscrowEnrollment === true || Array.isArray(before.recoveryMethods) && before.recoveryMethods.some(
30456
+ (method) => method?.type === "escrow"
30457
+ ));
30458
+ const didFromKey = /* @__PURE__ */ __name(async (key) => key === privateKey ? did : `${did}:mismatch`, "didFromKey");
29994
30459
  const { shares, shareVersion } = await withShareUpdateLock(
29995
30460
  storage2,
29996
30461
  storageId,
29997
30462
  () => withRotationLock(async () => {
29998
- const assertActive = /* @__PURE__ */ __name(() => {
29999
- if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
30000
- throw new Error("This recovery request was cancelled.");
30001
- }
30002
- }, "assertActive");
30003
- assertActive();
30004
- const candidates = await readPendingShareCandidates(storage2, storageId);
30005
- if (pending.rebind && pending.rebind.storageId !== storageId) {
30006
- throw new Error("Active account changed during identity recovery");
30007
- }
30008
- if (candidates.some(
30009
- (candidate) => candidate.share !== pending.rebind?.shares.deviceShare
30010
- )) {
30011
- throw new Error(
30012
- "Reconcile the pending share update before binding a sign-in"
30013
- );
30014
- }
30015
- let shareVersion2;
30016
- const retry = pending.rebind?.attempted ?? false;
30017
- if (retry && pending.rebind) {
30018
- const current = await fetchAuthShareRaw(
30019
- serverUrl,
30020
- params.token,
30021
- params.providerType,
30022
- void 0,
30023
- tenantId
30024
- );
30025
- const authShare = authShareToString(current?.authShare);
30026
- if (authShare && current?.shareVersion != null) {
30027
- const health = await verifyStoredShares(
30028
- {
30029
- getDevice: /* @__PURE__ */ __name(async () => pending.rebind.shares.deviceShare, "getDevice"),
30030
- getAuth: /* @__PURE__ */ __name(async () => authShare, "getAuth")
30031
- },
30032
- pending.primaryDid,
30033
- async (key) => key === pending.privateKey ? pending.primaryDid : ""
30463
+ const { snapshot } = await resolvePendingShareCandidates(
30464
+ storage2,
30465
+ storageId,
30466
+ async () => {
30467
+ const data = await fetchAuthShareRaw(
30468
+ serverUrl,
30469
+ token,
30470
+ providerType,
30471
+ void 0,
30472
+ tenantId
30034
30473
  );
30035
- if (health.healthy) shareVersion2 = current.shareVersion;
30036
- }
30474
+ return {
30475
+ currentVersion: data?.shareVersion ?? null,
30476
+ authShare: authShareToString(data?.authShare)
30477
+ };
30478
+ },
30479
+ did,
30480
+ didFromKey,
30481
+ assertActive,
30482
+ recordPromotion(did),
30483
+ async (version) => authShareToString(
30484
+ (await fetchAuthShareRaw(
30485
+ serverUrl,
30486
+ token,
30487
+ providerType,
30488
+ version,
30489
+ tenantId
30490
+ ))?.authShare
30491
+ )
30492
+ );
30493
+ if (!snapshot.currentVersion) {
30494
+ throw new Error("This account could not be found. Sign in again.");
30037
30495
  }
30496
+ const expectedShareVersion = snapshot.currentVersion;
30497
+ const didAuthVp = await requestFreshDidAuthVp(
30498
+ serverUrl,
30499
+ privateKey,
30500
+ did,
30501
+ signDidAuthVp,
30502
+ tenantId
30503
+ );
30038
30504
  assertActive();
30039
- if (shareVersion2 === void 0) {
30040
- const didAuthVp = await requestFreshDidAuthVp(
30041
- serverUrl,
30042
- pending.privateKey,
30043
- pending.primaryDid,
30044
- signDidAuthVp,
30045
- tenantId
30046
- );
30047
- assertActive();
30048
- pending.rebind ??= {
30049
- shares: (await splitAndVerify(pending.privateKey)).shares,
30050
- storageId,
30051
- attempted: false
30052
- };
30053
- const rebind = pending.rebind;
30054
- if (!candidates.some(
30055
- (candidate) => candidate.share === rebind.shares.deviceShare
30056
- )) {
30505
+ let shareVersion2;
30506
+ let stagedShare = "";
30507
+ const shares2 = await atomicShareUpdate(privateKey, {
30508
+ storeDevice: /* @__PURE__ */ __name(async (share) => {
30509
+ assertActive();
30510
+ stagedShare = share;
30057
30511
  await addPendingShareCandidate(
30058
30512
  storage2,
30059
- { share: rebind.shares.deviceShare, createdAt: Date.now() },
30513
+ { share, createdAt: Date.now() },
30060
30514
  storageId
30061
30515
  );
30062
- }
30063
- assertActive();
30064
- rebind.attempted = true;
30065
- try {
30516
+ }, "storeDevice"),
30517
+ clearDevice: /* @__PURE__ */ __name(() => removePendingShareCandidate(storage2, stagedShare, storageId), "clearDevice"),
30518
+ storeAuth: /* @__PURE__ */ __name(async (share) => {
30519
+ assertActive();
30066
30520
  const response = await postJson(
30067
- `${serverUrl}/keys/recovery-session/rebind`,
30521
+ `${serverUrl}/keys/revoke-other-devices`,
30068
30522
  {
30069
- recoverySessionToken: pending.rebindSessionToken,
30070
- providerType: params.providerType,
30071
- primaryDid: pending.primaryDid,
30072
- authShare: {
30073
- encryptedData: rebind.shares.authShare,
30074
- encryptedDek: "",
30075
- iv: ""
30076
- }
30523
+ authToken: token,
30524
+ providerType,
30525
+ primaryDid: did,
30526
+ authShare: { encryptedData: share, encryptedDek: "", iv: "" },
30527
+ expectedShareVersion
30077
30528
  },
30078
- {
30079
- ...buildHeaders("", didAuthVp, tenantId),
30080
- "X-Auth-Token": params.token
30081
- }
30529
+ buildHeaders(token, didAuthVp, tenantId)
30082
30530
  );
30083
30531
  shareVersion2 = response.shareVersion;
30084
- } catch (error) {
30085
- if (error instanceof ShareWriteRejectedError && !retry) {
30086
- await removePendingShareCandidate(
30087
- storage2,
30088
- rebind.shares.deviceShare,
30089
- storageId
30090
- );
30091
- pending.rebind = void 0;
30092
- }
30093
- throw error;
30094
- }
30095
- }
30096
- if (shareVersion2 === void 0 || !pending.rebind) {
30097
- throw new Error("Server did not confirm the new share");
30532
+ }, "storeAuth")
30533
+ });
30534
+ if (shareVersion2 === void 0) {
30535
+ throw new Error("Server did not return a share version");
30098
30536
  }
30099
- assertActive();
30100
- const shares2 = pending.rebind.shares;
30101
30537
  await storage2.storeDeviceShare(shares2.deviceShare, storageId);
30102
30538
  await storage2.storeShareVersion(shareVersion2, storageId);
30103
- await clearPendingShareCandidates(storage2, storageId);
30539
+ await removePendingShareCandidate(storage2, shares2.deviceShare, storageId);
30104
30540
  assertActive();
30105
30541
  lastEmailShare = shares2.emailShare;
30106
30542
  lastShareVersion = shareVersion2;
@@ -30108,44 +30544,36 @@ function createSSSStrategy(config) {
30108
30544
  currentVersion: shareVersion2,
30109
30545
  resolvedVersion: shareVersion2,
30110
30546
  authShare: shares2.authShare,
30111
- primaryDid: pending.primaryDid
30547
+ primaryDid: did
30112
30548
  };
30113
- pendingIdentityRecovery = void 0;
30114
30549
  return { shares: shares2, shareVersion: shareVersion2 };
30115
30550
  })
30116
30551
  );
30117
- const enrolled = pending.fromEscrow && await tryEnrollEscrow(
30118
- params.token,
30119
- params.providerType,
30120
- pending.privateKey,
30121
- pending.primaryDid,
30552
+ if (!hadEscrow || !config.escrow?.enabled) {
30553
+ return { shareVersion, escrowEnrolled: false, pinKept: false };
30554
+ }
30555
+ const pinMaterial = pin === null ? void 0 : await freshPinMaterial(pin);
30556
+ const enrollArgs = [
30557
+ token,
30558
+ providerType,
30559
+ privateKey,
30560
+ did,
30122
30561
  shares,
30123
30562
  shareVersion,
30124
30563
  signDidAuthVp,
30125
30564
  void 0,
30126
- void 0,
30127
- true,
30565
+ pinMaterial,
30566
+ !pinMaterial,
30128
30567
  storage2.assertCurrent,
30129
30568
  params.options?.recoveryEmail,
30130
30569
  params.options?.recoveryLanguage
30131
- );
30132
- if (activeStorageId !== storageId || storageGeneration !== generation) {
30133
- throw new Error("This recovery request was cancelled.");
30134
- }
30570
+ ];
30571
+ const attempt = await attemptEnrollEscrow(...enrollArgs);
30572
+ const enrolled = attempt.enrolled || attempt.error instanceof EscrowRequestError && attempt.error.status === 403 && await tryEnrollEscrow(...enrollArgs);
30135
30573
  if (!enrolled) {
30136
- unenrolledRotation = {
30137
- shares,
30138
- shareVersion,
30139
- primaryDid: pending.primaryDid,
30140
- clearPin: true
30141
- };
30574
+ unenrolledRotation = { shares, shareVersion, primaryDid: did, clearPin: true };
30142
30575
  }
30143
- return {
30144
- privateKey: pending.privateKey,
30145
- did: pending.primaryDid,
30146
- escrowEnrolled: enrolled,
30147
- escrowRecovery: pending.fromEscrow
30148
- };
30576
+ return { shareVersion, escrowEnrolled: enrolled, pinKept: enrolled && !!pinMaterial };
30149
30577
  },
30150
30578
  // --- Contact method management ---
30151
30579
  async upgradeContactMethod(token, providerType, previousPhone, email, code) {
@@ -30457,6 +30885,7 @@ export {
30457
30885
  NitroAttestationError,
30458
30886
  PIN_MAX_LENGTH,
30459
30887
  PIN_MIN_LENGTH,
30888
+ PinSignInStaleError,
30460
30889
  SSSApiClient,
30461
30890
  SSSKeyManager,
30462
30891
  SSS_THRESHOLD,