@learncard/lca-api-service 1.6.0 → 1.6.2

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.
Files changed (37) hide show
  1. package/AGENTS.md +21 -0
  2. package/CHANGELOG.md +31 -0
  3. package/dist/app.d.ts +41 -0
  4. package/dist/cache/recoverySessions.d.ts +5 -0
  5. package/dist/didkit_wasm_bg.wasm +0 -0
  6. package/dist/helpers/accountSecureLink.d.ts +4 -0
  7. package/dist/helpers/accountSecureLink.test.d.ts +1 -0
  8. package/dist/index.js +2 -2
  9. package/dist/models/EscrowHold.d.ts +4 -1
  10. package/dist/models/UserKey.d.ts +199 -1
  11. package/dist/publish-metafile.json +1 -1
  12. package/dist/routes/escrow.d.ts +4 -0
  13. package/dist/routes/keys.d.ts +47 -1
  14. package/dist/services/escrow-enclave/enrollmentLease.d.ts +8 -0
  15. package/dist/services/escrow-enclave/notifications.d.ts +1 -1
  16. package/package.json +11 -11
  17. package/project.json +1 -1
  18. package/src/cache/recoverySessions.ts +38 -0
  19. package/src/helpers/accountSecureLink.test.ts +26 -0
  20. package/src/helpers/accountSecureLink.ts +16 -0
  21. package/src/jobs/escrowBlobRewrap.ts +95 -3
  22. package/src/models/EscrowHold.ts +23 -6
  23. package/src/models/UserKey.escrowPin.test.ts +21 -0
  24. package/src/models/UserKey.ts +553 -47
  25. package/src/routes/escrow.ts +326 -67
  26. package/src/routes/keys.ts +416 -7
  27. package/src/services/delivery/adapters/postmark.adapter.ts +11 -0
  28. package/src/services/escrow-enclave/README.md +8 -3
  29. package/src/services/escrow-enclave/enrollmentLease.ts +124 -0
  30. package/src/services/escrow-enclave/notifications.test.ts +69 -0
  31. package/src/services/escrow-enclave/notifications.ts +19 -1
  32. package/test/jobs/escrowHoldReminders.spec.ts +88 -4
  33. package/test/keys-escrow.spec.ts +4 -0
  34. package/test/keys-lifecycle-invariants.spec.ts +76 -0
  35. package/test/keys-pin-lease.spec.ts +1132 -0
  36. package/test/keys-pin-sign-in.spec.ts +1152 -0
  37. package/tsconfig.json +13 -14
package/AGENTS.md CHANGED
@@ -127,6 +127,27 @@ The `escrow` tRPC router is disabled unless `ESCROW_ENCLAVE_MODE` is configured.
127
127
  | `POST /keys/escrow/cancel-link` | `escrow.cancelRecoveryByLink` | Cancels a pending hold via the single-use email cancel link (no DID auth) |
128
128
  | `POST /keys/escrow/complete` | `escrow.completeRecovery` | Claim an elapsed hold and release sealed recovery material once |
129
129
 
