@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/serverless-local.yml
CHANGED
|
@@ -36,8 +36,12 @@ provider:
|
|
|
36
36
|
stage: ${opt:stage, "dev"}
|
|
37
37
|
region: ${opt:region, "us-east-1"}
|
|
38
38
|
environment:
|
|
39
|
-
|
|
39
|
+
# serverless-offline sets AWS_LAMBDA_FUNCTION_NAME; stage config stays opt-in locally.
|
|
40
|
+
LAMBDA_STAGE: ${env:CONFIG_STAGE, ''}
|
|
40
41
|
SEED: ${env:SEED}
|
|
42
|
+
SA_SEED_LOCAL_KEK: ${env:SA_SEED_LOCAL_KEK, ''}
|
|
43
|
+
SA_SEED_ENCRYPT_WRITES: ${env:SA_SEED_ENCRYPT_WRITES, 'false'}
|
|
44
|
+
SA_SEED_ALLOW_LEGACY_READ: ${env:SA_SEED_ALLOW_LEGACY_READ, 'true'}
|
|
41
45
|
DOMAIN_NAME: ${env:DOMAIN_NAME}
|
|
42
46
|
OPENAI_API_KEY: ${env:OPENAI_API_KEY}
|
|
43
47
|
METABASE_SECRET_KEY: ${env:METABASE_SECRET_KEY}
|
|
@@ -51,6 +55,16 @@ provider:
|
|
|
51
55
|
MONGO_DB_NAME: ${env:MONGO_DB_NAME}
|
|
52
56
|
AUTHORIZED_DIDS: ${env:AUTHORIZED_DIDS}
|
|
53
57
|
GOOGLE_APPLICATION_CREDENTIAL: ${env:GOOGLE_APPLICATION_CREDENTIAL, ""}
|
|
58
|
+
KEYCLOAK_ISSUERS: ${env:KEYCLOAK_ISSUERS, ""}
|
|
59
|
+
KEYCLOAK_AUDIENCES: ${env:KEYCLOAK_AUDIENCES, ""}
|
|
60
|
+
KEYCLOAK_JWKS_URL_OVERRIDES: ${env:KEYCLOAK_JWKS_URL_OVERRIDES, ""}
|
|
61
|
+
OIDC_ISSUER: ${env:OIDC_ISSUER, ""}
|
|
62
|
+
OIDC_SIGNING_KEY_JWK: ${env:OIDC_SIGNING_KEY_JWK, ""}
|
|
63
|
+
OIDC_CLIENT_ID: ${env:OIDC_CLIENT_ID, ""}
|
|
64
|
+
OIDC_CLIENT_SECRET: ${env:OIDC_CLIENT_SECRET, ""}
|
|
65
|
+
OIDC_REDIRECT_URIS: ${env:OIDC_REDIRECT_URIS, ""}
|
|
66
|
+
GOOGLE_OAUTH_CLIENT_IDS: ${env:GOOGLE_OAUTH_CLIENT_IDS, ""}
|
|
67
|
+
APPLE_OAUTH_CLIENT_IDS: ${env:APPLE_OAUTH_CLIENT_IDS, ""}
|
|
54
68
|
REDIS_HOST:
|
|
55
69
|
'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Address]
|
|
56
70
|
REDIS_PORT:
|
|
@@ -98,6 +112,17 @@ functions:
|
|
|
98
112
|
url:
|
|
99
113
|
cors: true
|
|
100
114
|
|
|
115
|
+
oidc:
|
|
116
|
+
handler: lambda.oidcHandler
|
|
117
|
+
vpc:
|
|
118
|
+
securityGroupIds:
|
|
119
|
+
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
120
|
+
subnetIds:
|
|
121
|
+
- Ref: PrivateSubnetA
|
|
122
|
+
events:
|
|
123
|
+
- httpApi: GET /.well-known/openid-configuration
|
|
124
|
+
- httpApi: ANY /oidc/{proxy+}
|
|
125
|
+
|
|
101
126
|
swagger:
|
|
102
127
|
handler: lambda.swaggerUiHandler
|
|
103
128
|
package:
|
|
@@ -223,6 +248,8 @@ resources:
|
|
|
223
248
|
Properties:
|
|
224
249
|
AutoMinorVersionUpgrade: true
|
|
225
250
|
Engine: redis
|
|
251
|
+
# GETDEL (login codes/tickets/auth codes) needs Redis >= 6.2
|
|
252
|
+
EngineVersion: '7.0'
|
|
226
253
|
CacheNodeType: ${self:custom.config.CACHE_INSTANCE_SIZE}
|
|
227
254
|
NumCacheNodes: 1
|
|
228
255
|
VpcSecurityGroupIds:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const { functionEnvironment, pickNonEmpty } = require('@learncard/service-config/function-env.cjs');
|
|
2
|
+
|
|
3
|
+
const PROVIDER_SECRETS = [
|
|
4
|
+
'SEED',
|
|
5
|
+
'MONGO_URI',
|
|
6
|
+
'MONGO_DB_NAME',
|
|
7
|
+
'AUTHORIZED_DIDS',
|
|
8
|
+
'METABASE_SECRET_KEY',
|
|
9
|
+
'OPENAI_API_KEY',
|
|
10
|
+
'POSTMARK_SERVER_TOKEN',
|
|
11
|
+
'SCOUTS_SSO_CLIENT_SECRET',
|
|
12
|
+
'SENTRY_DSN',
|
|
13
|
+
'LEARN_CLOUD_URL',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const API_SECRETS = [
|
|
17
|
+
'GOOGLE_APPLICATION_CREDENTIAL',
|
|
18
|
+
'POSTHOG_API_KEY',
|
|
19
|
+
'ESCROW_RELAY_URL',
|
|
20
|
+
'ESCROW_RELAY_AUTH_TOKEN',
|
|
21
|
+
'ESCROW_ENCLAVE_SOFTWARE_PRIVATE_KEYS_JSON',
|
|
22
|
+
'ESCROW_ENCLAVE_REMOTE_URL',
|
|
23
|
+
'ESCROW_ENCLAVE_REMOTE_TOKEN',
|
|
24
|
+
'KEYCLOAK_JWKS_URL_OVERRIDES',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Non-secret operator toggles forwarded in bundle mode too, so enabling RUNTIME_SECRETS_ID
|
|
28
|
+
// can never silently change them: escrow stays off unless ESCROW_ENCLAVE_MODE is set, and
|
|
29
|
+
// ESCROW_RELEASE_KILL_SWITCH must be flippable without editing the bundle.
|
|
30
|
+
const API_TOGGLES = ['ESCROW_ENCLAVE_MODE', 'ESCROW_RELEASE_KILL_SWITCH'];
|
|
31
|
+
|
|
32
|
+
// Serverless v3 supplies CLI options to file functions and preserves CF intrinsics.
|
|
33
|
+
// No stage-file values are forwarded from the deployment environment.
|
|
34
|
+
exports.provider = ({ options = {} } = {}) => ({
|
|
35
|
+
LAMBDA_STAGE: options.stage || 'dev',
|
|
36
|
+
// Non-secret product selector for the shared stage files (config.<tenant>.<stage>.json).
|
|
37
|
+
CONFIG_TENANT: process.env.CONFIG_TENANT || 'learncard',
|
|
38
|
+
PORT: String(options.httpPort || '3000'),
|
|
39
|
+
SA_SEED_KMS_KEY_ARN: { 'Fn::GetAtt': ['SigningAuthoritySeedKey', 'Arn'] },
|
|
40
|
+
REDIS_HOST: { 'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Address'] },
|
|
41
|
+
REDIS_PORT: { 'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Port'] },
|
|
42
|
+
...functionEnvironment({ always: [], fallback: PROVIDER_SECRETS }),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// API/migration and the default-role didWeb/swagger functions get the bundle pointer.
|
|
46
|
+
// Reading the bundle does not grant the default role signing-authority KMS access.
|
|
47
|
+
exports.api = () =>
|
|
48
|
+
functionEnvironment({
|
|
49
|
+
always: ['RUNTIME_SECRETS_ID', ...API_TOGGLES],
|
|
50
|
+
fallback: API_SECRETS,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Scheduled escrow jobs load the full app (Mongo, Postmark, seed) through lambda.ts, so they
|
|
54
|
+
// need the same bundle pointer and escrow settings as the API functions.
|
|
55
|
+
exports.escrow = exports.api;
|
|
56
|
+
|
|
57
|
+
// OIDC has its own signing-key secret and cannot read the runtime bundle.
|
|
58
|
+
exports.oidc = () => pickNonEmpty(['OIDC_CLIENT_SECRET', 'OIDC_REDIRECT_URIS']);
|
package/serverless.yml
CHANGED
|
@@ -129,32 +129,17 @@ provider:
|
|
|
129
129
|
timeout: 29
|
|
130
130
|
stage: ${opt:stage, "dev"}
|
|
131
131
|
region: ${opt:region, "us-east-1"}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
POSTMARK_FROM_EMAIL: ${env:POSTMARK_FROM_EMAIL, ''}
|
|
144
|
-
POSTMARK_BRAND_NAME: ${env:POSTMARK_BRAND_NAME, ''}
|
|
145
|
-
PORT: ${opt:httpPort, "3000"}
|
|
146
|
-
MONGO_URI: ${env:MONGO_URI, ''}
|
|
147
|
-
MONGO_DB_NAME: ${env:MONGO_DB_NAME, ''}
|
|
148
|
-
AUTHORIZED_DIDS: ${env:AUTHORIZED_DIDS, ''}
|
|
149
|
-
GOOGLE_APPLICATION_CREDENTIAL: ${env:GOOGLE_APPLICATION_CREDENTIAL, ''}
|
|
150
|
-
SCOUTS_SSO_CLIENT_SECRET: ${env:SCOUTS_SSO_CLIENT_SECRET, ''}
|
|
151
|
-
LEARN_CLOUD_URL: ${env:LEARN_CLOUD_URL, ''}
|
|
152
|
-
SENTRY_DSN: ${env:SENTRY_DSN, ''}
|
|
153
|
-
SENTRY_ENV: ${env:SENTRY_ENV, ''}
|
|
154
|
-
REDIS_HOST:
|
|
155
|
-
'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Address]
|
|
156
|
-
REDIS_PORT:
|
|
157
|
-
'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Port]
|
|
132
|
+
# Bundle mode emits only infra; fallback mode retains the existing GitHub secrets.
|
|
133
|
+
environment: ${file(./serverless.function-env.cjs):provider}
|
|
134
|
+
# Only didWeb/swagger use this generated role. Explicit function roles do not inherit it.
|
|
135
|
+
# Bundle access must not grant cryptographic use of the hosted signing-authority KMS key.
|
|
136
|
+
iam:
|
|
137
|
+
role:
|
|
138
|
+
statements:
|
|
139
|
+
- Effect: Allow
|
|
140
|
+
Action: secretsmanager:GetSecretValue
|
|
141
|
+
Resource:
|
|
142
|
+
Fn::Sub: arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:lca-api/${sls:stage}/runtime-secrets-*
|
|
158
143
|
tracing:
|
|
159
144
|
apiGateway: true
|
|
160
145
|
lambda: true
|
|
@@ -167,6 +152,9 @@ provider:
|
|
|
167
152
|
functions:
|
|
168
153
|
trpc:
|
|
169
154
|
handler: lambda.trpcHandler
|
|
155
|
+
role: SigningAuthorityExecutionRole
|
|
156
|
+
dependsOn: [SigningAuthoritySeedKeyPolicy]
|
|
157
|
+
environment: ${file(./serverless.function-env.cjs):api}
|
|
170
158
|
vpc:
|
|
171
159
|
securityGroupIds:
|
|
172
160
|
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
@@ -177,6 +165,9 @@ functions:
|
|
|
177
165
|
|
|
178
166
|
api:
|
|
179
167
|
handler: lambda.openApiHandler
|
|
168
|
+
role: SigningAuthorityExecutionRole
|
|
169
|
+
dependsOn: [SigningAuthoritySeedKeyPolicy]
|
|
170
|
+
environment: ${file(./serverless.function-env.cjs):api}
|
|
180
171
|
vpc:
|
|
181
172
|
securityGroupIds:
|
|
182
173
|
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
@@ -187,6 +178,7 @@ functions:
|
|
|
187
178
|
|
|
188
179
|
didWeb:
|
|
189
180
|
handler: lambda.didWebHandler
|
|
181
|
+
environment: ${file(./serverless.function-env.cjs):api}
|
|
190
182
|
vpc:
|
|
191
183
|
securityGroupIds:
|
|
192
184
|
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
@@ -198,8 +190,44 @@ functions:
|
|
|
198
190
|
url:
|
|
199
191
|
cors: true
|
|
200
192
|
|
|
193
|
+
oidc:
|
|
194
|
+
handler: oidcLambda.handler
|
|
195
|
+
# Only this function may read the private signing key (see OidcExecutionRole).
|
|
196
|
+
role: OidcExecutionRole
|
|
197
|
+
environment: ${file(./serverless.function-env.cjs):oidc}
|
|
198
|
+
vpc:
|
|
199
|
+
securityGroupIds:
|
|
200
|
+
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
201
|
+
subnetIds:
|
|
202
|
+
- Ref: PrivateSubnetA
|
|
203
|
+
events:
|
|
204
|
+
- httpApi: GET /.well-known/openid-configuration
|
|
205
|
+
- httpApi: ANY /oidc/{proxy+}
|
|
206
|
+
|
|
207
|
+
seedMigration:
|
|
208
|
+
handler: seedMigrationLambda.handler
|
|
209
|
+
role: SigningAuthorityExecutionRole
|
|
210
|
+
timeout: 900
|
|
211
|
+
reservedConcurrency: 1
|
|
212
|
+
dependsOn: [SigningAuthoritySeedKeyPolicy]
|
|
213
|
+
# Shares the SigningAuthority role, so it reads the runtime bundle when present and
|
|
214
|
+
# falls back to the same credentials as trpc/api otherwise (it needs SEED + Mongo).
|
|
215
|
+
environment: ${file(./serverless.function-env.cjs):api}
|
|
216
|
+
# IAM invoke only: intentionally no events, schedule, or function URL.
|
|
217
|
+
vpc:
|
|
218
|
+
securityGroupIds:
|
|
219
|
+
- Fn::GetAtt: [ServerlessSecurityGroup, GroupId]
|
|
220
|
+
subnetIds:
|
|
221
|
+
- Ref: PrivateSubnetA
|
|
222
|
+
|
|
201
223
|
swagger:
|
|
202
224
|
handler: lambda.swaggerUiHandler
|
|
225
|
+
environment: ${file(./serverless.function-env.cjs):api}
|
|
226
|
+
vpc:
|
|
227
|
+
securityGroupIds:
|
|
228
|
+
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
229
|
+
subnetIds:
|
|
230
|
+
- Ref: PrivateSubnetA
|
|
203
231
|
package:
|
|
204
232
|
patterns:
|
|
205
233
|
- src/swagger-ui/**
|
|
@@ -207,12 +235,248 @@ functions:
|
|
|
207
235
|
- httpApi: GET /docs
|
|
208
236
|
- httpApi: GET /docs/{trpc+}
|
|
209
237
|
|
|
238
|
+
# Always deployed (no per-stage conditional: `schedule.enabled` needs a
|
|
239
|
+
# literal boolean, not an interpolated ESCROW_ENCLAVE_MODE env string);
|
|
240
|
+
# the handler itself no-ops when escrow is disabled for this stage/tenant.
|
|
241
|
+
escrowHoldReminders:
|
|
242
|
+
handler: lambda.escrowHoldRemindersHandler
|
|
243
|
+
timeout: 120
|
|
244
|
+
environment: ${file(./serverless.function-env.cjs):escrow}
|
|
245
|
+
vpc:
|
|
246
|
+
securityGroupIds:
|
|
247
|
+
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
248
|
+
subnetIds:
|
|
249
|
+
- Ref: PrivateSubnetA
|
|
250
|
+
events:
|
|
251
|
+
- schedule: rate(1 hour)
|
|
252
|
+
|
|
253
|
+
# Same always-deployed/no-op-when-disabled shape as escrowHoldReminders
|
|
254
|
+
# above; a bounded batch each run, so an hourly rate is generous relative
|
|
255
|
+
# to how quickly a key rotation's previous-key backlog should drain.
|
|
256
|
+
escrowBlobRewrap:
|
|
257
|
+
handler: lambda.escrowBlobRewrapHandler
|
|
258
|
+
timeout: 120
|
|
259
|
+
environment: ${file(./serverless.function-env.cjs):escrow}
|
|
260
|
+
vpc:
|
|
261
|
+
securityGroupIds:
|
|
262
|
+
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
263
|
+
subnetIds:
|
|
264
|
+
- Ref: PrivateSubnetA
|
|
265
|
+
events:
|
|
266
|
+
- schedule: rate(1 hour)
|
|
267
|
+
|
|
210
268
|
resources:
|
|
269
|
+
# Escrow enclave over PrivateLink: created only when the stage sets
|
|
270
|
+
# ESCROW_ENCLAVE_ENDPOINT_SERVICE_NAME (the enclave account's endpoint
|
|
271
|
+
# service). TLS trust for the hostname is pinned in enclaveTrustAnchors.ts.
|
|
272
|
+
Conditions:
|
|
273
|
+
EscrowEnclavePrivateLink:
|
|
274
|
+
Fn::Not:
|
|
275
|
+
- Fn::Equals: ['${env:ESCROW_ENCLAVE_ENDPOINT_SERVICE_NAME, ""}', '']
|
|
276
|
+
Outputs:
|
|
277
|
+
SigningAuthoritySeedKeyArn:
|
|
278
|
+
Value:
|
|
279
|
+
Fn::GetAtt: [SigningAuthoritySeedKey, Arn]
|
|
280
|
+
SigningAuthoritySeedMigrationFunctionName:
|
|
281
|
+
Value:
|
|
282
|
+
Ref: SeedMigrationLambdaFunction
|
|
211
283
|
Resources:
|
|
284
|
+
# Only the two signing entry points and the migration worker assume this role.
|
|
285
|
+
# Keep KMS references out of the role itself to avoid a Role -> Key -> Role cycle.
|
|
286
|
+
SigningAuthorityExecutionRole:
|
|
287
|
+
Type: AWS::IAM::Role
|
|
288
|
+
Properties:
|
|
289
|
+
AssumeRolePolicyDocument:
|
|
290
|
+
Version: '2012-10-17'
|
|
291
|
+
Statement:
|
|
292
|
+
- Effect: Allow
|
|
293
|
+
Principal:
|
|
294
|
+
Service: lambda.amazonaws.com
|
|
295
|
+
Action: sts:AssumeRole
|
|
296
|
+
ManagedPolicyArns:
|
|
297
|
+
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
|
|
298
|
+
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AWSXRayDaemonWriteAccess
|
|
299
|
+
Policies:
|
|
300
|
+
- PolicyName: runtime-secrets
|
|
301
|
+
PolicyDocument:
|
|
302
|
+
Version: '2012-10-17'
|
|
303
|
+
Statement:
|
|
304
|
+
# seedMigration shares this role; bundle access is acceptable for now.
|
|
305
|
+
- Effect: Allow
|
|
306
|
+
Action: secretsmanager:GetSecretValue
|
|
307
|
+
Resource:
|
|
308
|
+
Fn::Sub: arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:lca-api/${sls:stage}/runtime-secrets-*
|
|
309
|
+
OidcExecutionRole:
|
|
310
|
+
Type: AWS::IAM::Role
|
|
311
|
+
Properties:
|
|
312
|
+
AssumeRolePolicyDocument:
|
|
313
|
+
Version: '2012-10-17'
|
|
314
|
+
Statement:
|
|
315
|
+
- Effect: Allow
|
|
316
|
+
Principal:
|
|
317
|
+
Service: lambda.amazonaws.com
|
|
318
|
+
Action: sts:AssumeRole
|
|
319
|
+
ManagedPolicyArns:
|
|
320
|
+
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
|
|
321
|
+
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/AWSXRayDaemonWriteAccess
|
|
322
|
+
Policies:
|
|
323
|
+
- PolicyName: oidc-signing-key
|
|
324
|
+
PolicyDocument:
|
|
325
|
+
Version: '2012-10-17'
|
|
326
|
+
Statement:
|
|
327
|
+
# SecretString is the private JWK JSON; only its id belongs in Lambda env.
|
|
328
|
+
- Effect: Allow
|
|
329
|
+
Action: secretsmanager:GetSecretValue
|
|
330
|
+
Resource:
|
|
331
|
+
Fn::Sub: arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:lca-api/${sls:stage}/oidc-signing-jwk-*
|
|
332
|
+
SigningAuthoritySeedKey:
|
|
333
|
+
Type: AWS::KMS::Key
|
|
334
|
+
DeletionPolicy: Retain
|
|
335
|
+
UpdateReplacePolicy: Retain
|
|
336
|
+
Properties:
|
|
337
|
+
Description: Envelope encryption for hosted signing-authority seeds
|
|
338
|
+
KeySpec: SYMMETRIC_DEFAULT
|
|
339
|
+
KeyUsage: ENCRYPT_DECRYPT
|
|
340
|
+
EnableKeyRotation: true
|
|
341
|
+
PendingWindowInDays: 30
|
|
342
|
+
KeyPolicy:
|
|
343
|
+
Version: '2012-10-17'
|
|
344
|
+
Statement:
|
|
345
|
+
- Sid: AccountKeyAdministration
|
|
346
|
+
Effect: Allow
|
|
347
|
+
Principal:
|
|
348
|
+
AWS:
|
|
349
|
+
Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:root
|
|
350
|
+
Action:
|
|
351
|
+
- kms:DescribeKey
|
|
352
|
+
- kms:GetKeyPolicy
|
|
353
|
+
- kms:PutKeyPolicy
|
|
354
|
+
- kms:ListResourceTags
|
|
355
|
+
- kms:TagResource
|
|
356
|
+
- kms:UntagResource
|
|
357
|
+
- kms:GetKeyRotationStatus
|
|
358
|
+
- kms:EnableKeyRotation
|
|
359
|
+
- kms:DisableKeyRotation
|
|
360
|
+
- kms:UpdateKeyDescription
|
|
361
|
+
- kms:EnableKey
|
|
362
|
+
- kms:DisableKey
|
|
363
|
+
- kms:ScheduleKeyDeletion
|
|
364
|
+
- kms:CancelKeyDeletion
|
|
365
|
+
- kms:CreateAlias
|
|
366
|
+
- kms:UpdateAlias
|
|
367
|
+
- kms:DeleteAlias
|
|
368
|
+
Resource: '*'
|
|
369
|
+
- Sid: LcaApiSeedEncryption
|
|
370
|
+
Effect: Allow
|
|
371
|
+
Principal:
|
|
372
|
+
AWS:
|
|
373
|
+
Fn::GetAtt: [SigningAuthorityExecutionRole, Arn]
|
|
374
|
+
Action: [kms:GenerateDataKey, kms:Decrypt]
|
|
375
|
+
Resource: '*'
|
|
376
|
+
Condition:
|
|
377
|
+
StringEquals:
|
|
378
|
+
kms:EncryptionContext:purpose: lca-signing-authority-seed
|
|
379
|
+
- Sid: DenyOtherCryptographicPrincipals
|
|
380
|
+
Effect: Deny
|
|
381
|
+
Principal: '*'
|
|
382
|
+
Action:
|
|
383
|
+
[kms:Decrypt, 'kms:ReEncrypt*', 'kms:GenerateDataKey*', kms:Encrypt]
|
|
384
|
+
Resource: '*'
|
|
385
|
+
Condition:
|
|
386
|
+
ArnNotEquals:
|
|
387
|
+
aws:PrincipalArn:
|
|
388
|
+
Fn::GetAtt: [SigningAuthorityExecutionRole, Arn]
|
|
389
|
+
SigningAuthoritySeedKeyAlias:
|
|
390
|
+
Type: AWS::KMS::Alias
|
|
391
|
+
Properties:
|
|
392
|
+
AliasName: alias/${self:service}-${self:provider.stage}-sa-seeds
|
|
393
|
+
TargetKeyId:
|
|
394
|
+
Ref: SigningAuthoritySeedKey
|
|
395
|
+
# The direct execution-role grant in the KEY policy authorizes cryptographic use.
|
|
396
|
+
# This matching IAM allow is not required by that grant; it documents the scope and
|
|
397
|
+
# provides a dependsOn deployment barrier. Keeping it separate avoids a Role -> Key
|
|
398
|
+
# -> Role CloudFormation dependency cycle. Swagger/didWeb use the generated role.
|
|
399
|
+
SigningAuthoritySeedKeyPolicy:
|
|
400
|
+
Type: AWS::IAM::Policy
|
|
401
|
+
Properties:
|
|
402
|
+
PolicyName: ${self:service}-${self:provider.stage}-sa-seeds
|
|
403
|
+
Roles:
|
|
404
|
+
- Ref: SigningAuthorityExecutionRole
|
|
405
|
+
PolicyDocument:
|
|
406
|
+
Version: '2012-10-17'
|
|
407
|
+
Statement:
|
|
408
|
+
- Effect: Allow
|
|
409
|
+
Action: [kms:GenerateDataKey, kms:Decrypt]
|
|
410
|
+
Resource:
|
|
411
|
+
Fn::GetAtt: [SigningAuthoritySeedKey, Arn]
|
|
412
|
+
Condition:
|
|
413
|
+
StringEquals:
|
|
414
|
+
kms:EncryptionContext:purpose: lca-signing-authority-seed
|
|
212
415
|
VPC:
|
|
213
416
|
Type: AWS::EC2::VPC
|
|
214
417
|
Properties:
|
|
215
418
|
CidrBlock: '10.0.0.0/16'
|
|
419
|
+
# A private hosted zone needs DNS hostnames; left at the default elsewhere.
|
|
420
|
+
EnableDnsHostnames:
|
|
421
|
+
Fn::If: [EscrowEnclavePrivateLink, true, Ref: AWS::NoValue]
|
|
422
|
+
EscrowEnclaveEndpointSecurityGroup:
|
|
423
|
+
Type: AWS::EC2::SecurityGroup
|
|
424
|
+
Condition: EscrowEnclavePrivateLink
|
|
425
|
+
Properties:
|
|
426
|
+
GroupDescription: Escrow enclave PrivateLink endpoint, 8443 from lca-api only
|
|
427
|
+
VpcId:
|
|
428
|
+
Ref: VPC
|
|
429
|
+
SecurityGroupIngress:
|
|
430
|
+
- IpProtocol: tcp
|
|
431
|
+
FromPort: 8443
|
|
432
|
+
ToPort: 8443
|
|
433
|
+
SourceSecurityGroupId:
|
|
434
|
+
Ref: ServerlessSecurityGroup
|
|
435
|
+
EscrowEnclaveEndpoint:
|
|
436
|
+
Type: AWS::EC2::VPCEndpoint
|
|
437
|
+
Condition: EscrowEnclavePrivateLink
|
|
438
|
+
Properties:
|
|
439
|
+
VpcEndpointType: Interface
|
|
440
|
+
ServiceName: ${env:ESCROW_ENCLAVE_ENDPOINT_SERVICE_NAME, ''}
|
|
441
|
+
VpcId:
|
|
442
|
+
Ref: VPC
|
|
443
|
+
SubnetIds:
|
|
444
|
+
- Ref: PrivateSubnetA
|
|
445
|
+
SecurityGroupIds:
|
|
446
|
+
- Ref: EscrowEnclaveEndpointSecurityGroup
|
|
447
|
+
PrivateDnsEnabled: false
|
|
448
|
+
EscrowEnclaveHostedZone:
|
|
449
|
+
Type: AWS::Route53::HostedZone
|
|
450
|
+
Condition: EscrowEnclavePrivateLink
|
|
451
|
+
Properties:
|
|
452
|
+
Name: ${env:ESCROW_ENCLAVE_HOSTNAME, ''}
|
|
453
|
+
VPCs:
|
|
454
|
+
- VPCId:
|
|
455
|
+
Ref: VPC
|
|
456
|
+
VPCRegion: ${self:provider.region}
|
|
457
|
+
EscrowEnclaveDnsRecord:
|
|
458
|
+
Type: AWS::Route53::RecordSet
|
|
459
|
+
Condition: EscrowEnclavePrivateLink
|
|
460
|
+
Properties:
|
|
461
|
+
HostedZoneId:
|
|
462
|
+
Ref: EscrowEnclaveHostedZone
|
|
463
|
+
Name: ${env:ESCROW_ENCLAVE_HOSTNAME, ''}
|
|
464
|
+
Type: A
|
|
465
|
+
# DnsEntries[0] is "<hosted zone id>:<regional endpoint DNS name>".
|
|
466
|
+
AliasTarget:
|
|
467
|
+
HostedZoneId:
|
|
468
|
+
Fn::Select:
|
|
469
|
+
- 0
|
|
470
|
+
- Fn::Split:
|
|
471
|
+
- ':'
|
|
472
|
+
- Fn::Select: [0, Fn::GetAtt: [EscrowEnclaveEndpoint, DnsEntries]]
|
|
473
|
+
DNSName:
|
|
474
|
+
Fn::Select:
|
|
475
|
+
- 1
|
|
476
|
+
- Fn::Split:
|
|
477
|
+
- ':'
|
|
478
|
+
- Fn::Select: [0, Fn::GetAtt: [EscrowEnclaveEndpoint, DnsEntries]]
|
|
479
|
+
EvaluateTargetHealth: false
|
|
216
480
|
IP:
|
|
217
481
|
Type: AWS::EC2::EIP
|
|
218
482
|
Properties:
|
|
@@ -323,6 +587,10 @@ resources:
|
|
|
323
587
|
Properties:
|
|
324
588
|
AutoMinorVersionUpgrade: true
|
|
325
589
|
Engine: redis
|
|
590
|
+
# GETDEL (login codes/tickets/auth codes) needs Redis >= 6.2. The
|
|
591
|
+
# version is deliberately NOT pinned here: existing clusters are
|
|
592
|
+
# already on 7.x, and a pin below the running version is rejected
|
|
593
|
+
# by CloudFormation as a downgrade (failing the whole deploy).
|
|
326
594
|
CacheNodeType: ${self:custom.config.CACHE_INSTANCE_SIZE}
|
|
327
595
|
NumCacheNodes: 1
|
|
328
596
|
VpcSecurityGroupIds:
|
|
@@ -12,12 +12,19 @@ export const getNotificationsCollection = () => {
|
|
|
12
12
|
|
|
13
13
|
export const Notifications = getNotificationsCollection();
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Fire-and-forget at module load: every Lambda imports this, so an uncaught
|
|
16
|
+
// rejection (e.g. a Mongo connect that outlives a frozen invocation) would
|
|
17
|
+
// crash whichever function thaws next.
|
|
18
|
+
const logIndexFailure = (error: unknown): void => {
|
|
19
|
+
console.error('Notifications index creation failed:', error);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
Notifications.createIndex({ 'to.did': 1, read: 1, sent: -1, _id: 1 }).catch(logIndexFailure);
|
|
16
23
|
Notifications.createIndex({
|
|
17
24
|
'to.did': 1,
|
|
18
25
|
type: 1,
|
|
19
26
|
'data.metadata.connectionPrompt.promptId': 1,
|
|
20
|
-
});
|
|
27
|
+
}).catch(logIndexFailure);
|
|
21
28
|
|
|
22
29
|
const LEGACY_CREDENTIAL_REFRESH_INDEX = 'to.did_1_type_1_data.metadata.deliveryKey_1';
|
|
23
30
|
const MANAGED_CREDENTIAL_REFRESH_INDEX = 'managed_credential_refresh_delivery_key_unique';
|
|
@@ -3,6 +3,7 @@ import { getEphemeralLearnCard } from '@helpers/learnCard.helpers';
|
|
|
3
3
|
import { SigningAuthorities } from '.';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import { logSeedEncryptionFailure, seedEncryption } from '@helpers/seedEncryption.helpers';
|
|
6
7
|
|
|
7
8
|
export const createSigningAuthorityForDID = async (
|
|
8
9
|
ownerDid: string,
|
|
@@ -10,19 +11,22 @@ export const createSigningAuthorityForDID = async (
|
|
|
10
11
|
): Promise<string | false> => {
|
|
11
12
|
const seed =
|
|
12
13
|
environment.NODE_ENV === 'test' ? 'e'.repeat(64) : crypto.randomBytes(32).toString('hex');
|
|
13
|
-
const
|
|
14
|
+
const _id = uuidv4();
|
|
14
15
|
try {
|
|
16
|
+
const did = (await getEphemeralLearnCard(seed)).id.did();
|
|
17
|
+
const identity = { _id, ownerDid, did, name };
|
|
18
|
+
// Plaintext writes support the compatibility deployment and offline opt-in defaults.
|
|
19
|
+
const secret = environment.SA_SEED_ENCRYPT_WRITES
|
|
20
|
+
? await seedEncryption.encrypt(seed, identity)
|
|
21
|
+
: { seed };
|
|
15
22
|
return (
|
|
16
23
|
await SigningAuthorities.insertOne({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
did,
|
|
20
|
-
name,
|
|
21
|
-
seed,
|
|
24
|
+
...identity,
|
|
25
|
+
...secret,
|
|
22
26
|
})
|
|
23
27
|
).insertedId;
|
|
24
28
|
} catch (e) {
|
|
25
|
-
|
|
29
|
+
logSeedEncryptionFailure(e, 'create', { _id });
|
|
26
30
|
return false;
|
|
27
31
|
}
|
|
28
32
|
};
|
|
@@ -7,4 +7,7 @@ export const getSigningAuthoritiesCollection = () => {
|
|
|
7
7
|
|
|
8
8
|
export const SigningAuthorities = getSigningAuthoritiesCollection();
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// Fire-and-forget at module load; see accesslayer/notifications for why it must not reject.
|
|
11
|
+
SigningAuthorities.createIndex({ ownerDid: 1, name: 1 }, { unique: true }).catch(error => {
|
|
12
|
+
console.error('Signing authority unique index creation failed:', error);
|
|
13
|
+
});
|
package/src/app.ts
CHANGED
|
@@ -10,7 +10,9 @@ import { analyticsRouter } from '@routes/analytics';
|
|
|
10
10
|
export { createContext } from '@routes';
|
|
11
11
|
import { preferencesRouter } from '@routes/preferences';
|
|
12
12
|
import { keysRouter } from '@routes/keys';
|
|
13
|
+
import { escrowRouter } from '@routes/escrow';
|
|
13
14
|
import { qrLoginRouter } from '@routes/qr-login';
|
|
15
|
+
import { authRouter } from '@routes/auth';
|
|
14
16
|
import { testRouter, type TestRouter } from '@routes/test';
|
|
15
17
|
import { environment } from '@environment';
|
|
16
18
|
|
|
@@ -25,7 +27,9 @@ const routes = {
|
|
|
25
27
|
analytics: analyticsRouter,
|
|
26
28
|
preferences: preferencesRouter,
|
|
27
29
|
keys: keysRouter,
|
|
30
|
+
escrow: escrowRouter,
|
|
28
31
|
qrLogin: qrLoginRouter,
|
|
32
|
+
auth: authRouter,
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
export const appRouter = t.router<typeof routes & { test?: TestRouter }>({
|
package/src/cache/challenges.ts
CHANGED
|
@@ -4,6 +4,7 @@ export const getChallengeCacheKey = (did: string, challenge: string): string =>
|
|
|
4
4
|
`challenge|${did}|${challenge}`;
|
|
5
5
|
|
|
6
6
|
export const VALID = 'valid';
|
|
7
|
+
export const DID_CHALLENGE_TTL_SECS = 5 * 60;
|
|
7
8
|
|
|
8
9
|
export const isChallengeValidForDid = async (
|
|
9
10
|
did: string,
|
|
@@ -14,8 +15,17 @@ export const isChallengeValidForDid = async (
|
|
|
14
15
|
return result === VALID ? result : undefined;
|
|
15
16
|
};
|
|
16
17
|
|
|
18
|
+
/** Atomically validate and consume a challenge so concurrent replays cannot both succeed. */
|
|
19
|
+
export const consumeChallengeForDid = async (did: string, challenge: string): Promise<boolean> => {
|
|
20
|
+
const key = getChallengeCacheKey(did, challenge);
|
|
21
|
+
const redis = cache.redis ?? cache.node;
|
|
22
|
+
const value = await redis.getdel(key);
|
|
23
|
+
|
|
24
|
+
return value === VALID;
|
|
25
|
+
};
|
|
26
|
+
|
|
17
27
|
export const setValidChallengeForDid = async (did: string, challenge: string) => {
|
|
18
|
-
return cache.set(getChallengeCacheKey(did, challenge), VALID);
|
|
28
|
+
return cache.set(getChallengeCacheKey(did, challenge), VALID, DID_CHALLENGE_TTL_SECS);
|
|
19
29
|
};
|
|
20
30
|
|
|
21
31
|
export const setValidChallengesForDid = async (did: string, challenges: string[]) => {
|
|
@@ -25,7 +35,7 @@ export const setValidChallengesForDid = async (did: string, challenges: string[]
|
|
|
25
35
|
return acc;
|
|
26
36
|
}, {});
|
|
27
37
|
|
|
28
|
-
return cache.mset(values);
|
|
38
|
+
return cache.mset(values, DID_CHALLENGE_TTL_SECS);
|
|
29
39
|
};
|
|
30
40
|
|
|
31
41
|
export const invalidateChallengeForDid = async (did: string, challenge: string) => {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import cache from '@cache';
|
|
3
|
+
import { consumeMatchingCode } from './getDel';
|
|
4
|
+
|
|
5
|
+
vi.mock('@cache', async () => {
|
|
6
|
+
const { default: Redis } = await import('ioredis-mock');
|
|
7
|
+
return { default: { node: new Redis() } };
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await cache.node.flushall();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('consumeMatchingCode', () => {
|
|
15
|
+
it('preserves a valid code after a wrong guess', async () => {
|
|
16
|
+
await cache.node.set('login-code:test@example.com', '123456');
|
|
17
|
+
expect(await consumeMatchingCode('login-code:test@example.com', '000000')).toBe(false);
|
|
18
|
+
expect(await cache.node.get('login-code:test@example.com')).toBe('123456');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('allows exactly one concurrent redemption and rejects replay', async () => {
|
|
22
|
+
await cache.node.set('login-code:test@example.com', '123456');
|
|
23
|
+
const results = await Promise.all([
|
|
24
|
+
consumeMatchingCode('login-code:test@example.com', '123456'),
|
|
25
|
+
consumeMatchingCode('login-code:test@example.com', '123456'),
|
|
26
|
+
]);
|
|
27
|
+
expect(results.filter(Boolean)).toHaveLength(1);
|
|
28
|
+
expect(await consumeMatchingCode('login-code:test@example.com', '123456')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('rejects missing codes', async () => {
|
|
32
|
+
expect(await consumeMatchingCode('login-code:missing@example.com', '123456')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|