@learncard/learn-cloud-service 2.5.34 → 2.5.36

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.
Files changed (124) hide show
  1. package/.env.example +24 -12
  2. package/CHANGELOG.md +43 -0
  3. package/bootstrapLambda.test.ts +68 -0
  4. package/config/config.dev.json +8 -0
  5. package/config/config.example.json +8 -0
  6. package/config/config.json +1 -0
  7. package/config/config.production.json +8 -0
  8. package/config/config.scouts.dev.json +3 -0
  9. package/config/config.scouts.production.json +4 -0
  10. package/didWebLambda.test.ts +41 -0
  11. package/didWebLambda.ts +13 -19
  12. package/didWebLambdaApp.ts +23 -0
  13. package/dist/accesslayer/share-content/delete.d.ts +5 -0
  14. package/dist/accesslayer/share-content/delete.d.ts.map +1 -0
  15. package/dist/accesslayer/share-content/index.d.ts +8 -0
  16. package/dist/accesslayer/share-content/index.d.ts.map +1 -0
  17. package/dist/accesslayer/share-content/initialize.d.ts +5 -0
  18. package/dist/accesslayer/share-content/initialize.d.ts.map +1 -0
  19. package/dist/accesslayer/share-content/internal.d.ts +12 -0
  20. package/dist/accesslayer/share-content/internal.d.ts.map +1 -0
  21. package/dist/accesslayer/share-content/put.d.ts +5 -0
  22. package/dist/accesslayer/share-content/put.d.ts.map +1 -0
  23. package/dist/accesslayer/share-content/read.d.ts +7 -0
  24. package/dist/accesslayer/share-content/read.d.ts.map +1 -0
  25. package/dist/accesslayer/share-content/types.d.ts +85 -0
  26. package/dist/accesslayer/share-content/types.d.ts.map +1 -0
  27. package/dist/config/applyDockerStageConfig.d.ts +2 -0
  28. package/dist/config/applyDockerStageConfig.d.ts.map +1 -0
  29. package/dist/config/environment.d.ts +12 -0
  30. package/dist/config/environment.d.ts.map +1 -1
  31. package/dist/config/serverlessFunctionEnv.test.d.ts +2 -0
  32. package/dist/config/serverlessFunctionEnv.test.d.ts.map +1 -0
  33. package/dist/config/stageConfig.d.ts +10 -0
  34. package/dist/config/stageConfig.d.ts.map +1 -0
  35. package/dist/config/stageConfig.test.d.ts +2 -0
  36. package/dist/config/stageConfig.test.d.ts.map +1 -0
  37. package/dist/didkit_wasm_bg.wasm +0 -0
  38. package/dist/docker-entry.d.ts +1 -1
  39. package/dist/docker-entry.d.ts.map +1 -1
  40. package/dist/helpers/share-content-auth/adapters.d.ts +12 -0
  41. package/dist/helpers/share-content-auth/adapters.d.ts.map +1 -0
  42. package/dist/helpers/share-content-auth/canonical.d.ts +15 -0
  43. package/dist/helpers/share-content-auth/canonical.d.ts.map +1 -0
  44. package/dist/helpers/share-content-auth/claims.d.ts +19 -0
  45. package/dist/helpers/share-content-auth/claims.d.ts.map +1 -0
  46. package/dist/helpers/share-content-auth/config.d.ts +12 -0
  47. package/dist/helpers/share-content-auth/config.d.ts.map +1 -0
  48. package/dist/helpers/share-content-auth/index.d.ts +8 -0
  49. package/dist/helpers/share-content-auth/index.d.ts.map +1 -0
  50. package/dist/helpers/share-content-auth/token.d.ts +28 -0
  51. package/dist/helpers/share-content-auth/token.d.ts.map +1 -0
  52. package/dist/helpers/share-content-auth/types.d.ts +70 -0
  53. package/dist/helpers/share-content-auth/types.d.ts.map +1 -0
  54. package/dist/helpers/share-content-auth/verifier.d.ts +9 -0
  55. package/dist/helpers/share-content-auth/verifier.d.ts.map +1 -0
  56. package/dist/index.js +2 -2
  57. package/dist/models/ShareContent.d.ts +160 -0
  58. package/dist/models/ShareContent.d.ts.map +1 -0
  59. package/dist/models/index.d.ts +1 -0
  60. package/dist/models/index.d.ts.map +1 -1
  61. package/dist/share-content/config.d.ts +28 -0
  62. package/dist/share-content/config.d.ts.map +1 -0
  63. package/dist/share-content/index.d.ts +6 -0
  64. package/dist/share-content/index.d.ts.map +1 -0
  65. package/dist/share-content/plugin.d.ts +5 -0
  66. package/dist/share-content/plugin.d.ts.map +1 -0
  67. package/dist/share-content/production.d.ts +15 -0
  68. package/dist/share-content/production.d.ts.map +1 -0
  69. package/dist/share-content/runtime.d.ts +23 -0
  70. package/dist/share-content/runtime.d.ts.map +1 -0
  71. package/dist/share-content/types.d.ts +23 -0
  72. package/dist/share-content/types.d.ts.map +1 -0
  73. package/esbuildPlugins.cjs +1 -0
  74. package/lambda.ts +22 -92
  75. package/lambdaApp.ts +163 -0
  76. package/oidcLambda.test.ts +41 -0
  77. package/oidcLambda.ts +13 -19
  78. package/oidcLambdaApp.ts +23 -0
  79. package/package.json +18 -16
  80. package/serverless-local.yml +3 -1
  81. package/serverless.function-env.cjs +31 -0
  82. package/serverless.yml +31 -22
  83. package/src/accesslayer/share-content/delete.ts +124 -0
  84. package/src/accesslayer/share-content/index.ts +70 -0
  85. package/src/accesslayer/share-content/initialize.ts +24 -0
  86. package/src/accesslayer/share-content/internal.ts +89 -0
  87. package/src/accesslayer/share-content/put.ts +93 -0
  88. package/src/accesslayer/share-content/read.ts +124 -0
  89. package/src/accesslayer/share-content/types.ts +155 -0
  90. package/src/config/applyDockerStageConfig.ts +6 -0
  91. package/src/config/environment.ts +33 -0
  92. package/src/config/serverlessFunctionEnv.test.ts +81 -0
  93. package/src/config/stageConfig.test.ts +26 -0
  94. package/src/config/stageConfig.ts +24 -0
  95. package/src/docker-entry.ts +21 -0
  96. package/src/helpers/share-content-auth/adapters.ts +48 -0
  97. package/src/helpers/share-content-auth/canonical.ts +91 -0
  98. package/src/helpers/share-content-auth/claims.ts +164 -0
  99. package/src/helpers/share-content-auth/config.ts +160 -0
  100. package/src/helpers/share-content-auth/index.ts +59 -0
  101. package/src/helpers/share-content-auth/token.ts +148 -0
  102. package/src/helpers/share-content-auth/types.ts +195 -0
  103. package/src/helpers/share-content-auth/verifier.ts +220 -0
  104. package/src/models/ShareContent.ts +198 -0
  105. package/src/models/index.ts +1 -0
  106. package/src/share-content/config.ts +199 -0
  107. package/src/share-content/index.ts +5 -0
  108. package/src/share-content/plugin.ts +317 -0
  109. package/src/share-content/production.ts +107 -0
  110. package/src/share-content/runtime.ts +90 -0
  111. package/src/share-content/types.ts +59 -0
  112. package/test/share-content-auth.spec.ts +1161 -0
  113. package/test/share-content-config.spec.ts +252 -0
  114. package/test/share-content-didkit.spec.ts +411 -0
  115. package/test/share-content-endpoint.spec.ts +825 -0
  116. package/test/share-content-redis-setup.ts +18 -0
  117. package/test/share-content.spec.ts +681 -0
  118. package/vitest.config.ts +4 -0
  119. package/vitest.share-content-config-unit.config.ts +8 -0
  120. package/vitest.share-content-endpoint.config.ts +21 -0
  121. package/vitest.share-content.config.ts +19 -0
  122. package/xApiLambda.test.ts +41 -0
  123. package/xApiLambda.ts +13 -19
  124. package/xApiLambdaApp.ts +23 -0
