@learncard/lca-api-service 1.3.2 → 1.4.0

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 (97) hide show
  1. package/.env.example +26 -2
  2. package/AGENTS.md +59 -44
  3. package/CHANGELOG.md +41 -0
  4. package/compose.env +5 -0
  5. package/dist/accesslayer/signing-authority/index.d.ts +8 -2
  6. package/dist/app.d.ts +297 -8
  7. package/dist/cache/challenges.d.ts +2 -0
  8. package/dist/cache/recoverySessions.d.ts +42 -0
  9. package/dist/config/environment.d.ts +152 -2
  10. package/dist/config/environment.test.d.ts +1 -0
  11. package/dist/helpers/learnCard.signingAuthority.test.d.ts +1 -0
  12. package/dist/helpers/rateLimit.helpers.d.ts +4 -0
  13. package/dist/helpers/seedEncryption.helpers.d.ts +23 -0
  14. package/dist/helpers/seedEncryption.helpers.test.d.ts +1 -0
  15. package/dist/index.js +2 -2
  16. package/dist/migrations/signingAuthoritySeeds.d.ts +13 -0
  17. package/dist/migrations/signingAuthoritySeedsCli.test.d.ts +1 -0
  18. package/dist/models/EscrowHold.d.ts +75 -0
  19. package/dist/models/SigningAuthority.d.ts +7 -1
  20. package/dist/models/UserKey.d.ts +316 -3
  21. package/dist/models/UserKey.escrowPin.test.d.ts +1 -0
  22. package/dist/models/index.d.ts +1 -0
  23. package/dist/routes/escrow.d.ts +150 -0
  24. package/dist/routes/index.d.ts +6 -0
  25. package/dist/routes/keys.d.ts +181 -7
  26. package/dist/routes/qr-login.d.ts +1 -1
  27. package/dist/services/escrow-enclave/index.d.ts +9 -0
  28. package/dist/services/escrow-enclave/notifications.d.ts +9 -0
  29. package/dist/services/escrow-enclave/softwareEnclave.d.ts +17 -0
  30. package/dist/services/escrow-enclave/softwareEnclave.test.d.ts +1 -0
  31. package/dist/services/escrow-enclave/types.d.ts +67 -0
  32. package/dist/types/seed-encryption.d.ts +31 -0
  33. package/dist/types/seed-migration.d.ts +49 -0
  34. package/lambda.ts +8 -3
  35. package/package.json +17 -11
  36. package/scripts/check-seed-encryption-infrastructure.mjs +123 -0
  37. package/scripts/check-signing-authority-redis.mjs +93 -0
  38. package/scripts/migrate-signing-authority-seeds-local.sh +51 -0
  39. package/scripts/migrate-signing-authority-seeds.mjs +105 -0
  40. package/seedMigrationLambda.test.ts +145 -0
  41. package/seedMigrationLambda.ts +50 -0
  42. package/serverless-local.yml +3 -0
  43. package/serverless.escrow-env.cjs +20 -0
  44. package/serverless.yml +133 -0
  45. package/src/accesslayer/signing-authority/create.ts +11 -7
  46. package/src/app.ts +2 -0
  47. package/src/cache/challenges.ts +12 -2
  48. package/src/cache/in-memory-lru.ts +1 -0
  49. package/src/cache/recoverySessions.ts +252 -0
  50. package/src/config/environment.test.ts +116 -0
  51. package/src/config/environment.ts +97 -0
  52. package/src/docker-entry.ts +4 -0
  53. package/src/helpers/learnCard.helpers.test.ts +11 -1
  54. package/src/helpers/learnCard.helpers.ts +57 -45
  55. package/src/helpers/learnCard.signingAuthority.test.ts +194 -0
  56. package/src/helpers/rateLimit.helpers.ts +112 -0
  57. package/src/helpers/seedEncryption.helpers.test.ts +427 -0
  58. package/src/helpers/seedEncryption.helpers.ts +372 -0
  59. package/src/helpers/signingAuthority.helpers.ts +4 -1
  60. package/src/migrations/signingAuthoritySeeds.ts +430 -0
  61. package/src/migrations/signingAuthoritySeedsCli.test.ts +70 -0
  62. package/src/models/EscrowHold.ts +225 -0
  63. package/src/models/SigningAuthority.ts +14 -3
  64. package/src/models/UserKey.escrowPin.test.ts +49 -0
  65. package/src/models/UserKey.ts +1024 -40
  66. package/src/models/index.ts +1 -0
  67. package/src/models/pruneOrphanedRecoveryMethods.ts +3 -1
  68. package/src/routes/credentials.ts +8 -0
  69. package/src/routes/escrow.ts +826 -0
  70. package/src/routes/firebase.ts +90 -48
  71. package/src/routes/index.ts +98 -5
  72. package/src/routes/keys.ts +1017 -174
  73. package/src/routes/qr-login.ts +51 -53
  74. package/src/services/delivery/adapters/postmark.adapter.ts +5 -0
  75. package/src/services/escrow-enclave/README.md +51 -0
  76. package/src/services/escrow-enclave/index.ts +36 -0
  77. package/src/services/escrow-enclave/notifications.ts +14 -0
  78. package/src/services/escrow-enclave/softwareEnclave.test.ts +249 -0
  79. package/src/services/escrow-enclave/softwareEnclave.ts +168 -0
  80. package/src/services/escrow-enclave/types.ts +76 -0
  81. package/src/types/seed-encryption.ts +51 -0
  82. package/src/types/seed-migration.ts +61 -0
  83. package/test/credentials.spec.ts +2 -0
  84. package/test/keys-escrow.spec.ts +1389 -0
  85. package/test/keys-identity-rebind.spec.ts +456 -0
  86. package/test/keys-lifecycle-invariants.spec.ts +785 -0
  87. package/test/keys-recovery-confirmation.spec.ts +586 -0
  88. package/test/keys-recovery-rate-limit.spec.ts +123 -0
  89. package/test/keys-secret-leakage.spec.ts +265 -0
  90. package/test/models/escrow.spec.ts +210 -0
  91. package/test/signing-authority-http.spec.ts +161 -0
  92. package/test/signing-authority-seed-migration.spec.ts +414 -0
  93. package/test/signing-authority.spec.ts +40 -0
  94. package/vitest.config.ts +1 -1
  95. package/vitest.integration.config.ts +5 -0
  96. package/dist/cache/signing-authority.d.ts +0 -6
  97. package/src/cache/signing-authority.ts +0 -37
