@learncard/lca-api-service 1.4.0 → 1.5.1

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 (191) hide show
  1. package/.env.example +73 -0
  2. package/AGENTS.md +106 -10
  3. package/CHANGELOG.md +52 -0
  4. package/bootstrapLambda.test.ts +76 -0
  5. package/bundle.mjs +9 -1
  6. package/config/config.dev.json +23 -0
  7. package/config/config.example.json +22 -0
  8. package/config/config.json +1 -0
  9. package/config/config.production.json +12 -0
  10. package/config/config.scouts.dev.json +11 -0
  11. package/config/config.scouts.production.json +11 -0
  12. package/dist/app.d.ts +67 -3
  13. package/dist/cache/getDel.d.ts +2 -0
  14. package/dist/cache/getDel.test.d.ts +1 -0
  15. package/dist/cache/login-tickets.d.ts +12 -0
  16. package/dist/config/applyDockerStageConfig.d.ts +1 -0
  17. package/dist/config/cacheEnvironment.d.ts +16 -0
  18. package/dist/config/environment.d.ts +101 -0
  19. package/dist/config/escrowBundleMode.test.d.ts +1 -0
  20. package/dist/config/escrowDeployment.test.d.ts +1 -0
  21. package/dist/config/oidcEnvironment.d.ts +74 -0
  22. package/dist/config/serverlessFunctionEnv.test.d.ts +1 -0
  23. package/dist/config/stageConfig.d.ts +62 -0
  24. package/dist/config/stageConfig.test.d.ts +1 -0
  25. package/dist/didkit_wasm_bg.wasm +0 -0
  26. package/dist/docker-entry.d.ts +1 -1
  27. package/dist/helpers/auth.helpers.d.ts +1 -1
  28. package/dist/helpers/escrowCancelLink.d.ts +7 -0
  29. package/dist/helpers/escrowCancelLink.test.d.ts +1 -0
  30. package/dist/helpers/escrowCancelToken.d.ts +4 -0
  31. package/dist/helpers/escrowCancelToken.test.d.ts +1 -0
  32. package/dist/helpers/keycloak.helpers.d.ts +23 -0
  33. package/dist/helpers/login-ticket.helpers.d.ts +31 -0
  34. package/dist/helpers/login-ticket.helpers.test.d.ts +1 -0
  35. package/dist/helpers/oidc.helpers.d.ts +47 -0
  36. package/dist/helpers/rate-limit.helpers.d.ts +6 -0
  37. package/dist/helpers/rate-limit.helpers.test.d.ts +1 -0
  38. package/dist/helpers/social-token.helpers.d.ts +19 -0
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.js +2 -2
  41. package/dist/jobs/escrowBlobRewrap.d.ts +9 -0
  42. package/dist/jobs/escrowHoldReminders.d.ts +12 -0
  43. package/dist/models/AuthSubject.d.ts +20 -0
  44. package/dist/models/EscrowHold.d.ts +79 -1
  45. package/dist/models/Notification.d.ts +5 -0
  46. package/dist/models/UserKey.d.ts +11 -1
  47. package/dist/models/index.d.ts +1 -0
  48. package/dist/oidc.d.ts +5 -0
  49. package/dist/publish-metafile.json +1 -0
  50. package/dist/routes/auth.d.ts +31 -0
  51. package/dist/routes/escrow.d.ts +18 -4
  52. package/dist/routes/keys.d.ts +1 -0
  53. package/dist/routes/notifications.d.ts +5 -0
  54. package/dist/routes/signing-authority.d.ts +14 -0
  55. package/dist/services/escrow-enclave/blobIdentity.d.ts +10 -0
  56. package/dist/services/escrow-enclave/blobIdentity.test.d.ts +1 -0
  57. package/dist/services/escrow-enclave/cancellation.d.ts +3 -0
  58. package/dist/services/escrow-enclave/enclaveTrustAnchors.d.ts +1 -0
  59. package/dist/services/escrow-enclave/index.d.ts +16 -0
  60. package/dist/services/escrow-enclave/notifications.d.ts +19 -4
  61. package/dist/services/escrow-enclave/notifications.test.d.ts +1 -0
  62. package/dist/services/escrow-enclave/production.test.d.ts +1 -0
  63. package/dist/services/escrow-enclave/remoteEnclave.contract.test.d.ts +1 -0
  64. package/dist/services/escrow-enclave/remoteEnclave.d.ts +20 -0
  65. package/dist/services/escrow-enclave/remoteEnclave.test.d.ts +1 -0
  66. package/dist/services/escrow-enclave/remoteEnclave.tls.test.d.ts +1 -0
  67. package/dist/services/escrow-enclave/softwareEnclave.d.ts +12 -2
  68. package/dist/services/escrow-enclave/types.d.ts +52 -8
  69. package/dist/types/notifications.d.ts +5 -0
  70. package/esbuildPlugins.cjs +1 -0
  71. package/lambda.ts +27 -117
  72. package/lambdaApp.ts +146 -0
  73. package/oidcLambda.test.ts +38 -0
  74. package/oidcLambda.ts +18 -0
  75. package/oidcLambdaApp.ts +11 -0
  76. package/package.json +22 -13
  77. package/project.json +8 -1
  78. package/scripts/README.md +88 -0
  79. package/scripts/check-seed-encryption-infrastructure.mjs +18 -2
  80. package/scripts/escrow-blob-mode-report.test.ts +227 -0
  81. package/scripts/escrow-blob-mode-report.ts +465 -0
  82. package/scripts/keycloak-admin.ts +108 -0
  83. package/scripts/keycloak-provider-links.ts +147 -0
  84. package/scripts/print-seed-rollout-flags.ts +21 -0
  85. package/scripts/provision-keycloak-users.ts +419 -0
  86. package/seedMigrationApp.test.ts +145 -0
  87. package/seedMigrationApp.ts +50 -0
  88. package/seedMigrationLambda.test.ts +30 -135
  89. package/seedMigrationLambda.ts +15 -46
  90. package/serverless-local.yml +25 -1
  91. package/serverless.function-env.cjs +58 -0
  92. package/serverless.yml +174 -37
  93. package/src/accesslayer/notifications/index.ts +9 -2
  94. package/src/accesslayer/signing-authority/index.ts +4 -1
  95. package/src/app.ts +2 -0
  96. package/src/cache/getDel.test.ts +34 -0
  97. package/src/cache/getDel.ts +32 -0
  98. package/src/cache/index.ts +1 -1
  99. package/src/cache/login-tickets.ts +26 -0
  100. package/src/config/applyDockerStageConfig.ts +6 -0
  101. package/src/config/cacheEnvironment.ts +34 -0
  102. package/src/config/environment.test.ts +45 -2
  103. package/src/config/environment.ts +90 -23
  104. package/src/config/escrowBundleMode.test.ts +52 -0
  105. package/src/config/escrowDeployment.test.ts +114 -0
  106. package/src/config/oidcEnvironment.ts +59 -0
  107. package/src/config/serverlessFunctionEnv.test.ts +98 -0
  108. package/src/config/stageConfig.test.ts +26 -0
  109. package/src/config/stageConfig.ts +24 -0
  110. package/src/docker-entry.ts +9 -0
  111. package/src/helpers/auth.helpers.ts +68 -30
  112. package/src/helpers/escrowCancelLink.test.ts +60 -0
  113. package/src/helpers/escrowCancelLink.ts +38 -0
  114. package/src/helpers/escrowCancelToken.test.ts +105 -0
  115. package/src/helpers/escrowCancelToken.ts +47 -0
  116. package/src/helpers/keycloak.helpers.ts +120 -0
  117. package/src/helpers/login-ticket.helpers.test.ts +45 -0
  118. package/src/helpers/login-ticket.helpers.ts +91 -0
  119. package/src/helpers/oidc.helpers.ts +354 -0
  120. package/src/helpers/rate-limit.helpers.test.ts +61 -0
  121. package/src/helpers/rate-limit.helpers.ts +73 -0
  122. package/src/helpers/social-token.helpers.ts +144 -0
  123. package/src/index.ts +1 -0
  124. package/src/jobs/escrowBlobRewrap.ts +121 -0
  125. package/src/jobs/escrowHoldReminders.ts +177 -0
  126. package/src/models/AuthSubject.ts +147 -0
  127. package/src/models/EscrowHold.ts +321 -5
  128. package/src/models/UserKey.ts +91 -6
  129. package/src/models/index.ts +1 -0
  130. package/src/oidc.ts +371 -0
  131. package/src/routes/auth.ts +173 -0
  132. package/src/routes/escrow.ts +281 -34
  133. package/src/routes/keys.ts +5 -0
  134. package/src/routes/signing-authority.ts +39 -1
  135. package/src/services/delivery/adapters/postmark.adapter.ts +43 -1
  136. package/src/services/escrow-enclave/README.md +81 -10
  137. package/src/services/escrow-enclave/__fixtures__/tls/pinned-ca.pem +11 -0
  138. package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.key +5 -0
  139. package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.pem +12 -0
  140. package/src/services/escrow-enclave/__fixtures__/tls/rogue-ca.pem +11 -0
  141. package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.key +5 -0
  142. package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.pem +12 -0
  143. package/src/services/escrow-enclave/blobIdentity.test.ts +42 -0
  144. package/src/services/escrow-enclave/blobIdentity.ts +55 -0
  145. package/src/services/escrow-enclave/cancellation.ts +59 -0
  146. package/src/services/escrow-enclave/enclaveTrustAnchors.ts +34 -0
  147. package/src/services/escrow-enclave/index.ts +145 -7
  148. package/src/services/escrow-enclave/notifications.test.ts +377 -0
  149. package/src/services/escrow-enclave/notifications.ts +340 -6
  150. package/src/services/escrow-enclave/production.test.ts +34 -0
  151. package/src/services/escrow-enclave/remoteEnclave.contract.test.ts +344 -0
  152. package/src/services/escrow-enclave/remoteEnclave.test.ts +490 -0
  153. package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +62 -0
  154. package/src/services/escrow-enclave/remoteEnclave.ts +263 -0
  155. package/src/services/escrow-enclave/softwareEnclave.test.ts +231 -30
  156. package/src/services/escrow-enclave/softwareEnclave.ts +97 -11
  157. package/src/services/escrow-enclave/types.ts +63 -17
  158. package/test/auth-tickets.spec.ts +480 -0
  159. package/test/helpers/keycloak-broker.ts +181 -0
  160. package/test/helpers/live-mongo.ts +9 -0
  161. package/test/jobs/escrowBlobRewrap.spec.ts +262 -0
  162. package/test/jobs/escrowHoldReminders.spec.ts +319 -0
  163. package/test/keycloak-admin.spec.ts +58 -0
  164. package/test/keycloak-broker-roundtrip.integration.spec.ts +40 -0
  165. package/test/keycloak-migration.integration.spec.ts +266 -0
  166. package/test/keycloak-provider-links.spec.ts +201 -0
  167. package/test/keycloak-social-broker.integration.spec.ts +228 -0
  168. package/test/keycloak-verify.integration.spec.ts +100 -0
  169. package/test/keycloak-verify.spec.ts +307 -0
  170. package/test/keys-escrow.spec.ts +963 -24
  171. package/test/models/authSubjectIndexes.spec.ts +132 -0
  172. package/test/models/escrow.spec.ts +375 -13
  173. package/test/notifications.spec.ts +4 -0
  174. package/test/oidc-e2e/broker.spec.ts +116 -0
  175. package/test/oidc-e2e/compose.yaml +33 -0
  176. package/test/oidc-e2e/fixtures.ts +245 -0
  177. package/test/oidc-e2e/lock.test.mjs +73 -0
  178. package/test/oidc-e2e/playwright.config.ts +21 -0
  179. package/test/oidc-e2e/run.mjs +312 -0
  180. package/test/oidc-e2e/tsconfig.json +10 -0
  181. package/test/oidc-e2e/types.ts +21 -0
  182. package/test/oidc.integration.spec.ts +54 -0
  183. package/test/oidc.spec.ts +697 -0
  184. package/test/oidcLambdaApp.boot.spec.ts +105 -0
  185. package/test/provision-keycloak-users.spec.ts +299 -0
  186. package/test/signing-authority.spec.ts +50 -0
  187. package/tsconfig.build.json +1 -1
  188. package/tsconfig.json +1 -0
  189. package/vitest.config.ts +24 -1
  190. package/vitest.integration.config.ts +20 -3
  191. package/serverless.escrow-env.cjs +0 -20