package/lambda.ts CHANGED
@@ -1,102 +1,32 @@
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';
1
+ import { bootstrapLambda } from '@learncard/service-config';
5
2
 
6
- import app from './src/openapi';
7
- import { appRouter, createContext } from './src/app';
8
- import { createOpenApiAwsLambdaHandler } from './src/helpers/shim';
9
- import {
10
- handleTrpcError,
11
- sentryBeforeSend,
12
- getTracesSampleRate,
13
- } from './src/helpers/sentry.helpers';
14
- import { environment } from './src/config/environment';
15
- import { toServerlessApplication } from './src/helpers/serverlessApplication';
3
+ import { base, stages } from './src/config/stageConfig';
16
4
 
17
- Sentry.AWSLambda.init({
18
- dsn: environment.SENTRY_DSN,
19
- environment: environment.SENTRY_ENV,
20
- enabled: Boolean(environment.SENTRY_DSN),
21
- tracesSampleRate: getTracesSampleRate(),
22
- beforeSend: sentryBeforeSend,
23
- integrations: [
24
- new Sentry.Integrations.Console(),
25
- new Sentry.Integrations.Http({ tracing: true }),
26
- new Sentry.Integrations.ContextLines(),
27
- new Sentry.Integrations.Mongo(),
28
- ],
29
- });
30
-
31
- export const swaggerUiHandler = serverlessHttp(toServerlessApplication(app), {
32
- basePath: '/docs',
33
- });
34
-
35
- export const _openApiHandler = createOpenApiAwsLambdaHandler({
36
- router: appRouter,
37
- responseMeta: () => {
38
- return {
39
- headers: {
40
- 'Access-Control-Allow-Origin': '*',
41
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
42
- 'Access-Control-Allow-Headers': 'Authorization, Content-Type',
43
- },
44
- };
45
- },
46
- createContext,
47
- onError: handleTrpcError,
48
- });
5
+ type LambdaApp = typeof import('./lambdaApp');
49
6
 