package/.env.example CHANGED
@@ -4,6 +4,12 @@
4
4
  NODE_ENV=development
5
5
  PORT=3000
6
6
  SEED=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
7
+ # Local development keeps plaintext behavior by default. To opt in, enable encrypted
8
+ # writes and set a key from `openssl rand -hex 32`; retain it for the database lifetime.
9
+ SA_SEED_LOCAL_KEK=
10
+ SA_SEED_KMS_KEY_ARN=
11
+ SA_SEED_ENCRYPT_WRITES=false
12
+ SA_SEED_ALLOW_LEGACY_READ=true
7
13
  MONGO_URI=mongodb://localhost:27017
8
14
  MONGO_DB_NAME=learncard
9
15
  DOMAIN_NAME=
@@ -25,6 +31,24 @@ POSTMARK_ENDORSEMENT_REQUEST_TEMPLATE_ALIAS=
25
31
  POSTMARK_RECOVERY_EMAIL_CODE_TEMPLATE_ALIAS=
26
32
  POSTMARK_RECOVERY_KEY_TEMPLATE_ALIAS=
27
33
 
34
+ ESCROW_RELAY_URL=
35
+ ESCROW_RELAY_AUTH_TOKEN=
36
+
37
+ # Automatic (escrow) recovery. Unset disables all /keys/escrow routes.
38
+ # Local dev: set ESCROW_ENCLAVE_MODE=software with the disposable fixture below, which
39
+ # pairs with the public key in apps/learn-card-app/environments/learncard/config.local.json.
40
+ # The fixture is NOT a secret and must never be used outside local development.
41
+ # See docs/how-to-guides/deploy-infrastructure/sss-key-management-config.md.
42
+ ESCROW_ENCLAVE_MODE=
43
+ ESCROW_ENCLAVE_ACTIVE_KEY_ID=
44
+ ESCROW_ENCLAVE_SOFTWARE_PRIVATE_KEYS_JSON=
45
+ # ESCROW_ENCLAVE_MODE=software
46
+ # ESCROW_ENCLAVE_ACTIVE_KEY_ID=local-dev
47
+ # ESCROW_ENCLAVE_SOFTWARE_PRIVATE_KEYS_JSON={"local-dev":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv1vJWrZWg8ydq4ibGe6/IqEw/BEIcgI+Y3c6hnoeF2ehRANCAAQ08rGhw2reTThF/ecfafLzjphjmqvySHJZwqKfqSn8YPK/1GMWODPWounlJ2MZb/iS/D69K0k9durGsGjaSb0I"}
48
+ # Waiting period in ms (default 7 days). Local QA only: shorten to one minute.
49
+ ESCROW_HOLD_DURATION_MS=
50
+ # ESCROW_HOLD_DURATION_MS=60000
51
+
28
52
  ANDROID_PUSH_ICON=