130
+ ### Recovery sessions, rebind and PIN sign-in
131
+
132
+ Lost-login recovery issues single-use, 15-minute tokens (`src/cache/recoverySessions.ts`): a `'recover'` token from `/keys/recovery-session/verify`, then a `'rebind'` token from `useRecoverySession` or `/keys/escrow/complete`.
133
+
134
+ | Route | Procedure | Effect |
135
+ | ----------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | `POST /keys/recovery-session/rebind` | `keys.completeRecoveryRebind` | Wiping rebind: new login replaces the old, new split, history purged, methods `pending`, escrow cleared |
137
+ | `POST /keys/recovery-session/pin-sign-in` | `keys.completePinSignIn` | Keep mode: adds the new login (old one stays); shares, history, methods, escrow and contact unchanged. Only PIN-release tokens are accepted |
138
+ | `POST /keys/revoke-other-devices` | `keys.revokeOtherDevices` | "Secure my account": owner-authenticated security rotation (same wipe as rebind; only the caller's login stays), CAS on `expectedShareVersion` |
139
+
140
+ - Only `/keys/escrow/complete` for a `releasePolicy:'pin'` hold mints a `'rebind'` token with `origin:'pin-release'` plus the released `primaryDid`/`shareVersion`. Keep mode trusts those server-side fields, never client input. Waiting-period and `useRecoverySession` tokens can only use the wiping rebind.
141
+ - Every recovery token records `userKey.securityEpoch` (missing = 0) at mint; a `'rebind'` token from `useRecoverySession` inherits the `'recover'` session's epoch. Wiping rotations (`/rebind`, revoke) `$inc` the epoch in the same update, and `/rebind` and `/pin-sign-in` include the epoch in their update filter, so any token minted before a revoke or wiping rebind gets CONFLICT. `useRecoverySession` and escrow start (with a recovery session) also reject a stale epoch. Non-wiping rotations leave the epoch alone.
142
+ - Keep mode never spends the PIN-release token, so `/rebind` also only accepts one while `userKey.shareVersion` still equals the released version. `/pin-sign-in` peeks at the token before consuming it, so a rejected hold/phrase token or a wrong DID does not burn it.
143
+ - The revoke re-splits the same key; it does not evict a device that already holds the private key (see `services/escrow-enclave-app/SECURITY.md` D19). After the commit it emails the contact and verified recovery addresses (`account-secured`, awaited within a 3 s budget, fail-open); the copy assumes the reader may not have run it and may have lost their login, says the recovery methods were reset, and sends them to the tenant support address (or the organization, when there is none) rather than to lost-login recovery, which the revoke itself disabled.
144
+ - A successful PIN release sends the `escrow-pin-sign-in` notice ("Not you? Secure your account", link to `/account/secure` on the tenant app URL) instead of `escrow-hold-released`. It is sent fail-open within the escrow notification budget.
145
+ - `getEscrowPinStatus().wrongAttempts` counts verified wrong entries only and is omitted for legacy records that cannot tell (until a reservation initialises the verified counter to 0; clients therefore also read the status before a release).
146
+ - `escrow.enroll` accepts `expectedPinCharges: { attemptsRemaining, wrongAttempts }` with a new `pinSalt` (the keep-mode same-PIN refresh). It is checked against the current PIN, then (software mode) again in the write's filter, or (authority mode) by the lease claim below, so a guess reserved or resolved after the client's check makes it CONFLICT instead of being wiped by the fresh generation. A non-zero `wrongAttempts` is always refused.
147
+ - **PIN-operation lease (authority mode).** Every relayed owner-signed intent (`escrow.enroll`, `escrow.disable`/`remove`) runs under `applyEnrollmentIntentUnderLease` (`src/routes/escrow.ts`): `claimEscrowEnrollmentLease` sets `userKey.escrowEnrollmentLease` in one write conditioned on the read blob + PIN record, no other lease and no live `escrowPinCompares` marker; then the authority call; then `commitEscrowEnrollmentLease`, conditioned on the lease id and the replaced `blobHash` (not on charges), which also removes the lease. `reserveEscrowPinAttempt` refuses while any lease exists and pushes a compare marker (`compareId`) that `completeRecovery` pulls once the authority answers (kept until its TTL after a transport failure). `startRecovery` and `completeRecovery` answer 429 `Automatic recovery is being updated…` under a live lease, before creating a hold or charging; `completeRecovery` also refuses (FORBIDDEN, uncharged) a PIN hold whose `holdRecord.hold.blobHash` is not the current blob's. The rewrap job (`src/jobs/escrowBlobRewrap.ts`) claims the same lease (`action: 'rewrap'`) around `rewrapEscrowBlob` in authority mode. Releasing a compare marker is best effort (its TTL covers a miss). TTLs: `ESCROW_ENROLLMENT_LEASE_TTL_MS` / `ESCROW_PIN_COMPARE_TTL_MS` = 120 s (4x the 30 s max `ESCROW_ENCLAVE_REMOTE_TIMEOUT_MS`).
148
+ - **Reconciliation.** If the commit write misses or the authority call fails ambiguously, `settleEscrowEnrollmentLease` (`src/services/escrow-enclave/enrollmentLease.ts`) reads the authority state and adopts it when it is exactly the lease's commit (enrollment version +1, epoch, PIN generation, share version, returned envelope identity), using the salt/PIN action recorded in the lease; a definite refusal releases the lease; an unknown outcome keeps it until expiry. `settleExpiredEscrowLease` runs on start/complete/enroll/disable/authority-state, so a crash converges on the next request. A settle that finds the same commit already persisted counts as adopted. A lost response still fails the request even when its commit is adopted (no receipt to return). `/keys/auth-share` reports `escrowBlobIdentity` so authority-mode clients can detect and repair any copy mismatch that is left, and `escrowUpdating` while a lease is held so they do not mistake an in-progress operation for a stale copy. Live-Mongo coverage: `test/keys-pin-lease.spec.ts`.
149
+ - Software-mode PIN carry ignores releases completed before `escrowPin.enabledAt`, so a PIN re-set at the same version after a PIN sign-in survives the next rotation.
150
+
130
151
  ### Important Invariants
131
152
 
132
153
  - **Auth shares are encrypted at rest** with a KEK derived from `SEED`. Losing `SEED` means every stored auth share is permanently unrecoverable.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @welibraryos/lca-api-service
2
2
 
3
+ ## 1.6.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1905](https://github.com/learningeconomy/LearnCard/pull/1905) [`c20460f6fef685272227f506f536b6811f90b8a0`](https://github.com/learningeconomy/LearnCard/commit/c20460f6fef685272227f506f536b6811f90b8a0) Thanks [@Custard7](https://github.com/Custard7)! - Make the recovery PIN the main way to sign in on a new device, without resetting the account. A PIN sign-in now keeps the account's shares, history, recovery methods and escrow: the device stores the released recovery piece at the current version and re-sets the same PIN for a fresh attempt budget only when no wrong entry is recorded against it; otherwise the PIN keeps its reduced budget and the app asks for a new, different PIN (refusing the PIN just used, also after a restart on that device, via a tag keyed by the account key), asking again on each app start until one is chosen; the same-PIN refresh is skipped when another charge landed during the release, and lca-api writes it only while the PIN still shows the charges the client saw (`expectedPinCharges`) (a lost login is linked with the new `POST /keys/recovery-session/pin-sign-in`, accepted only after a PIN release). The server emails the account's verified addresses on every PIN sign-in ("New device signed in with your PIN. Not you? Secure your account."), and the PIN status reports earlier wrong entries. The app leads its recovery screen with PIN entry (no sign-out warnings on that path; the 7-day hold keeps them), asks once after signing in whether a device was lost or the PIN is known to someone else (warning about earlier wrong entries), and adds "Secure my account" / "Sign out of all other devices" (`POST /keys/revoke-other-devices`), reachable from that question, Account Recovery settings and the email link `/account/secure`. That action re-splits the key, purges older versions and unlinks every login except the current one; it does not change the key itself, so a device that is already signed in may keep working until it signs out (the app says so). The revoke emails the account's contact and verified recovery addresses (new `account-secured` template, written for an owner who did not do it and may have lost their login: it says the recovery methods were reset and points to the tenant's support address, or to the organization when there is none). Every recovery token (recovery-email code, phrase, passkey, backup, waiting-period hold and PIN) is now bound to an account security epoch that every revoke and wiping rebind advances, so no token minted before one can rebind or re-link after it. Recovery is one-at-a-time: the coordinator rejects a second recovery while one is pending (`RecoveryInProgressError`; after a cancel, a new recovery-email code can be requested at once) and the app disables switching methods while a PIN sign-in is in flight. `@learncard/email-templates` omits an absent `locale` rather than passing `undefined` (`exactOptionalPropertyTypes`-safe) and exports each template from one declaration.
8
+
9
+ - Updated dependencies [[`372c1431e78a9708454f9c4c2f2c99dd5486b8e9`](https://github.com/learningeconomy/LearnCard/commit/372c1431e78a9708454f9c4c2f2c99dd5486b8e9), [`c20460f6fef685272227f506f536b6811f90b8a0`](https://github.com/learningeconomy/LearnCard/commit/c20460f6fef685272227f506f536b6811f90b8a0)]:
10
+ - @learncard/sss-key-manager@0.2.6
11
+ - @learncard/types@5.24.1
12
+ - @learncard/email-templates@1.2.2
13
+ - @learncard/core@9.4.42
14
+ - @learncard/helpers@1.7.2
15
+ - @learncard/init@2.5.8
16
+ - @learncard/did-web-plugin@1.1.42
17
+ - @learncard/didkit-plugin@1.10.8
18
+ - @learncard/didkit-plugin-node@0.3.8
19
+
20
+ ## 1.6.1
21
+
22
+ ### Patch Changes
23
+
24
+ - [#1850](https://github.com/learningeconomy/LearnCard/pull/1850) [`578e831f41bc762ff12e5ad9be6ba765f2bed33d`](https://github.com/learningeconomy/LearnCard/commit/578e831f41bc762ff12e5ad9be6ba765f2bed33d) Thanks [@Custard7](https://github.com/Custard7)! - Keep provisional Web3Auth migrations that have a confirmed escrow enrollment out of the 30-day purge of abandoned migrations, so users who skip the recovery PIN don't lose escrow.
25
+
26
+ - [#1859](https://github.com/learningeconomy/LearnCard/pull/1859) [`b20b375870032c22ea9109d2b102ddbc4a4f3248`](https://github.com/learningeconomy/LearnCard/commit/b20b375870032c22ea9109d2b102ddbc4a4f3248) Thanks [@goblincore](https://github.com/goblincore)! - Use a unique final sort key for escrow reminder, start-notification and cancellation queues so equal-priority records form predictable batches.
27
+
28
+ - Updated dependencies [[`b0c7891371fa621a1c2c4a419a1d4f330ed2993a`](https://github.com/learningeconomy/LearnCard/commit/b0c7891371fa621a1c2c4a419a1d4f330ed2993a), [`4ec92c41442b56341c1205e6ddd6ae784aecc990`](https://github.com/learningeconomy/LearnCard/commit/4ec92c41442b56341c1205e6ddd6ae784aecc990)]:
29
+ - @learncard/sss-key-manager@0.2.5
30
+ - @learncard/didkit-plugin@1.10.7
31
+ - @learncard/init@2.5.7
32
+ - @learncard/didkit-plugin-node@0.3.7
33
+
3
34
  ## 1.6.0
4
35
 
5
36
  ### Minor Changes
package/dist/app.d.ts CHANGED
@@ -1041,6 +1041,7 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1041
1041
  state: "enabled" | "none" | "locked" | "stale";
1042
1042
  enabled: boolean;
1043
1043
  attemptsRemaining: number;
1044
+ wrongAttempts?: number | undefined;
1044
1045
  salt?: string | undefined;
1045
1046
  };
1046
1047
  primaryDid: string | null;
@@ -1089,6 +1090,39 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1089
1090
  };
1090
1091
  meta: import("trpc-to-openapi").OpenApiMeta;
1091
1092
  }>;
1093
+ completePinSignIn: import("@trpc/server").TRPCMutationProcedure<{
1094
+ input: {
1095
+ recoverySessionToken: string;
1096
+ providerType: "firebase" | "supertokens" | "keycloak" | "oidc";
1097
+ primaryDid: string;
1098
+ newAuthToken?: string | undefined;
1099
+ };
1100
+ output: {
1101
+ success: true;
1102
+ shareVersion: number;
1103
+ linked: boolean;
1104
+ };
1105
+ meta: import("trpc-to-openapi").OpenApiMeta;
1106
+ }>;
1107
+ revokeOtherDevices: import("@trpc/server").TRPCMutationProcedure<{
1108
+ input: {
1109
+ providerType: "firebase" | "supertokens" | "keycloak" | "oidc";
1110
+ primaryDid: string;
1111
+ authShare: {
1112
+ encryptedData: string;
1113
+ encryptedDek: string;
1114
+ iv: string;
1115
+ };
1116
+ expectedShareVersion: number;
1117
+ authToken?: string | undefined;
1118
+ };
1119
+ output: {
1120
+ success: true;
1121
+ shareVersion: number;
1122
+ recoveryMethodsRequireConfirmation: ("email" | "passkey" | "backup" | "phrase" | "escrow")[];
1123
+ };
1124
+ meta: import("trpc-to-openapi").OpenApiMeta;
1125
+ }>;
1092
1126
  issueDidChallenge: import("@trpc/server").TRPCMutationProcedure<{
1093
1127
  input: {
1094
1128
  did: string;
@@ -1127,11 +1161,14 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1127
1161
  state: "enabled" | "none" | "locked" | "stale";
1128
1162
  enabled: boolean;
1129
1163
  attemptsRemaining: number;
1164
+ wrongAttempts?: number | undefined;
1130
1165
  salt?: string | undefined;
1131
1166
  };
1132
1167
  priorEscrowEnrollment: boolean;
1133
1168
  sssActivationState: "active" | "provisional";
1134
1169
  escrowStale?: "mode-mismatch" | "key-rotated" | undefined;
1170
+ escrowBlobIdentity?: string | undefined;
1171
+ escrowUpdating?: boolean | undefined;
1135
1172
  } | null;
1136
1173
  meta: import("trpc-to-openapi").OpenApiMeta;
1137
1174
  }>;
@@ -1433,6 +1470,10 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1433
1470
  iv: string;
1434
1471
  ciphertext: string;
1435
1472
  } | null | undefined;
1473
+ expectedPinCharges?: {
1474
+ attemptsRemaining: number;
1475
+ wrongAttempts: number;
1476
+ } | undefined;
1436
1477
  };
1437
1478
  output: {
1438
1479
  success: true;
@@ -10,6 +10,10 @@ export interface RecoverySessionRecord {
10
10
  type: AuthProviderType;
11
11
  id: string;
12
12
  };
13
+ origin?: 'pin-release';
14
+ primaryDid?: string;
15
+ shareVersion?: number;
16
+ securityEpoch?: number;
13
17
  }
14
18
  export interface RecoveryOtpRecord {
15
19
  codeHash: string;
@@ -36,6 +40,7 @@ export declare const getRecoveryOtpLockCacheKey: (email: string) => string;
36
40
  export declare const getRecoveryOtpSendCacheKey: (email: string) => string;
37
41
  export declare const getRecoverySessionCacheKey: (token: string) => string;
38
42
  export declare const createRecoverySession: (record: RecoverySessionRecord, ttl?: number) => Promise<string>;
43
+ export declare const peekRecoverySession: (token: string, expectedScope: RecoverySessionScope) => Promise<RecoverySessionRecord | null>;
39
44
  export declare const consumeRecoverySession: (token: string, expectedScope: RecoverySessionScope) => Promise<RecoverySessionRecord | null>;
40
45
  export declare const storeRecoveryOtp: (email: string, record: RecoveryOtpRecord) => Promise<void>;
41
46
  export declare const verifyRecoveryOtp: (email: string, submittedCodeHash: string) => Promise<RecoveryOtpVerificationResult>;
Binary file
@@ -0,0 +1,4 @@
1
+ export declare const ACCOUNT_SECURE_PATH = "/account/secure";
2
+ export declare const buildAccountSecureUrl: ({ baseUrl }: {
3
+ baseUrl: string;
4
+ }) => string;
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/index.js
3
- size: 166355147 bytes
4
- sha256: 0b23e44f11e205fe9e6dd5502e0a80c343603806b4356fdeda1937f70cc49de7
3
+ size: 167146968 bytes
4
+ sha256: e837ca38f23dbcecdd16d4cc36272db9d19b6ffac4d5142e91b4a5fd4c9af1cd
@@ -141,6 +141,7 @@ export declare const EscrowHoldValidator: z.ZodObject<{
141
141
  "pin-locked": "pin-locked";
142
142
  started: "started";
143
143
  reminder: "reminder";
144
+ "pin-sign-in": "pin-sign-in";
144
145
  }>;
145
146
  sentAt: z.ZodDate;
146
147
  }, z.core.$strip>>>;
@@ -166,7 +167,9 @@ export declare const cancelEscrowHold: (id: string, cancelledBy: "did" | "system
166
167
  export declare const cancelEscrowHoldByCancelToken: (id: string, cancelTokenHash: string) => Promise<EscrowHold | null>;
167
168
  export declare const recordEscrowHoldConfirmation: (id: string, confirmedAt: Date, releaseAfter: Date) => Promise<EscrowHold | null>;
168
169
  export declare const completeEscrowHold: (id: string) => Promise<EscrowHold | null>;
169
- export declare const hasCompletedEscrowHoldForVersion: (authProvider: AuthProviderMapping, shareVersion: number) => Promise<boolean>;
170
+ export declare const hasCompletedEscrowHoldForVersion: (authProvider: AuthProviderMapping, shareVersion: number, options?: {
171
+ completedAfter?: Date;
172
+ }) => Promise<boolean>;
170
173
  export declare const expireStaleEscrowHolds: (now: Date) => Promise<number>;
171
174
  export declare const markClaimedEscrowHoldFailed: (id: string, reason: "pin-mismatch" | "pin-locked" | "release-failed", completedAt: Date) => Promise<void>;
172
175
  export declare const retryClaimedEscrowHold: (id: string, completedAt: Date) => Promise<void>;
@@ -126,6 +126,49 @@ export declare const EscrowPinValidator: z.ZodObject<{
126
126
  disabledAt: z.ZodOptional<z.ZodDate>;
127
127
  shareVersion: z.ZodNumber;
128
128
  }, z.core.$strip>;
129
+ export declare const ESCROW_ENROLLMENT_LEASE_TTL_MS = 120000;
130
+ export declare const ESCROW_PIN_COMPARE_TTL_MS = 120000;
131
+ export declare const EscrowEnrollmentLeaseValidator: z.ZodObject<{
132
+ id: z.ZodString;
133
+ expiresAt: z.ZodDate;
134
+ action: z.ZodEnum<{
135
+ enroll: "enroll";
136
+ replace: "replace";
137
+ disable: "disable";
138
+ carry: "carry";
139
+ rewrap: "rewrap";
140
+ }>;
141
+ pinAction: z.ZodEnum<{
142
+ set: "set";
143
+ change: "change";
144
+ clear: "clear";
145
+ keep: "keep";
146
+ }>;
147
+ pin: z.ZodEnum<{
148
+ clear: "clear";
149
+ keep: "keep";
150
+ fresh: "fresh";
151
+ }>;
152
+ pinSalt: z.ZodOptional<z.ZodString>;
153
+ optOut: z.ZodOptional<z.ZodBoolean>;
154
+ shareVersion: z.ZodNumber;
155
+ authorityGeneration: z.ZodString;
156
+ expectedEnrollmentVersion: z.ZodNumber;
157
+ expectedPinGeneration: z.ZodOptional<z.ZodNumber>;
158
+ enrollmentEpoch: z.ZodNumber;
159
+ sourceBlobHash: z.ZodNullable<z.ZodString>;
160
+ measurements: z.ZodObject<{
161
+ imageSha384: z.ZodOptional<z.ZodString>;
162
+ pcr0: z.ZodOptional<z.ZodString>;
163
+ pcr1: z.ZodOptional<z.ZodString>;
164
+ pcr2: z.ZodOptional<z.ZodString>;
165
+ }, z.core.$strip>;
166
+ }, z.core.$strip>;
167
+ export type EscrowEnrollmentLease = z.infer<typeof EscrowEnrollmentLeaseValidator>;
168
+ export declare const EscrowPinCompareValidator: z.ZodObject<{
169
+ id: z.ZodString;
170
+ expiresAt: z.ZodDate;
171
+ }, z.core.$strip>;
129
172
  export declare const EscrowPinStatusValidator: z.ZodObject<{
130
173
  state: z.ZodEnum<{
131
174
  none: "none";
@@ -135,6 +178,7 @@ export declare const EscrowPinStatusValidator: z.ZodObject<{
135
178
  }>;
136
179
  enabled: z.ZodBoolean;
137
180
  attemptsRemaining: z.ZodNumber;
181
+ wrongAttempts: z.ZodOptional<z.ZodNumber>;
138
182
  salt: z.ZodOptional<z.ZodString>;
139
183
  }, z.core.$strip>;
140
184
  export declare const getEscrowPinStatus: (userKey: MongoUserKeyType) => z.infer<typeof EscrowPinStatusValidator>;
@@ -192,6 +236,46 @@ declare const MongoUserKeyBaseValidator: z.ZodObject<{
192
236
  shareVersion: z.ZodNumber;
193
237
  }, z.core.$strip>>;
194
238
  escrowOptedOutAt: z.ZodOptional<z.ZodDate>;
239
+ escrowEnrollmentLease: z.ZodOptional<z.ZodObject<{
240
+ id: z.ZodString;
241
+ expiresAt: z.ZodDate;
242
+ action: z.ZodEnum<{
243
+ enroll: "enroll";
244
+ replace: "replace";
245
+ disable: "disable";
246
+ carry: "carry";
247
+ rewrap: "rewrap";
248
+ }>;
249
+ pinAction: z.ZodEnum<{
250
+ set: "set";
251
+ change: "change";
252
+ clear: "clear";
253
+ keep: "keep";
254
+ }>;
255
+ pin: z.ZodEnum<{
256
+ clear: "clear";
257
+ keep: "keep";
258
+ fresh: "fresh";
259
+ }>;
260
+ pinSalt: z.ZodOptional<z.ZodString>;
261
+ optOut: z.ZodOptional<z.ZodBoolean>;
262
+ shareVersion: z.ZodNumber;
263
+ authorityGeneration: z.ZodString;
264
+ expectedEnrollmentVersion: z.ZodNumber;
265
+ expectedPinGeneration: z.ZodOptional<z.ZodNumber>;
266
+ enrollmentEpoch: z.ZodNumber;
267
+ sourceBlobHash: z.ZodNullable<z.ZodString>;
268
+ measurements: z.ZodObject<{
269
+ imageSha384: z.ZodOptional<z.ZodString>;
270
+ pcr0: z.ZodOptional<z.ZodString>;
271
+ pcr1: z.ZodOptional<z.ZodString>;
272
+ pcr2: z.ZodOptional<z.ZodString>;
273
+ }, z.core.$strip>;
274
+ }, z.core.$strip>>;
275
+ escrowPinCompares: z.ZodOptional<z.ZodArray<z.ZodObject<{
276
+ id: z.ZodString;
277
+ expiresAt: z.ZodDate;
278
+ }, z.core.$strip>>>;
195
279
  _id: z.ZodOptional<z.ZodString>;
196
280
  contactMethod: z.ZodObject<{
197
281
  type: z.ZodEnum<{
@@ -271,6 +355,7 @@ declare const MongoUserKeyBaseValidator: z.ZodObject<{
271
355
  provisional: "provisional";
272
356
  }>>;
273
357
  provisionalCreatedAt: z.ZodOptional<z.ZodDate>;
358
+ securityEpoch: z.ZodOptional<z.ZodNumber>;
274
359
  createdAt: z.ZodDate;
275
360
  updatedAt: z.ZodDate;
276
361
  }, z.core.$strip>;
@@ -328,6 +413,46 @@ export declare const MongoUserKeyValidator: z.ZodObject<{
328
413
  shareVersion: z.ZodNumber;
329
414
  }, z.core.$strip>>;
330
415
  escrowOptedOutAt: z.ZodOptional<z.ZodDate>;
416
+ escrowEnrollmentLease: z.ZodOptional<z.ZodObject<{
417
+ id: z.ZodString;
418
+ expiresAt: z.ZodDate;
419
+ action: z.ZodEnum<{
420
+ enroll: "enroll";
421
+ replace: "replace";
422
+ disable: "disable";
423
+ carry: "carry";
424
+ rewrap: "rewrap";
425
+ }>;
426
+ pinAction: z.ZodEnum<{
427
+ set: "set";
428
+ change: "change";
429
+ clear: "clear";
430
+ keep: "keep";
431
+ }>;
432
+ pin: z.ZodEnum<{
433
+ clear: "clear";
434
+ keep: "keep";
435
+ fresh: "fresh";
436
+ }>;
437
+ pinSalt: z.ZodOptional<z.ZodString>;
438
+ optOut: z.ZodOptional<z.ZodBoolean>;
439
+ shareVersion: z.ZodNumber;
440
+ authorityGeneration: z.ZodString;
441
+ expectedEnrollmentVersion: z.ZodNumber;
442
+ expectedPinGeneration: z.ZodOptional<z.ZodNumber>;
443
+ enrollmentEpoch: z.ZodNumber;
444
+ sourceBlobHash: z.ZodNullable<z.ZodString>;
445
+ measurements: z.ZodObject<{
446
+ imageSha384: z.ZodOptional<z.ZodString>;
447
+ pcr0: z.ZodOptional<z.ZodString>;
448
+ pcr1: z.ZodOptional<z.ZodString>;
449
+ pcr2: z.ZodOptional<z.ZodString>;
450
+ }, z.core.$strip>;
451
+ }, z.core.$strip>>;
452
+ escrowPinCompares: z.ZodOptional<z.ZodArray<z.ZodObject<{
453
+ id: z.ZodString;
454
+ expiresAt: z.ZodDate;
455
+ }, z.core.$strip>>>;
331
456
  _id: z.ZodOptional<z.ZodString>;
332
457
  contactMethod: z.ZodObject<{
333
458
  type: z.ZodEnum<{
@@ -407,6 +532,7 @@ export declare const MongoUserKeyValidator: z.ZodObject<{
407
532
  provisional: "provisional";
408
533
  }>>;
409
534
  provisionalCreatedAt: z.ZodOptional<z.ZodDate>;
535
+ securityEpoch: z.ZodOptional<z.ZodNumber>;
410
536
  createdAt: z.ZodDate;
411
537
  updatedAt: z.ZodDate;
412
538
  }, z.core.$loose>;
@@ -519,6 +645,31 @@ export declare const getUserKeysCollection: () => import("mongodb").Collection<{
519
645
  disabledAt?: Date | undefined;
520
646
  } | undefined;
521
647
  escrowOptedOutAt?: Date | undefined;
648
+ escrowEnrollmentLease?: {
649
+ id: string;
650
+ expiresAt: Date;
651
+ action: "enroll" | "replace" | "disable" | "carry" | "rewrap";
652
+ pinAction: "set" | "change" | "clear" | "keep";
653
+ pin: "clear" | "keep" | "fresh";
654
+ shareVersion: number;
655
+ authorityGeneration: string;
656
+ expectedEnrollmentVersion: number;
657
+ enrollmentEpoch: number;
658
+ sourceBlobHash: string | null;
659
+ measurements: {
660
+ imageSha384?: string | undefined;
661
+ pcr0?: string | undefined;
662
+ pcr1?: string | undefined;
663
+ pcr2?: string | undefined;
664
+ };
665
+ pinSalt?: string | undefined;
666
+ optOut?: boolean | undefined;
667
+ expectedPinGeneration?: number | undefined;
668
+ } | undefined;
669
+ escrowPinCompares?: {
670
+ id: string;
671
+ expiresAt: Date;
672
+ }[] | undefined;
522
673
  _id?: string | undefined;
523
674
  authShare?: {
524
675
  encryptedData: string;
@@ -531,6 +682,7 @@ export declare const getUserKeysCollection: () => import("mongodb").Collection<{
531
682
  migratedAt?: Date | undefined;
532
683
  sssActivationState?: "active" | "provisional" | undefined;
533
684
  provisionalCreatedAt?: Date | undefined;
685
+ securityEpoch?: number | undefined;
534
686
  }>;
535
687
  export declare const createUserKeysIndexes: () => Promise<void>;
536
688
  export declare const ensureUserKeysIndexes: () => Promise<void>;
@@ -543,6 +695,7 @@ export declare const upsertUserKey: (contactMethod: ContactMethod, data: Partial
543
695
  export declare const upsertUserKeyByAuthProvider: (contactMethod: ContactMethod, authProvider: AuthProviderMapping, data: Partial<Omit<MongoUserKeyType, "_id" | "contactMethod" | "createdAt" | "authProviders">>, expectedVersion?: number) => Promise<MongoUserKeyType>;
544
696
  export declare const addAuthProviderToUserKey: (contactMethod: ContactMethod, authProvider: AuthProviderMapping) => Promise<void>;
545
697
  export declare const addRecoveryMethodToUserKey: (contactMethod: ContactMethod, recoveryMethod: RecoveryMethod) => Promise<void>;
698
+ export declare const getSecurityEpoch: (userKey: Pick<MongoUserKeyType, "securityEpoch">) => number;
546
699
  export declare const setEscrowBlobByAuthProvider: (authProvider: AuthProviderMapping, blob: Omit<EscrowBlob, "enrollmentEpoch" | "blobHash">, expectedShareVersion: number, pin?: {
547
700
  salt: string;
548
701
  }, carryFrom?: {
@@ -552,6 +705,28 @@ export declare const setEscrowBlobByAuthProvider: (authProvider: AuthProviderMap
552
705
  enrollmentEpoch: number;
553
706
  expectedBlob?: EscrowBlob;
554
707
  expectedPin?: MongoUserKeyType["escrowPin"];
708
+ }, expectedPinCharges?: {
709
+ failedAttempts: number;
710
+ verifiedFailedAttempts: number;
711
+ }) => Promise<MongoUserKeyType | null>;
712
+ export interface ClaimEscrowEnrollmentLeaseInput {
713
+ lease: EscrowEnrollmentLease;
714
+ expectedBlob?: EscrowBlob;
715
+ expectedPin?: MongoUserKeyType['escrowPin'];
716
+ requireOptedIn: boolean;
717
+ }
718
+ export declare const claimEscrowEnrollmentLease: (authProvider: AuthProviderMapping, input: ClaimEscrowEnrollmentLeaseInput) => Promise<MongoUserKeyType | null>;
719
+ export declare const releaseEscrowEnrollmentLease: (authProvider: AuthProviderMapping, leaseId: string) => Promise<void>;
720
+ export interface EscrowAuthorityCommit {
721
+ envelope: EscrowBlob['envelope'] | null;
722
+ authorityGeneration: string;
723
+ enrollmentVersion: number;
724
+ enrollmentEpoch: number;
725
+ receipt?: AuthorityRecord;
726
+ stateProof?: AuthorityRecord;
727
+ }
728
+ export declare const commitEscrowEnrollmentLease: (authProvider: AuthProviderMapping, lease: EscrowEnrollmentLease, commit: EscrowAuthorityCommit, { adoptWithoutLease }?: {
729
+ adoptWithoutLease?: boolean;
555
730
  }) => Promise<MongoUserKeyType | null>;
556
731
  export declare const rewrapEscrowBlobByAuthProvider: (authProvider: AuthProviderMapping, oldBlob: EscrowBlob, oldPin: MongoUserKeyType["escrowPin"], newEnvelope: EscrowBlob["envelope"], identity: Pick<EscrowBlob, "enclaveKeyId" | "enclaveMode" | "measurements">, authority?: Pick<AuthorityMutationResult, "authorityGeneration" | "enrollmentVersion"> & {
557
732
  receipt?: AuthorityRecord;
@@ -566,7 +741,8 @@ export declare const clearEscrowByAuthProvider: (authProvider: AuthProviderMappi
566
741
  };
567
742
  }) => Promise<MongoUserKeyType | null>;
568
743
  export declare const setEscrowOptInByAuthProvider: (authProvider: AuthProviderMapping) => Promise<MongoUserKeyType | null>;
569
- export declare const reserveEscrowPinAttempt: (authProvider: AuthProviderMapping, shareVersion: number, expectedCiphertext?: string) => Promise<MongoUserKeyType | null>;
744
+ export declare const reserveEscrowPinAttempt: (authProvider: AuthProviderMapping, shareVersion: number, expectedCiphertext?: string, compareId?: string) => Promise<MongoUserKeyType | null>;
745
+ export declare const releaseEscrowPinCompare: (authProvider: AuthProviderMapping, compareId: string) => Promise<void>;
570
746
  export declare const refundEscrowPinAttempt: (authProvider: AuthProviderMapping, shareVersion: number, expectedCiphertext: string) => Promise<void>;
571
747
  export declare const recordEscrowPinFailure: (authProvider: AuthProviderMapping, shareVersion: number, expectedCiphertext: string) => Promise<MongoUserKeyType | null>;
572
748
  export declare const disableEscrowPin: (authProvider: AuthProviderMapping, shareVersion?: number, expectedCiphertext?: string) => Promise<boolean>;
@@ -591,8 +767,30 @@ export interface CompleteIdentityRebindInput {
591
767
  newContactMethod: ContactMethod;
592
768
  primaryDid: string;
593
769
  authShare: ServerEncryptedShare;
770
+ expectedShareVersion?: number;
771
+ expectedSecurityEpoch: number;
594
772
  }
595
773
  export declare const completeIdentityRebind: (input: CompleteIdentityRebindInput) => Promise<{
596
774
  shareVersion: number;
597
775
  methodsRequiringConfirmation: RecoveryMethod["type"][];
598
776
  } | null>;
777
+ export interface LinkAuthProviderKeepingSharesInput {
778
+ oldAuthProvider: AuthProviderMapping;
779
+ newAuthProvider: AuthProviderMapping;
780
+ primaryDid: string;
781
+ shareVersion: number;
782
+ securityEpoch: number;
783
+ }
784
+ export declare const linkAuthProviderKeepingShares: (input: LinkAuthProviderKeepingSharesInput) => Promise<{
785
+ shareVersion: number;
786
+ } | null>;
787
+ export interface RevokeOtherDeviceSharesInput {
788
+ authProvider: AuthProviderMapping;
789
+ primaryDid: string;
790
+ expectedShareVersion: number;
791
+ authShare: ServerEncryptedShare;
792
+ }
793
+ export declare const revokeOtherDeviceShares: (input: RevokeOtherDeviceSharesInput) => Promise<{
794
+ shareVersion: number;
795
+ methodsRequiringConfirmation: RecoveryMethod["type"][];
796
+ } | null>;