50
- export const _trpcHandler = awsLambdaRequestHandler({
51
- allowMethodOverride: true,
52
- router: appRouter,
53
- createContext,
54
- onError: handleTrpcError,
55
- responseMeta: () => {
56
- return {
57
- headers: {
58
- 'Access-Control-Allow-Origin': '*',
59
- 'Access-Control-Allow-Methods': '*',
60
- 'Access-Control-Allow-Headers': 'authorization',
61
- },
62
- };
63
- },
7
+ const getApplication = bootstrapLambda<LambdaApp>({
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('./lambdaApp'),
64
14
  });
65
15
 
66
- export const openApiHandler = Sentry.AWSLambda.wrapHandler(
67
- async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
68
- if (event.requestContext.http.method === 'OPTIONS') {
69
- return {
70
- statusCode: 200,
71
- headers: {
72
- 'Access-Control-Allow-Origin': '*',
73
- 'Access-Control-Allow-Headers': '*',
74
- 'Access-Control-Allow-Methods': '*',
75
- },
76
- };
77
- }
16
+ export const trpcHandler = async (...args: Parameters<LambdaApp['trpcHandler']>) =>
17
+ (await getApplication()).trpcHandler(...args);
78
18
 
79
- context.callbackWaitsForEmptyEventLoop = false;
19
+ export const openApiHandler = async (...args: Parameters<LambdaApp['openApiHandler']>) =>
20
+ (await getApplication()).openApiHandler(...args);
80
21
 
81
- return _openApiHandler(event, context);
82
- }
83
- );
22
+ export const _trpcHandler = async (...args: Parameters<LambdaApp['_trpcHandler']>) =>
23
+ (await getApplication())._trpcHandler(...args);
84
24
 
85
- export const trpcHandler = Sentry.AWSLambda.wrapHandler(
86
- async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
87
- if (event.requestContext.http.method === 'OPTIONS') {
88
- return {
89
- statusCode: 200,
90
- headers: {
91
- 'Access-Control-Allow-Origin': '*',
92
- 'Access-Control-Allow-Headers': '*',
93
- 'Access-Control-Allow-Methods': '*',
94
- },
95
- };
96
- }
25
+ export const _openApiHandler = async (...args: Parameters<LambdaApp['_openApiHandler']>) =>
26
+ (await getApplication())._openApiHandler(...args);
97
27
 
98
- context.callbackWaitsForEmptyEventLoop = false;
28
+ export const shareContentHandler = async (...args: Parameters<LambdaApp['shareContentHandler']>) =>
29
+ (await getApplication()).shareContentHandler(...args);
99
30
 
100
- return _trpcHandler(event, context);
101
- }
102
- );
31
+ export const swaggerUiHandler = async (...args: Parameters<LambdaApp['swaggerUiHandler']>) =>
32
+ (await getApplication()).swaggerUiHandler(...args);
package/lambdaApp.ts ADDED
@@ -0,0 +1,163 @@
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
+ import Fastify, { type FastifyInstance } from 'fastify';
6
+
7
+ import app from './src/openapi';
8
+ import { appRouter, createContext } from './src/app';
9
+ import { createOpenApiAwsLambdaHandler } from './src/helpers/shim';
10
+ import {
11
+ handleTrpcError,
12
+ sentryBeforeSend,
13
+ getTracesSampleRate,
14
+ } from './src/helpers/sentry.helpers';
15
+ import { environment } from './src/config/environment';
16
+ import { toServerlessApplication } from './src/helpers/serverlessApplication';
17
+ import {
18
+ assertShareContentEnvironmentConfiguration,
19
+ createShareContentRuntimeFromEnvironment,
20
+ registerShareContentRoutes,
21
+ } from './src/share-content';
22
+
23
+ Sentry.AWSLambda.init({
24
+ dsn: environment.SENTRY_DSN,
25
+ environment: environment.SENTRY_ENV,
26
+ enabled: Boolean(environment.SENTRY_DSN),
27
+ tracesSampleRate: getTracesSampleRate(),
28
+ beforeSend: sentryBeforeSend,
29
+ integrations: [
30
+ new Sentry.Integrations.Console(),
31
+ new Sentry.Integrations.Http({ tracing: true }),
32
+ new Sentry.Integrations.ContextLines(),
33
+ new Sentry.Integrations.Mongo(),
34
+ ],
35
+ });
36
+
37
+ // Fail the cold start on an enabled-but-invalid LC-2187 share-content
38
+ // configuration; a disabled configuration is a no-op.
39
+ assertShareContentEnvironmentConfiguration();
40
+
41
+ let shareContentProxyPromise: Promise<ReturnType<typeof serverlessHttp> | null> | null = null;
42
+
43
+ /**
44
+ * Lazy per-container boot for the LC-2187 service routes. Resolving the runtime
45
+ * awaits repository index initialization and opens Redis only when enabled, so a
46
+ * disabled deployment installs nothing and returns 404 for every internal path.
47
+ */
48
+ const getShareContentProxy = async (): Promise<ReturnType<typeof serverlessHttp> | null> => {
49
+ if (!shareContentProxyPromise) {
50
+ shareContentProxyPromise = (async () => {
51
+ const runtime = await createShareContentRuntimeFromEnvironment();
52
+
53
+ if (!runtime.enabled) return null;
54
+
55
+ const shareContentApp: FastifyInstance = Fastify({ maxParamLength: 5000 });
56
+ await registerShareContentRoutes(shareContentApp, runtime);
57
+ await shareContentApp.ready();
58
+
59
+ return serverlessHttp(toServerlessApplication(shareContentApp.server));
60
+ })();
61
+ }
62
+
63
+ return shareContentProxyPromise;
64
+ };
65
+
66
+ export const shareContentHandler = async (
67
+ event: APIGatewayProxyEventV2,
68
+ context: Context
69
+ ): Promise<APIGatewayProxyResultV2> => {
70
+ let proxy: ReturnType<typeof serverlessHttp> | null;
71
+
72
+ try {
73
+ proxy = await getShareContentProxy();
74
+ } catch {
75
+ // Never return a raw boot error. Allow a later invocation to retry.
76
+ shareContentProxyPromise = null;
77
+
78
+ return {
79
+ statusCode: 503,
80
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' },
81
+ body: JSON.stringify({ ok: false, error: 'UNAVAILABLE' }),
82
+ };
83
+ }
84
+
85
+ if (!proxy) {
86
+ return { statusCode: 404, headers: { 'Cache-Control': 'no-store' }, body: '' };
87
+ }
88
+
89
+ return proxy(event, context);
90
+ };
91
+
92
+ export const swaggerUiHandler = serverlessHttp(toServerlessApplication(app), {
93
+ basePath: '/docs',
94
+ });
95
+
96
+ export const _openApiHandler = createOpenApiAwsLambdaHandler({
97
+ router: appRouter,
98
+ responseMeta: () => {
99
+ return {
100
+ headers: {
101
+ 'Access-Control-Allow-Origin': '*',
102
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
103
+ 'Access-Control-Allow-Headers': 'Authorization, Content-Type',
104
+ },
105
+ };
106
+ },
107
+ createContext,
108
+ onError: handleTrpcError,
109
+ });
110
+
111
+ export const _trpcHandler = awsLambdaRequestHandler({
112
+ allowMethodOverride: true,
113
+ router: appRouter,
114
+ createContext,
115
+ onError: handleTrpcError,
116
+ responseMeta: () => {
117
+ return {
118
+ headers: {
119
+ 'Access-Control-Allow-Origin': '*',
120
+ 'Access-Control-Allow-Methods': '*',
121
+ 'Access-Control-Allow-Headers': 'authorization',
122
+ },
123
+ };
124
+ },
125
+ });
126
+
127
+ export const openApiHandler = Sentry.AWSLambda.wrapHandler(
128
+ async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
129
+ if (event.requestContext.http.method === 'OPTIONS') {
130
+ return {
131
+ statusCode: 200,
132
+ headers: {
133
+ 'Access-Control-Allow-Origin': '*',
134
+ 'Access-Control-Allow-Headers': '*',
135
+ 'Access-Control-Allow-Methods': '*',
136
+ },
137
+ };
138
+ }
139
+
140
+ context.callbackWaitsForEmptyEventLoop = false;
141
+
142
+ return _openApiHandler(event, context);
143
+ }
144
+ );
145
+
146
+ export const trpcHandler = Sentry.AWSLambda.wrapHandler(
147
+ async (event: APIGatewayProxyEventV2, context: Context): Promise<APIGatewayProxyResultV2> => {
148
+ if (event.requestContext.http.method === 'OPTIONS') {
149
+ return {
150
+ statusCode: 200,
151
+ headers: {
152
+ 'Access-Control-Allow-Origin': '*',
153
+ 'Access-Control-Allow-Headers': '*',
154
+ 'Access-Control-Allow-Methods': '*',
155
+ },
156
+ };
157
+ }
158
+
159
+ context.callbackWaitsForEmptyEventLoop = false;
160
+
161
+ return _trpcHandler(event, context);
162
+ }
163
+ );
@@ -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('./oidcLambdaApp', () => ({ oidcHandler: mocks.handler }));
9
+
10
+ beforeEach(() => {
11
+ vi.resetModules();
12
+ vi.clearAllMocks();
13
+ mocks.bootstrap.mockReturnValue(mocks.getApp);
14
+ mocks.getApp.mockResolvedValue({ oidcHandler: mocks.handler });
15
+ mocks.handler.mockResolvedValue({ statusCode: 200 });
16
+ });
17
+
18
+ it('bootstraps the oidc app with checked-in stage config and the deploy stage', async () => {
19
+ vi.stubEnv('AWS_LAMBDA_FUNCTION_NAME', 'learn-cloud-service-dev-oidc');
20
+ vi.stubEnv('LAMBDA_STAGE', 'dev');
21
+ await import('./oidcLambda');
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 { oidcHandler } = await import('./oidcLambda');
34
+ const event = {} as APIGatewayProxyEventV2;
35
+ const context = {} as Context;
36
+ const callback = vi.fn();
37
+ await oidcHandler(event, context, callback);
38
+ await oidcHandler(event, context, callback);
39
+ expect(mocks.getApp).toHaveBeenCalledTimes(2);
40
+ expect(mocks.handler).toHaveBeenLastCalledWith(event, context, callback);
41
+ });
package/oidcLambda.ts CHANGED
@@ -1,23 +1,17 @@
1
- import serverlessHttp from 'serverless-http';
2
- import * as Sentry from '@sentry/serverless';
1
+ import { bootstrapLambda } from '@learncard/service-config';
3
2
 
