@learncard/lca-api-service 1.4.0 → 1.5.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.
- package/.env.example +73 -0
- package/AGENTS.md +106 -10
- package/CHANGELOG.md +43 -0
- package/bootstrapLambda.test.ts +76 -0
- package/config/config.dev.json +23 -0
- package/config/config.example.json +22 -0
- package/config/config.json +1 -0
- package/config/config.production.json +12 -0
- package/config/config.scouts.dev.json +11 -0
- package/config/config.scouts.production.json +11 -0
- package/dist/app.d.ts +67 -3
- package/dist/cache/getDel.d.ts +2 -0
- package/dist/cache/getDel.test.d.ts +1 -0
- package/dist/cache/login-tickets.d.ts +12 -0
- package/dist/config/applyDockerStageConfig.d.ts +1 -0
- package/dist/config/cacheEnvironment.d.ts +16 -0
- package/dist/config/environment.d.ts +101 -0
- package/dist/config/escrowBundleMode.test.d.ts +1 -0
- package/dist/config/escrowDeployment.test.d.ts +1 -0
- package/dist/config/oidcEnvironment.d.ts +74 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts +1 -0
- package/dist/config/stageConfig.d.ts +62 -0
- package/dist/config/stageConfig.test.d.ts +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/helpers/auth.helpers.d.ts +1 -1
- package/dist/helpers/escrowCancelLink.d.ts +7 -0
- package/dist/helpers/escrowCancelLink.test.d.ts +1 -0
- package/dist/helpers/escrowCancelToken.d.ts +4 -0
- package/dist/helpers/escrowCancelToken.test.d.ts +1 -0
- package/dist/helpers/keycloak.helpers.d.ts +23 -0
- package/dist/helpers/login-ticket.helpers.d.ts +31 -0
- package/dist/helpers/login-ticket.helpers.test.d.ts +1 -0
- package/dist/helpers/oidc.helpers.d.ts +47 -0
- package/dist/helpers/rate-limit.helpers.d.ts +6 -0
- package/dist/helpers/rate-limit.helpers.test.d.ts +1 -0
- package/dist/helpers/social-token.helpers.d.ts +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/jobs/escrowBlobRewrap.d.ts +9 -0
- package/dist/jobs/escrowHoldReminders.d.ts +12 -0
- package/dist/models/AuthSubject.d.ts +20 -0
- package/dist/models/EscrowHold.d.ts +79 -1
- package/dist/models/Notification.d.ts +5 -0
- package/dist/models/UserKey.d.ts +11 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/oidc.d.ts +5 -0
- package/dist/routes/auth.d.ts +31 -0
- package/dist/routes/escrow.d.ts +18 -4
- package/dist/routes/keys.d.ts +1 -0
- package/dist/routes/notifications.d.ts +5 -0
- package/dist/routes/signing-authority.d.ts +14 -0
- package/dist/services/escrow-enclave/blobIdentity.d.ts +10 -0
- package/dist/services/escrow-enclave/blobIdentity.test.d.ts +1 -0
- package/dist/services/escrow-enclave/cancellation.d.ts +3 -0
- package/dist/services/escrow-enclave/enclaveTrustAnchors.d.ts +1 -0
- package/dist/services/escrow-enclave/index.d.ts +16 -0
- package/dist/services/escrow-enclave/notifications.d.ts +19 -4
- package/dist/services/escrow-enclave/notifications.test.d.ts +1 -0
- package/dist/services/escrow-enclave/production.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.contract.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.d.ts +20 -0
- package/dist/services/escrow-enclave/remoteEnclave.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.tls.test.d.ts +1 -0
- package/dist/services/escrow-enclave/softwareEnclave.d.ts +12 -2
- package/dist/services/escrow-enclave/types.d.ts +52 -8
- package/dist/types/notifications.d.ts +5 -0
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +27 -117
- package/lambdaApp.ts +146 -0
- package/oidcLambda.test.ts +38 -0
- package/oidcLambda.ts +18 -0
- package/oidcLambdaApp.ts +11 -0
- package/package.json +23 -13
- package/scripts/README.md +88 -0
- package/scripts/check-seed-encryption-infrastructure.mjs +18 -2
- package/scripts/escrow-blob-mode-report.test.ts +227 -0
- package/scripts/escrow-blob-mode-report.ts +465 -0
- package/scripts/keycloak-admin.ts +108 -0
- package/scripts/keycloak-provider-links.ts +147 -0
- package/scripts/print-seed-rollout-flags.ts +21 -0
- package/scripts/provision-keycloak-users.ts +419 -0
- package/seedMigrationApp.test.ts +145 -0
- package/seedMigrationApp.ts +50 -0
- package/seedMigrationLambda.test.ts +30 -135
- package/seedMigrationLambda.ts +15 -46
- package/serverless-local.yml +25 -1
- package/serverless.function-env.cjs +58 -0
- package/serverless.yml +172 -37
- package/src/accesslayer/notifications/index.ts +9 -2
- package/src/accesslayer/signing-authority/index.ts +4 -1
- package/src/app.ts +2 -0
- package/src/cache/getDel.test.ts +34 -0
- package/src/cache/getDel.ts +32 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/login-tickets.ts +26 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/cacheEnvironment.ts +34 -0
- package/src/config/environment.test.ts +45 -2
- package/src/config/environment.ts +90 -23
- package/src/config/escrowBundleMode.test.ts +52 -0
- package/src/config/escrowDeployment.test.ts +114 -0
- package/src/config/oidcEnvironment.ts +59 -0
- package/src/config/serverlessFunctionEnv.test.ts +98 -0
- package/src/config/stageConfig.test.ts +26 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +9 -0
- package/src/helpers/auth.helpers.ts +68 -30
- package/src/helpers/escrowCancelLink.test.ts +60 -0
- package/src/helpers/escrowCancelLink.ts +38 -0
- package/src/helpers/escrowCancelToken.test.ts +105 -0
- package/src/helpers/escrowCancelToken.ts +47 -0
- package/src/helpers/keycloak.helpers.ts +120 -0
- package/src/helpers/login-ticket.helpers.test.ts +45 -0
- package/src/helpers/login-ticket.helpers.ts +91 -0
- package/src/helpers/oidc.helpers.ts +354 -0
- package/src/helpers/rate-limit.helpers.test.ts +61 -0
- package/src/helpers/rate-limit.helpers.ts +73 -0
- package/src/helpers/social-token.helpers.ts +144 -0
- package/src/index.ts +1 -0
- package/src/jobs/escrowBlobRewrap.ts +121 -0
- package/src/jobs/escrowHoldReminders.ts +177 -0
- package/src/models/AuthSubject.ts +147 -0
- package/src/models/EscrowHold.ts +321 -5
- package/src/models/UserKey.ts +91 -6
- package/src/models/index.ts +1 -0
- package/src/oidc.ts +371 -0
- package/src/routes/auth.ts +173 -0
- package/src/routes/escrow.ts +281 -34
- package/src/routes/keys.ts +5 -0
- package/src/routes/signing-authority.ts +39 -1
- package/src/services/delivery/adapters/postmark.adapter.ts +43 -1
- package/src/services/escrow-enclave/README.md +81 -10
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.pem +12 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.pem +12 -0
- package/src/services/escrow-enclave/blobIdentity.test.ts +42 -0
- package/src/services/escrow-enclave/blobIdentity.ts +55 -0
- package/src/services/escrow-enclave/cancellation.ts +59 -0
- package/src/services/escrow-enclave/enclaveTrustAnchors.ts +34 -0
- package/src/services/escrow-enclave/index.ts +145 -7
- package/src/services/escrow-enclave/notifications.test.ts +377 -0
- package/src/services/escrow-enclave/notifications.ts +340 -6
- package/src/services/escrow-enclave/production.test.ts +34 -0
- package/src/services/escrow-enclave/remoteEnclave.contract.test.ts +344 -0
- package/src/services/escrow-enclave/remoteEnclave.test.ts +490 -0
- package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +62 -0
- package/src/services/escrow-enclave/remoteEnclave.ts +263 -0
- package/src/services/escrow-enclave/softwareEnclave.test.ts +231 -30
- package/src/services/escrow-enclave/softwareEnclave.ts +97 -11
- package/src/services/escrow-enclave/types.ts +63 -17
- package/test/auth-tickets.spec.ts +480 -0
- package/test/helpers/keycloak-broker.ts +181 -0
- package/test/helpers/live-mongo.ts +9 -0
- package/test/jobs/escrowBlobRewrap.spec.ts +262 -0
- package/test/jobs/escrowHoldReminders.spec.ts +319 -0
- package/test/keycloak-admin.spec.ts +58 -0
- package/test/keycloak-broker-roundtrip.integration.spec.ts +40 -0
- package/test/keycloak-migration.integration.spec.ts +266 -0
- package/test/keycloak-provider-links.spec.ts +201 -0
- package/test/keycloak-social-broker.integration.spec.ts +228 -0
- package/test/keycloak-verify.integration.spec.ts +100 -0
- package/test/keycloak-verify.spec.ts +307 -0
- package/test/keys-escrow.spec.ts +963 -24
- package/test/models/authSubjectIndexes.spec.ts +132 -0
- package/test/models/escrow.spec.ts +375 -13
- package/test/notifications.spec.ts +4 -0
- package/test/oidc-e2e/broker.spec.ts +116 -0
- package/test/oidc-e2e/compose.yaml +33 -0
- package/test/oidc-e2e/fixtures.ts +245 -0
- package/test/oidc-e2e/lock.test.mjs +73 -0
- package/test/oidc-e2e/playwright.config.ts +21 -0
- package/test/oidc-e2e/run.mjs +312 -0
- package/test/oidc-e2e/tsconfig.json +10 -0
- package/test/oidc-e2e/types.ts +21 -0
- package/test/oidc.integration.spec.ts +54 -0
- package/test/oidc.spec.ts +697 -0
- package/test/oidcLambdaApp.boot.spec.ts +105 -0
- package/test/provision-keycloak-users.spec.ts +299 -0
- package/test/signing-authority.spec.ts +50 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +24 -1
- package/vitest.integration.config.ts +20 -3
- 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
|
|
61
|
-
|
|
|
62
|
-
| `GET /keys/escrow/attestation`
|
|
63
|
-
| `POST /keys/escrow`
|
|
64
|
-
| `DELETE /keys/escrow`
|
|
65
|
-
| `POST /keys/escrow/opt-in`
|
|
66
|
-
| `POST /keys/escrow/recover`
|
|
67
|
-
| `GET /keys/escrow/status`
|
|
68
|
-
| `POST /keys/escrow/cancel`
|
|
69
|
-
| `POST /keys/escrow/
|
|
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,48 @@
|
|
|
1
1
|
# @welibraryos/lca-api-service
|
|
2
2
|
|
|
3
|
+
## 1.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#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.
|
|
8
|
+
|
|
9
|
+
- [#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.
|
|
10
|
+
|
|
11
|
+
- [#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,
|
|
12
|
+
single-use login tickets, authorization codes and opaque access tokens. The
|
|
13
|
+
`auth.requestLoginTicket` and `auth.requestSocialLoginTicket` routes verify email
|
|
14
|
+
codes or native Google/Apple proofs. Keycloak brokers the redirect flow without
|
|
15
|
+
an additional login form. Includes Docker/Lambda wiring, a development broker
|
|
16
|
+
fixture and protocol tests. Phone OTP and full live broker round-trip CI remain deferred.
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- [#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.
|
|
21
|
+
|
|
22
|
+
- [#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).
|
|
23
|
+
|
|
24
|
+
- [#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.
|
|
25
|
+
|
|
26
|
+
- [#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.
|
|
27
|
+
|
|
28
|
+
- [#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.
|
|
29
|
+
|
|
30
|
+
- [#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.
|
|
31
|
+
|
|
32
|
+
- [#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.
|
|
33
|
+
|
|
34
|
+
- 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)]:
|
|
35
|
+
- @learncard/types@5.23.0
|
|
36
|
+
- @learncard/email-templates@1.2.1
|
|
37
|
+
- @learncard/sss-key-manager@0.2.1
|
|
38
|
+
- @learncard/service-config@0.1.1
|
|
39
|
+
- @learncard/didkit-plugin@1.10.3
|
|
40
|
+
- @learncard/core@9.4.38
|
|
41
|
+
- @learncard/helpers@1.6.2
|
|
42
|
+
- @learncard/init@2.5.2
|
|
43
|
+
- @learncard/did-web-plugin@1.1.38
|
|
44
|
+
- @learncard/didkit-plugin-node@0.3.3
|
|
45
|
+
|
|
3
46
|
## 1.4.0
|
|
4
47
|
|
|
5
48
|
### 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
|
+
});
|
|
@@ -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:
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LoginTicketPayload {
|
|
2
|
+
subject: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
emailVerified?: boolean;
|
|
5
|
+
phoneNumber?: string;
|
|
6
|
+
phoneNumberVerified?: boolean;
|
|
7
|
+
name?: string;
|
|
8
|
+
picture?: string;
|
|
9
|
+
identityKey: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const issueLoginTicket: (payload: LoginTicketPayload) => Promise<string>;
|
|
12
|
+
export declare const redeemLoginTicket: (ticket: string) => Promise<LoginTicketPayload | null>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const cacheEnvironmentShape: {
|
|
4
|
+
REDIS_HOST: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
|
|
5
|
+
REDIS_PORT: z.ZodPreprocess<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
|
|
6
|
+
};
|
|
7
|
+
export declare const cacheEnvironmentSchema: z.ZodObject<{
|
|
8
|
+
REDIS_HOST: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
|
|
9
|
+
REDIS_PORT: z.ZodPreprocess<z.ZodOptional<z.ZodCoercedNumber<unknown>>>;
|
|
10
|
+
}, z.core.$strip>;
|
|
11
|
+
export type CacheEnvironment = z.output<typeof cacheEnvironmentSchema>;
|
|
12
|
+
export declare const parseCacheEnvironment: (raw: Record<string, unknown>, source?: string) => CacheEnvironment;
|
|
13
|
+
export declare const environment: {
|
|
14
|
+
REDIS_HOST?: string | undefined;
|
|
15
|
+
REDIS_PORT?: number | undefined;
|
|
16
|
+
};
|