@learncard/learn-cloud-service 2.5.35 → 2.5.37
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 +24 -25
- package/CHANGELOG.md +30 -0
- package/bootstrapLambda.test.ts +68 -0
- package/bundle.mjs +9 -1
- package/config/config.dev.json +8 -0
- package/config/config.example.json +8 -0
- package/config/config.json +1 -0
- package/config/config.production.json +8 -0
- package/config/config.scouts.dev.json +3 -0
- package/config/config.scouts.production.json +4 -0
- package/didWebLambda.test.ts +41 -0
- package/didWebLambda.ts +13 -19
- package/didWebLambdaApp.ts +23 -0
- package/dist/config/applyDockerStageConfig.d.ts +2 -0
- package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
- package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
- package/dist/config/stageConfig.d.ts +10 -0
- package/dist/config/stageConfig.d.ts.map +1 -0
- package/dist/config/stageConfig.test.d.ts +2 -0
- package/dist/config/stageConfig.test.d.ts.map +1 -0
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/docker-entry.d.ts +1 -1
- package/dist/docker-entry.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/publish-metafile.json +1 -0
- package/esbuildPlugins.cjs +1 -0
- package/lambda.ts +22 -153
- package/lambdaApp.ts +163 -0
- package/oidcLambda.test.ts +41 -0
- package/oidcLambda.ts +13 -19
- package/oidcLambdaApp.ts +23 -0
- package/package.json +17 -16
- package/project.json +8 -1
- package/serverless-local.yml +3 -1
- package/serverless.function-env.cjs +31 -0
- package/serverless.yml +15 -29
- package/src/config/applyDockerStageConfig.ts +6 -0
- package/src/config/serverlessFunctionEnv.test.ts +81 -0
- package/src/config/stageConfig.test.ts +26 -0
- package/src/config/stageConfig.ts +24 -0
- package/src/docker-entry.ts +2 -0
- package/vitest.config.ts +3 -0
- package/xApiLambda.test.ts +41 -0
- package/xApiLambda.ts +13 -19
- package/xApiLambdaApp.ts +23 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const functions: {
|
|
6
|
+
provider: (context?: {
|
|
7
|
+
options: { stage: string; httpPort?: string };
|
|
8
|
+
}) => Record<string, unknown>;
|
|
9
|
+
} = require('../../serverless.function-env.cjs');
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.stubEnv('RUNTIME_SECRETS_ID', '');
|
|
13
|
+
vi.stubEnv('LEARN_CLOUD_SEED', 'seed');
|
|
14
|
+
vi.stubEnv('LEARN_CLOUD_MONGO_URI', 'mongodb://localhost:27017');
|
|
15
|
+
// XAPI_ENDPOINT is a user-classified bundle secret, so populate a stale fallback value
|
|
16
|
+
// to prove it never leaks onto the provider once a runtime bundle is configured.
|
|
17
|
+
vi.stubEnv('XAPI_ENDPOINT', 'https://stale.xapi.example.com');
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => vi.unstubAllEnvs());
|
|
20
|
+
|
|
21
|
+
describe('learn-cloud-service serverless function environments', () => {
|
|
22
|
+
it('omits all credential fallbacks in bundle mode', () => {
|
|
23
|
+
vi.stubEnv('RUNTIME_SECRETS_ID', 'learn-cloud-service/dev/runtime-secrets');
|
|
24
|
+
expect(Object.keys(functions.provider()).sort()).toEqual([
|
|
25
|
+
'CONFIG_TENANT',
|
|
26
|
+
'LAMBDA_STAGE',
|
|
27
|
+
'PORT',
|
|
28
|
+
'REDIS_HOST',
|
|
29
|
+
'REDIS_PORT',
|
|
30
|
+
'RUNTIME_SECRETS_ID',
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('drops the XAPI_ENDPOINT bundle secret in bundle mode even when a stale fallback is set', () => {
|
|
35
|
+
vi.stubEnv('RUNTIME_SECRETS_ID', 'learn-cloud-service/dev/runtime-secrets');
|
|
36
|
+
expect(functions.provider()).not.toHaveProperty('XAPI_ENDPOINT');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('retains the existing provider credentials only in fallback mode', () => {
|
|
40
|
+
const provider = functions.provider();
|
|
41
|
+
expect(provider.LEARN_CLOUD_SEED).toBe('seed');
|
|
42
|
+
expect(provider.LEARN_CLOUD_MONGO_URI).toBe('mongodb://localhost:27017');
|
|
43
|
+
expect(provider.XAPI_ENDPOINT).toBe('https://stale.xapi.example.com');
|
|
44
|
+
expect(provider.RUNTIME_SECRETS_ID).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('preserves the CloudFormation Redis intrinsics and CLI options', () => {
|
|
48
|
+
const provider = functions.provider({ options: { stage: 'production', httpPort: '5100' } });
|
|
49
|
+
expect(provider.LAMBDA_STAGE).toBe('production');
|
|
50
|
+
expect(provider.PORT).toBe('5100');
|
|
51
|
+
expect(provider.REDIS_HOST).toEqual({
|
|
52
|
+
'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Address'],
|
|
53
|
+
});
|
|
54
|
+
expect(provider.REDIS_PORT).toEqual({
|
|
55
|
+
'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Port'],
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it.each(['', 'bundle'])(
|
|
60
|
+
'resolves the full provider environment through Serverless v3 (%s)',
|
|
61
|
+
async bundle => {
|
|
62
|
+
const source = require('serverless/lib/configuration/variables/sources/file');
|
|
63
|
+
vi.stubEnv('RUNTIME_SECRETS_ID', bundle);
|
|
64
|
+
const options = { stage: 'production', httpPort: '5100' };
|
|
65
|
+
const result = await source.resolve({
|
|
66
|
+
serviceDir: new URL('../../', import.meta.url).pathname,
|
|
67
|
+
params: [new URL('../../serverless.function-env.cjs', import.meta.url).pathname],
|
|
68
|
+
address: 'provider',
|
|
69
|
+
options,
|
|
70
|
+
resolveConfigurationProperty: vi.fn(),
|
|
71
|
+
resolveVariable: vi.fn(),
|
|
72
|
+
});
|
|
73
|
+
expect(result.value).toEqual(functions.provider({ options }));
|
|
74
|
+
expect(result.value.LAMBDA_STAGE).toBe('production');
|
|
75
|
+
expect(result.value.PORT).toBe('5100');
|
|
76
|
+
expect(result.value.REDIS_HOST).toEqual({
|
|
77
|
+
'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Address'],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { afterEach, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Model a non-empty .env value arriving before the stage defaults are applied.
|
|
4
|
+
vi.mock('dotenv/config', () => {
|
|
5
|
+
vi.stubEnv('SENTRY_ENV', 'self-hosted');
|
|
6
|
+
return {};
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
afterEach(() => vi.unstubAllEnvs());
|
|
10
|
+
|
|
11
|
+
it('loads local env before applying statically imported stage defaults', async () => {
|
|
12
|
+
const { applyLearnCloudStageConfig, stages } = await import('./stageConfig');
|
|
13
|
+
for (const key of Object.keys(stages.dev)) {
|
|
14
|
+
if (key !== 'SENTRY_ENV') vi.stubEnv(key, '');
|
|
15
|
+
}
|
|
16
|
+
applyLearnCloudStageConfig('dev');
|
|
17
|
+
expect(process.env.SENTRY_ENV).toBe('self-hosted');
|
|
18
|
+
expect(process.env.SERVER_URL).toBe(stages.dev.SERVER_URL);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('does not select a deployed stage for an unknown local stage', async () => {
|
|
22
|
+
const { applyLearnCloudStageConfig } = await import('./stageConfig');
|
|
23
|
+
vi.stubEnv('SERVER_URL', '');
|
|
24
|
+
applyLearnCloudStageConfig('unregistered-self-hosted-stage');
|
|
25
|
+
expect(process.env.SERVER_URL).toBe('');
|
|
26
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { applyStageConfig } from '@learncard/service-config';
|
|
3
|
+
|
|
4
|
+
import baseConfig from '../../config/config.json';
|
|
5
|
+
import devConfig from '../../config/config.dev.json';
|
|
6
|
+
import productionConfig from '../../config/config.production.json';
|
|
7
|
+
import scoutsDevConfig from '../../config/config.scouts.dev.json';
|
|
8
|
+
import scoutsProductionConfig from '../../config/config.scouts.production.json';
|
|
9
|
+
|
|
10
|
+
export const base: Record<string, string> = baseConfig;
|
|
11
|
+
|
|
12
|
+
export const stages = {
|
|
13
|
+
dev: devConfig,
|
|
14
|
+
production: productionConfig,
|
|
15
|
+
// ScoutPass deploys the same services and stage names; CONFIG_TENANT=scouts selects these.
|
|
16
|
+
'scouts.dev': scoutsDevConfig,
|
|
17
|
+
'scouts.production': scoutsProductionConfig,
|
|
18
|
+
} satisfies Record<string, Record<string, string>>;
|
|
19
|
+
|
|
20
|
+
// Must run before any module that reads configuration on import (e.g. @environment):
|
|
21
|
+
// merges base + the selected stage into process.env; explicit non-empty env still wins.
|
|
22
|
+
export const applyLearnCloudStageConfig = (stage: string | undefined): void => {
|
|
23
|
+
applyStageConfig({ base, stages, stage });
|
|
24
|
+
};
|
package/src/docker-entry.ts
CHANGED
package/vitest.config.ts
CHANGED
|
@@ -7,6 +7,9 @@ export default createVitestConfig(nodePreset, {
|
|
|
7
7
|
'test/query.helpers.spec.ts',
|
|
8
8
|
'test/xapi.helpers.spec.ts',
|
|
9
9
|
'test/share-content-auth.spec.ts',
|
|
10
|
+
'src/config/stageConfig.test.ts',
|
|
11
|
+
'src/config/serverlessFunctionEnv.test.ts',
|
|
12
|
+
'*Lambda.test.ts',
|
|
10
13
|
],
|
|
11
14
|
},
|
|
12
15
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { APIGatewayProxyEventV2, Context } from 'aws-lambda';
|
|
2
|
+
import { beforeEach, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({ bootstrap: vi.fn(), getApp: vi.fn(), handler: vi.fn() }));
|
|
5
|
+
|
|
6
|
+
vi.mock('@learncard/service-config', () => ({ bootstrapLambda: mocks.bootstrap }));
|
|
7
|
+
vi.mock('./src/config/stageConfig', () => ({ base: {}, stages: { dev: {}, production: {} } }));
|
|
8
|
+
vi.mock('./xApiLambdaApp', () => ({ xApiHandler: mocks.handler }));
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.resetModules();
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
mocks.bootstrap.mockReturnValue(mocks.getApp);
|
|
14
|
+
mocks.getApp.mockResolvedValue({ xApiHandler: mocks.handler });
|
|
15
|
+
mocks.handler.mockResolvedValue({ statusCode: 200 });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('bootstraps the xApi app with checked-in stage config and the deploy stage', async () => {
|
|
19
|
+
vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', 'learn-cloud-service-dev-xapi');
|
|
20
|
+
vi.stubEnv('LAMBDA_STAGE', 'dev');
|
|
21
|
+
await import('./xApiLambda');
|
|
22
|
+
expect(mocks.bootstrap).toHaveBeenCalledTimes(1);
|
|
23
|
+
expect(mocks.bootstrap).toHaveBeenCalledWith({
|
|
24
|
+
base: {},
|
|
25
|
+
stages: { dev: {}, production: {} },
|
|
26
|
+
stage: 'dev',
|
|
27
|
+
importApp: expect.any(Function),
|
|
28
|
+
});
|
|
29
|
+
vi.unstubAllEnvs();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('resolves the app once per cold start and forwards every argument', async () => {
|
|
33
|
+
const { xApiHandler } = await import('./xApiLambda');
|
|
34
|
+
const event = {} as APIGatewayProxyEventV2;
|
|
35
|
+
const context = {} as Context;
|
|
36
|
+
const callback = vi.fn();
|
|
37
|
+
await xApiHandler(event, context, callback);
|
|
38
|
+
await xApiHandler(event, context, callback);
|
|
39
|
+
expect(mocks.getApp).toHaveBeenCalledTimes(2);
|
|
40
|
+
expect(mocks.handler).toHaveBeenLastCalledWith(event, context, callback);
|
|
41
|
+
});
|
package/xApiLambda.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as Sentry from '@sentry/serverless';
|
|
1
|
+
import { bootstrapLambda } from '@learncard/service-config';
|
|
3
2
|
|
|
4
|
-
import
|
|
5
|
-
import { environment } from './src/config/environment';
|
|
6
|
-
import { toServerlessApplication } from './src/helpers/serverlessApplication';
|
|
3
|
+
import { base, stages } from './src/config/stageConfig';
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
new Sentry.Integrations.Mongo(),
|
|
18
|
-
],
|
|
5
|
+
type XApiApp = typeof import('./xApiLambdaApp');
|
|
6
|
+
|
|
7
|
+
const getApplication = bootstrapLambda<XApiApp>({
|
|
8
|
+
base,
|
|
9
|
+
stages,
|
|
10
|
+
stage: process.env.AWS_LAMBDA_FUNCTION_NAME
|
|
11
|
+
? process.env.LAMBDA_STAGE
|
|
12
|
+
: process.env.CONFIG_STAGE,
|
|
13
|
+
importApp: () => import('./xApiLambdaApp'),
|
|
19
14
|
});
|
|
20
15
|
|
|
21
|
-
export const xApiHandler =
|
|
22
|
-
|
|
23
|
-
);
|
|
16
|
+
export const xApiHandler = async (...args: Parameters<XApiApp['xApiHandler']>) =>
|
|
17
|
+
(await getApplication()).xApiHandler(...args);
|
package/xApiLambdaApp.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import serverlessHttp from 'serverless-http';
|
|
2
|
+
import * as Sentry from '@sentry/serverless';
|
|
3
|
+
|
|
4
|
+
import xApiApp from './src/xapi';
|
|
5
|
+
import { environment } from './src/config/environment';
|
|
6
|
+
import { toServerlessApplication } from './src/helpers/serverlessApplication';
|
|
7
|
+
|
|
8
|
+
Sentry.AWSLambda.init({
|
|
9
|
+
dsn: environment.SENTRY_DSN,
|
|
10
|
+
environment: environment.SENTRY_ENV,
|
|
11
|
+
enabled: Boolean(environment.SENTRY_DSN),
|
|
12
|
+
tracesSampleRate: 1.0,
|
|
13
|
+
integrations: [
|
|
14
|
+
new Sentry.Integrations.Console(),
|
|
15
|
+
new Sentry.Integrations.Http(),
|
|
16
|
+
new Sentry.Integrations.ContextLines(),
|
|
17
|
+
new Sentry.Integrations.Mongo(),
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const xApiHandler = Sentry.AWSLambda.wrapHandler(
|
|
22
|
+
serverlessHttp(toServerlessApplication(xApiApp))
|
|
23
|
+
);
|