@learncard/lca-api-service 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +73 -0
- package/AGENTS.md +106 -10
- package/CHANGELOG.md +52 -0
- package/bootstrapLambda.test.ts +76 -0
- package/bundle.mjs +9 -1
- 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/publish-metafile.json +1 -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 +22 -13
- package/project.json +8 -1
- 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 +174 -37
- package/src/accesslayer/notifications/index.ts +9 -2
- package/src/accesslayer/signing-authority/index.ts +4 -1
- package/src/app.ts +2 -0
- package/src/cache/getDel.test.ts +34 -0
- package/src/cache/getDel.ts +32 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/login-tickets.ts +26 -0
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/cacheEnvironment.ts +34 -0
- package/src/config/environment.test.ts +45 -2
- package/src/config/environment.ts +90 -23
- package/src/config/escrowBundleMode.test.ts +52 -0
- package/src/config/escrowDeployment.test.ts +114 -0
- package/src/config/oidcEnvironment.ts +59 -0
- package/src/config/serverlessFunctionEnv.test.ts +98 -0
- package/src/config/stageConfig.test.ts +26 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +9 -0
- package/src/helpers/auth.helpers.ts +68 -30
- package/src/helpers/escrowCancelLink.test.ts +60 -0
- package/src/helpers/escrowCancelLink.ts +38 -0
- package/src/helpers/escrowCancelToken.test.ts +105 -0
- package/src/helpers/escrowCancelToken.ts +47 -0
- package/src/helpers/keycloak.helpers.ts +120 -0
- package/src/helpers/login-ticket.helpers.test.ts +45 -0
- package/src/helpers/login-ticket.helpers.ts +91 -0
- package/src/helpers/oidc.helpers.ts +354 -0
- package/src/helpers/rate-limit.helpers.test.ts +61 -0
- package/src/helpers/rate-limit.helpers.ts +73 -0
- package/src/helpers/social-token.helpers.ts +144 -0
- package/src/index.ts +1 -0
- package/src/jobs/escrowBlobRewrap.ts +121 -0
- package/src/jobs/escrowHoldReminders.ts +177 -0
- package/src/models/AuthSubject.ts +147 -0
- package/src/models/EscrowHold.ts +321 -5
- package/src/models/UserKey.ts +91 -6
- package/src/models/index.ts +1 -0
- package/src/oidc.ts +371 -0
- package/src/routes/auth.ts +173 -0
- package/src/routes/escrow.ts +281 -34
- package/src/routes/keys.ts +5 -0
- package/src/routes/signing-authority.ts +39 -1
- package/src/services/delivery/adapters/postmark.adapter.ts +43 -1
- package/src/services/escrow-enclave/README.md +81 -10
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/pinned-leaf.pem +12 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-ca.pem +11 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.key +5 -0
- package/src/services/escrow-enclave/__fixtures__/tls/rogue-leaf.pem +12 -0
- package/src/services/escrow-enclave/blobIdentity.test.ts +42 -0
- package/src/services/escrow-enclave/blobIdentity.ts +55 -0
- package/src/services/escrow-enclave/cancellation.ts +59 -0
- package/src/services/escrow-enclave/enclaveTrustAnchors.ts +34 -0
- package/src/services/escrow-enclave/index.ts +145 -7
- package/src/services/escrow-enclave/notifications.test.ts +377 -0
- package/src/services/escrow-enclave/notifications.ts +340 -6
- package/src/services/escrow-enclave/production.test.ts +34 -0
- package/src/services/escrow-enclave/remoteEnclave.contract.test.ts +344 -0
- package/src/services/escrow-enclave/remoteEnclave.test.ts +490 -0
- package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +62 -0
- package/src/services/escrow-enclave/remoteEnclave.ts +263 -0
- package/src/services/escrow-enclave/softwareEnclave.test.ts +231 -30
- package/src/services/escrow-enclave/softwareEnclave.ts +97 -11
- package/src/services/escrow-enclave/types.ts +63 -17
- package/test/auth-tickets.spec.ts +480 -0
- package/test/helpers/keycloak-broker.ts +181 -0
- package/test/helpers/live-mongo.ts +9 -0
- package/test/jobs/escrowBlobRewrap.spec.ts +262 -0
- package/test/jobs/escrowHoldReminders.spec.ts +319 -0
- package/test/keycloak-admin.spec.ts +58 -0
- package/test/keycloak-broker-roundtrip.integration.spec.ts +40 -0
- package/test/keycloak-migration.integration.spec.ts +266 -0
- package/test/keycloak-provider-links.spec.ts +201 -0
- package/test/keycloak-social-broker.integration.spec.ts +228 -0
- package/test/keycloak-verify.integration.spec.ts +100 -0
- package/test/keycloak-verify.spec.ts +307 -0
- package/test/keys-escrow.spec.ts +963 -24
- package/test/models/authSubjectIndexes.spec.ts +132 -0
- package/test/models/escrow.spec.ts +375 -13
- package/test/notifications.spec.ts +4 -0
- package/test/oidc-e2e/broker.spec.ts +116 -0
- package/test/oidc-e2e/compose.yaml +33 -0
- package/test/oidc-e2e/fixtures.ts +245 -0
- package/test/oidc-e2e/lock.test.mjs +73 -0
- package/test/oidc-e2e/playwright.config.ts +21 -0
- package/test/oidc-e2e/run.mjs +312 -0
- package/test/oidc-e2e/tsconfig.json +10 -0
- package/test/oidc-e2e/types.ts +21 -0
- package/test/oidc.integration.spec.ts +54 -0
- package/test/oidc.spec.ts +697 -0
- package/test/oidcLambdaApp.boot.spec.ts +105 -0
- package/test/provision-keycloak-users.spec.ts +299 -0
- package/test/signing-authority.spec.ts +50 -0
- package/tsconfig.build.json +1 -1
- package/tsconfig.json +1 -0
- package/vitest.config.ts +24 -1
- package/vitest.integration.config.ts +20 -3
- package/serverless.escrow-env.cjs +0 -20
package/lambdaApp.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import serverlessHttp from 'serverless-http';
|
|
2
|
+
import type { Context, APIGatewayProxyResultV2, APIGatewayProxyEventV2 } from 'aws-lambda';
|
|
3
|
+
import { awsLambdaRequestHandler } from '@trpc/server/adapters/aws-lambda';
|
|
4
|
+
import * as Sentry from '@sentry/serverless';
|
|
5
|
+
|
|
6
|
+
import app from './src/openapi';
|
|
7
|
+
import didWebApp from './src/dids';
|
|
8
|
+
import { appRouter, createContext } from './src/app';
|
|
9
|
+
import { getEmptyLearnCard } from './src/helpers/learnCard.helpers';
|
|
10
|
+
import { createOpenApiAwsLambdaHandler } from './src/helpers/shim';
|
|
11
|
+
import {
|
|
12
|
+
handleTrpcError,
|
|
13
|
+
sentryBeforeSend,
|
|
14
|
+
getTracesSampleRate,
|
|
15
|
+
} from './src/helpers/sentry.helpers';
|
|
16
|
+
import { environment } from './src/config/environment';
|
|
17
|
+
import { toServerlessApplication } from './src/helpers/serverlessApplication';
|
|
18
|
+
import { ensureUserKeysIndexes, createEscrowHoldsIndexes } from './src/models';
|
|
19
|
+
import { runEscrowHoldReminders } from './src/jobs/escrowHoldReminders';
|
|
20
|
+
import { runEscrowBlobRewrap } from './src/jobs/escrowBlobRewrap';
|
|
21
|
+
|
|
22
|
+
const startupPromise = Promise.all([
|
|
23
|
+
getEmptyLearnCard(), // Load WASM in for better cold starts
|
|
24
|
+
ensureUserKeysIndexes(),
|
|
25
|
+
createEscrowHoldsIndexes(),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const isWarmupEvent = (event: APIGatewayProxyEventV2): boolean =>
|
|
29
|
+
'source' in event && event.source === 'serverless-plugin-warmup';
|
|
30
|
+
|
|
31
|
+
Sentry.AWSLambda.init({
|
|
32
|
+
dsn: environment.SENTRY_DSN,
|
|
33
|
+
environment: environment.SENTRY_ENV,
|
|
34
|
+
enabled: Boolean(environment.SENTRY_DSN),
|
|
35
|
+
tracesSampleRate: getTracesSampleRate(),
|
|
36
|
+
beforeSend: sentryBeforeSend,
|
|
37
|
+
integrations: [
|
|
38
|
+
new Sentry.Integrations.Console(),
|
|
39
|
+
new Sentry.Integrations.Http({ tracing: true }),
|
|
40
|
+
new Sentry.Integrations.ContextLines(),
|
|
41
|
+
new Sentry.Integrations.Mongo(),
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const swaggerUiHandler = serverlessHttp(toServerlessApplication(app), {
|
|
46
|
+
basePath: '/docs',
|
|
47
|
+
});
|
|
48
|
+
export const didWebHandler = serverlessHttp(toServerlessApplication(didWebApp));
|
|
49
|
+
|
|
50
|
+
export const _openApiHandler = createOpenApiAwsLambdaHandler({
|
|
51
|
+
router: appRouter,
|
|
52
|
+
responseMeta: () => {
|
|
53
|
+
return {
|
|
54
|
+
headers: {
|
|
55
|
+
'Access-Control-Allow-Origin': '*',
|
|
56
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
57
|
+
'Access-Control-Allow-Headers': 'Authorization, Content-Type',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
createContext,
|
|
62
|
+
onError: handleTrpcError,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const _trpcHandler = awsLambdaRequestHandler({
|
|
66
|
+
router: appRouter,
|
|
67
|
+
createContext,
|
|
68
|
+
onError: handleTrpcError,
|
|
69
|
+
responseMeta: () => {
|
|
70
|
+
return {
|
|
71
|
+
headers: {
|
|
72
|
+
'Access-Control-Allow-Origin': '*',
|
|
73
|
+
'Access-Control-Allow-Methods': '*',
|
|
74
|
+
'Access-Control-Allow-Headers': 'authorization',
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const openApiHandler = Sentry.AWSLambda.wrapHandler(
|
|
81
|
+
async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
|
|
82
|
+
await startupPromise;
|
|
83
|
+
|
|
84
|
+
if (isWarmupEvent(event)) {
|
|
85
|
+
console.log('[Warmup] Initializing LearnCard...');
|
|
86
|
+
await getEmptyLearnCard();
|
|
87
|
+
console.log('[Warmup] Done!');
|
|
88
|
+
return 'All done! 😄';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (event.requestContext.http.method === 'OPTIONS') {
|
|
92
|
+
return {
|
|
93
|
+
statusCode: 200,
|
|
94
|
+
headers: {
|
|
95
|
+
'Access-Control-Allow-Origin': '*',
|
|
96
|
+
'Access-Control-Allow-Headers': '*',
|
|
97
|
+
'Access-Control-Allow-Methods': '*',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return _openApiHandler(event, context);
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
export const trpcHandler = Sentry.AWSLambda.wrapHandler(
|
|
107
|
+
async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
|
|
108
|
+
await startupPromise;
|
|
109
|
+
|
|
110
|
+
if (isWarmupEvent(event)) {
|
|
111
|
+
console.log('[Warmup] Initializing LearnCard...');
|
|
112
|
+
await getEmptyLearnCard();
|
|
113
|
+
console.log('[Warmup] Done!');
|
|
114
|
+
return 'All done! 😄';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (event.requestContext.http.method === 'OPTIONS') {
|
|
118
|
+
return {
|
|
119
|
+
statusCode: 200,
|
|
120
|
+
headers: {
|
|
121
|
+
'Access-Control-Allow-Origin': '*',
|
|
122
|
+
'Access-Control-Allow-Headers': '*',
|
|
123
|
+
'Access-Control-Allow-Methods': '*',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return _trpcHandler(event, context);
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Scheduled (EventBridge) functions, not API-Gateway-triggered: they share
|
|
133
|
+
// startupPromise with the handlers above; warmup/OPTIONS handling doesn't apply.
|
|
134
|
+
export const escrowHoldRemindersHandler = Sentry.AWSLambda.wrapHandler(async (): Promise<void> => {
|
|
135
|
+
await startupPromise;
|
|
136
|
+
const counts = await runEscrowHoldReminders();
|
|
137
|
+
console.log('[escrow-hold-reminders] completed', counts);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// P9.3: migrates escrow blobs off a retiring enclave key for accounts that
|
|
141
|
+
// never sign in again (P9.2 already covers accounts that do, on next login).
|
|
142
|
+
export const escrowBlobRewrapHandler = Sentry.AWSLambda.wrapHandler(async (): Promise<void> => {
|
|
143
|
+
await startupPromise;
|
|
144
|
+
const counts = await runEscrowBlobRewrap();
|
|
145
|
+
console.log('[escrow-blob-rewrap] completed', counts);
|
|
146
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
|
|
2
|
+
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({ applyStage: vi.fn(), handler: vi.fn(), importApp: vi.fn() }));
|
|
5
|
+
vi.mock('./src/config/stageConfig', () => ({ applyLcaApiStageConfig: mocks.applyStage }));
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetModules();
|
|
9
|
+
vi.clearAllMocks();
|
|
10
|
+
mocks.handler.mockResolvedValue({ statusCode: 200 });
|
|
11
|
+
vi.doMock('./oidcLambdaApp', () => {
|
|
12
|
+
mocks.importApp();
|
|
13
|
+
expect(mocks.applyStage).toHaveBeenCalledTimes(1);
|
|
14
|
+
return { oidcHandler: mocks.handler };
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => vi.unstubAllEnvs());
|
|
18
|
+
|
|
19
|
+
it.each([
|
|
20
|
+
['lca-api-dev-oidc', 'dev'],
|
|
21
|
+
['', 'local'],
|
|
22
|
+
])(
|
|
23
|
+
'applies the appropriate stage before the isolated OIDC import (%s)',
|
|
24
|
+
async (functionName, expectedStage) => {
|
|
25
|
+
vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', functionName);
|
|
26
|
+
vi.stubEnv('LAMBDA_STAGE', 'dev');
|
|
27
|
+
vi.stubEnv('CONFIG_STAGE', 'local');
|
|
28
|
+
const { handler } = await import('./oidcLambda');
|
|
29
|
+
const event = {} as APIGatewayProxyEventV2;
|
|
30
|
+
const context = {} as Context;
|
|
31
|
+
const callback = vi.fn();
|
|
32
|
+
await Promise.all([handler(event, context, callback), handler(event, context, callback)]);
|
|
33
|
+
expect(mocks.applyStage).toHaveBeenCalledExactlyOnceWith(expectedStage);
|
|
34
|
+
expect(mocks.importApp).toHaveBeenCalledTimes(1);
|
|
35
|
+
expect(mocks.handler).toHaveBeenCalledTimes(2);
|
|
36
|
+
expect(mocks.handler).toHaveBeenLastCalledWith(event, context, callback);
|
|
37
|
+
}
|
|
38
|
+
);
|
package/oidcLambda.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { applyLcaApiStageConfig } from './src/config/stageConfig';
|
|
2
|
+
|
|
3
|
+
type OidcApp = typeof import('./oidcLambdaApp');
|
|
4
|
+
|
|
5
|
+
let application: Promise<OidcApp> | undefined;
|
|
6
|
+
|
|
7
|
+
export const handler = async (...args: Parameters<OidcApp['oidcHandler']>) => {
|
|
8
|
+
if (!application) {
|
|
9
|
+
applyLcaApiStageConfig(
|
|
10
|
+
process.env.AWS_LAMBDA_FUNCTION_NAME
|
|
11
|
+
? process.env.LAMBDA_STAGE
|
|
12
|
+
: process.env.CONFIG_STAGE
|
|
13
|
+
);
|
|
14
|
+
// This role cannot read the runtime bundle. Cache imports, including failures.
|
|
15
|
+
application = import('./oidcLambdaApp');
|
|
16
|
+
}
|
|
17
|
+
return (await application).oidcHandler(...args);
|
|
18
|
+
};
|
package/oidcLambdaApp.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import serverlessHttp from 'serverless-http';
|
|
2
|
+
|
|
3
|
+
import { app as oidcApp } from './src/oidc';
|
|
4
|
+
import { toServerlessApplication } from './src/helpers/serverlessApplication';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dedicated OIDC Lambda entrypoint. Unlike `lambdaApp.ts`, this imports only the
|
|
8
|
+
* Fastify OIDC surface and its focused environment, so the OIDC function boots
|
|
9
|
+
* without SEED/MONGO/Postmark/Firebase or a runtime-secrets bundle.
|
|
10
|
+
*/
|
|
11
|
+
export const oidcHandler = serverlessHttp(toServerlessApplication(oidcApp));
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/lca-api-service",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24.12 <25"
|
|
7
7
|
},
|
|
8
|
-
"packageManager": "bun@1.
|
|
8
|
+
"packageManager": "bun@1.4.2",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"types": "dist/index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"migrate:sa-seeds:local": "bash scripts/migrate-signing-authority-seeds-local.sh",
|
|
13
13
|
"migrate:sa-seeds": "node scripts/migrate-signing-authority-seeds.mjs",
|
|
14
14
|
"check:sa-infra": "node scripts/check-seed-encryption-infrastructure.mjs",
|
|
15
|
+
"sa-seed:flags": "bun --conditions=development scripts/print-seed-rollout-flags.ts",
|
|
15
16
|
"check:sa-redis": "node scripts/check-signing-authority-redis.mjs",
|
|
17
|
+
"provision:keycloak": "bun --conditions=development scripts/provision-keycloak-users.ts",
|
|
16
18
|
"test": "vitest run --config vitest.config.ts",
|
|
17
19
|
"test:watch": "vitest --config vitest.config.ts",
|
|
18
20
|
"test:coverage": "vitest run --coverage --config vitest.config.ts",
|
|
@@ -27,7 +29,9 @@
|
|
|
27
29
|
"dev": "bun --conditions=development --watch src/docker-entry.ts",
|
|
28
30
|
"start:docker": "bun --conditions=development src/docker-entry.ts",
|
|
29
31
|
"serverless-deploy": "node scripts/check-seed-encryption-infrastructure.mjs && node ../../../scripts/prepare-didkit-native-layer.cjs && serverless deploy",
|
|
30
|
-
"serverless-deploy-scouts": "serverless deploy --config serverless-scouts.yml"
|
|
32
|
+
"serverless-deploy-scouts": "serverless deploy --config serverless-scouts.yml",
|
|
33
|
+
"test:oidc:e2e": "node test/oidc-e2e/run.mjs",
|
|
34
|
+
"typecheck:oidc:e2e": "tsc --noEmit -p test/oidc-e2e/tsconfig.json"
|
|
31
35
|
},
|
|
32
36
|
"keywords": [],
|
|
33
37
|
"author": "WeLibrary, LLC (www.welibrary.io)",
|
|
@@ -49,21 +53,25 @@
|
|
|
49
53
|
"serverless-http": "3.2.0",
|
|
50
54
|
"serverless-offline": "^14.4.0",
|
|
51
55
|
"serverless-prune-plugin": "^2.1.0",
|
|
52
|
-
"shx": "^0.3.4"
|
|
56
|
+
"shx": "^0.3.4",
|
|
57
|
+
"@playwright/test": "^1.42.0"
|
|
53
58
|
},
|
|
54
59
|
"dependencies": {
|
|
55
60
|
"@aws-sdk/client-kms": "^3.1139.0",
|
|
61
|
+
"@aws-sdk/client-secrets-manager": "^3.1139.0",
|
|
56
62
|
"@fastify/cors": "^11.2.0",
|
|
63
|
+
"@fastify/formbody": "^8.0.2",
|
|
64
|
+
"@fastify/rate-limit": "^10.2.0",
|
|
57
65
|
"@fastify/static": "^10.1.1",
|
|
58
|
-
"@learncard/core": "9.4.
|
|
59
|
-
"@learncard/did-web-plugin": "1.1.
|
|
60
|
-
"@learncard/didkit-plugin": "1.10.
|
|
61
|
-
"@learncard/didkit-plugin-node": "0.3.
|
|
62
|
-
"@learncard/email-templates": "1.2.
|
|
63
|
-
"@learncard/helpers": "1.6.
|
|
64
|
-
"@learncard/init": "2.5.
|
|
65
|
-
"@learncard/types": "5.
|
|
66
|
-
"@learncard/sss-key-manager": "0.2.
|
|
66
|
+
"@learncard/core": "9.4.38",
|
|
67
|
+
"@learncard/did-web-plugin": "1.1.38",
|
|
68
|
+
"@learncard/didkit-plugin": "1.10.3",
|
|
69
|
+
"@learncard/didkit-plugin-node": "0.3.3",
|
|
70
|
+
"@learncard/email-templates": "1.2.1",
|
|
71
|
+
"@learncard/helpers": "1.6.2",
|
|
72
|
+
"@learncard/init": "2.5.3",
|
|
73
|
+
"@learncard/types": "5.23.0",
|
|
74
|
+
"@learncard/sss-key-manager": "0.2.1",
|
|
67
75
|
"@sentry/serverless": "^7.61.0",
|
|
68
76
|
"@trpc/server": "11.8.0",
|
|
69
77
|
"@types/jsonwebtoken": "^9.0.8",
|
|
@@ -79,6 +87,7 @@
|
|
|
79
87
|
"firebase-admin": "13.4.0",
|
|
80
88
|
"ioredis": "^5.2.2",
|
|
81
89
|
"ioredis-mock": "^8.2.2",
|
|
90
|
+
"jose": "^6.2.12",
|
|
82
91
|
"jsonwebtoken": "^9.0.2",
|
|
83
92
|
"jwt-decode": "^3.1.2",
|
|
84
93
|
"libsodium-wrappers": "^0.7.11",
|
package/project.json
CHANGED
|
@@ -14,7 +14,14 @@
|
|
|
14
14
|
"targets": {
|
|
15
15
|
"build": {
|
|
16
16
|
"executor": "nx:run-script",
|
|
17
|
-
"inputs": [
|
|
17
|
+
"inputs": [
|
|
18
|
+
"source",
|
|
19
|
+
"{projectRoot}/bundle.mjs",
|
|
20
|
+
"{projectRoot}/esbuildPlugins.cjs",
|
|
21
|
+
"{projectRoot}/package.json",
|
|
22
|
+
"{projectRoot}/tsconfig*.json",
|
|
23
|
+
"{workspaceRoot}/packages/service-config/src/**/*"
|
|
24
|
+
],
|
|
18
25
|
"options": {
|
|
19
26
|
"script": "build"
|
|
20
27
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# lca-api scripts
|
|
2
|
+
|
|
3
|
+
Operational scripts run directly with `bun scripts/<name>.ts` from
|
|
4
|
+
`services/learn-card-network/lca-api/`. These are not part of the deployed
|
|
5
|
+
service and are not built by `bun run build`; they import `src/` files by
|
|
6
|
+
relative path (not the `@alias` path mappings used inside `src/`, since
|
|
7
|
+
`scripts/` is intentionally outside this package's `tsconfig.json` `include`
|
|
8
|
+
and those aliases don't resolve from here — see `escrow-blob-mode-report.ts`
|
|
9
|
+
if you need the full explanation).
|
|
10
|
+
|
|
11
|
+
## `strip-email.ts`
|
|
12
|
+
|
|
13
|
+
Removes the email attribute from a Firebase user by UID, via the Identity
|
|
14
|
+
Toolkit REST API (the Admin SDK can't clear an email, only replace it).
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun scripts/strip-email.ts <uid>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires `GOOGLE_APPLICATION_CREDENTIAL`.
|
|
21
|
+
|
|
22
|
+
## `escrow-blob-mode-report.ts` (P6.2)
|
|
23
|
+
|
|
24
|
+
Read-only counts report over the `userkeys` and `escrowholds` collections:
|
|
25
|
+
escrow blob counts by `enclaveMode` / `enclaveKeyId`, enabled-PIN count,
|
|
26
|
+
pending-hold counts by `releasePolicy`, opted-out count, and a software→nitro
|
|
27
|
+
"re-seal" breakdown. Never writes to the database and never prints PII (counts
|
|
28
|
+
only — no DIDs, emails, or Mongo `_id`s).
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Human-readable table
|
|
32
|
+
bun scripts/escrow-blob-mode-report.ts
|
|
33
|
+
|
|
34
|
+
# Machine-readable JSON
|
|
35
|
+
bun scripts/escrow-blob-mode-report.ts --json
|
|
36
|
+
|
|
37
|
+
# Also print the CloudWatch PutMetricData payload for the counts
|
|
38
|
+
bun scripts/escrow-blob-mode-report.ts --json --publish-metrics
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires the same env as the lca-api server: `SEED`, `MONGO_URI`,
|
|
42
|
+
`MONGO_DB_NAME` (point `MONGO_URI` at whichever environment you want to report
|
|
43
|
+
on). Full flag/limitation reference lives in the script's own header comment.
|
|
44
|
+
|
|
45
|
+
**CloudWatch:** `--publish-metrics` does not call AWS itself — lca-api has no
|
|
46
|
+
`@aws-sdk/client-cloudwatch` dependency (only
|
|
47
|
+
`services/learn-card-network/ai-agent` does elsewhere in this monorepo, and it
|
|
48
|
+
isn't shared with lca-api). The flag prints the exact `MetricData` payload
|
|
49
|
+
(namespace `LearnCard/Escrow`, metrics `EscrowBlobs` per `EnclaveMode`
|
|
50
|
+
dimension and `EscrowBlobsStale`) for the P7.1 monitor Lambda or an ops job
|
|
51
|
+
that already has AWS SDK access to forward.
|
|
52
|
+
|
|
53
|
+
### Running it locally against a throwaway Mongo
|
|
54
|
+
|
|
55
|
+
There's no persistent local Mongo fixture for scripts (unlike `test/*.spec.ts`,
|
|
56
|
+
which get one via `vitest-setup.ts`'s `mongodb-memory-server` global setup).
|
|
57
|
+
To try the report against real data without touching a real environment, spin
|
|
58
|
+
up your own `mongodb-memory-server` instance and point the script at it:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// tmp-run-report.mjs (delete after use — do not commit)
|
|
62
|
+
import { MongoMemoryServer } from 'mongodb-memory-server';
|
|
63
|
+
import { spawnSync } from 'node:child_process';
|
|
64
|
+
|
|
65
|
+
const mongod = await MongoMemoryServer.create();
|
|
66
|
+
// ...seed `userkeys` / `escrowholds` via a MongoClient here...
|
|
67
|
+
|
|
68
|
+
spawnSync('bun', ['scripts/escrow-blob-mode-report.ts', '--json'], {
|
|
69
|
+
env: {
|
|
70
|
+
...process.env,
|
|
71
|
+
NODE_ENV: 'development',
|
|
72
|
+
SEED: 'a'.repeat(64),
|
|
73
|
+
MONGO_URI: mongod.getUri(),
|
|
74
|
+
MONGO_DB_NAME: 'report-harness',
|
|
75
|
+
},
|
|
76
|
+
stdio: 'inherit',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await mongod.stop();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Testing
|
|
83
|
+
|
|
84
|
+
`escrow-blob-mode-report.test.ts` covers the pure aggregation → report
|
|
85
|
+
formatting logic (zero-count modes, an unknown `enclaveMode`/`releasePolicy`
|
|
86
|
+
value, the CloudWatch payload shape) without touching Mongo. It's picked up by
|
|
87
|
+
the same `bun run test` as `src/**/*.test.ts` — `vitest.config.ts`'s `include`
|
|
88
|
+
was extended to also match `scripts/**/*.test.ts`.
|
|
@@ -67,8 +67,24 @@ for (const name of ['swagger', 'didWeb']) {
|
|
|
67
67
|
assert.equal(config.functions[name].role, undefined);
|
|
68
68
|
}
|
|
69
69
|
assert.equal(resources.SigningAuthorityExecutionRole.Type, 'AWS::IAM::Role');
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// provider.environment is a Serverless file variable; resolve it like Serverless does, in
|
|
71
|
+
// both runtime-secrets modes, so the KMS ARN and the no-local-KEK invariant are still checked.
|
|
72
|
+
assert.equal(config.provider.environment, '${file(./serverless.function-env.cjs):provider}');
|
|
73
|
+
const functionEnv = require('../serverless.function-env.cjs');
|
|
74
|
+
const savedEnv = { ...process.env };
|
|
75
|
+
try {
|
|
76
|
+
for (const bundle of ['', 'lca-api/dev/runtime-secrets']) {
|
|
77
|
+
process.env.RUNTIME_SECRETS_ID = bundle;
|
|
78
|
+
process.env.SA_SEED_LOCAL_KEK = 'must-not-reach-lambda';
|
|
79
|
+
const provider = functionEnv.provider({ options: { stage: 'dev' } });
|
|
80
|
+
assert.deepEqual(provider.SA_SEED_KMS_KEY_ARN, keyArn);
|
|
81
|
+
// Deployed functions use KMS only; the local KEK is for offline development.
|
|
82
|
+
for (const env of [provider, functionEnv.api(), functionEnv.escrow()])
|
|
83
|
+
assert.equal(env.SA_SEED_LOCAL_KEK, undefined);
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
process.env = savedEnv;
|
|
87
|
+
}
|
|
72
88
|
if (packaged) {
|
|
73
89
|
const lambda = resources.SeedMigrationLambdaFunction;
|
|
74
90
|
assert.equal(lambda.Properties.Timeout, 900);
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Pure-logic test only: the script module (and, transitively, src/models/UserKey.ts)
|
|
4
|
+
// imports the real Mongo connection module for the CLI's live-query path, which
|
|
5
|
+
// requires a real Mongo global outside test-mode integration setup. Stub it so
|
|
6
|
+
// importing the module here never needs one — same pattern as
|
|
7
|
+
// src/models/UserKey.escrowPin.test.ts, but by relative path: `scripts/` sits
|
|
8
|
+
// outside this package's tsconfig `include`, so path-alias mocks (`@mongo`)
|
|
9
|
+
// resolved from here don't reliably match the alias-resolved import inside
|
|
10
|
+
// src/models/UserKey.ts, even though both point at the same file.
|
|
11
|
+
vi.mock('../src/mongo', () => ({ default: {} }));
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
buildCloudWatchMetricPayload,
|
|
15
|
+
buildReport,
|
|
16
|
+
activeModeFromEnv,
|
|
17
|
+
classifyEnclaveKeyIds,
|
|
18
|
+
CLOUDWATCH_NAMESPACE,
|
|
19
|
+
type RawEscrowCounts,
|
|
20
|
+
} from './escrow-blob-mode-report';
|
|
21
|
+
|
|
22
|
+
const emptyRaw: RawEscrowCounts = {
|
|
23
|
+
totalUserKeys: 0,
|
|
24
|
+
enclaveModeCounts: [],
|
|
25
|
+
enclaveKeyIdCounts: [],
|
|
26
|
+
escrowPinEnabledCount: 0,
|
|
27
|
+
optedOutCount: 0,
|
|
28
|
+
pendingHoldsByPolicy: [],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe('activeModeFromEnv', () => {
|
|
32
|
+
it('maps remote to nitro', () => {
|
|
33
|
+
expect(activeModeFromEnv('remote')).toBe('nitro');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('maps software, undefined, and anything else to software', () => {
|
|
37
|
+
expect(activeModeFromEnv('software')).toBe('software');
|
|
38
|
+
expect(activeModeFromEnv(undefined)).toBe('software');
|
|
39
|
+
expect(activeModeFromEnv('bogus')).toBe('software');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('buildReport', () => {
|
|
44
|
+
const now = new Date('2026-09-25T00:00:00.000Z');
|
|
45
|
+
|
|
46
|
+
it('reports zero counts for known modes/policies with no data', () => {
|
|
47
|
+
const report = buildReport(emptyRaw, 'nitro', now);
|
|
48
|
+
|
|
49
|
+
expect(report.generatedAt).toBe(now.toISOString());
|
|
50
|
+
expect(report.activeEnclaveMode).toBe('nitro');
|
|
51
|
+
expect(report.byEnclaveMode).toEqual({ software: 0, nitro: 0 });
|
|
52
|
+
expect(report.pendingHoldsByReleasePolicy).toEqual({ hold: 0, pin: 0 });
|
|
53
|
+
expect(report.totals).toEqual({
|
|
54
|
+
userKeys: 0,
|
|
55
|
+
escrowBlobs: 0,
|
|
56
|
+
escrowPinEnabled: 0,
|
|
57
|
+
optedOut: 0,
|
|
58
|
+
pendingHolds: 0,
|
|
59
|
+
});
|
|
60
|
+
expect(report.reseal).toEqual({ activeCount: 0, staleCount: 0, staleRatePercent: 0 });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('computes the re-seal split for a normal software/nitro mix', () => {
|
|
64
|
+
const raw: RawEscrowCounts = {
|
|
65
|
+
...emptyRaw,
|
|
66
|
+
totalUserKeys: 20,
|
|
67
|
+
enclaveModeCounts: [
|
|
68
|
+
{ mode: 'software', count: 7 },
|
|
69
|
+
{ mode: 'nitro', count: 3 },
|
|
70
|
+
],
|
|
71
|
+
escrowPinEnabledCount: 2,
|
|
72
|
+
optedOutCount: 1,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const report = buildReport(raw, 'nitro', now);
|
|
76
|
+
|
|
77
|
+
expect(report.byEnclaveMode).toEqual({ software: 7, nitro: 3 });
|
|
78
|
+
expect(report.totals.escrowBlobs).toBe(10);
|
|
79
|
+
expect(report.reseal).toEqual({ activeCount: 3, staleCount: 7, staleRatePercent: 70 });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('surfaces an unknown enclaveMode value instead of dropping it, and counts it as stale', () => {
|
|
83
|
+
const raw: RawEscrowCounts = {
|
|
84
|
+
...emptyRaw,
|
|
85
|
+
enclaveModeCounts: [
|
|
86
|
+
{ mode: 'software', count: 1 },
|
|
87
|
+
{ mode: 'nitro', count: 1 },
|
|
88
|
+
{ mode: 'legacy-v0', count: 2 },
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const report = buildReport(raw, 'nitro', now);
|
|
93
|
+
|
|
94
|
+
expect(report.byEnclaveMode).toEqual({ software: 1, nitro: 1, 'legacy-v0': 2 });
|
|
95
|
+
expect(report.totals.escrowBlobs).toBe(4);
|
|
96
|
+
// activeCount only counts the active mode bucket; the unknown mode is stale too.
|
|
97
|
+
expect(report.reseal).toEqual({ activeCount: 1, staleCount: 3, staleRatePercent: 75 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rounds the stale rate to 2 decimal places', () => {
|
|
101
|
+
const raw: RawEscrowCounts = {
|
|
102
|
+
...emptyRaw,
|
|
103
|
+
enclaveModeCounts: [
|
|
104
|
+
{ mode: 'software', count: 1 },
|
|
105
|
+
{ mode: 'nitro', count: 2 },
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const report = buildReport(raw, 'nitro', now);
|
|
110
|
+
|
|
111
|
+
// stale = 1, total = 3 -> 33.333...% rounds to 33.33
|
|
112
|
+
expect(report.reseal.staleRatePercent).toBe(33.33);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('passes through enclaveKeyId counts and an unknown release policy value', () => {
|
|
116
|
+
const raw: RawEscrowCounts = {
|
|
117
|
+
...emptyRaw,
|
|
118
|
+
enclaveKeyIdCounts: [
|
|
119
|
+
{ keyId: 'key-a', count: 4 },
|
|
120
|
+
{ keyId: 'key-b', count: 1 },
|
|
121
|
+
],
|
|
122
|
+
pendingHoldsByPolicy: [
|
|
123
|
+
{ policy: 'hold', count: 3 },
|
|
124
|
+
{ policy: 'weird-future-policy', count: 1 },
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const report = buildReport(raw, 'software', now);
|
|
129
|
+
|
|
130
|
+
expect(report.byEnclaveKeyId).toEqual({ 'key-a': 4, 'key-b': 1 });
|
|
131
|
+
expect(report.pendingHoldsByReleasePolicy).toEqual({
|
|
132
|
+
hold: 3,
|
|
133
|
+
pin: 0,
|
|
134
|
+
'weird-future-policy': 1,
|
|
135
|
+
});
|
|
136
|
+
expect(report.totals.pendingHolds).toBe(4);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('buildCloudWatchMetricPayload', () => {
|
|
141
|
+
it('emits EscrowBlobs per mode dimension plus a single EscrowBlobsStale datum', () => {
|
|
142
|
+
const report = buildReport(
|
|
143
|
+
{
|
|
144
|
+
...emptyRaw,
|
|
145
|
+
enclaveModeCounts: [
|
|
146
|
+
{ mode: 'software', count: 5 },
|
|
147
|
+
{ mode: 'nitro', count: 2 },
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
'nitro',
|
|
151
|
+
new Date('2026-09-25T00:00:00.000Z')
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const payload = buildCloudWatchMetricPayload(report);
|
|
155
|
+
|
|
156
|
+
expect(payload.Namespace).toBe(CLOUDWATCH_NAMESPACE);
|
|
157
|
+
expect(payload.MetricData).toHaveLength(3);
|
|
158
|
+
expect(payload.MetricData).toEqual(
|
|
159
|
+
expect.arrayContaining([
|
|
160
|
+
{
|
|
161
|
+
MetricName: 'EscrowBlobs',
|
|
162
|
+
Value: 5,
|
|
163
|
+
Unit: 'Count',
|
|
164
|
+
Timestamp: report.generatedAt,
|
|
165
|
+
Dimensions: [{ Name: 'EnclaveMode', Value: 'software' }],
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
MetricName: 'EscrowBlobs',
|
|
169
|
+
Value: 2,
|
|
170
|
+
Unit: 'Count',
|
|
171
|
+
Timestamp: report.generatedAt,
|
|
172
|
+
Dimensions: [{ Name: 'EnclaveMode', Value: 'nitro' }],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
MetricName: 'EscrowBlobsStale',
|
|
176
|
+
Value: 5,
|
|
177
|
+
Unit: 'Count',
|
|
178
|
+
Timestamp: report.generatedAt,
|
|
179
|
+
},
|
|
180
|
+
])
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('still emits a zero-value EscrowBlobsStale datum when there is no data at all', () => {
|
|
185
|
+
const report = buildReport(emptyRaw, 'software', new Date('2026-09-25T00:00:00.000Z'));
|
|
186
|
+
|
|
187
|
+
const payload = buildCloudWatchMetricPayload(report);
|
|
188
|
+
|
|
189
|
+
const stale = payload.MetricData.find(datum => datum.MetricName === 'EscrowBlobsStale');
|
|
190
|
+
expect(stale?.Value).toBe(0);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('classifyEnclaveKeyIds (P9.3)', () => {
|
|
195
|
+
it('splits counts into current/previous/unknown, listing unknown keyIds sorted', () => {
|
|
196
|
+
const classification = classifyEnclaveKeyIds(
|
|
197
|
+
{ 'key-2': 10, 'key-1': 4, 'key-0': 1, unknown: 1 },
|
|
198
|
+
'key-2',
|
|
199
|
+
['key-1', 'key-0']
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(classification).toEqual({
|
|
203
|
+
current: 10,
|
|
204
|
+
previous: 5,
|
|
205
|
+
unknown: 1,
|
|
206
|
+
unknownKeyIds: ['unknown'],
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('reports zero previous/unknown when every blob is already on the current key', () => {
|
|
211
|
+
const classification = classifyEnclaveKeyIds({ 'key-2': 3 }, 'key-2', ['key-1']);
|
|
212
|
+
|
|
213
|
+
expect(classification).toEqual({ current: 3, previous: 0, unknown: 0, unknownKeyIds: [] });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('treats an empty previous-key list as everything non-current being unknown', () => {
|
|
217
|
+
const classification = classifyEnclaveKeyIds(
|
|
218
|
+
{ 'key-2': 2, 'retired-1': 1, 'retired-2': 1 },
|
|
219
|
+
'key-2',
|
|
220
|
+
[]
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(classification.previous).toBe(0);
|
|
224
|
+
expect(classification.unknown).toBe(2);
|
|
225
|
+
expect(classification.unknownKeyIds).toEqual(['retired-1', 'retired-2']);
|
|
226
|
+
});
|
|
227
|
+
});
|