4
- import oidcApp from './src/oidc';
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
- 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
- ],
5
+ type OidcApp = typeof import('./oidcLambdaApp');
6
+
7
+ const getApplication = bootstrapLambda<OidcApp>({
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('./oidcLambdaApp'),
19
14
  });
20
15
 
21
- export const oidcHandler = Sentry.AWSLambda.wrapHandler(
22
- serverlessHttp(toServerlessApplication(oidcApp))
23
- );
16
+ export const oidcHandler = async (...args: Parameters<OidcApp['oidcHandler']>) =>
17
+ (await getApplication()).oidcHandler(...args);
@@ -0,0 +1,23 @@
1
+ import serverlessHttp from 'serverless-http';
2
+ import * as Sentry from '@sentry/serverless';
3
+
4
+ import oidcApp from './src/oidc';
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 oidcHandler = Sentry.AWSLambda.wrapHandler(
22
+ serverlessHttp(toServerlessApplication(oidcApp))
23
+ );
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@learncard/learn-cloud-service",
3
- "version": "2.5.34",
3
+ "version": "2.5.36",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=24.12 <25"
7
7
  },
8
- "packageManager": "bun@1.3.14",
8
+ "packageManager": "bun@1.4.2",
9
9
  "main": "dist/index.js",
10
10
  "types": "dist/index.d.ts",
