@learncard/lca-api-service 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +73 -0
- package/AGENTS.md +106 -10
- package/CHANGELOG.md +43 -0
- package/bootstrapLambda.test.ts +76 -0
- package/config/config.dev.json +23 -0
- package/config/config.example.json +22 -0
- package/config/config.json +1 -0
- package/config/config.production.json +12 -0
- package/config/config.scouts.dev.json +11 -0
- package/config/config.scouts.production.json +11 -0
- package/dist/app.d.ts +67 -3
- package/dist/cache/getDel.d.ts +2 -0
- package/dist/cache/getDel.test.d.ts +1 -0
- package/dist/cache/login-tickets.d.ts +12 -0
- package/dist/config/applyDockerStageConfig.d.ts +1 -0
- package/dist/config/cacheEnvironment.d.ts +16 -0
- package/dist/config/environment.d.ts +101 -0
- package/dist/config/escrowBundleMode.test.d.ts +1 -0
- package/dist/config/escrowDeployment.test.d.ts +1 -0
- package/dist/config/oidcEnvironment.d.ts +74 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts +1 -0
- package/dist/config/stageConfig.d.ts +62 -0
- package/dist/config/stageConfig.test.d.ts +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/helpers/auth.helpers.d.ts +1 -1
- package/dist/helpers/escrowCancelLink.d.ts +7 -0
- package/dist/helpers/escrowCancelLink.test.d.ts +1 -0
- package/dist/helpers/escrowCancelToken.d.ts +4 -0
- package/dist/helpers/escrowCancelToken.test.d.ts +1 -0
- package/dist/helpers/keycloak.helpers.d.ts +23 -0
- package/dist/helpers/login-ticket.helpers.d.ts +31 -0
- package/dist/helpers/login-ticket.helpers.test.d.ts +1 -0
- package/dist/helpers/oidc.helpers.d.ts +47 -0
- package/dist/helpers/rate-limit.helpers.d.ts +6 -0
- package/dist/helpers/rate-limit.helpers.test.d.ts +1 -0
- package/dist/helpers/social-token.helpers.d.ts +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/jobs/escrowBlobRewrap.d.ts +9 -0
- package/dist/jobs/escrowHoldReminders.d.ts +12 -0
- package/dist/models/AuthSubject.d.ts +20 -0
- package/dist/models/EscrowHold.d.ts +79 -1
- package/dist/models/Notification.d.ts +5 -0
- package/dist/models/UserKey.d.ts +11 -1
- package/dist/models/index.d.ts +1 -0
- package/dist/oidc.d.ts +5 -0
- package/dist/routes/auth.d.ts +31 -0
- package/dist/routes/escrow.d.ts +18 -4
- package/dist/routes/keys.d.ts +1 -0
- package/dist/routes/notifications.d.ts +5 -0
- package/dist/routes/signing-authority.d.ts +14 -0
- package/dist/services/escrow-enclave/blobIdentity.d.ts +10 -0
- package/dist/services/escrow-enclave/blobIdentity.test.d.ts +1 -0
- package/dist/services/escrow-enclave/cancellation.d.ts +3 -0
- package/dist/services/escrow-enclave/enclaveTrustAnchors.d.ts +1 -0
- package/dist/services/escrow-enclave/index.d.ts +16 -0
- package/dist/services/escrow-enclave/notifications.d.ts +19 -4
- package/dist/services/escrow-enclave/notifications.test.d.ts +1 -0
- package/dist/services/escrow-enclave/production.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.contract.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.d.ts +20 -0
- package/dist/services/escrow-enclave/remoteEnclave.test.d.ts +1 -0
- package/dist/services/escrow-enclave/remoteEnclave.tls.test.d.ts +1 -0
- package/dist/services/escrow-enclave/softwareEnclave.d.ts +12 -2
- package/dist/services/escrow-enclave/types.d.ts +52 -8
- package/dist/types/notifications.d.ts +5 -0
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +27 -117
- package/lambdaApp.ts +146 -0
- package/oidcLambda.test.ts +38 -0
- package/oidcLambda.ts +18 -0
- package/oidcLambdaApp.ts +11 -0
- package/package.json +23 -13
- package/scripts/README.md +88 -0
- package/scripts/check-seed-encryption-infrastructure.mjs +18 -2
- package/scripts/escrow-blob-mode-report.test.ts +227 -0
- package/scripts/escrow-blob-mode-report.ts +465 -0
- package/scripts/keycloak-admin.ts +108 -0
- package/scripts/keycloak-provider-links.ts +147 -0
- package/scripts/print-seed-rollout-flags.ts +21 -0
- package/scripts/provision-keycloak-users.ts +419 -0
- package/seedMigrationApp.test.ts +145 -0
- package/seedMigrationApp.ts +50 -0
- package/seedMigrationLambda.test.ts +30 -135
- package/seedMigrationLambda.ts +15 -46
- package/serverless-local.yml +25 -1
- package/serverless.function-env.cjs +58 -0
- package/serverless.yml +172 -37
- package/src/accesslayer/notifications/index.ts +9 -2
- package/src/accesslayer/signing-authority/index.ts +4 -1
- package/src/app.ts +2 -0
- package/src/cache/getDel.test.ts +34 -0
- package/src/cache/getDel.ts +32 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/login-tickets.ts +26 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/cacheEnvironment.ts +34 -0
- package/src/config/environment.test.ts +45 -2
- package/src/config/environment.ts +90 -23
- package/src/config/escrowBundleMode.test.ts +52 -0
- package/src/config/escrowDeployment.test.ts +114 -0
- package/src/config/oidcEnvironment.ts +59 -0
- package/src/config/serverlessFunctionEnv.test.ts +98 -0
- package/src/config/stageConfig.test.ts +26 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +9 -0
- package/src/helpers/auth.helpers.ts +68 -30
- package/src/helpers/escrowCancelLink.test.ts +60 -0
- package/src/helpers/escrowCancelLink.ts +38 -0
- package/src/helpers/escrowCancelToken.test.ts +105 -0
- package/src/helpers/escrowCancelToken.ts +47 -0
- package/src/helpers/keycloak.helpers.ts +120 -0
- package/src/helpers/login-ticket.helpers.test.ts +45 -0
- package/src/helpers/login-ticket.helpers.ts +91 -0
- package/src/helpers/oidc.helpers.ts +354 -0
- package/src/helpers/rate-limit.helpers.test.ts +61 -0
- package/src/helpers/rate-limit.helpers.ts +73 -0
- package/src/helpers/social-token.helpers.ts +144 -0
- package/src/index.ts +1 -0
- package/src/jobs/escrowBlobRewrap.ts +121 -0
- package/src/jobs/escrowHoldReminders.ts +177 -0
- package/src/models/AuthSubject.ts +147 -0
- package/src/models/EscrowHold.ts +321 -5
- package/src/models/UserKey.ts +91 -6
- package/src/models/index.ts +1 -0
- package/src/oidc.ts +371 -0
- package/src/routes/auth.ts +173 -0
- package/src/routes/escrow.ts +281 -34
- package/src/routes/keys.ts +5 -0
- package/src/routes/signing-authority.ts +39 -1
- package/src/services/delivery/adapters/postmark.adapter.ts +43 -1
- package/src/services/escrow-enclave/README.md +81 -10
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.pem +12 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.pem +12 -0
- package/src/services/escrow-enclave/blobIdentity.test.ts +42 -0
- package/src/services/escrow-enclave/blobIdentity.ts +55 -0
- package/src/services/escrow-enclave/cancellation.ts +59 -0
- package/src/services/escrow-enclave/enclaveTrustAnchors.ts +34 -0
- package/src/services/escrow-enclave/index.ts +145 -7
- package/src/services/escrow-enclave/notifications.test.ts +377 -0
- package/src/services/escrow-enclave/notifications.ts +340 -6
- package/src/services/escrow-enclave/production.test.ts +34 -0
- package/src/services/escrow-enclave/remoteEnclave.contract.test.ts +344 -0
- package/src/services/escrow-enclave/remoteEnclave.test.ts +490 -0
- package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +62 -0
- package/src/services/escrow-enclave/remoteEnclave.ts +263 -0
- package/src/services/escrow-enclave/softwareEnclave.test.ts +231 -30
- package/src/services/escrow-enclave/softwareEnclave.ts +97 -11
- package/src/services/escrow-enclave/types.ts +63 -17
- package/test/auth-tickets.spec.ts +480 -0
- package/test/helpers/keycloak-broker.ts +181 -0
- package/test/helpers/live-mongo.ts +9 -0
- package/test/jobs/escrowBlobRewrap.spec.ts +262 -0
- package/test/jobs/escrowHoldReminders.spec.ts +319 -0
- package/test/keycloak-admin.spec.ts +58 -0
- package/test/keycloak-broker-roundtrip.integration.spec.ts +40 -0
- package/test/keycloak-migration.integration.spec.ts +266 -0
- package/test/keycloak-provider-links.spec.ts +201 -0
- package/test/keycloak-social-broker.integration.spec.ts +228 -0
- package/test/keycloak-verify.integration.spec.ts +100 -0
- package/test/keycloak-verify.spec.ts +307 -0
- package/test/keys-escrow.spec.ts +963 -24
- package/test/models/authSubjectIndexes.spec.ts +132 -0
- package/test/models/escrow.spec.ts +375 -13
- package/test/notifications.spec.ts +4 -0
- package/test/oidc-e2e/broker.spec.ts +116 -0
- package/test/oidc-e2e/compose.yaml +33 -0
- package/test/oidc-e2e/fixtures.ts +245 -0
- package/test/oidc-e2e/lock.test.mjs +73 -0
- package/test/oidc-e2e/playwright.config.ts +21 -0
- package/test/oidc-e2e/run.mjs +312 -0
- package/test/oidc-e2e/tsconfig.json +10 -0
- package/test/oidc-e2e/types.ts +21 -0
- package/test/oidc.integration.spec.ts +54 -0
- package/test/oidc.spec.ts +697 -0
- package/test/oidcLambdaApp.boot.spec.ts +105 -0
- package/test/provision-keycloak-users.spec.ts +299 -0
- package/test/signing-authority.spec.ts +50 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +24 -1
- package/vitest.integration.config.ts +20 -3
- package/serverless.escrow-env.cjs +0 -20
|
@@ -1,145 +1,40 @@
|
|
|
1
1
|
import type { Context } from 'aws-lambda';
|
|
2
|
-
import {
|
|
3
|
-
import type { SeedMigrationResult } from './src/types/seed-migration';
|
|
2
|
+
import { beforeEach, expect, it, vi } from 'vitest';
|
|
4
3
|
|
|
5
|
-
const mocks = vi.hoisted(() => ({
|
|
6
|
-
connect: vi.fn(),
|
|
7
|
-
close: vi.fn(),
|
|
8
|
-
runBatch: vi.fn(),
|
|
9
|
-
mongodb: {},
|
|
10
|
-
}));
|
|
4
|
+
const mocks = vi.hoisted(() => ({ bootstrap: vi.fn(), getApp: vi.fn(), handler: vi.fn() }));
|
|
11
5
|
|
|
12
|
-
vi.mock('
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}));
|
|
16
|
-
vi.mock('./src/config/environment', () => ({
|
|
17
|
-
environment: { SA_SEED_ENCRYPT_WRITES: true },
|
|
18
|
-
}));
|
|
19
|
-
vi.mock('./src/migrations/signingAuthoritySeeds', async importOriginal => ({
|
|
20
|
-
...(await importOriginal<typeof import('./src/migrations/signingAuthoritySeeds')>()),
|
|
21
|
-
runSeedMigrationBatch: mocks.runBatch,
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
import { handler } from './seedMigrationLambda';
|
|
25
|
-
import { SeedMigrationError } from './src/migrations/signingAuthoritySeeds';
|
|
26
|
-
|
|
27
|
-
const result: SeedMigrationResult = {
|
|
28
|
-
phase: 'prepare',
|
|
29
|
-
done: false,
|
|
30
|
-
processed: 1,
|
|
31
|
-
counts: { total: 2, encrypted: 1, legacyOnly: 1, malformed: 0, plaintextRemaining: 2 },
|
|
32
|
-
countsReconciled: false,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const context = (): Context => ({
|
|
36
|
-
callbackWaitsForEmptyEventLoop: true,
|
|
37
|
-
functionName: 'seedMigration',
|
|
38
|
-
functionVersion: '$LATEST',
|
|
39
|
-
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789012:function:seedMigration',
|
|
40
|
-
memoryLimitInMB: '1024',
|
|
41
|
-
awsRequestId: 'test-request',
|
|
42
|
-
logGroupName: 'test-group',
|
|
43
|
-
logStreamName: 'test-stream',
|
|
44
|
-
getRemainingTimeInMillis: () => 60_000,
|
|
45
|
-
done: vi.fn(),
|
|
46
|
-
fail: vi.fn(),
|
|
47
|
-
succeed: vi.fn(),
|
|
48
|
-
});
|
|
6
|
+
vi.mock('@learncard/service-config', () => ({ bootstrapLambda: mocks.bootstrap }));
|
|
7
|
+
vi.mock('./src/config/stageConfig', () => ({ base: {}, stages: { dev: {}, production: {} } }));
|
|
8
|
+
vi.mock('./seedMigrationApp', () => ({ handler: mocks.handler }));
|
|
49
9
|
|
|
50
10
|
beforeEach(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
mocks.
|
|
54
|
-
|
|
11
|
+
vi.resetModules();
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
mocks.bootstrap.mockReturnValue(mocks.getApp);
|
|
14
|
+
mocks.getApp.mockResolvedValue({ handler: mocks.handler });
|
|
15
|
+
mocks.handler.mockResolvedValue({ phase: 'prepare', done: true });
|
|
55
16
|
});
|
|
56
17
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
await vi.waitFor(() => expect(mocks.close).toHaveBeenCalledOnce());
|
|
70
|
-
expect(resolved).not.toHaveBeenCalled();
|
|
71
|
-
expect(mocks.runBatch).toHaveBeenCalledWith(
|
|
72
|
-
mocks.mongodb,
|
|
73
|
-
{ phase: 'prepare', batchSize: 1 },
|
|
74
|
-
{ encryptedWritesEnabled: true, remainingTime: expect.any(Function) }
|
|
75
|
-
);
|
|
76
|
-
expect(lambdaContext.callbackWaitsForEmptyEventLoop).toBe(false);
|
|
77
|
-
|
|
78
|
-
finishClose();
|
|
79
|
-
await invocation;
|
|
80
|
-
expect(resolved).toHaveBeenCalledWith(result);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('connects and closes for each sequential warm invocation', async () => {
|
|
84
|
-
await handler({ phase: 'prepare' }, context());
|
|
85
|
-
await handler({ phase: 'prepare' }, context());
|
|
86
|
-
expect(mocks.connect).toHaveBeenCalledTimes(2);
|
|
87
|
-
expect(mocks.runBatch).toHaveBeenCalledTimes(2);
|
|
88
|
-
expect(mocks.close).toHaveBeenCalledTimes(2);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('closes after a connection failure without returning connection details', async () => {
|
|
92
|
-
mocks.connect.mockRejectedValue(new Error('mongodb://user:secret@private-host'));
|
|
93
|
-
await expect(handler({ phase: 'prepare' }, context())).rejects.toMatchObject({
|
|
94
|
-
category: 'operation_failed',
|
|
95
|
-
message: 'Signing authority seed migration stopped: operation_failed',
|
|
96
|
-
});
|
|
97
|
-
expect(mocks.runBatch).not.toHaveBeenCalled();
|
|
98
|
-
expect(mocks.close).toHaveBeenCalledOnce();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('closes after an unexpected batch failure without returning query details', async () => {
|
|
102
|
-
mocks.runBatch.mockRejectedValue(new Error('sensitive seed query'));
|
|
103
|
-
await expect(handler({ phase: 'prepare' }, context())).rejects.toMatchObject({
|
|
104
|
-
category: 'operation_failed',
|
|
105
|
-
message: 'Signing authority seed migration stopped: operation_failed',
|
|
106
|
-
});
|
|
107
|
-
expect(mocks.close).toHaveBeenCalledOnce();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('preserves the migration failure when cleanup also fails', async () => {
|
|
111
|
-
mocks.runBatch.mockRejectedValue(new SeedMigrationError('verification_required'));
|
|
112
|
-
mocks.close.mockRejectedValue(new Error('sensitive cleanup detail'));
|
|
113
|
-
await expect(handler({ phase: 'purge' }, context())).rejects.toMatchObject({
|
|
114
|
-
category: 'verification_required',
|
|
115
|
-
});
|
|
116
|
-
expect(mocks.close).toHaveBeenCalledOnce();
|
|
117
|
-
expect(console.error).toHaveBeenCalledExactlyOnceWith({
|
|
118
|
-
event: 'signing_authority_seed_migration_failure',
|
|
119
|
-
operation: 'migration_connection_cleanup',
|
|
120
|
-
category: 'operation_failed',
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('sanitizes cleanup errors after an otherwise successful batch', async () => {
|
|
125
|
-
mocks.close.mockRejectedValue(new Error('mongodb://user:secret@private-host'));
|
|
126
|
-
await expect(handler({ phase: 'prepare' }, context())).rejects.toMatchObject({
|
|
127
|
-
category: 'operation_failed',
|
|
128
|
-
message: 'Signing authority seed migration stopped: operation_failed',
|
|
129
|
-
});
|
|
130
|
-
expect(console.error).toHaveBeenCalledExactlyOnceWith({
|
|
131
|
-
event: 'signing_authority_seed_migration_failure',
|
|
132
|
-
operation: 'migration_connection_cleanup',
|
|
133
|
-
category: 'operation_failed',
|
|
134
|
-
});
|
|
18
|
+
it('bootstraps the migration app with checked-in stage config and the deploy stage', async () => {
|
|
19
|
+
vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', 'lca-api-production-seedMigration');
|
|
20
|
+
vi.stubEnv('LAMBDA_STAGE', 'production');
|
|
21
|
+
await import('./seedMigrationLambda');
|
|
22
|
+
expect(mocks.bootstrap).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(mocks.bootstrap).toHaveBeenCalledWith({
|
|
24
|
+
base: {},
|
|
25
|
+
stages: { dev: {}, production: {} },
|
|
26
|
+
stage: 'production',
|
|
27
|
+
importApp: expect.any(Function),
|
|
135
28
|
});
|
|
29
|
+
vi.unstubAllEnvs();
|
|
30
|
+
});
|
|
136
31
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
32
|
+
it('resolves the app once per cold start and forwards every argument', async () => {
|
|
33
|
+
const { handler } = await import('./seedMigrationLambda');
|
|
34
|
+
const event = { phase: 'prepare' };
|
|
35
|
+
const context = {} as Context;
|
|
36
|
+
await expect(handler(event, context)).resolves.toEqual({ phase: 'prepare', done: true });
|
|
37
|
+
await handler(event, context);
|
|
38
|
+
expect(mocks.getApp).toHaveBeenCalledTimes(2);
|
|
39
|
+
expect(mocks.handler).toHaveBeenLastCalledWith(event, context);
|
|
145
40
|
});
|
package/seedMigrationLambda.ts
CHANGED
|
@@ -1,50 +1,19 @@
|
|
|
1
1
|
import type { Context } from 'aws-lambda';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import { runSeedMigrationBatch, SeedMigrationError } from './src/migrations/signingAuthoritySeeds';
|
|
2
|
+
import { bootstrapLambda } from '@learncard/service-config';
|
|
3
|
+
|
|
4
|
+
import { base, stages } from './src/config/stageConfig';
|
|
6
5
|
import type { SeedMigrationResult } from './src/types/seed-migration';
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
.
|
|
7
|
+
type SeedMigrationApp = typeof import('./seedMigrationApp');
|
|
8
|
+
|
|
9
|
+
const getApplication = bootstrapLambda<SeedMigrationApp>({
|
|
10
|
+
base,
|
|
11
|
+
stages,
|
|
12
|
+
stage: process.env.AWS_LAMBDA_FUNCTION_NAME
|
|
13
|
+
? process.env.LAMBDA_STAGE
|
|
14
|
+
: process.env.CONFIG_STAGE,
|
|
15
|
+
importApp: () => import('./seedMigrationApp'),
|
|
16
|
+
});
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const parsed = requestValidator.safeParse(event);
|
|
18
|
-
if (!parsed.success) throw new SeedMigrationError('invalid_request');
|
|
19
|
-
context.callbackWaitsForEmptyEventLoop = false;
|
|
20
|
-
let result: SeedMigrationResult;
|
|
21
|
-
let cleanupFailed = false;
|
|
22
|
-
try {
|
|
23
|
-
await client.connect();
|
|
24
|
-
result = await runSeedMigrationBatch(mongodb, parsed.data, {
|
|
25
|
-
encryptedWritesEnabled: environment.SA_SEED_ENCRYPT_WRITES,
|
|
26
|
-
remainingTime: () => context.getRemainingTimeInMillis(),
|
|
27
|
-
});
|
|
28
|
-
} catch (error) {
|
|
29
|
-
// Mongo connection/lease errors can contain URLs or query values. Never let Lambda serialize them.
|
|
30
|
-
throw new SeedMigrationError(
|
|
31
|
-
error instanceof SeedMigrationError ? error.category : 'operation_failed'
|
|
32
|
-
);
|
|
33
|
-
} finally {
|
|
34
|
-
// This private worker releases its pool between batches, including failed connections.
|
|
35
|
-
// The module-level client reconnects when a warm invocation starts its next batch.
|
|
36
|
-
try {
|
|
37
|
-
await client.close();
|
|
38
|
-
} catch {
|
|
39
|
-
cleanupFailed = true;
|
|
40
|
-
console.error({
|
|
41
|
-
event: 'signing_authority_seed_migration_failure',
|
|
42
|
-
operation: 'migration_connection_cleanup',
|
|
43
|
-
category: 'operation_failed',
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
// Reached only after a successful batch; never mask its original failure in finally.
|
|
48
|
-
if (cleanupFailed) throw new SeedMigrationError('operation_failed');
|
|
49
|
-
return result;
|
|
50
|
-
};
|
|
18
|
+
export const handler = async (event: unknown, context: Context): Promise<SeedMigrationResult> =>
|
|
19
|
+
(await getApplication()).handler(event, context);
|
package/serverless-local.yml
CHANGED
|
@@ -36,7 +36,8 @@ 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}
|
|
41
42
|
SA_SEED_LOCAL_KEK: ${env:SA_SEED_LOCAL_KEK, ''}
|
|
42
43
|
SA_SEED_ENCRYPT_WRITES: ${env:SA_SEED_ENCRYPT_WRITES, 'false'}
|
|
@@ -54,6 +55,16 @@ provider:
|
|
|
54
55
|
MONGO_DB_NAME: ${env:MONGO_DB_NAME}
|
|
55
56
|
AUTHORIZED_DIDS: ${env:AUTHORIZED_DIDS}
|
|
56
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, ""}
|
|
57
68
|
REDIS_HOST:
|
|
58
69
|
'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Address]
|
|
59
70
|
REDIS_PORT:
|
|
@@ -101,6 +112,17 @@ functions:
|
|
|
101
112
|
url:
|
|
102
113
|
cors: true
|
|
103
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
|
+
|
|
104
126
|
swagger:
|
|
105
127
|
handler: lambda.swaggerUiHandler
|
|
106
128
|
package:
|
|
@@ -226,6 +248,8 @@ resources:
|
|
|
226
248
|
Properties:
|
|
227
249
|
AutoMinorVersionUpgrade: true
|
|
228
250
|
Engine: redis
|
|
251
|
+
# GETDEL (login codes/tickets/auth codes) needs Redis >= 6.2
|
|
252
|
+
EngineVersion: '7.0'
|
|
229
253
|
CacheNodeType: ${self:custom.config.CACHE_INSTANCE_SIZE}
|
|
230
254
|
NumCacheNodes: 1
|
|
231
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
|
@@ -18,10 +18,6 @@ custom:
|
|
|
18
18
|
number: 3
|
|
19
19
|
config:
|
|
20
20
|
CACHE_INSTANCE_SIZE: cache.t2.micro
|
|
21
|
-
# Lambda caps each function's environment at 4KB. Escrow config is only read
|
|
22
|
-
# by the trpc/api handlers, so it is attached per-function (not provider-wide)
|
|
23
|
-
# and unset keys are omitted entirely.
|
|
24
|
-
escrowEnvironment: ${file(./serverless.escrow-env.cjs)}
|
|
25
21
|
esbuild:
|
|
26
22
|
# serverless-esbuild does not support Bun as a Lambda artifact packager.
|
|
27
23
|
# Bun remains the repo installer/runner; pnpm is used only for packaging externals.
|
|
@@ -133,37 +129,17 @@ provider:
|
|
|
133
129
|
timeout: 29
|
|
134
130
|
stage: ${opt:stage, "dev"}
|
|
135
131
|
region: ${opt:region, "us-east-1"}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
POSTMARK_SERVER_TOKEN: ${env:POSTMARK_SERVER_TOKEN, ''}
|
|
148
|
-
POSTMARK_LOGIN_CODE_TEMPLATE_ALIAS: ${env:POSTMARK_LOGIN_CODE_TEMPLATE_ALIAS, ''}
|
|
149
|
-
POSTMARK_ENDORSEMENT_REQUEST_TEMPLATE_ALIAS: ${env:POSTMARK_ENDORSEMENT_REQUEST_TEMPLATE_ALIAS, ''}
|
|
150
|
-
POSTMARK_RECOVERY_EMAIL_CODE_TEMPLATE_ALIAS: ${env:POSTMARK_RECOVERY_EMAIL_CODE_TEMPLATE_ALIAS, ''}
|
|
151
|
-
POSTMARK_RECOVERY_KEY_TEMPLATE_ALIAS: ${env:POSTMARK_RECOVERY_KEY_TEMPLATE_ALIAS, ''}
|
|
152
|
-
POSTMARK_FROM_EMAIL: ${env:POSTMARK_FROM_EMAIL, ''}
|
|
153
|
-
POSTMARK_BRAND_NAME: ${env:POSTMARK_BRAND_NAME, ''}
|
|
154
|
-
PORT: ${opt:httpPort, "3000"}
|
|
155
|
-
MONGO_URI: ${env:MONGO_URI, ''}
|
|
156
|
-
MONGO_DB_NAME: ${env:MONGO_DB_NAME, ''}
|
|
157
|
-
AUTHORIZED_DIDS: ${env:AUTHORIZED_DIDS, ''}
|
|
158
|
-
GOOGLE_APPLICATION_CREDENTIAL: ${env:GOOGLE_APPLICATION_CREDENTIAL, ''}
|
|
159
|
-
SCOUTS_SSO_CLIENT_SECRET: ${env:SCOUTS_SSO_CLIENT_SECRET, ''}
|
|
160
|
-
LEARN_CLOUD_URL: ${env:LEARN_CLOUD_URL, ''}
|
|
161
|
-
SENTRY_DSN: ${env:SENTRY_DSN, ''}
|
|
162
|
-
SENTRY_ENV: ${env:SENTRY_ENV, ''}
|
|
163
|
-
REDIS_HOST:
|
|
164
|
-
'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Address]
|
|
165
|
-
REDIS_PORT:
|
|
166
|
-
'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-*
|
|
167
143
|
tracing:
|
|
168
144
|
apiGateway: true
|
|
169
145
|
lambda: true
|
|
@@ -178,7 +154,7 @@ functions:
|
|
|
178
154
|
handler: lambda.trpcHandler
|
|
179
155
|
role: SigningAuthorityExecutionRole
|
|
180
156
|
dependsOn: [SigningAuthoritySeedKeyPolicy]
|
|
181
|
-
environment: ${
|
|
157
|
+
environment: ${file(./serverless.function-env.cjs):api}
|
|
182
158
|
vpc:
|
|
183
159
|
securityGroupIds:
|
|
184
160
|
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
@@ -191,7 +167,7 @@ functions:
|
|
|
191
167
|
handler: lambda.openApiHandler
|
|
192
168
|
role: SigningAuthorityExecutionRole
|
|
193
169
|
dependsOn: [SigningAuthoritySeedKeyPolicy]
|
|
194
|
-
environment: ${
|
|
170
|
+
environment: ${file(./serverless.function-env.cjs):api}
|
|
195
171
|
vpc:
|
|
196
172
|
securityGroupIds:
|
|
197
173
|
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
@@ -202,6 +178,7 @@ functions:
|
|
|
202
178
|
|
|
203
179
|
didWeb:
|
|
204
180
|
handler: lambda.didWebHandler
|
|
181
|
+
environment: ${file(./serverless.function-env.cjs):api}
|
|
205
182
|
vpc:
|
|
206
183
|
securityGroupIds:
|
|
207
184
|
- 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
|
|
@@ -213,12 +190,29 @@ functions:
|
|
|
213
190
|
url:
|
|
214
191
|
cors: true
|
|
215
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
|
+
|
|
216
207
|
seedMigration:
|
|
217
208
|
handler: seedMigrationLambda.handler
|
|
218
209
|
role: SigningAuthorityExecutionRole
|
|
219
210
|
timeout: 900
|
|
220
211
|
reservedConcurrency: 1
|
|
221
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}
|
|
222
216
|
# IAM invoke only: intentionally no events, schedule, or function URL.
|
|
223
217
|
vpc:
|
|
224
218
|
securityGroupIds:
|
|
@@ -228,6 +222,12 @@ functions:
|
|
|
228
222
|
|
|
229
223
|
swagger:
|
|
230
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
|
|
231
231
|
package:
|
|
232
232
|
patterns:
|
|
233
233
|
- src/swagger-ui/**
|
|
@@ -235,7 +235,44 @@ functions:
|
|
|
235
235
|
- httpApi: GET /docs
|
|
236
236
|
- httpApi: GET /docs/{trpc+}
|
|
237
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
|
+
|
|
238
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, ""}', '']
|
|
239
276
|
Outputs:
|
|
240
277
|
SigningAuthoritySeedKeyArn:
|
|
241
278
|
Value:
|
|
@@ -259,6 +296,39 @@ resources:
|
|
|
259
296
|
ManagedPolicyArns:
|
|
260
297
|
- Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
|
|
261
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-*
|
|
262
332
|
SigningAuthoritySeedKey:
|
|
263
333
|
Type: AWS::KMS::Key
|
|
264
334
|
DeletionPolicy: Retain
|
|
@@ -346,6 +416,67 @@ resources:
|
|
|
346
416
|
Type: AWS::EC2::VPC
|
|
347
417
|
Properties:
|
|
348
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
|
|
349
480
|
IP:
|
|
350
481
|
Type: AWS::EC2::EIP
|
|
351
482
|
Properties:
|
|
@@ -456,6 +587,10 @@ resources:
|
|
|
456
587
|
Properties:
|
|
457
588
|
AutoMinorVersionUpgrade: true
|
|
458
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).
|
|
459
594
|
CacheNodeType: ${self:custom.config.CACHE_INSTANCE_SIZE}
|
|
460
595
|
NumCacheNodes: 1
|
|
461
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';
|