29
53
  REDIS_HOST=
30
54
  REDIS_PORT=
@@ -34,8 +58,8 @@ TROOP_DASHBOARD_ID=
34
58
 
35
59
  SENTRY_DSN=
36
60
  SENTRY_ENV=development
37
- IS_OFFLINE=false
61
+ IS_OFFLINE=true
38
62
  IS_CI=false
39
63
  IS_E2E_TEST=false
40
64
  SKIP_DIDKIT_NAPI=false
41
- CI=false
65
+ CI=false
package/AGENTS.md CHANGED
@@ -4,22 +4,22 @@ The LearnCard Application API (`lca-api`) is the tRPC service backing client-sid
4
4
 
5
5
  ## Build & Development Commands
6
6
 
7
- - Build: `bun run build`
8
- - Dev: `bun run dev` — watches and rebuilds
9
- - Start: `bun run start` — local server (default port from env, typically 5100)
10
- - Test: `bun run test`
11
- - Run tests once (non-watch): `bun run test -- run` (equivalent to `vitest run`)
12
- - Run single test: `bun run test test/credentials.spec.ts`
13
- - Typecheck: `bunx tsc --noEmit`
7
+ - Build: `bun run build`
8
+ - Dev: `bun run dev` — watches and rebuilds
9
+ - Start: `bun run start` — local server (default port from env, typically 5100)
10
+ - Test: `bun run test`
11
+ - Run tests once (non-watch): `bun run test -- run` (equivalent to `vitest run`)
12
+ - Run single test: `bun run test test/credentials.spec.ts`
13
+ - Typecheck: `bunx tsc --noEmit`
14
14
 
15
15
  ## Code Style Guidelines
16
16
 
17
- - **TypeScript**: Strict typing, interfaces in dedicated files under `src/types/`
18
- - **Imports**: Path aliases (`@helpers/*`, `@routes/*`, `@cache/*`)
19
- - **Routes**: tRPC routers under `src/routes/` — one router per resource
20
- - **Error handling**: `TRPCError` with specific codes (`BAD_REQUEST`, `UNAUTHORIZED`, `INTERNAL_SERVER_ERROR`, `NOT_FOUND`)
21
- - **Named exports only** — no one-line default exports
22
- - **Explicit return types** on all exported functions
17
+ - **TypeScript**: Strict typing, interfaces in dedicated files under `src/types/`
18
+ - **Imports**: Path aliases (`@helpers/*`, `@routes/*`, `@cache/*`)
19
+ - **Routes**: tRPC routers under `src/routes/` — one router per resource
20
+ - **Error handling**: `TRPCError` with specific codes (`BAD_REQUEST`, `UNAUTHORIZED`, `INTERNAL_SERVER_ERROR`, `NOT_FOUND`)
21
+ - **Named exports only** — no one-line default exports
22
+ - **Explicit return types** on all exported functions
23
23
 
24
24
  ## Request Lifecycle & Context
25
25
 
@@ -32,33 +32,48 @@ Every request goes through `createContext` in `src/routes/index.ts`, which:
32
32
 
33
33
  ### Route Procedures
34
34
 
35
- - `openRoute` — unauthenticated, tenant + Sentry tracing attached
36
- - `didRoute` — requires `ctx.user.did` (DID-auth VP verified, challenge may be stale)
37
- - `didAndChallengeRoute` — requires `didRoute` + live challenge
38
- - `authorizedDidRoute` — requires `didRoute` + DID in `AUTHORIZED_DIDS` env allowlist
35
+ - `openRoute` — unauthenticated, tenant + Sentry tracing attached
36
+ - `didRoute` — requires `ctx.user.did` (DID-auth VP verified, challenge may be stale)
37
+ - `didAndChallengeRoute` — requires `didRoute` + live challenge
38
+ - `authorizedDidRoute` — requires `didRoute` + DID in `AUTHORIZED_DIDS` env allowlist
39
39
 
40
40
  ## SSS Key Management Routes (`src/routes/keys.ts`)
41
41
 
42
42
  The `keysRouter` implements the server side of Shamir Secret Sharing key management used by `@learncard/sss-key-manager`:
43
43
 
44
- | Route | Purpose |
45
- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
46
- | `POST /keys/auth-share` | Fetch the encrypted auth share for a user by contact method + provider |
47
- | `PUT /keys/auth-share` | Store / rotate the auth share; manages `shareVersion` for historical lookup |
48
- | `GET /keys/recovery` | Fetch an encrypted recovery share (passkey / phrase / backup) |
49
- | `POST /keys/recovery` | Register a recovery method (passkey / phrase / backup / email) |
50
- | `POST /keys/recovery-email` | Start recovery email verification (sends 6-digit OTP) |
51
- | `POST /keys/recovery-email/verify` | Verify the OTP and persist the recovery email on the UserKey |
52
- | `POST /keys/email-backup` | Relay the emailed recovery share to the user's primary or recovery email. **Share is never persisted** — in-memory for the duration of the request. |
53
- | `POST /keys/upgrade-contact-method` | Upgrade phone-only users to email + phone |
54
- | `POST /keys/migrate` | Mark a UserKey as migrated from the legacy Web3Auth pathway |
44
+ | Route | Purpose |
45
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------- |
46
+ | `POST /keys/auth-share` | Fetch the encrypted auth share for a user by contact method + provider |
47
+ | `PUT /keys/auth-share` | Store / rotate the auth share; manages `shareVersion` for historical lookup |
48
+ | `GET /keys/recovery` | Fetch an encrypted recovery share (passkey / phrase / backup) |
49
+ | `POST /keys/recovery` | Register a recovery method (passkey / phrase / backup / email) |
50
+ | `POST /keys/recovery-email` | Start recovery email verification (sends 6-digit OTP) |
51
+ | `POST /keys/recovery-email/verify` | Verify the OTP and persist the recovery email on the UserKey |
52
+ | `POST /keys/email-backup` | Proxy a client-encrypted recovery payload to the isolated email relay. The share is never visible to `lca-api`. |
53
+ | `POST /keys/upgrade-contact-method` | Upgrade phone-only users to email + phone |
54
+ | `POST /keys/migrate` | Mark a UserKey as migrated from the legacy Web3Auth pathway |
55
+
56
+ ### Escrow recovery routes
57
+
58
+ The `escrow` tRPC router is disabled unless `ESCROW_ENCLAVE_MODE` is configured.
59
+
60
+ | Route | Procedure | Purpose |
61
+ | ------------------------------ | ------------------------- | ------------------------------------------------------------------ |
62
+ | `GET /keys/escrow/attestation` | `escrow.getAttestation` | Enclave public key and hold duration |
63
+ | `POST /keys/escrow` | `escrow.enroll` | Verify and store automatic recovery material |
64
+ | `DELETE /keys/escrow` | `escrow.remove` | Remove material and optionally opt out |
65
+ | `POST /keys/escrow/opt-in` | `escrow.optIn` | Allow enrollment again |
66
+ | `POST /keys/escrow/recover` | `escrow.startRecovery` | Start a waiting period without resetting an existing hold |
67
+ | `GET /keys/escrow/status` | `escrow.getStatus` | Check status; active devices send provider auth via `X-Auth-Token` |
68
+ | `POST /keys/escrow/cancel` | `escrow.cancelRecovery` | DID owner cancels a pending hold |
69
+ | `POST /keys/escrow/complete` | `escrow.completeRecovery` | Claim an elapsed hold and release sealed recovery material once |
55
70
 
56
71
  ### Important Invariants
57
72
 
58
- - **Auth shares are encrypted at rest** with a KEK derived from `SEED`. Losing `SEED` means every stored auth share is permanently unrecoverable.
59
- - **Recovery shares are encrypted client-side** (except the phrase method, which stores only shareVersion metadata).
60
- - **Email share is never written to the DB.** The `sendEmailBackup` route holds it in memory only for the duration of the request; Postmark receives it but never stores it.
61
- - **`shareVersion` pairs a device share with a matching auth share.** On rotation, previous versions are kept in `previousAuthShares` so users with stale device shares can still recover.
73
+ - **Auth shares are encrypted at rest** with a KEK derived from `SEED`. Losing `SEED` means every stored auth share is permanently unrecoverable.
74
+ - **Recovery shares are encrypted client-side** (except the phrase method, which stores only shareVersion metadata).
75
+ - **Email share is encrypted before leaving the client.** The `sendEmailBackup` route receives only a relay-key ciphertext envelope and a confirmation code, then waits for isolated relay acceptance before recording pending recovery metadata.
76
+ - **`shareVersion` pairs a device share with a matching auth share.** On rotation, previous versions are kept in `previousAuthShares` so users with stale device shares can still recover.
62
77
 
63
78
  ## Email Delivery & Tenant Branding
64
79
 
@@ -90,10 +105,10 @@ The adapter additionally re-throws when local rendering fails for a **sentinel a
90
105
 
91
106
  ### Rules for Route Authors
92
107
 
93
- - **Always pass `branding: ctx.tenant?.emailBranding`** to `getDeliveryService().send()`. Calls without branding render with LearnCard defaults regardless of the caller's tenant.
94
- - **Use `getFrom({ mailbox, branding: ctx.tenant?.emailBranding })`** for the `from` field so the domain matches the tenant (e.g. `recovery@vetpass.app`).
95
- - **Use the local template ID as the `templateAlias`** — e.g. `'recovery-key'`, `'recovery-email-code'`, `'login-verification-code'`, `'endorsement-request'`. These are pre-registered as sentinels in `LOCAL_TEMPLATE_ALIASES`. Env-var overrides (`POSTMARK_*_TEMPLATE_ALIAS`) are optional; `templateAlias: RECOVERY_KEY_TEMPLATE_ALIAS || 'recovery-key'` is the canonical pattern.
96
- - **Do not write plain-text fallbacks.** The adapter renders the React Email template regardless; plain-text branches are dead code and should be deleted on sight.
108
+ - **Always pass `branding: ctx.tenant?.emailBranding`** to `getDeliveryService().send()`. Calls without branding render with LearnCard defaults regardless of the caller's tenant.
109
+ - **Use `getFrom({ mailbox, branding: ctx.tenant?.emailBranding })`** for the `from` field so the domain matches the tenant (e.g. `recovery@vetpass.app`).
110
+ - **Use the local template ID as the `templateAlias`** — e.g. `'recovery-key'`, `'recovery-email-code'`, `'login-verification-code'`, `'endorsement-request'`. These are pre-registered as sentinels in `LOCAL_TEMPLATE_ALIASES`. Env-var overrides (`POSTMARK_*_TEMPLATE_ALIAS`) are optional; `templateAlias: RECOVERY_KEY_TEMPLATE_ALIAS || 'recovery-key'` is the canonical pattern.
111
+ - **Do not write plain-text fallbacks.** The adapter renders the React Email template regardless; plain-text branches are dead code and should be deleted on sight.
97
112
 
98
113
  ### Env Vars
99
114
 
@@ -108,14 +123,14 @@ See [`docs/how-to-guides/deploy-infrastructure/sss-key-management-config.md`](..
108
123
 
109
124
  ## Testing Notes
110
125
 
111
- - Test files in `test/` spin up a Fastify server via `getClient()` and exercise routes with `fetch`.
112
- - `IS_E2E_TEST=true` disables Firebase Admin SDK calls and switches email delivery to the log adapter.
113
- - For tenant-aware tests, set `X-Tenant-Id` on the request; `createContext` will pick it up and `ctx.tenant.emailBranding` will be populated from the registry in `@learncard/email-templates`.
126
+ - Test files using `getClient()` exercise tRPC procedures via `appRouter.createCaller()`, not an HTTP server. Header-context tests inject `providerToken`; they do not verify HTTP header extraction or CORS.
127
+ - `IS_E2E_TEST=true` disables Firebase Admin SDK calls and switches email delivery to the log adapter.
128
+ - For tenant-aware tests, set `X-Tenant-Id` on the request; `createContext` will pick it up and `ctx.tenant.emailBranding` will be populated from the registry in `@learncard/email-templates`.
114
129
 
115
130
  ## Cross-References
116
131
 
117
- - [Tenant-Branded Emails (architecture)](../../../docs/core-concepts/tenant-branded-emails.md) — end-to-end email flow
118
- - [Configure Tenant-Branded Emails](../../../docs/how-to-guides/configure-tenant-branded-emails.md) — operator-facing setup
119
- - [SSS Key Management Config](../../../docs/how-to-guides/deploy-infrastructure/sss-key-management-config.md) — env var reference for deployers
120
- - [`@learncard/email-templates` README](../../../packages/email-templates/README.md) — template catalog, tenant registry, preview server
121
- - [`@learncard/sss-key-manager`](../../../packages/sss-key-manager/README.md) — client side of SSS key management
132
+ - [Tenant-Branded Emails (architecture)](../../../docs/core-concepts/tenant-branded-emails.md) — end-to-end email flow
133
+ - [Configure Tenant-Branded Emails](../../../docs/how-to-guides/configure-tenant-branded-emails.md) — operator-facing setup
134
+ - [SSS Key Management Config](../../../docs/how-to-guides/deploy-infrastructure/sss-key-management-config.md) — env var reference for deployers
135
+ - [`@learncard/email-templates` README](../../../packages/email-templates/README.md) — template catalog, tenant registry, preview server
136
+ - [`@learncard/sss-key-manager`](../../../packages/sss-key-manager/README.md) — client side of SSS key management
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @welibraryos/lca-api-service
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1565](https://github.com/learningeconomy/LearnCard/pull/1565) [`4b83aa9ee802f0d7a54adc5f053a7376fa103cdd`](https://github.com/learningeconomy/LearnCard/commit/4b83aa9ee802f0d7a54adc5f053a7376fa103cdd) Thanks [@Custard7](https://github.com/Custard7)! - Escrow PIN fast release: an optional PIN release policy on the existing escrow-recovery substrate.
8
+
9
+ - The escrow recovery share now supports a second release policy alongside the 7-day hold: `pin`. A user who sets an optional 6-12 digit PIN can recover immediately by presenting it, instead of waiting for the hold. Custodianship is unchanged — this adds a release policy on the already-sealed blob, not a second custodial share.
10
+ - PIN verifier lives inside the enclave-sealed envelope (`EscrowBlobPlaintext.pinVerifier`); a leaked DB alone yields nothing to brute force. Client derives the proof with Argon2id and the enclave constant-time compares.
11
+ - 10 lifetime failed attempts locks the PIN (`EscrowPinLockedError`); the 7-day hold path is always available as a fallback. Setting/changing a PIN rotates the escrow share.
12
+ - Carry the sealed PIN verifier across share rotations while preserving its salt and attempt counters; explicit removal and unavailable carry still enroll without a PIN.
13
+ - New app UI: recovery PIN setup overlay after first setup (skippable), a Recovery PIN row in recovery settings (set/change/remove), and a PIN-first step in the recovery flow with fallback to the existing hold.
14
+
15
+ - [#1529](https://github.com/learningeconomy/LearnCard/pull/1529) [`0e262737aafb8248e88e05039c83a3f30be6750f`](https://github.com/learningeconomy/LearnCard/commit/0e262737aafb8248e88e05039c83a3f30be6750f) Thanks [@Custard7](https://github.com/Custard7)! - SSS prod hardening: confirmed recovery enrollment, lost-login identity rebind, and an isolated email relay.
16
+
17
+ - Recovery methods now carry `confirmedAt` and must be proven before they count (email confirmation code, phrase challenge words, backup re-decrypt, passkey round trip). New SSS accounts are `active` immediately; web3auth migrations stay `provisional` until a method is confirmed.
18
+ - New recovery-session flow lets a user whose sign-in identity is gone recover via a verified personal email and bind a new sign-in.
19
+ - Email recovery shares are encrypted on the client to an isolated relay's public key; `lca-api` never sees plaintext. Provider tokens move from query strings to the `X-Auth-Token` header.
20
+ - Sensitive key routes require a single-use DID challenge; key records are keyed by immutable provider ID.
21
+ - `recovery-key` email template now requires `confirmationCode`.
22
+ - Auth-share initialization requires the provider uniqueness index before inserting, preventing competing first writes from creating duplicate accounts. Existing records with missing or BSON-null auth material retain atomic update protection.
23
+ - Automatic stale-key cleanup preserves unresolved pending shares for delayed commits; explicit device forgetting still removes them. Custom SSS storage adapters now implement `deleteDeviceShare(id?)` to remove only the selected share and its version.
24
+
25
+ ### Patch Changes
26
+
27
+ - [#1601](https://github.com/learningeconomy/LearnCard/pull/1601) [`3850da277fe17d421bc055cef3cac65616b7626d`](https://github.com/learningeconomy/LearnCard/commit/3850da277fe17d421bc055cef3cac65616b7626d) Thanks [@rhen92](https://github.com/rhen92)! - fix: [LC-2207] Limit wrong code attempts on email login verification
28
+
29
+ - [#1636](https://github.com/learningeconomy/LearnCard/pull/1636) [`dab95a1313b10f0339a4284cb5dcdc2f459dcdd5`](https://github.com/learningeconomy/LearnCard/commit/dab95a1313b10f0339a4284cb5dcdc2f459dcdd5) Thanks [@Custard7](https://github.com/Custard7)! - Fix lca-api deploys failing on Lambda's 4KB environment limit: escrow variables are now attached only to the trpc/api functions, omitted when unset, and enclave settings are gated on `ESCROW_ENCLAVE_MODE`. SSS client errors now include the HTTP status code (HTTP/2 responses have an empty `statusText`).
30
+
31
+ - [#1619](https://github.com/learningeconomy/LearnCard/pull/1619) [`5639d80a9d111f8156469e6692c37ffb354d2ec7`](https://github.com/learningeconomy/LearnCard/commit/5639d80a9d111f8156469e6692c37ffb354d2ec7) Thanks [@gerardopar](https://github.com/gerardopar)! - feat: [LC-2161] - Encrypt hosted signing-authority seeds with AWS KMS
32
+
33
+ - Updated dependencies [[`4b83aa9ee802f0d7a54adc5f053a7376fa103cdd`](https://github.com/learningeconomy/LearnCard/commit/4b83aa9ee802f0d7a54adc5f053a7376fa103cdd), [`f7b54c45cf495e14cc7b2824653c8f0a63b61660`](https://github.com/learningeconomy/LearnCard/commit/f7b54c45cf495e14cc7b2824653c8f0a63b61660), [`dab95a1313b10f0339a4284cb5dcdc2f459dcdd5`](https://github.com/learningeconomy/LearnCard/commit/dab95a1313b10f0339a4284cb5dcdc2f459dcdd5), [`b184f5552abf37a11468d435e5cea07fc35e1993`](https://github.com/learningeconomy/LearnCard/commit/b184f5552abf37a11468d435e5cea07fc35e1993), [`0e262737aafb8248e88e05039c83a3f30be6750f`](https://github.com/learningeconomy/LearnCard/commit/0e262737aafb8248e88e05039c83a3f30be6750f), [`d436c39e00994e6d8c77e7b94985a51fe1f42adb`](https://github.com/learningeconomy/LearnCard/commit/d436c39e00994e6d8c77e7b94985a51fe1f42adb)]:
34
+ - @learncard/sss-key-manager@0.2.0
35
+ - @learncard/types@5.22.0
36
+ - @learncard/email-templates@1.2.0
37
+ - @learncard/helpers@1.6.1
38
+ - @learncard/core@9.4.37
39
+ - @learncard/init@2.5.1
40
+ - @learncard/did-web-plugin@1.1.37
41
+ - @learncard/didkit-plugin@1.10.2
42
+ - @learncard/didkit-plugin-node@0.3.2
43
+
3
44
  ## 1.3.2
4
45
 
5
46
  ### Patch Changes
package/compose.env ADDED
@@ -0,0 +1,5 @@
1
+ # Local Docker defaults only; encryption is opt-in and requires an explicit KEK.
2
+ # Load before optional .env files so existing custom keys and rollout settings win.
3
+ # For persistent data, configure a unique KEK before the first encrypted write and retain it.
4
+ SA_SEED_ENCRYPT_WRITES=false
5
+ SA_SEED_ALLOW_LEGACY_READ=true
@@ -1,16 +1,22 @@
1
1
  export declare const getSigningAuthoritiesCollection: () => import("mongodb").Collection<{
2
2
  ownerDid: string;
3
3
  name: string;
4
- seed: string;
5
4
  _id?: string | undefined;
5
+ seed?: string | undefined;
6
+ encryptedSeed?: string | undefined;
7
+ encryptedDek?: string | undefined;
8
+ keyVersion?: "kms-v1" | "local-v1" | undefined;
6
9
  did?: string | undefined;
7
10
  endpoint?: string | undefined;
8
11
  }>;
9
12
  export declare const SigningAuthorities: import("mongodb").Collection<{
10
13
  ownerDid: string;
11
14
  name: string;
12
- seed: string;
13
15
  _id?: string | undefined;
16
+ seed?: string | undefined;
17
+ encryptedSeed?: string | undefined;
18
+ encryptedDek?: string | undefined;
19
+ keyVersion?: "kms-v1" | "local-v1" | undefined;
14
20
  did?: string | undefined;
15
21
  endpoint?: string | undefined;
16
22
  }>;