11
11
  "scripts": {
@@ -61,27 +61,28 @@
61
61
  "serverless-offline": "^12.0.4",
62
62
  "serverless-prune-plugin": "^2.1.0",
63
63
  "shx": "^0.3.4",
64
- "swagger-ui-dist": "^5.18.2",
65
64
  "testcontainers": "^9.1.1"
66
65
  },
67
66
  "dependencies": {
68
67
  "@fastify/cors": "^11.2.0",
69
68
  "@fastify/rate-limit": "^10.2.0",
70
69
  "@fastify/static": "^10.1.1",
71
- "@learncard/core": "9.4.36",
72
- "@learncard/crypto-plugin": "1.1.36",
73
- "@learncard/did-web-plugin": "1.1.36",
74
- "@learncard/didkey-plugin": "1.1.36",
75
- "@learncard/didkit-plugin": "^1.10.1",
76
- "@learncard/didkit-plugin-node": "0.3.1",
77
- "@learncard/encryption-plugin": "1.1.36",
78
- "@learncard/expiration-plugin": "1.2.36",
79
- "@learncard/helpers": "1.6.0",
80
- "@learncard/learn-card-plugin": "1.2.36",
81
- "@learncard/types": "5.21.0",
82
- "@learncard/vc-plugin": "1.6.2",
83
- "@learncard/vc-templates-plugin": "1.1.36",
70
+ "@learncard/core": "9.4.38",
71
+ "@learncard/crypto-plugin": "1.1.38",
72
+ "@learncard/did-web-plugin": "1.1.38",
73
+ "@learncard/didkey-plugin": "1.1.38",
74
+ "@learncard/didkit-plugin": "^1.10.3",
75
+ "@learncard/didkit-plugin-node": "0.3.3",
76
+ "@learncard/encryption-plugin": "1.1.38",
77
+ "@learncard/expiration-plugin": "1.2.38",
78
+ "@learncard/helpers": "1.6.2",
79
+ "@learncard/learn-card-plugin": "1.2.38",
80
+ "@learncard/service-config": "0.1.1",
81
+ "@learncard/types": "5.23.0",
82
+ "@learncard/vc-plugin": "1.6.4",
83
+ "@learncard/vc-templates-plugin": "1.1.38",
84
84
  "@sentry/serverless": "7.61.0",
85
+ "@traversable/zod": "^0.0.57",
85
86
  "@trpc/server": "11.8.0",
86
87
  "@types/lodash": "^4.14.191",
87
88
  "@xapi/xapi": "^3.0.0",
@@ -103,6 +104,7 @@
103
104
  "neo4j-driver": "^4.3.0",
104
105
  "neogma": "^1.11.1",
105
106
  "simple-redis-mutex": "^1.3.1",
107
+ "swagger-ui-dist": "^5.18.2",
106
108
  "trpc-to-openapi": "^3.1.0",
107
109
  "tsc-alias": "^1.8.10",
108
110
  "zod-openapi": "^5.0.1",
@@ -40,9 +40,11 @@ provider:
40
40
  timeout: 20
41
41
  stage: ${opt:stage, "dev"}
42
42
  region: ${opt:region, "us-east-1"}
43
+ # serverless-offline sets AWS_LAMBDA_FUNCTION_NAME; stage config stays opt-in locally.
43
44
  environment:
44
45
  PORT: ${env:PORT, 3000}
45
- LAMBDA_STAGE: ${opt:stage, "dev"}
46
+ LAMBDA_STAGE: ${env:CONFIG_STAGE, ''}
47
+ CONFIG_STAGE: ${env:CONFIG_STAGE, ''}
46
48
  LEARN_CLOUD_SEED: ${env:LEARN_CLOUD_SEED}
47
49
  LEARN_CLOUD_MONGO_URI: ${env:LEARN_CLOUD_MONGO_URI}
48
50
  LEARN_CLOUD_MONGO_DB_NAME: ${env:LEARN_CLOUD_MONGO_DB_NAME}
@@ -0,0 +1,31 @@
1
+ const { functionEnvironment } = require('@learncard/service-config/function-env.cjs');
2
+
3
+ const PROVIDER_SECRETS = [
4
+ 'LEARN_CLOUD_SEED',
5
+ 'LEARN_CLOUD_MONGO_URI',
6
+ 'LEARN_CLOUD_MONGO_DB_NAME',
7
+ 'RSA_PRIVATE_KEY',
8
+ 'RSA_PUBLIC_KEY',
9
+ 'JWT_SIGNING_KEY',
10
+ 'XAPI_ENDPOINT',
11
+ 'XAPI_USERNAME',
12
+ 'XAPI_PASSWORD',
13
+ 'SENTRY_DSN',
14
+ ];
15
+
16
+ // Serverless v3 supplies CLI options to file functions and preserves CF intrinsics.
17
+ // Every function shares the one generated execution role, so the bundle pointer and the
18
+ // credential fallbacks sit together on the provider. Bundle mode emits only infra plus the
19
+ // pointer; fallback mode retains the per-value GitHub secrets. XAPI_ENDPOINT is a
20
+ // user-classified bundle secret (fallback env only), not infra, so it rides the fallback
21
+ // list and is dropped once a bundle is configured. Non-secret per-stage values are applied
22
+ // from checked-in stage config at cold start, never forwarded here.
23
+ exports.provider = ({ options = {}, env = process.env } = {}) => ({
24
+ LAMBDA_STAGE: options.stage || 'dev',
25
+ // Non-secret product selector for the shared stage files (config.<tenant>.<stage>.json).
26
+ CONFIG_TENANT: env.CONFIG_TENANT || 'learncard',
27
+ PORT: String(options.httpPort || '3000'),
28
+ REDIS_HOST: { 'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Address'] },
29
+ REDIS_PORT: { 'Fn::GetAtt': ['ElasticCacheCluster', 'RedisEndpoint.Port'] },
30
+ ...functionEnvironment({ always: ['RUNTIME_SECRETS_ID'], fallback: PROVIDER_SECRETS, env }),
31
+ });
package/serverless.yml CHANGED
@@ -118,28 +118,19 @@ provider:
118
118
  timeout: 29
