@learncard/sss-key-manager 0.2.4 → 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;
@@ -25077,6 +25083,7 @@ __export(index_exports, {
25077
25083
  AtomicUpdateError: () => AtomicUpdateError,
25078
25084
  AuthSessionError: () => import_types.AuthSessionError,
25079
25085
  DEFAULT_KDF_PARAMS: () => DEFAULT_KDF_PARAMS,
25086
+ DuplicateShareCoordinateError: () => DuplicateShareCoordinateError,
25080
25087
  EMAIL_RELAY_ALGORITHM: () => EMAIL_RELAY_ALGORITHM,
25081
25088
  EMAIL_RELAY_ENVELOPE_VERSION: () => EMAIL_RELAY_ENVELOPE_VERSION,
25082
25089
  ESCROW_ALGORITHM: () => ESCROW_ALGORITHM,
@@ -25097,6 +25104,7 @@ __export(index_exports, {
25097
25104
  NitroAttestationError: () => NitroAttestationError,
25098
25105
  PIN_MAX_LENGTH: () => PIN_MAX_LENGTH,
25099
25106
  PIN_MIN_LENGTH: () => PIN_MIN_LENGTH,
25107
+ PinSignInStaleError: () => PinSignInStaleError,
25100
25108
  SSSApiClient: () => SSSApiClient,
25101
25109
  SSSKeyManager: () => SSSKeyManager,
25102
25110
  SSS_THRESHOLD: () => SSS_THRESHOLD,
@@ -30153,6 +30161,15 @@ var EscrowPinLockedError = class extends Error {
30153
30161
  this.name = "EscrowPinLockedError";
30154
30162
  }
30155
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
+ };
30156
30173
 
30157
30174
  // src/escrow-pin.ts
30158
30175
  var PIN_MIN_LENGTH = 6;
@@ -30411,6 +30428,15 @@ var EscrowRequestError = class extends Error {
30411
30428
  __name(this, "EscrowRequestError");
30412
30429
  }
30413
30430
  };
30431
+ var DuplicateShareCoordinateError = class extends Error {
30432
+ static {
30433
+ __name(this, "DuplicateShareCoordinateError");
30434
+ }
30435
+ constructor() {
30436
+ super("Stored shares have the same coordinate and cannot be combined");
30437
+ this.name = "DuplicateShareCoordinateError";
30438
+ }
30439
+ };
30414
30440
  var fetchAuthShareRaw = /* @__PURE__ */ __name(async (serverUrl, token, providerType, shareVersion, tenantId) => {
30415
30441
  const response = await fetch(`${serverUrl}/keys/auth-share`, {
30416
30442
  method: "POST",
@@ -31006,7 +31032,7 @@ function createSSSStrategy(config) {
31006
31032
  assertCurrent();
31007
31033
  return result;
31008
31034
  }, "fetchAuthorityState");
31009
- 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) => {
31010
31036
  if (!config.escrow?.enabled) throw new Error("Escrow enrollment is disabled");
31011
31037
  if (!signDidAuthVp) throw new Error("DID proof signing is required for escrow enrollment");
31012
31038
  const identity = verifiedKey ?? await fetchVerifiedEnclaveKey();
@@ -31065,7 +31091,8 @@ function createSSSStrategy(config) {
31065
31091
  enclaveKeyId: keyId,
31066
31092
  ...pinMaterial ? { pinSalt: pinMaterial.pinSalt } : {},
31067
31093
  ...(signedIntent ? signedIntent.intent.pinAction === "clear" : clearPin) ? { clearPin: true } : {},
31068
- ...signedIntent ? { signedIntent, sourceEnvelope: authority?.envelope ?? null } : {}
31094
+ ...signedIntent ? { signedIntent, sourceEnvelope: authority?.envelope ?? null } : {},
31095
+ ...expectedPinCharges ? { expectedPinCharges } : {}
31069
31096
  })
31070
31097
  });
31071
31098
  if (signedIntent) {
@@ -31078,11 +31105,11 @@ function createSSSStrategy(config) {
31078
31105
  verifiedEscrowEnvelope = committed.envelope ?? void 0;
31079
31106
  }
31080
31107
  }, "enrollEscrow");
31081
- const tryEnrollEscrow = /* @__PURE__ */ __name(async (...args) => {
31082
- if (!config.escrow?.enabled) return false;
31108
+ const attemptEnrollEscrow = /* @__PURE__ */ __name(async (...args) => {
31109
+ if (!config.escrow?.enabled) return { enrolled: false };
31083
31110
  try {
31084
31111
  await enrollEscrow(...args);
31085
- return true;
31112
+ return { enrolled: true };
31086
31113
  } catch (err2) {
31087
31114
  try {
31088
31115
  if (config.onEscrowError) config.onEscrowError(err2);
@@ -31090,9 +31117,10 @@ function createSSSStrategy(config) {
31090
31117
  } catch {
31091
31118
  console.warn("SSS: escrow error reporting failed");
31092
31119
  }
31093
- return false;
31120
+ return { enrolled: false, error: err2 };
31094
31121
  }
31095
- }, "tryEnrollEscrow");
31122
+ }, "attemptEnrollEscrow");
31123
+ const tryEnrollEscrow = /* @__PURE__ */ __name(async (...args) => (await attemptEnrollEscrow(...args)).enrolled, "tryEnrollEscrow");
31096
31124
  const withAutoClear = /* @__PURE__ */ __name((shareVersion, autoClearedFrom) => {
31097
31125
  if (!autoClearedFrom) return { enrolled: true, changed: true, shareVersion };
31098
31126
  try {
@@ -31153,7 +31181,22 @@ function createSSSStrategy(config) {
31153
31181
  clearPinForEnrollment ||= current.hasPin && pin === void 0;
31154
31182
  }
31155
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;
31156
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" };
31157
31200
  hasPin = current.hasPin;
31158
31201
  const uncarryablePin = !clearPinForEnrollment && current.status === "active" && current.hasPin && pin === void 0 && !currentlyEnrolled && status.escrowPin?.state === "none";
31159
31202
  if (uncarryablePin && (config.autoClearUncarryablePin ?? DEFAULT_AUTO_CLEAR_UNCARRYABLE_PIN) && current.shareVersion !== null && status.shareVersion !== null && current.shareVersion < status.shareVersion && !status.recoveryMethods.some(
@@ -31281,6 +31324,378 @@ function createSSSStrategy(config) {
31281
31324
  }
31282
31325
  };
31283
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");
31284
31699
  return {
31285
31700
  name: "sss",
31286
31701
  capabilities: {
@@ -31350,6 +31765,10 @@ function createSSSStrategy(config) {
31350
31765
  };
31351
31766
  },
31352
31767
  async reconstructKey(localKey, remoteKey) {
31768
+ const coordinate = localKey.slice(-2).toLowerCase();
31769
+ if (localKey.length === remoteKey.length && /^(?:[0-9a-f]{2}){2,}$/i.test(localKey) && /^(?:[0-9a-f]{2}){2,}$/i.test(remoteKey) && coordinate !== "00" && coordinate === remoteKey.slice(-2).toLowerCase()) {
31770
+ throw new DuplicateShareCoordinateError();
31771
+ }
31353
31772
  return reconstructFromShares([localKey, remoteKey]);
31354
31773
  },
31355
31774
  async verifyKeys(localKey, remoteKey, expectedDid, didFromPrivateKey) {
@@ -31578,6 +31997,8 @@ function createSSSStrategy(config) {
31578
31997
  escrowOptedOut: data.escrowOptedOut === true,
31579
31998
  escrowPin: data.escrowPin,
31580
31999
  escrowStale: data.escrowStale,
32000
+ ...typeof data.escrowBlobIdentity === "string" ? { escrowBlobIdentity: data.escrowBlobIdentity } : {},
32001
+ ...data.escrowUpdating === true ? { escrowUpdating: true } : {},
31581
32002
  sssActivationState: data.sssActivationState ?? "active",
31582
32003
  ...typeof data.priorEscrowEnrollment === "boolean" ? { priorEscrowEnrollment: data.priorEscrowEnrollment } : {}
31583
32004
  };
@@ -31952,9 +32373,20 @@ function createSSSStrategy(config) {
31952
32373
  if (input.method === "escrow" || input.method === "escrow-pin") {
31953
32374
  if (!didFromPrivateKey)
31954
32375
  throw new Error("DID verification is required for escrow recovery");
31955
- 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,
31956
32380
  token,
31957
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 },
31958
32390
  expectedDid: input.expectedDid,
31959
32391
  options: { releasePolicy: "pin" }
31960
32392
  }) : input;
@@ -32006,16 +32438,32 @@ function createSSSStrategy(config) {
32006
32438
  "Recovery produced an incorrect key. Try another recovery method."
32007
32439
  );
32008
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
+ }
32009
32455
  pendingIdentityRecovery = {
32010
32456
  privateKey: privateKey2,
32011
32457
  primaryDid: material.primaryDid,
32012
32458
  recoveryShare: plaintext.recoveryShare,
32013
32459
  authShare,
32014
32460
  rebindSessionToken: material.rebindSessionToken,
32015
- 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 } } : {}
32016
32464
  };
32017
32465
  if (!token) return { privateKey: privateKey2, did: material.primaryDid };
32018
- return this.completeIdentityRecovery({
32466
+ return completeIdentityRecoveryWith({
32019
32467
  token,
32020
32468
  providerType,
32021
32469
  signDidAuthVp,
@@ -32590,125 +33038,128 @@ function createSSSStrategy(config) {
32590
33038
  pendingIdentityRecovery = void 0;
32591
33039
  },
32592
33040
  async completeIdentityRecovery(params) {
32593
- const storage2 = guardedStorage();
32594
- const generation = storageGeneration;
32595
- const pending = pendingIdentityRecovery;
32596
- const signDidAuthVp = params.signDidAuthVp;
32597
- if (!pending) throw new Error("No identity recovery is waiting for a new sign-in");
32598
- if (!signDidAuthVp) {
32599
- 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
+ );
32600
33060
  }
33061
+ const storage2 = guardedStorage();
32601
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");
32602
33082
  const { shares, shareVersion } = await withShareUpdateLock(
32603
33083
  storage2,
32604
33084
  storageId,
32605
33085
  () => withRotationLock(async () => {
32606
- const assertActive = /* @__PURE__ */ __name(() => {
32607
- if (activeStorageId !== storageId || pendingIdentityRecovery !== pending || storageGeneration !== generation) {
32608
- throw new Error("This recovery request was cancelled.");
32609
- }
32610
- }, "assertActive");
32611
- assertActive();
32612
- const candidates = await readPendingShareCandidates(storage2, storageId);
32613
- if (pending.rebind && pending.rebind.storageId !== storageId) {
32614
- throw new Error("Active account changed during identity recovery");
32615
- }
32616
- if (candidates.some(
32617
- (candidate) => candidate.share !== pending.rebind?.shares.deviceShare
32618
- )) {
32619
- throw new Error(
32620
- "Reconcile the pending share update before binding a sign-in"
32621
- );
32622
- }
32623
- let shareVersion2;
32624
- const retry = pending.rebind?.attempted ?? false;
32625
- if (retry && pending.rebind) {
32626
- const current = await fetchAuthShareRaw(
32627
- serverUrl,
32628
- params.token,
32629
- params.providerType,
32630
- void 0,
32631
- tenantId
32632
- );
32633
- const authShare = authShareToString(current?.authShare);
32634
- if (authShare && current?.shareVersion != null) {
32635
- const health = await verifyStoredShares(
32636
- {
32637
- getDevice: /* @__PURE__ */ __name(async () => pending.rebind.shares.deviceShare, "getDevice"),
32638
- getAuth: /* @__PURE__ */ __name(async () => authShare, "getAuth")
32639
- },
32640
- pending.primaryDid,
32641
- 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
32642
33096
  );
32643
- if (health.healthy) shareVersion2 = current.shareVersion;
32644
- }
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.");
32645
33118
  }
33119
+ const expectedShareVersion = snapshot.currentVersion;
33120
+ const didAuthVp = await requestFreshDidAuthVp(
33121
+ serverUrl,
33122
+ privateKey,
33123
+ did,
33124
+ signDidAuthVp,
33125
+ tenantId
33126
+ );
32646
33127
  assertActive();
32647
- if (shareVersion2 === void 0) {
32648
- const didAuthVp = await requestFreshDidAuthVp(
32649
- serverUrl,
32650
- pending.privateKey,
32651
- pending.primaryDid,
32652
- signDidAuthVp,
32653
- tenantId
32654
- );
32655
- assertActive();
32656
- pending.rebind ??= {
32657
- shares: (await splitAndVerify(pending.privateKey)).shares,
32658
- storageId,
32659
- attempted: false
32660
- };
32661
- const rebind = pending.rebind;
32662
- if (!candidates.some(
32663
- (candidate) => candidate.share === rebind.shares.deviceShare
32664
- )) {
33128
+ let shareVersion2;
33129
+ let stagedShare = "";
33130
+ const shares2 = await atomicShareUpdate(privateKey, {
33131
+ storeDevice: /* @__PURE__ */ __name(async (share) => {
33132
+ assertActive();
33133
+ stagedShare = share;
32665
33134
  await addPendingShareCandidate(
32666
33135
  storage2,
32667
- { share: rebind.shares.deviceShare, createdAt: Date.now() },
33136
+ { share, createdAt: Date.now() },
32668
33137
  storageId
32669
33138
  );
32670
- }
32671
- assertActive();
32672
- rebind.attempted = true;
32673
- try {
33139
+ }, "storeDevice"),
33140
+ clearDevice: /* @__PURE__ */ __name(() => removePendingShareCandidate(storage2, stagedShare, storageId), "clearDevice"),
33141
+ storeAuth: /* @__PURE__ */ __name(async (share) => {
33142
+ assertActive();
32674
33143
  const response = await postJson(
32675
- `${serverUrl}/keys/recovery-session/rebind`,
33144
+ `${serverUrl}/keys/revoke-other-devices`,
32676
33145
  {
32677
- recoverySessionToken: pending.rebindSessionToken,
32678
- providerType: params.providerType,
32679
- primaryDid: pending.primaryDid,
32680
- authShare: {
32681
- encryptedData: rebind.shares.authShare,
32682
- encryptedDek: "",
32683
- iv: ""
32684
- }
33146
+ authToken: token,
33147
+ providerType,
33148
+ primaryDid: did,
33149
+ authShare: { encryptedData: share, encryptedDek: "", iv: "" },
33150
+ expectedShareVersion
32685
33151
  },
32686
- {
32687
- ...buildHeaders("", didAuthVp, tenantId),
32688
- "X-Auth-Token": params.token
32689
- }
33152
+ buildHeaders(token, didAuthVp, tenantId)
32690
33153
  );
32691
33154
  shareVersion2 = response.shareVersion;
32692
- } catch (error) {
32693
- if (error instanceof ShareWriteRejectedError && !retry) {
32694
- await removePendingShareCandidate(
32695
- storage2,
32696
- rebind.shares.deviceShare,
32697
- storageId
32698
- );
32699
- pending.rebind = void 0;
32700
- }
32701
- throw error;
32702
- }
32703
- }
32704
- if (shareVersion2 === void 0 || !pending.rebind) {
32705
- 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");
32706
33159
  }
32707
- assertActive();
32708
- const shares2 = pending.rebind.shares;
32709
33160
  await storage2.storeDeviceShare(shares2.deviceShare, storageId);
32710
33161
  await storage2.storeShareVersion(shareVersion2, storageId);
32711
- await clearPendingShareCandidates(storage2, storageId);
33162
+ await removePendingShareCandidate(storage2, shares2.deviceShare, storageId);
32712
33163
  assertActive();
32713
33164
  lastEmailShare = shares2.emailShare;
32714
33165
  lastShareVersion = shareVersion2;
@@ -32716,44 +33167,36 @@ function createSSSStrategy(config) {
32716
33167
  currentVersion: shareVersion2,
32717
33168
  resolvedVersion: shareVersion2,
32718
33169
  authShare: shares2.authShare,
32719
- primaryDid: pending.primaryDid
33170
+ primaryDid: did
32720
33171
  };
32721
- pendingIdentityRecovery = void 0;
32722
33172
  return { shares: shares2, shareVersion: shareVersion2 };
32723
33173
  })
32724
33174
  );
32725
- const enrolled = pending.fromEscrow && await tryEnrollEscrow(
32726
- params.token,
32727
- params.providerType,
32728
- pending.privateKey,
32729
- 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,
32730
33184
  shares,
32731
33185
  shareVersion,
32732
33186
  signDidAuthVp,
32733
33187
  void 0,
32734
- void 0,
32735
- true,
33188
+ pinMaterial,
33189
+ !pinMaterial,
32736
33190
  storage2.assertCurrent,
32737
33191
  params.options?.recoveryEmail,
32738
33192
  params.options?.recoveryLanguage
32739
- );
32740
- if (activeStorageId !== storageId || storageGeneration !== generation) {
32741
- throw new Error("This recovery request was cancelled.");
32742
- }
33193
+ ];
33194
+ const attempt = await attemptEnrollEscrow(...enrollArgs);
33195
+ const enrolled = attempt.enrolled || attempt.error instanceof EscrowRequestError && attempt.error.status === 403 && await tryEnrollEscrow(...enrollArgs);
32743
33196
  if (!enrolled) {
32744
- unenrolledRotation = {
32745
- shares,
32746
- shareVersion,
32747
- primaryDid: pending.primaryDid,
32748
- clearPin: true
32749
- };
33197
+ unenrolledRotation = { shares, shareVersion, primaryDid: did, clearPin: true };
32750
33198
  }
32751
- return {
32752
- privateKey: pending.privateKey,
32753
- did: pending.primaryDid,
32754
- escrowEnrolled: enrolled,
32755
- escrowRecovery: pending.fromEscrow
32756
- };
33199
+ return { shareVersion, escrowEnrolled: enrolled, pinKept: enrolled && !!pinMaterial };
32757
33200
  },
32758
33201
  // --- Contact method management ---
32759
33202
  async upgradeContactMethod(token, providerType, previousPhone, email, code) {