package/.env.example CHANGED
@@ -1,7 +1,19 @@
1
1
  # LCA API environment
2
2
  # Copy to .env. Validation runs before database/client initialization.
3
+ #
4
+ # Config resolves lowest→highest: schema defaults < config/config.json <
5
+ # config/config.<stage>.json < runtime AWS secrets bundle (Lambda only) < real env.
6
+ # Empty values are unset. The config/config*.json stage files are PUBLIC BY DESIGN:
7
+ # put only non-secret config there. Secrets (credentials, AUTHORIZED_DIDS allowlist,
8
+ # internal endpoints, signing keys) stay in the runtime bundle or this local .env.
9
+ # Stage = LAMBDA_STAGE on Lambda, else CONFIG_STAGE; unknown stage uses config.json only.
10
+ # Self-hosting: copy config/config.example.json to a stage file, or just use this .env.
11
+ # Keep the entries you need locally even once a stage uses the runtime bundle.
3
12
 
4
13
  NODE_ENV=development
14
+ # Lambda sets LAMBDA_STAGE; elsewhere CONFIG_STAGE selects the config.<stage>.json file.
15
+ LAMBDA_STAGE=
16
+ CONFIG_STAGE=
5
17
  PORT=3000
6
18
  SEED=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
7
19
  # Local development keeps plaintext behavior by default. To opt in, enable encrypted
@@ -18,10 +30,57 @@ AUTHORIZED_DIDS=
18
30
  LEARN_CLOUD_URL=
19
31
  NETWORK_BRAIN_SERVICE_URL=
20
32
  GOOGLE_APPLICATION_CREDENTIAL=
33
+ # Lambda deploys may instead set RUNTIME_SECRETS_ID=lca-api/<stage>/runtime-secrets.
34
+ # This secret bundle holds secrets only; non-secret per-stage config lives in the
35
+ # checked-in config/config.<stage>.json files, not here and not in the bundle.
36
+ # SecretString: flat env-name -> string JSON (including the Firebase JSON as a string).
37
+ # Loaded before validation; non-empty explicit env wins. Leave unset for local/Docker/CI.
38
+ # Keep all GitHub runtime secret fallbacks until each stage has a complete bundle.
39
+ RUNTIME_SECRETS_ID=
21
40
  OPENAI_API_KEY=