119
119
  stage: ${opt:stage, "dev"}
120
120
  region: ${opt:region, "us-east-1"}
121
- environment:
122
- PORT: ${env:PORT, 3000}
123
- LAMBDA_STAGE: ${opt:stage, "dev"}
124
- LEARN_CLOUD_SEED: ${env:LEARN_CLOUD_SEED}
125
- LEARN_CLOUD_MONGO_URI: ${env:LEARN_CLOUD_MONGO_URI}
126
- LEARN_CLOUD_MONGO_DB_NAME: ${env:LEARN_CLOUD_MONGO_DB_NAME}
127
- XAPI_ENDPOINT: ${env:XAPI_ENDPOINT, "false"}
128
- XAPI_USERNAME: ${env:XAPI_USERNAME, "false"}
129
- XAPI_PASSWORD: ${env:XAPI_PASSWORD, "false"}
130
- RSA_PRIVATE_KEY: ${env:RSA_PRIVATE_KEY, "false"}
131
- RSA_PUBLIC_KEY: ${env:RSA_PUBLIC_KEY, "false"}
132
- SERVER_URL: ${env:SERVER_URL, "https://cloud.learncard.com"}
133
- JWT_SIGNING_KEY: ${env:JWT_SIGNING_KEY, ""}
134
- SENTRY_DSN: ${env:SENTRY_DSN}
135
- SENTRY_ENV: ${env:SENTRY_ENV}
136
- SENTRY_AUTH_TOKEN: ${env:SENTRY_AUTH_TOKEN}
137
- SENTRY_ORG: ${env:SENTRY_ORG}
138
- SENTRY_PROJECT: ${env:SENTRY_PROJECT}
139
- REDIS_HOST:
140
- 'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Address]
141
- REDIS_PORT:
142
- 'Fn::GetAtt': [ElasticCacheCluster, RedisEndpoint.Port]
121
+ # Infra values and the CF intrinsics live in the helper alongside the bundle pointer.
122
+ # Bundle mode emits only infra + RUNTIME_SECRETS_ID; fallback mode retains the GitHub
123
+ # secrets. Non-secret per-stage values (SENTRY_ENV, SERVER_URL, SHARE_CONTENT_*) are
124
+ # applied from checked-in stage config at cold start, not passed here.
125
+ environment: ${file(./serverless.function-env.cjs):provider}
126
+ # Only this generated role runs every function; grant it read of the runtime bundle.
127
+ iam:
128
+ role:
129
+ statements:
130
+ - Effect: Allow
131
+ Action: secretsmanager:GetSecretValue
132
+ Resource:
133
+ Fn::Sub: arn:${AWS::Partition}:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:learn-cloud-service/${sls:stage}/runtime-secrets-*
143
134
  tracing:
144
135
  apiGateway: true
145
136
  lambda: true
@@ -170,6 +161,24 @@ functions:
170
161
  events:
171
162
  - httpApi: ANY /api/{trpc+}
172
163
 
164
+ # LC-2187 service-only share-content transport. The handler returns 404
165
+ # for every path until configured with a
166
+ # complete configuration. Events are the exact reviewed operations; there is
167
+ # no wildcard/wildcard enumeration surface.
168
+ shareContent:
169
+ handler: lambda.shareContentHandler
170
+ vpc:
171
+ securityGroupIds:
172
+ - 'Fn::GetAtt': [ServerlessSecurityGroup, GroupId]
173
+ subnetIds:
174
+ - Ref: PrivateSubnetA
175
+ events:
176
+ - httpApi: POST /internal/share-content/put
177
+ - httpApi: POST /internal/share-content/get
178
+ - httpApi: POST /internal/share-content/stat
179
+ - httpApi: POST /internal/share-content/delete
180
+ - httpApi: POST /internal/share-content/readRecovery
181
+
173
182
  didWeb:
174
183
  handler: didWebLambda.didWebHandler
175
184
  vpc: