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