22
41
  METABASE_SECRET_KEY=
23
42
  SCOUTS_SSO_CLIENT_SECRET=
24
43
 
44
+ # Comma-separated exact Keycloak issuers (e.g. http://localhost:8081/realms/learncard).
45
+ KEYCLOAK_ISSUERS=
46
+ # Comma-separated allowed client IDs (e.g. learncard-app); matches aud or azp.
47
+ KEYCLOAK_AUDIENCES=
48
+ # Optional issuer=url pairs for internal JWKS endpoints, separated by commas.
49
+ # Example: http://localhost:8081/realms/learncard=http://keycloak:8080/realms/learncard/protocol/openid-connect/certs
50
+ KEYCLOAK_JWKS_URL_OVERRIDES=
51
+
52
+ # Minimal self-issued OIDC provider (AD-2). Keycloak trusts lca-api as an
53
+ # upstream Identity Provider: the app proves ownership in-app, exchanges the
54
+ # proof for a single-use login ticket, and Keycloak redeems it via the
55
+ # authorization-code flow (/oidc/authorize -> /oidc/token).
56
+ # OIDC_ISSUER: REQUIRED issuer identifier baked into discovery/tokens (no
57
+ # default; unset = 503).
58
+ # OIDC_SIGNING_KEY_JWK: local/Docker/CI override, JSON RSA private JWK with d, kid
59
+ # and alg RS256, SEPARATE from SEED. Takes precedence over the secret id.
60
+ # OIDC_SIGNING_KEY_SECRET_ID: deployed Lambda secret name or ARN, convention
61
+ # lca-api/<stage>/oidc-signing-jwk in the Lambda account/region. SecretString
62
+ # must be a plain string containing the same JWK JSON (no wrapper).
63
+ # Fetched and cached per process; failed loads retry and readiness returns 503.
64
+ # Redeploy after rotation. Neither source set: prod = 503; dev/CI = ephemeral key.
65
+ # OIDC_CLIENT_ID / OIDC_CLIENT_SECRET: credentials of the confidential Keycloak
66
+ # broker client that calls /oidc/token (default id 'keycloak-broker').
67
+ # OIDC_CLIENT_SECRET stays a GitHub-environment secret on the OIDC function and
68
+ # is NEVER placed in the runtime bundle; its signing key uses a separate AWS secret.
69
+ # OIDC_REDIRECT_URIS: optional redirect-uri allowlist override (defaults to
70
+ # each KEYCLOAK_ISSUERS entry plus /broker/lca-api/endpoint). Exact match only;
71
+ # when neither list is set every redirect is rejected.
72
+ OIDC_ISSUER=
73
+ OIDC_SIGNING_KEY_JWK=
74
+ OIDC_SIGNING_KEY_SECRET_ID=
75
+ OIDC_CLIENT_ID=
76
+ OIDC_CLIENT_SECRET=
77
+ OIDC_REDIRECT_URIS=
78
+
79
+ # Native Google/Apple social login: comma-separated allowed client ids.
80
+ # Issuers and JWKS are fixed to the respective provider.
81
+ GOOGLE_OAUTH_CLIENT_IDS=
82
+ APPLE_OAUTH_CLIENT_IDS=
83
+
25
84
  # Configure both Postmark credentials or leave both empty for log delivery.
26
85
  POSTMARK_SERVER_TOKEN=
27
86
  POSTMARK_FROM_EMAIL=
@@ -47,8 +106,22 @@ ESCROW_ENCLAVE_SOFTWARE_PRIVATE_KEYS_JSON=
47
106
  # ESCROW_ENCLAVE_SOFTWARE_PRIVATE_KEYS_JSON={"local-dev":"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv1vJWrZWg8ydq4ibGe6/IqEw/BEIcgI+Y3c6hnoeF2ehRANCAAQ08rGhw2reTThF/ecfafLzjphjmqvySHJZwqKfqSn8YPK/1GMWODPWounlJ2MZb/iS/D69K0k9durGsGjaSb0I"}
48
107
  # Waiting period in ms (default 7 days). Local QA only: shorten to one minute.
49
108
  ESCROW_HOLD_DURATION_MS=
109
+ ESCROW_HOLD_RESTART_MIN_AGE_MS=
50
110
  # ESCROW_HOLD_DURATION_MS=60000
51
111
 
112
+ # ESCROW_ENCLAVE_MODE=remote: HTTP client to the enclave-host parent (P3.3).
113
+ # URL must be https:// unless NODE_ENV=test or the host is localhost. Token
114
+ # is a bearer credential, minimum 32 characters. Timeout defaults to 10000ms
115
+ # (max 30000ms). Missing/invalid config fails escrow closed at request time
116
+ # rather than crashing the whole service at boot.
117
+ ESCROW_ENCLAVE_REMOTE_URL=
118
+ ESCROW_ENCLAVE_REMOTE_TOKEN=
119
+ ESCROW_ENCLAVE_REMOTE_TIMEOUT_MS=
120
+
121
+ # Emergency stop: true blocks every escrow release (read per request, no restart needed).
122
+ # Deploy toggle, forwarded even in runtime-secrets bundle mode.
123
+ ESCROW_RELEASE_KILL_SWITCH=
124
+
52
125
  ANDROID_PUSH_ICON=
53
126
  REDIS_HOST=
54
127
  REDIS_PORT=
package/AGENTS.md CHANGED
@@ -4,6 +4,64 @@ The LearnCard Application API (`lca-api`) is the tRPC service backing client-sid
4
4
 
5
5
  ## Build & Development Commands
6
6
 
7
+ ### Backend configuration model
8
+
9
+ Configuration resolves in layers, lowest to highest precedence:
10
+
11
+ 1. Schema defaults (`src/config/environment.ts` Zod `.default()`s).
12
+ 2. Checked-in **non-secret** base config `config/config.json`.
13
+ 3. Checked-in **non-secret** per-stage config `config/config.<stage>.json`.
14
+ 4. Runtime AWS secrets bundle (Lambda only).
15
+ 5. Real environment variables (always win). Empty strings are unset.
16
+
17
+ The stage is `LAMBDA_STAGE` on Lambda, else `CONFIG_STAGE`; an unknown stage uses
18
+ the base `config/config.json` only (no per-stage file). The config stage files are
19
+ **public by design** — put only non-secret config in them. Credentials,
20
+ authorization allowlists (`AUTHORIZED_DIDS`), and internal service endpoints are
21
+ **secrets** and belong in the runtime bundle or local env, never the stage files.
22
+
23
+ For Lambda `trpc`/`api`, set the deploy environment variable `RUNTIME_SECRETS_ID` to
24
+ `lca-api/<stage>/runtime-secrets` (or its ARN in the Lambda account/region). Provision
25
+ the secret before enabling the variable. Its SecretString is a flat JSON object of
26
+ UPPER_SNAKE_CASE environment names to string values, including **all** required
27
+ runtime credentials. For example, Firebase is encoded as a string:
28
+
29
+ ```json
30
+ { "GOOGLE_APPLICATION_CREDENTIAL": "{\"type\":\"service_account\",\"...\":\"...\"}" }
31
+ ```
32
+
33
+ The Firebase JSON is a **string inside the bundle**, not a nested object. No secret
34
+ values belong in checked-in files. `lambda.ts` loads and validates the bundle once
35
+ before importing `lambdaApp.ts` and the environment schema. Non-empty explicit env
36
+ values win; empty strings are unset. Failed secret loads stop startup with a
37
+ sanitized error and can retry; failed application imports remain memoized until
38
+ the process is recycled. Redeploy/recycle functions after rotating the bundle.
39
+
40
+ When the id is absent, no AWS lookup occurs. Deploys retain the GitHub
41
+ runtime secrets as environment fallbacks; keep those inputs
42
+ until every stage has a bundle. Self-hosters, Docker, local development and CI keep
43
+ using plain environment variables — copy `config/config.example.json` to a stage
44
+ file (register its static import in `src/config/stageConfig.ts` and rebuild) or
45
+ just supply a `.env`. `trpc`, `api`, and `seedMigration` use
46
+ `SigningAuthorityExecutionRole`. `didWeb` and `swagger` use the generated default
47
+ role, with only a scoped runtime-bundle read grant in addition to Serverless's
48
+ execution permissions, not signing-authority KMS access. All five receive the
49
+ bundle id and run in the service VPC. Explicit function roles do not inherit the
50
+ default role's grants. OIDC has an isolated
51
+ `oidcLambda.ts` → `oidcLambdaApp.ts` import path and focused OIDC/cache schemas,
52
+ so it never imports the API schema requiring seed and Mongo credentials. Keep that path free of Mongo, models and DIDKit:
53
+ a Mongo connect left pending when an idle sandbox freezes crashes the next request after thaw. OIDC gets
54
+ its broker settings and keeps its separate signing-key secret and IAM role.
55
+ `OIDC_CLIENT_SECRET` stays a GitHub-environment secret on the OIDC function and is
56
+ **never** placed in the runtime bundle; that function makes no AWS Secrets Manager
57
+ calls for it. Preserve the existing issuer/audience validation.
58
+
59
+ The Infisical → AWS sync and per-stage cutover are documented in the repo-root
60
+ [environment-variables.md](../../../environment-variables.md#infisical--aws-secrets-sync).
61
+ The shared `@learncard/service-config` package owns parsing, merging, runtime
62
+ loading, stage application and Lambda bootstrap. Brain-service and LearnCloud
63
+ adoption remains a separate task.
64
+
7
65
  - Build: `bun run build`
8
66
  - Dev: `bun run dev` — watches and rebuilds
9
67
  - Start: `bun run start` — local server (default port from env, typically 5100)
@@ -57,16 +115,17 @@ The `keysRouter` implements the server side of Shamir Secret Sharing key managem
57
115
 
58
116
  The `escrow` tRPC router is disabled unless `ESCROW_ENCLAVE_MODE` is configured.
59
117
 
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 |
118
+ | Route | Procedure | Purpose |
119
+ | ------------------------------- | ----------------------------- | ------------------------------------------------------------------------- |
120
+ | `GET /keys/escrow/attestation` | `escrow.getAttestation` | Enclave public key and hold duration |
121
+ | `POST /keys/escrow` | `escrow.enroll` | Verify and store automatic recovery material |
122
+ | `DELETE /keys/escrow` | `escrow.remove` | Remove material and optionally opt out |
123
+ | `POST /keys/escrow/opt-in` | `escrow.optIn` | Allow enrollment again |
124
+ | `POST /keys/escrow/recover` | `escrow.startRecovery` | Start a waiting period without resetting an existing hold |
125
+ | `GET /keys/escrow/status` | `escrow.getStatus` | Check status; active devices send provider auth via `X-Auth-Token` |
126
+ | `POST /keys/escrow/cancel` | `escrow.cancelRecovery` | DID owner cancels a pending hold |
127
+ | `POST /keys/escrow/cancel-link` | `escrow.cancelRecoveryByLink` | Cancels a pending hold via the single-use email cancel link (no DID auth) |
128
+ | `POST /keys/escrow/complete` | `escrow.completeRecovery` | Claim an elapsed hold and release sealed recovery material once |
70
129
 
71
130
  ### Important Invariants
72
131
 
@@ -75,6 +134,43 @@ The `escrow` tRPC router is disabled unless `ESCROW_ENCLAVE_MODE` is configured.
75
134
  - **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
135
  - **`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.
77
136
 
137
+ ## OIDC identity provider (AD-2)
138
+
139
+ `auth.requestLoginTicket` and `auth.requestSocialLoginTicket` prove ownership in-app and issue 60-second single-use tickets. The ticket travels in URLs (`login_hint` on the Keycloak auth URL and again on the `/oidc/authorize` redirect), so it lands in browser history and API Gateway access logs; single-use + the short TTL is what makes that acceptable. Do not lengthen the TTL without revisiting that.
140
+ `src/oidc.ts` serves discovery, JWKS, authorize, token and userinfo in Docker and Lambda; no CORS or duplicate tRPC endpoints.
141
+ Subjects are random permanent UUIDs in `AuthSubject`, keyed by normalized email or Google/Apple subject.
142
+ `requestSocialLoginTicket` is the one exception to "never auto-linked by email": when `verifySocialIdToken`
143
+ returns a verified email (Apple's private-relay addresses count), a brand-new `<provider>:<sub>` identity
144
+ adopts the existing `email:<address>` identity's `subject` via `getOrCreateAuthSubjectLinkedTo`
145
+ (`$setOnInsert`), so a user doesn't hit "Account already exists" switching from email-code to native
146
+ social sign-in. It never runs the other direction, and an already-existing `<provider>:<sub>` record
147
+ keeps its own `subject` — only a first-time native sign-in can be linked this way.
148
+ Require `OIDC_ISSUER`, an exact redirect allowlist, and a token client secret. Deployed Lambdas use `OIDC_SIGNING_KEY_SECRET_ID` (name or ARN of `lca-api/<stage>/oidc-signing-jwk`) pointing to a plain-string Secrets Manager SecretString containing the RSA private JWK JSON (`kty=RSA`, `d`, `kid`, `alg=RS256`). The key is fetched and cached per process; failures return 503 and retry on the next request. Redeploy after rotation. `OIDC_SIGNING_KEY_JWK` remains supported for local/Docker/CI and takes precedence. Any deployed stage (`NODE_ENV=production` or `LAMBDA_STAGE` set) fails closed without a configured key.
149
+ Login codes, tickets and authorization codes are consumed with `getDel` (`src/cache/getDel.ts`, Redis `GETDEL`, requires Redis >= 6.2).
150
+ Rate limiting is two layers, both keyed on `request.ip` (API Gateway `sourceIp` under `serverless-http`; `x-forwarded-for` is attacker-controlled and never trusted). Both fail open if Redis is unreachable. **Layer 1 — failures only** (`src/helpers/rate-limit.helpers.ts`, Redis counters, 10-minute window): the ticket routes use 5 per email plus a 50-per-IP backstop; `/oidc/authorize` (bad client/redirect, invalid ticket) and `/oidc/token` (`invalid_client` only) each use 50 per IP and answer `429 temporarily_unavailable` + `Retry-After` (or an OAuth error redirect once the `redirect_uri` is validated). `invalid_grant` at the token endpoint is deliberately not counted: only the broker (one NAT IP) can reach that branch, and counting the codes it relays would let anyone lock every user out. **Layer 2 — all requests** (`@fastify/rate-limit`, registered in `oidcFastifyPlugin`, Redis-backed when available): 300/min per IP globally so Keycloak's discovery/JWKS polling is never blocked; 60/min per IP on the browser-facing `/oidc/authorize`; a 3000/min sanity ceiling on the server-to-server `/oidc/token` and `/oidc/userinfo` (every legitimate call shares the broker's egress IP, so a tight cap there is a global login ceiling). Layer 2 exists mainly so CodeQL's `js/missing-rate-limiting` recognises the routes; do not remove either layer.
151
+ Unit coverage is in `test/oidc.spec.ts` and `test/auth-tickets.spec.ts`; broker import coverage is gated by `KEYCLOAK_INTEGRATION`.
152
+ Phone OTP remains deferred. The complete live broker round-trip and Firebase-era mapping proof
153
+ are in `test/keycloak-broker-roundtrip.integration.spec.ts` and
154
+ `test/keycloak-migration.integration.spec.ts`, gated on both `KEYCLOAK_INTEGRATION=true`
155
+ and `KEYCLOAK_ROUNDTRIP=true`. With both flags, `vitest.integration.config.ts` selects
156
+ only the live auth suites and aliases Mongo to the explicitly configured API database;
157
+ without them, existing temporary-Mongo integration suites retain their setup.
158
+ See `infra/keycloak/README.md` at the repository root for the local command.
159
+
160
+ `bun run provision:keycloak` previews the AD-10 mapping migration; `--apply` writes,
161
+ `--email` and `--limit` restrict a batch. Never use this operator-only email join in a
162
+ runtime login route. Dry-run performs no resource writes, including no index creation.
163
+ Existing AuthSubjects are read, not upserted, so repeat apply does not alter lastLoginAt.
164
+ The final global email mapping coverage gate must pass before cutover. Phone-only
165
+ accounts remain a separately reported deferred cohort.
166
+
167
+ `bun run test:oidc:e2e` also covers the complete email-code broker round-trip in Playwright, with real lca-api and disposable Keycloak/Redis/Mongo. Keep its cleanup contract: exact per-test user/subject deletion, private transient stores, and runner teardown on success/failure/signals. Never point it at shared environments.
168
+
169
+ ### Keycloak broker gotchas
170
+
171
+ - **Client secret encoding.** Keycloak's broker sends `client_secret_basic` credentials **raw** (not form-url-encoded, contrary to RFC 6749 §2.3.1). `/oidc/token` therefore compares the raw pair first and only falls back to the URL-decoded pair. Secrets containing `+` or `%` are fine.
172
+ - **Phase 2 migration must create the federated identity link.** The realm uses the default `first broker login` flow with `trustEmail: true`. If the migration script pre-creates a Keycloak user by email **without** also creating the `lca-api` federated identity link (`sub` = the `AuthSubject.subject` UUID), the user's first silent hop stops on Keycloak's "Handle Existing Account" web page ("User with email X already exists. How do you want to continue?"), which breaks the no-web-form guarantee. Pre-seed `AuthSubject` and the Keycloak `federatedIdentities` entry together.
173
+
78
174
  ## Email Delivery & Tenant Branding
79
175
 
80
176
  All transactional emails (login OTP, recovery email OTP, recovery key, endorsement request) are rendered locally via [`@learncard/email-templates`](../../../packages/email-templates/README.md) and delivered as raw HTML through Postmark.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @welibraryos/lca-api-service
2
2
 
3
+ ## 1.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1711](https://github.com/learningeconomy/LearnCard/pull/1711) [`63ded61d2b1b81ce1c8ad7b9415510da5e4c24ae`](https://github.com/learningeconomy/LearnCard/commit/63ded61d2b1b81ce1c8ad7b9415510da5e4c24ae) Thanks [@goblincore](https://github.com/goblincore)! - Exclude bundled service configuration from published service dependencies so public packages do not require the private service-config workspace package during installation. Keep runtime workspace dependencies for source-based service deployments.
8
+
9
+ - Updated dependencies []:
10
+ - @learncard/init@2.5.3
11
+
12
+ ## 1.5.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [#1589](https://github.com/learningeconomy/LearnCard/pull/1589) [`599f048b082d2ee64775f7a0ef972e171b8c2a8a`](https://github.com/learningeconomy/LearnCard/commit/599f048b082d2ee64775f7a0ef972e171b8c2a8a) Thanks [@Custard7](https://github.com/Custard7)! - Add Keycloak token verification (`verifyKeycloakToken`) using `jose`: exact issuer allowlist, `aud`/`azp` client allowlist, ID/Bearer token-type check, and required `email_verified` / `phone_number_verified` claims. Configured with `KEYCLOAK_ISSUERS`, `KEYCLOAK_AUDIENCES`, and optional `KEYCLOAK_JWKS_URL_OVERRIDES`. Disabled (rejects all tokens) until issuers are configured.
17
+
18
+ - [#1429](https://github.com/learningeconomy/LearnCard/pull/1429) [`1387cd0a08fa6b0a706c86a5a0376b6c7d706856`](https://github.com/learningeconomy/LearnCard/commit/1387cd0a08fa6b0a706c86a5a0376b6c7d706856) Thanks [@Custard7](https://github.com/Custard7)! - Add a trusted-service `ensureManagedSigningAuthority` route (`POST /signing-authority/managed`) that returns or creates a named signing authority for an owner DID through the standard encrypted-seed path. Brain service now provisions app signing authorities through this route instead of writing plaintext seeds to the LCA API database.
19
+
20
+ - [#1592](https://github.com/learningeconomy/LearnCard/pull/1592) [`1f1ff54118a5d08c97f57696ca45671f0ee982b8`](https://github.com/learningeconomy/LearnCard/commit/1f1ff54118a5d08c97f57696ca45671f0ee982b8) Thanks [@Custard7](https://github.com/Custard7)! - Add the lca-api OIDC identity provider with RS256 signing, permanent UUID subjects,
21
+ single-use login tickets, authorization codes and opaque access tokens. The
22
+ `auth.requestLoginTicket` and `auth.requestSocialLoginTicket` routes verify email
23
+ codes or native Google/Apple proofs. Keycloak brokers the redirect flow without
24
+ an additional login form. Includes Docker/Lambda wiring, a development broker
25
+ fixture and protocol tests. Phone OTP and full live broker round-trip CI remain deferred.
26
+
27
+ ### Patch Changes
28
+
29
+ - [#1621](https://github.com/learningeconomy/LearnCard/pull/1621) [`d7ca528898d988c65f0dc7084cfdcd41767648c5`](https://github.com/learningeconomy/LearnCard/commit/d7ca528898d988c65f0dc7084cfdcd41767648c5) Thanks [@Custard7](https://github.com/Custard7)! - Add remote enclave backend, hold notifications, cancel links, reminders, and kill switch for escrow recovery.
30
+
31
+ - [#1679](https://github.com/learningeconomy/LearnCard/pull/1679) [`77e5b0681f5567caa446da57a1746d148ffde27a`](https://github.com/learningeconomy/LearnCard/commit/77e5b0681f5567caa446da57a1746d148ffde27a) Thanks [@Custard7](https://github.com/Custard7)! - Catch module-load index creation failures so a Mongo connect that outlives a frozen Lambda invocation no longer crashes the next request (seen as a 500 on `/oidc/authorize` during Keycloak sign-in).
32
+
33
+ - [#1670](https://github.com/learningeconomy/LearnCard/pull/1670) [`7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2`](https://github.com/learningeconomy/LearnCard/commit/7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2) Thanks [@Custard7](https://github.com/Custard7)! - Honor CONFIG_STAGE in serverless-offline without implicitly applying a deployed stage's configuration during local startup.
34
+
35
+ - [#1662](https://github.com/learningeconomy/LearnCard/pull/1662) [`aef0f8cfdef94170104d2179fb04759c8dae7162`](https://github.com/learningeconomy/LearnCard/commit/aef0f8cfdef94170104d2179fb04759c8dae7162) Thanks [@Custard7](https://github.com/Custard7)! - Load deployed OIDC signing keys from Secrets Manager with retryable per-process caching instead of storing private JWKs in Lambda environment variables.
36
+
37
+ - [#1670](https://github.com/learningeconomy/LearnCard/pull/1670) [`7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2`](https://github.com/learningeconomy/LearnCard/commit/7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2) Thanks [@Custard7](https://github.com/Custard7)! - Document the layered backend configuration model: schema defaults, checked-in non-secret per-stage config files, the Lambda runtime AWS secrets bundle, and real environment variables, plus the Infisical → AWS sync and per-stage cutover.
38
+
39
+ - [#1670](https://github.com/learningeconomy/LearnCard/pull/1670) [`7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2`](https://github.com/learningeconomy/LearnCard/commit/7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2) Thanks [@Custard7](https://github.com/Custard7)! - Select backend stage files by tenant as well as stage. `CONFIG_TENANT=scouts` loads `config.scouts.<stage>.json` (generated from the live ScoutPass Lambdas), so ScoutPass deployments no longer inherit LearnCard domains, trust settings or seed-encryption flags; a named tenant without a stage file fails closed. `ESCROW_ENCLAVE_MODE` is now forwarded in runtime-secrets bundle mode too.
40
+
41
+ - [#1664](https://github.com/learningeconomy/LearnCard/pull/1664) [`4a237f41ea4ab0d3bd69f2066039518fdec20984`](https://github.com/learningeconomy/LearnCard/commit/4a237f41ea4ab0d3bd69f2066039518fdec20984) Thanks [@Custard7](https://github.com/Custard7)! - Load optional per-stage runtime secrets before Lambda initialization and scope authentication environment settings by function to avoid Lambda's 4KB limit. Preserve the Firebase environment fallback for stages without a bundle.
42
+
43
+ - Updated dependencies [[`6209da80c52ac056a7644dab14669192740c9dad`](https://github.com/learningeconomy/LearnCard/commit/6209da80c52ac056a7644dab14669192740c9dad), [`5890451789b18afee45dc5ecbab3fa30aa90f085`](https://github.com/learningeconomy/LearnCard/commit/5890451789b18afee45dc5ecbab3fa30aa90f085), [`1387cd0a08fa6b0a706c86a5a0376b6c7d706856`](https://github.com/learningeconomy/LearnCard/commit/1387cd0a08fa6b0a706c86a5a0376b6c7d706856), [`d7ca528898d988c65f0dc7084cfdcd41767648c5`](https://github.com/learningeconomy/LearnCard/commit/d7ca528898d988c65f0dc7084cfdcd41767648c5), [`d7ca528898d988c65f0dc7084cfdcd41767648c5`](https://github.com/learningeconomy/LearnCard/commit/d7ca528898d988c65f0dc7084cfdcd41767648c5), [`d7ca528898d988c65f0dc7084cfdcd41767648c5`](https://github.com/learningeconomy/LearnCard/commit/d7ca528898d988c65f0dc7084cfdcd41767648c5), [`8f3bc66ab3d3e5e1edb3fbf5f362025875a914ba`](https://github.com/learningeconomy/LearnCard/commit/8f3bc66ab3d3e5e1edb3fbf5f362025875a914ba), [`1387cd0a08fa6b0a706c86a5a0376b6c7d706856`](https://github.com/learningeconomy/LearnCard/commit/1387cd0a08fa6b0a706c86a5a0376b6c7d706856), [`267cf40291b3a63caf178857be0e57feaf5e0948`](https://github.com/learningeconomy/LearnCard/commit/267cf40291b3a63caf178857be0e57feaf5e0948), [`7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2`](https://github.com/learningeconomy/LearnCard/commit/7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2), [`c78c982f9f4911f1ca2973380f19d80db775e012`](https://github.com/learningeconomy/LearnCard/commit/c78c982f9f4911f1ca2973380f19d80db775e012), [`590adf48867d6c6fd504e66a11c9827d5102122d`](https://github.com/learningeconomy/LearnCard/commit/590adf48867d6c6fd504e66a11c9827d5102122d)]:
44
+ - @learncard/types@5.23.0
45
+ - @learncard/email-templates@1.2.1
46
+ - @learncard/sss-key-manager@0.2.1
47
+ - @learncard/service-config@0.1.1
48
+ - @learncard/didkit-plugin@1.10.3
49
+ - @learncard/core@9.4.38
50
+ - @learncard/helpers@1.6.2
51
+ - @learncard/init@2.5.2
52
+ - @learncard/did-web-plugin@1.1.38
53
+ - @learncard/didkit-plugin-node@0.3.3
54
+
3
55
  ## 1.4.0
4
56
 
5
57
  ### Minor Changes
@@ -0,0 +1,76 @@
1
+ import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
2
+ import { beforeEach, expect, it, vi } from 'vitest';
3
+
4
+ const mocks = vi.hoisted(() => ({ bootstrap: vi.fn(), getApp: vi.fn(), handler: vi.fn() }));
5
+
6
+ vi.mock('@learncard/service-config', () => ({ bootstrapLambda: mocks.bootstrap }));
7
+ vi.mock('./src/config/stageConfig', () => ({ base: {}, stages: { dev: {}, production: {} } }));
8
+ vi.mock('./oidcLambda', () => ({ handler: mocks.handler }));
9
+ vi.mock('./lambdaApp', () =>
10
+ Object.fromEntries(
11
+ ['trpcHandler', 'openApiHandler', 'swaggerUiHandler', 'didWebHandler', 'oidcHandler'].map(
12
+ name => [name, mocks.handler]
13
+ )
14
+ )
15
+ );
16
+
17
+ beforeEach(() => {
18
+ vi.resetModules();
19
+ vi.clearAllMocks();
20
+ mocks.bootstrap.mockReturnValue(mocks.getApp);
21
+ mocks.handler.mockResolvedValue({ statusCode: 200 });
22
+ });
23
+
24
+ it('bootstraps the API once with checked-in stage config', async () => {
25
+ vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', 'lca-api-dev-api');
26
+ vi.stubEnv('LAMBDA_STAGE', 'dev');
27
+ await import('./lambda');
28
+ expect(mocks.bootstrap).toHaveBeenCalledTimes(1);
29
+ expect(mocks.bootstrap).toHaveBeenCalledWith({
30
+ base: {},
31
+ stages: { dev: {}, production: {} },
32
+ stage: 'dev',
33
+ importApp: expect.any(Function),
34
+ });
35
+ vi.unstubAllEnvs();
36
+ });
37
+
38
+ it('uses CONFIG_STAGE outside Lambda rather than LAMBDA_STAGE', async () => {
39
+ vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', '');
40
+ vi.stubEnv('LAMBDA_STAGE', 'production');
41
+ vi.stubEnv('CONFIG_STAGE', 'dev');
42
+ await import('./lambda');
43
+ expect(mocks.bootstrap).toHaveBeenCalledWith(expect.objectContaining({ stage: 'dev' }));
44
+ vi.unstubAllEnvs();
45
+ });
46
+
47
+ it('forwards every handler through its bootstrap accessor', async () => {
48
+ mocks.getApp.mockResolvedValue(
49
+ Object.fromEntries(
50
+ [
51
+ 'trpcHandler',
52
+ 'openApiHandler',
53
+ 'swaggerUiHandler',
54
+ 'didWebHandler',
55
+ 'oidcHandler',
56
+ ].map(name => [name, mocks.handler])
57
+ )
58
+ );
59
+ const lambda = await import('./lambda');
60
+ const event = {} as APIGatewayProxyEventV2;
61
+ const context = {} as Context;
62
+ const callback = vi.fn();
63
+ const handlers = [
64
+ lambda.trpcHandler,
65
+ lambda.openApiHandler,
66
+ lambda.swaggerUiHandler,
67
+ lambda.didWebHandler,
68
+ ];
69
+ for (const handler of handlers) await handler(event, context, callback);
70
+ expect(mocks.bootstrap).toHaveBeenCalledTimes(1);
71
+ expect(mocks.getApp).toHaveBeenCalledTimes(handlers.length);
72
+ expect(mocks.handler).toHaveBeenCalledTimes(handlers.length);
73
+ expect(mocks.handler).toHaveBeenCalledWith(event, context, callback);
74
+ await lambda.oidcHandler(event, context, callback);
75
+ expect(mocks.getApp).toHaveBeenCalledTimes(handlers.length);
76
+ });
package/bundle.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
1
3
  import { build } from 'esbuild';
2
4
  import plugins from './esbuildPlugins.cjs';
3
5
 
@@ -9,6 +11,7 @@ const finalBuildObj = {
9
11
  entryPoints: ['src/index.ts'],
10
12
  platform: 'node',
11
13
  bundle: true,
14
+ metafile: true,
12
15
  format: 'cjs',
13
16
  outdir: 'dist',
14
17
  target: 'node20',
@@ -22,6 +25,11 @@ if (process.env.NODE_ENV !== 'production') {
22
25
  finalBuildObj.minify = false;
23
26
  }
24
27
 
25
- build(finalBuildObj).then(() => {
28
+ build(finalBuildObj).then(result => {
29
+ // Publish preparation verifies private helpers are not external runtime imports.
30
+ writeFileSync(
31
+ join(finalBuildObj.outdir, 'publish-metafile.json'),
32
+ JSON.stringify(result.metafile)
33
+ );
26
34
  console.log(`🎁 Done building main bundle! (${Date.now() - startTime}ms)`);
27
35
  });
@@ -0,0 +1,23 @@
1
+ {
2
+ "APPLE_OAUTH_CLIENT_IDS": "com.learncard.app,app.learncard.www",
3
+ "DOMAIN_NAME": "staging.api.learncard.app",
4
+ "ESCROW_ENCLAVE_ACTIVE_KEY_ID": "escrow-staging-v1",
5
+ "ESCROW_ENCLAVE_REMOTE_TIMEOUT_MS": "10000",
6
+ "ESCROW_HOLD_DURATION_MS": "600000",
7
+ "ESCROW_HOLD_RESTART_MIN_AGE_MS": "300000",
8
+ "GOOGLE_OAUTH_CLIENT_IDS": "776298253175-74or0s6rg2k2mmdks6d45ro0i4te1bvf.apps.googleusercontent.com,776298253175-kkf562eofl541vu0oedfdvrk40bpkbh7.apps.googleusercontent.com",
9
+ "KEYCLOAK_AUDIENCES": "learncard-app",
10
+ "KEYCLOAK_ISSUERS": "https://auth.staging.learncard.app/realms/learncard",
11
+ "OIDC_CLIENT_ID": "keycloak-broker",
12
+ "OIDC_ISSUER": "https://staging.api.learncard.app",
13
+ "OIDC_SIGNING_KEY_SECRET_ID": "lca-api/dev/oidc-signing-jwk",
14
+ "POSTMARK_BRAND_NAME": "LearnCard",
15
+ "POSTMARK_ENDORSEMENT_REQUEST_TEMPLATE_ALIAS": "universal-inbox-claim-1",
16
+ "POSTMARK_FROM_EMAIL": "learncard.com",
17
+ "POSTMARK_LOGIN_CODE_TEMPLATE_ALIAS": "contact-method-verification-1",
18
+ "POSTMARK_RECOVERY_EMAIL_CODE_TEMPLATE_ALIAS": "recovery-email-verification",
19
+ "POSTMARK_RECOVERY_KEY_TEMPLATE_ALIAS": "recovery-key-backup",
20
+ "SA_SEED_ALLOW_LEGACY_READ": "false",
21
+ "SA_SEED_ENCRYPT_WRITES": "true",
22
+ "SENTRY_ENV": "lca-api-staging"
23
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "APPLE_OAUTH_CLIENT_IDS": "com.example.app,app.example.www",
3
+ "DOMAIN_NAME": "api.example.com",
4
+ "ESCROW_ENCLAVE_ACTIVE_KEY_ID": "example-2026-01",
5
+ "ESCROW_HOLD_DURATION_MS": "604800000",
6
+ "ESCROW_HOLD_RESTART_MIN_AGE_MS": "86400000",
7
+ "GOOGLE_OAUTH_CLIENT_IDS": "000000000000-exampleexampleexampleexampleexam.apps.googleusercontent.com",
8
+ "KEYCLOAK_AUDIENCES": "example-app",
9
+ "KEYCLOAK_ISSUERS": "https://auth.example.com/realms/example",
10
+ "OIDC_CLIENT_ID": "keycloak-broker",
11
+ "OIDC_ISSUER": "https://api.example.com",
12
+ "OIDC_SIGNING_KEY_SECRET_ID": "lca-api/example/oidc-signing-jwk",
13
+ "POSTMARK_BRAND_NAME": "Example",
14
+ "POSTMARK_ENDORSEMENT_REQUEST_TEMPLATE_ALIAS": "universal-inbox-claim-1",
15
+ "POSTMARK_FROM_EMAIL": "example.com",
16
+ "POSTMARK_LOGIN_CODE_TEMPLATE_ALIAS": "contact-method-verification-1",
17
+ "POSTMARK_RECOVERY_EMAIL_CODE_TEMPLATE_ALIAS": "recovery-email-verification",
18
+ "POSTMARK_RECOVERY_KEY_TEMPLATE_ALIAS": "recovery-key-backup",
19
+ "SA_SEED_ALLOW_LEGACY_READ": "false",
20
+ "SA_SEED_ENCRYPT_WRITES": "true",
21
+ "SENTRY_ENV": "lca-api-example"
22
+ }
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1,12 @@
1
+ {
2
+ "DOMAIN_NAME": "api.learncard.app",
3
+ "POSTMARK_BRAND_NAME": "LearnCard",
4
+ "POSTMARK_ENDORSEMENT_REQUEST_TEMPLATE_ALIAS": "universal-inbox-claim-1",
5
+ "POSTMARK_FROM_EMAIL": "learncard.com",
6
+ "POSTMARK_LOGIN_CODE_TEMPLATE_ALIAS": "contact-method-verification-1",
7
+ "POSTMARK_RECOVERY_EMAIL_CODE_TEMPLATE_ALIAS": "recovery-email-verification",
8
+ "POSTMARK_RECOVERY_KEY_TEMPLATE_ALIAS": "recovery-key-backup",
9
+ "SA_SEED_ALLOW_LEGACY_READ": "false",
10
+ "SA_SEED_ENCRYPT_WRITES": "true",
11
+ "SENTRY_ENV": "lca-api-production"
12
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "DOMAIN_NAME": "staging.api.scoutnetwork.org",
3
+ "POSTMARK_BRAND_NAME": "ScoutPass",
4
+ "POSTMARK_FROM_EMAIL": "staging.scoutnetwork.org",
5
+ "POSTMARK_LOGIN_CODE_TEMPLATE_ALIAS": "sp-login-verification",
6
+ "POSTMARK_RECOVERY_EMAIL_CODE_TEMPLATE_ALIAS": "sp-recovery-email-verification",
7
+ "POSTMARK_RECOVERY_KEY_TEMPLATE_ALIAS": "recovery-key-backup",
8
+ "SA_SEED_ALLOW_LEGACY_READ": "false",
9
+ "SA_SEED_ENCRYPT_WRITES": "true",
10
+ "SENTRY_ENV": "scouts-api-staging"
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "DOMAIN_NAME": "api.scoutnetwork.org",
3
+ "POSTMARK_BRAND_NAME": "ScoutPass",
4
+ "POSTMARK_FROM_EMAIL": "scoutnetwork.org",
5
+ "POSTMARK_LOGIN_CODE_TEMPLATE_ALIAS": "sp-login-verification",
6
+ "POSTMARK_RECOVERY_EMAIL_CODE_TEMPLATE_ALIAS": "sp-recovery-email-verification",
7
+ "POSTMARK_RECOVERY_KEY_TEMPLATE_ALIAS": "recovery-key-backup",
8
+ "SA_SEED_ALLOW_LEGACY_READ": "true",
9
+ "SA_SEED_ENCRYPT_WRITES": "false",
10
+ "SENTRY_ENV": "lca-api-production"
11
+ }
package/dist/app.d.ts CHANGED
@@ -214,6 +214,11 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
214
214
  approvedAt: string;
215
215
  contractUpdatedAt: string;
216
216
  } | undefined;
217
+ referral?: {
218
+ requestId: string;
219
+ requestedBy: string;
220
+ externalReferenceId?: string | undefined;
221
+ } | undefined;
217
222
  uris?: string[] | undefined;
218
223
  } | undefined;
219
224
  inbox?: {
@@ -305,6 +310,20 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
305
310
  };
306
311
  meta: import("trpc-to-openapi").OpenApiMeta;
307
312
  }>;
313
+ ensureManagedSigningAuthority: import("@trpc/server").TRPCMutationProcedure<{
314
+ input: {
315
+ name: string;
316
+ ownerDid: string;
317
+ };
318
+ output: {
319
+ ownerDid: string;
320
+ name: string;
321
+ did: string;
322
+ endpoint: string;
323
+ _id?: string | undefined;
324
+ };
325
+ meta: import("trpc-to-openapi").OpenApiMeta;
326
+ }>;
308
327
  signingAuthorities: import("@trpc/server").TRPCQueryProcedure<{
309
328
  input: void;
310
329
  output: {
@@ -1108,6 +1127,7 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1108
1127
  salt?: string | undefined;
1109
1128
  };
1110
1129
  sssActivationState: "provisional" | "active";
1130
+ escrowStale?: "mode-mismatch" | "key-rotated" | undefined;
1111
1131
  } | null;
1112
1132
  meta: import("trpc-to-openapi").OpenApiMeta;
1113
1133
  }>;
@@ -1302,7 +1322,9 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1302
1322
  transformer: false;
1303
1323
  }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
1304
1324
  getAttestation: import("@trpc/server").TRPCQueryProcedure<{
1305
- input: Record<string, never>;
1325
+ input: {
1326
+ nonce?: string | undefined;
1327
+ };
1306
1328
  output: {
1307
1329
  attestation: {
1308
1330
  mode: "software" | "nitro";
@@ -1316,6 +1338,7 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1316
1338
  };
1317
1339
  document: string;
1318
1340
  issuedAt: string;
1341
+ previousKeyIds?: string[] | undefined;
1319
1342
  };
1320
1343
  holdDurationMs: number;
1321
1344
  };
@@ -1376,10 +1399,10 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1376
1399
  recoverySessionToken?: string | undefined;
1377
1400
  };
1378
1401
  output: {
1402
+ holdId: string;
1403
+ releasePolicy: "pin" | "hold";
1379
1404
  requestedAt: string;
1380
1405
  releaseAfter: string;
1381
- releasePolicy: "pin" | "hold";
1382
- holdId: string;
1383
1406
  status: "pending";
1384
1407
  resumeToken: string | null;
1385
1408
  pinSalt?: string | undefined;
@@ -1417,6 +1440,16 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1417
1440
  };
1418
1441
  meta: import("trpc-to-openapi").OpenApiMeta;
1419
1442
  }>;
1443
+ cancelRecoveryByLink: import("@trpc/server").TRPCMutationProcedure<{
1444
+ input: {
1445
+ holdId: string;
1446
+ token: string;
1447
+ };
1448
+ output: {
1449
+ cancelled: boolean;
1450
+ };
1451
+ meta: import("trpc-to-openapi").OpenApiMeta;
1452
+ }>;
1420
1453
  completeRecovery: import("@trpc/server").TRPCMutationProcedure<{
1421
1454
  input: {
1422
1455
  holdId: string;
@@ -1500,6 +1533,37 @@ export declare const appRouter: import("@trpc/server").TRPCBuiltRouter<{
1500
1533
  meta: import("trpc-to-openapi").OpenApiMeta;
1501
1534
  }>;
1502
1535
  }>>;
1536
+ auth: import("@trpc/server").TRPCBuiltRouter<{
1537
+ ctx: import("@routes").Context;
1538
+ meta: import("trpc-to-openapi").OpenApiMeta;
1539
+ errorShape: import("@trpc/server").TRPCDefaultErrorShape;
1540
+ transformer: false;
1541
+ }, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
1542
+ requestLoginTicket: import("@trpc/server").TRPCMutationProcedure<{
1543
+ input: {
1544
+ email: string;
1545
+ code: string;
1546
+ };
1547
+ output: {
1548
+ success: boolean;
1549
+ ticket?: string | undefined;
1550
+ error?: string | undefined;
1551
+ };
1552
+ meta: import("trpc-to-openapi").OpenApiMeta;
1553
+ }>;
1554
+ requestSocialLoginTicket: import("@trpc/server").TRPCMutationProcedure<{
1555
+ input: {
1556
+ provider: "google" | "apple";
1557
+ idToken: string;
1558
+ };
1559
+ output: {
1560
+ success: boolean;
1561
+ ticket?: string | undefined;
1562
+ error?: string | undefined;
1563
+ };
1564
+ meta: import("trpc-to-openapi").OpenApiMeta;
1565
+ }>;
1566
+ }>>;
1503
1567
  } & {
1504
1568
  test?: TestRouter;
1505
1569
  }>>;
@@ -0,0 +1,2 @@
1
+ export declare const getDel: (key: string) => Promise<string | null>;
2
+ export declare const consumeMatchingCode: (key: string, code: string) => Promise<boolean>;
@@ -0,0 +1 @@
1
+ export {};