@learncard/lca-api-service 1.5.3 → 1.6.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.
Files changed (45) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +26 -0
  3. package/bundle.mjs +1 -1
  4. package/dist/app.d.ts +4 -1
  5. package/dist/config/cacheEnvironment.d.ts +4 -4
  6. package/dist/config/environment.d.ts +145 -114
  7. package/dist/config/oidcEnvironment.d.ts +22 -22
  8. package/dist/didkit_wasm_bg.wasm +0 -0
  9. package/dist/helpers/bedrockGeneration.d.ts +17 -0
  10. package/dist/helpers/bedrockGeneration.test.d.ts +1 -0
  11. package/dist/helpers/bedrockSchema.d.ts +10 -0
  12. package/dist/helpers/imageGeneration.d.ts +42 -0
  13. package/dist/helpers/imageGeneration.test.d.ts +1 -0
  14. package/dist/index.js +2 -2
  15. package/dist/models/Notification.d.ts +3 -1
  16. package/dist/publish-metafile.json +1 -1
  17. package/dist/routes/ai.skill-icons.test.d.ts +1 -0
  18. package/dist/routes/keys.d.ts +1 -0
  19. package/dist/routes/notifications.d.ts +3 -1
  20. package/dist/types/notifications.d.ts +3 -1
  21. package/docs/bedrock-generation.md +119 -0
  22. package/docs/image-generation.md +120 -0
  23. package/package.json +20 -15
  24. package/scripts/node24-runtime.cjs +9 -0
  25. package/scripts/verify-node24-layer.cjs +49 -0
  26. package/serverless-local.yml +3 -1
  27. package/serverless.function-env.cjs +2 -4
  28. package/serverless.yml +76 -3
  29. package/src/config/environment.ts +14 -0
  30. package/src/helpers/bedrockGeneration.test.ts +222 -0
  31. package/src/helpers/bedrockGeneration.ts +110 -0
  32. package/src/helpers/bedrockSchema.ts +352 -0
  33. package/src/helpers/imageGeneration.test.ts +456 -0
  34. package/src/helpers/imageGeneration.ts +432 -0
  35. package/src/routes/ai.skill-icons.test.ts +92 -0
  36. package/src/routes/ai.test.ts +68 -91
  37. package/src/routes/ai.ts +31 -143
  38. package/src/routes/keys.ts +18 -0
  39. package/src/services/escrow-enclave/enclaveTrustAnchors.ts +19 -0
  40. package/src/services/escrow-enclave/logFailure.test.ts +8 -0
  41. package/src/services/escrow-enclave/logFailure.ts +17 -0
  42. package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +10 -0
  43. package/test/keys-auth-share-escrow.spec.ts +103 -0
  44. package/test/oidc.spec.ts +41 -2
  45. package/tsconfig.bedrock.json +10 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -153,6 +153,7 @@ export declare const keysRouter: import("@trpc/server").TRPCBuiltRouter<{
153
153
  attemptsRemaining: number;
154
154
  salt?: string | undefined;
155
155
  };
156
+ priorEscrowEnrollment: boolean;
156
157
  sssActivationState: "active" | "provisional";
157
158
  escrowStale?: "mode-mismatch" | "key-rotated" | undefined;
158
159
  } | null;
@@ -99,6 +99,7 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
99
99
  email?: string | undefined;
100
100
  image?: string | undefined;
101
101
  heroImage?: string | undefined;
102
+ linkedInOrganizationId?: string | undefined;
102
103
  websiteLink?: string | undefined;
103
104
  isServiceProfile?: boolean | undefined;
104
105
  type?: string | undefined;
@@ -138,6 +139,7 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
138
139
  email?: string | undefined;
139
140
  image?: string | undefined;
140
141
  heroImage?: string | undefined;
142
+ linkedInOrganizationId?: string | undefined;
141
143
  websiteLink?: string | undefined;
142
144
  isServiceProfile?: boolean | undefined;
143
145
  type?: string | undefined;
@@ -174,7 +176,7 @@ export declare const notificationsRouter: import("@trpc/server").TRPCBuiltRouter
174
176
  vpUris?: string[] | undefined;
175
177
  transaction?: {
176
178
  id: string;
177
- action: "write" | "consent" | "update" | "sync" | "withdraw";
179
+ action: "write" | "update" | "consent" | "sync" | "withdraw";
178
180
  date: string;
179
181
  expiresAt?: string | undefined;
180
182
  oneTime?: boolean | undefined;
@@ -126,6 +126,7 @@ export declare const PaginatedNotificationsValidator: z.ZodObject<{
126
126
  email: z.ZodOptional<z.ZodOptional<z.ZodString>>;
127
127
  image: z.ZodOptional<z.ZodOptional<z.ZodString>>;
128
128
  heroImage: z.ZodOptional<z.ZodOptional<z.ZodString>>;
129
+ linkedInOrganizationId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
129
130
  websiteLink: z.ZodOptional<z.ZodOptional<z.ZodString>>;
130
131
  isServiceProfile: z.ZodOptional<z.ZodOptional<z.ZodDefault<z.ZodBoolean>>>;
131
132
  type: z.ZodOptional<z.ZodOptional<z.ZodString>>;
@@ -172,6 +173,7 @@ export declare const PaginatedNotificationsValidator: z.ZodObject<{
172
173
  email: z.ZodOptional<z.ZodOptional<z.ZodString>>;
173
174
  image: z.ZodOptional<z.ZodOptional<z.ZodString>>;
174
175
  heroImage: z.ZodOptional<z.ZodOptional<z.ZodString>>;
176
+ linkedInOrganizationId: z.ZodOptional<z.ZodOptional<z.ZodString>>;
175
177
  websiteLink: z.ZodOptional<z.ZodOptional<z.ZodString>>;
176
178
  isServiceProfile: z.ZodOptional<z.ZodOptional<z.ZodDefault<z.ZodBoolean>>>;
177
179
  type: z.ZodOptional<z.ZodOptional<z.ZodString>>;
@@ -244,9 +246,9 @@ export declare const PaginatedNotificationsValidator: z.ZodObject<{
244
246
  }, z.core.$strip>>;
245
247
  id: z.ZodString;
246
248
  action: z.ZodEnum<{
249
+ update: "update";
247
250
  write: "write";
248
251
  consent: "consent";
249
- update: "update";
250
252
  sync: "sync";
251
253
  withdraw: "withdraw";
252
254
  }>;
@@ -0,0 +1,119 @@
1
+ # Bedrock text generation (LC-2271)
2
+
3
+ The three LCA text generation routes use the OpenAI SDK Responses API at
4
+ `https://bedrock-runtime.us-east-1.amazonaws.com/openai/v1`. This follows the merged
5
+ [AI Passport implementation](https://github.com/WeLibraryOS/AI-Passport/pull/92),
6
+ inspected at main `300c0af3125a4f1b1785820a2d33866723a88f30`.
7
+
8
+ | Route | Exact Runtime model profile | Effort | Reason |
9
+ | --------------------- | --------------------------- | -------- | -------------------------------------------------------------------------------------- |
10
+ | `generateBoostInfo` | `us.openai.gpt-6.1-sol` | `medium` | Interpret arbitrary descriptions and generate coherent credential copy across locales. |
11
+ | `generateBoostSkills` | `us.openai.gpt-6.1-sol` | `medium` | Infer appropriate skills and subskills within the existing hierarchy. |
12
+ | `generateSkillIcons` | `us.openai.gpt-6-luna` | `none` | Straightforward semantic mapping of supplied names to emoji. |
13
+
14
+ AWS's current [Sol model card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-6-1-sol.html),
15
+ [Luna model card](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-openai-gpt-6-luna.html)
16
+ and [Responses compatibility guide](https://docs.aws.amazon.com/bedrock/latest/userguide/inference-responses-api.html)
17
+ confirm these Runtime profiles and API support. The 8,192-token request ceiling is below
18
+ both documented model output limits. Although the current cards advertise native JSON
19
+ Schema output, this migration retains AI Passport's required-function pattern and
20
+ local validation to preserve existing optional-category semantics.
21
+
22
+ `generateImage` defaults to Bedrock Stable Image Core (`stability.stable-image-core-v1:1`)
23
+ in us-west-2, signed with SigV4 from the execution role, and still stores the result
24
+ through Filestack. DALL-E 3 (`OPENAI_API_KEY`) is only an explicit `IMAGE_PROVIDER=openai`
25
+ fallback; see `image-generation.md`. Embeddings and Nova are outside this migration.
26
+
27
+ ## Runtime contract
28
+
29
+ - The official `@aws/bedrock-token-generator` resolves short-term bearer tokens from
30
+ the normal AWS SDK credential chain in `us-east-1`. On Lambda this means the execution
31
+ role and its rotating credentials. The SDK requests a token for every operation;
32
+ no process-lifetime static bearer is stored by LCA. AI Passport's Fly web-identity
33
+ override is specific to Fly and is not needed for this Lambda service.
34
+ - `BEDROCK_BASE_URL` is optional; only the exact default URL above, with an optional
35
+ trailing slash, is accepted. No arbitrary endpoint, model alias or family override.
36
+ The option is scoped to API functions and can also come from `RUNTIME_SECRETS_ID`.
37
+ - OpenAI SDK version is pinned to `6.47.0`, matching the inspected implementation.
38
+ No ambient OpenAI organization/project headers; SDK logging is off; redirects fail.
39
+ - These routes remain non-streaming. Requests use `store:false`, required function
40
+ selection, `parallel_tool_calls:false`, and a non-strict projected JSON schema.
41
+ The original Zod validators enforce enums, optional categories, unions and string
42
+ bounds after JSON parsing. Native strict structured-output support is not assumed.
43
+ - Requests have an 8,192 output-token ceiling (including reasoning) and a 25-second
44
+ SDK timeout (below the existing 29-second Lambda timeout). Failed/incomplete lifecycle, refusal, missing/wrong/duplicate tool,
45
+ malformed JSON and invalid output fail closed. Token-limit exhaustion is an error,
46
+ never a partial success. Input and output sizing still require staging acceptance.
47
+ - No automatic retry, provider fallback, or model downgrade. HTTP 429 maps to
48
+ `TOO_MANY_REQUESTS`, 503/504 to `SERVICE_UNAVAILABLE`, and other failures to a
49
+ sanitized `INTERNAL_SERVER_ERROR`. Provider bodies, validation values and credential
50
+ causes are not retained in those errors. Quota failures are not automatically retried.
51
+ - Icons still fill omitted requested names with `⭐` after a valid response and discard
52
+ unsolicited names. Provider failures now propagate instead of being logged raw and
53
+ subsequently crashing on an undefined completion. Learner DIDs are no longer sent
54
+ in text-generation provider metadata. Existing authenticated route access remains.
55
+
56
+ ## Deployment and coordination gates
57
+
58
+ Before rollout, obtain approved staging tests for all three real schemas, locales,
59
+ typical and maximum input sizes, token ceilings, timeout behavior and error handling.
60
+ Offline tests do not establish provider acceptance, entitlement, latency or generation
61
+ quality. Live model calls require separate approval.
62
+
63
+ Responses does not support attaching native Bedrock Guardrails; if the approved data-handling
64
+ policy requires filtering, a separately approved pre-invocation path is a rollout gate.
65
+
66
+ The deployed Lambda role must have approved Bedrock/default-project access for both
67
+ exact cross-region US profiles and access to the Runtime endpoint. Confirm rotating
68
+ credential/token operation and the applicable retention, DPA, subprocessor, guardrail
69
+ and invocation-logging posture before learner traffic. The reviewed dev policy is managed by
70
+ Serverless/CloudFormation and is created with the stack on a `dev`/`us-east-1` deploy, so no
71
+ manual IAM edits are needed. This PR does not port AI Passport's
72
+ application-specific privacy gateway into LCA. US profiles can route across US regions;
73
+ the endpoint is not a single-region processing guarantee.
74
+
75
+ [LearnCard PR1670](https://github.com/learningeconomy/LearnCard/pull/1670) merged on
76
+ 2026-10-09 at `7656a57d30c6d80b8a25a8bb14bcd3b08b3648c2`. This branch is synced with
77
+ main and preserves its `@learncard/service-config` bootstrap, `CONFIG_TENANT`,
78
+ tenant/stage selection and runtime-bundle precedence. The optional endpoint is
79
+ forwarded by the current API function-environment helper in both bundle and fallback
80
+ modes; OIDC and provider environments do not receive it. There is no new bearer-token
81
+ secret to provision. Configuration tests use the current deployment contract rather
82
+ than restoring the superseded function-environment test.
83
+
84
+ Rollback uses the prior application revision with its prior OpenAI credential, after
85
+ explicit deployment approval. There is no runtime silent fallback to the old models.
86
+
87
+ ## Node 24 and owner-triggered staging deployment
88
+
89
+ All LCA Lambda functions and the native DIDKit layer declare `nodejs24.x`; esbuild
90
+ targets Node 24. The native addon uses stable N-API v4, supported by Node 24.
91
+ Both Node 20 and Node 24 Lambda use AL2023. Before deployment, the LCA-only
92
+ `verify-node24-layer.cjs` preflight requires the Linux x64 GNU artifact, both
93
+ packaged runtime dependencies, libc symbols no newer than glibc 2.34, and an actual
94
+ Node 24 native key-to-DID smoke check. Missing/incompatible artifacts stop the action
95
+ before Serverless deployment; WASM fallback is not evidence of native compatibility.
96
+
97
+ LCA pins serverless-esbuild 1.57.2 for Node 24 target support. The service-local
98
+ `node24-runtime.cjs` plugin adds Node 24 to Serverless 3.40's older runtime schema
99
+ without disabling validation or changing other services. Deployment tools run on
100
+ Node 24.12.0 in the LCA action. Local offline configuration uses Node 24 too.
101
+
102
+ The `LcaDevBedrockGenerationPolicy` CloudFormation resource attaches the scoped
103
+ policy to `SigningAuthorityExecutionRole` only for stage `dev` in `us-east-1`.
104
+ It preserves existing grants and also covers seedMigration, which shares that role.
105
+ Production receives no new Bedrock grant from this resource. ARNs use the deployment
106
+ account and partition; source profiles/default project stay in us-east-1, and the
107
+ verified backing models are scoped to us-east-1/us-east-2/us-west-2. Short-term bearer
108
+ use, matching model/profile conditions and default-project authorization are explicit.
109
+ The same policy also carries `StableImageCoreUsWest2` (InvokeModel on the us-west-2
110
+ Stable Image Core model only) for image generation; see `image-generation.md`.
111
+
112
+ Owner workflow: select this branch in Deploy, target environment `staging`, and
113
+ enable only `deploy-lca-api`. The LearnCard staging leg selects GitHub environment
114
+ `learn-card-app-api-staging`; SERVERLESS_STAGE and SERVERLESS_REGION come from its
115
+ variables and must be `dev`/`us-east-1` for this policy. The action downloads/builds
116
+ the native artifact, runs the preflight, and Serverless owns role-policy and runtime
117
+ updates. Do not trigger it until the owner approves deployment. After the stack
118
+ finishes, repeat role simulations and runtime checks before separately approved
119
+ synthetic model calls. CI and native preflight do not prove live model acceptance.
@@ -0,0 +1,120 @@
1
+ # Configurable image generation
2
+
3
+ `ai.generateImage({ prompt })` still requires a nonempty prompt and returns `{ url }`.
4
+ The LCA SDK still returns the URL string. Text generation is unchanged.
5
+ Amazon Bedrock Stable Image Core (`stability.stable-image-core-v1:1`, us-west-2) is
6
+ the default provider, so with LC-2271 every LCA model call goes through Bedrock.
7
+ Output intentionally differs from DALL-E (different model and style); the contract
8
+ is unchanged: one square PNG, uploaded through Filestack, returned as `{ url }`.
9
+ DALL-E 3 (one 1024 × 1024 image, URL stored through Filestack) remains available
10
+ only as an explicit fallback with `IMAGE_PROVIDER=openai`.
11
+
12
+ ## Configuration
13
+
14
+ Use the existing service-config layering: schema defaults → base/stage JSON →
15
+ runtime secrets bundle → explicit nonempty environment. No new secrets loader is added.
16
+
17
+ | Setting | Default | Allowed values |
18
+ | ---------------------- | -------------------------------- | -------------------------------- |
19
+ | IMAGE_PROVIDER | bedrock | bedrock, openai |
20
+ | BEDROCK_IMAGE_REGION | us-west-2 | us-west-2 |
21
+ | BEDROCK_IMAGE_MODEL_ID | stability.stable-image-core-v1:1 | stability.stable-image-core-v1:1 |
22
+ | IMAGE_TIMEOUT_MS | 25000 | integer 1000–25000 |
23
+
24
+ Only the verified candidate and region are accepted for now. Additional models
25
+ require their own request/response validation. Do not choose Nova Canvas (retired
26
+ September 30, 2026). Lambda deployers can set these nonsecret values in existing stage JSON (rebuild
27
+ and deploy) or explicit Lambda environment; Docker/local may use environment.
28
+ The deadline is capped at 25 seconds, matching text generation, so the request
29
+ finishes inside the 29-second Lambda timeout (and the API Gateway HTTP API's 30-second
30
+ integration limit) rather than being killed mid-upload. Recycle functions after
31
+ configuration changes.
32
+
33
+ `OPENAI_API_KEY` is now only needed for the DALL-E fallback (`IMAGE_PROVIDER=openai`).
34
+ Text routes use Bedrock since LC-2271 (#1684) and the default image provider uses
35
+ Bedrock, so with the defaults no LCA model call needs an OpenAI key. To fall back
36
+ (e.g. rollback), set `IMAGE_PROVIDER=openai` in the stage JSON or runtime-secrets
37
+ bundle and make sure `OPENAI_API_KEY` is available, then recycle the functions.
38
+ There is no automatic fallback between providers.
39
+
40
+ ## Access and approval prerequisites
41
+
42
+ Because Bedrock is the default, every stage that deploys this code needs the
43
+ following before `generateImage` will work (otherwise it returns the sanitized
44
+ INTERNAL_SERVER_ERROR). No live calls, Marketplace subscriptions/terms acceptance,
45
+ deployment, or merge were performed for this PR. An authorized operator must
46
+ verify model availability and entitlement in **us-west-2** and approve any required
47
+ Marketplace subscription/terms. Runtime access is model-scoped:
48
+
49
+ - Action: `bedrock:InvokeModel` (no streaming action, no wildcard)
50
+ - Resource: `arn:aws:bedrock:us-west-2::foundation-model/stability.stable-image-core-v1:1`
51
+
52
+ Use the workload's normal AWS credential chain and existing execution role. The
53
+ client pins SigV4 (`authSchemePreference: ['sigv4']`), so the rotating execution-role
54
+ credentials are used and a long-term `AWS_BEARER_TOKEN_BEDROCK` API key is never
55
+ picked up, consistent with the text adapter's short-term-bearer-only policy.
56
+ `generateImage` runs in the `trpc`/`api` functions, which use
57
+ `SigningAuthorityExecutionRole`.
58
+
59
+ **Dev/staging (stage `dev`, us-east-1): managed in `serverless.yml`.** The grant is
60
+ the `StableImageCoreUsWest2` statement in #1684's `LcaDevBedrockGenerationPolicy`,
61
+ under the same `LcaDevBedrockGeneration` condition (stage `dev` in us-east-1), so
62
+ Serverless/CloudFormation applies it with the stack and removes it on rollback. Do
63
+ not add it by hand in the console. `.github/tests/test-lca-node24.test.cjs` pins the
64
+ exact statement and rejects streaming actions and wildcard resources.
65
+
66
+ **Production still needs a grant.** The condition excludes every other stage,
67
+ so production (and ScoutPass stages) get no Bedrock access from this policy. Before
68
+ deploying this default to production, add a reviewed production grant for the same
69
+ single action/resource (alongside the production text-generation grant), or set
70
+ `IMAGE_PROVIDER=openai` for that stage as a stopgap.
71
+
72
+ Regional network connectivity (NAT egress from the VPC Lambdas to
73
+ `bedrock-runtime.us-west-2.amazonaws.com`) and existing Filestack storage access
74
+ must also be verified by the operator.
75
+
76
+ ## Behavior and evaluation
77
+
78
+ The Bedrock adapter sends one InvokeModel request with `aspect_ratio: "1:1"`
79
+ and `output_format: "png"`. It accepts exactly one successful image, validates
80
+ base64, PNG header and square dimensions (640–1536), bounds decoded bytes to
81
+ 10 MiB, response bytes to 14 MiB and PNG decompression to 20 MiB, then uploads bytes through existing Filestack.
82
+ Filtering (a `Filter reason:` finish reason, or a content-filter ValidationException) returns a
83
+ sanitized BAD_REQUEST. Provider failures are classified by AWS error name as well as HTTP status:
84
+ throttling and quota rejections (`ThrottlingException`, `ModelNotReadyException`, HTTP 429, and
85
+ `ServiceQuotaExceededException`, which Bedrock returns as HTTP **400**) map to TOO_MANY_REQUESTS;
86
+ `ServiceUnavailableException`/`InternalServerException`, HTTP 500/503, model timeouts (408/504)
87
+ and our own deadline map to SERVICE_UNAVAILABLE. Access, validation, payload and storage failures
88
+ return a generic INTERNAL_SERVER_ERROR. Client messages never include provider text.
89
+
90
+ Throttling/quota and unavailable rejections (not billed) get at most two extra attempts with full
91
+ jitter (≤ 500 ms, then ≤ 1 s), and only while at least 12 s of the deadline remains. Timeouts,
92
+ access, validation and filter errors are never retried, and DALL-E is never retried.
93
+
94
+ Every failure logs one `image_generation_failure` record (`console.error`, so CloudWatch) with
95
+ allowlisted fields only: provider, modelId, region, stage (invoke/decode/storage/timeout/config),
96
+ attempts, category, errorName, errorCode, httpStatus, awsRequestId, sdkRetryable, retryable,
97
+ durationMs, promptLength, promptHash (first 12 hex of SHA-256), finishReason, decodeReason and
98
+ the clientCode returned. Retries log `image_generation_retry`; success logs
99
+ `image_generation_success` with attempts and duration. Prompts, provider messages, headers,
100
+ credentials, URLs and image bytes are never logged or returned. A deadline bounds the request and
101
+ aborts model work; storage already in flight may finish after the deadline. No automatic provider
102
+ fallback is present. A manual retry after timeout may still incur another charge.
103
+
104
+ Output differing from DALL-E is accepted. Before learner traffic, still smoke-test
105
+ a small fixed prompt set on staging for badge/icon readability, filter behavior,
106
+ latency, failure rate and actual storage/display behavior. Verify current
107
+ [Bedrock pricing](https://aws.amazon.com/bedrock/pricing/) in the target region
108
+ and model before any calls; record invocation count and total cost, including
109
+ failed or timed-out requests.
110
+
111
+ Request/response fields were checked against the
112
+ [AWS Stable Image Core documentation](https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-diffusion-stable-image-core-text-image-request-response.html).
113
+
114
+ ## Integration with text LLM PR #1684
115
+
116
+ This branch now contains #1684 (merged in, base `feat/lca-bedrock-lc2205`): Node 24
117
+ runtime, OpenAI SDK 6.47.0 and the Bedrock text adapter. Text routes use
118
+ `generateStructuredOutput`; the image route uses `generateStoredImage`. Neither
119
+ provider selection controls the other: `BEDROCK_BASE_URL` configures text only and
120
+ `IMAGE_PROVIDER`/`BEDROCK_IMAGE_*` configure images only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/lca-api-service",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "",
5
5
  "engines": {
6
6
  "node": ">=24.12 <25"
@@ -28,10 +28,11 @@
28
28
  "start-p-5000": "PORT=5000 serverless offline --httpPort 5000 --websocketPort 5001 --lambdaPort 5002 --albPort 5003 --config serverless-local.yml",
29
29
  "dev": "bun --conditions=development --watch src/docker-entry.ts",
30
30
  "start:docker": "bun --conditions=development src/docker-entry.ts",
31
- "serverless-deploy": "node scripts/check-seed-encryption-infrastructure.mjs && node ../../../scripts/prepare-didkit-native-layer.cjs && serverless deploy",
31
+ "serverless-deploy": "node scripts/check-seed-encryption-infrastructure.mjs && node ../../../scripts/prepare-didkit-native-layer.cjs && node scripts/verify-node24-layer.cjs && serverless deploy",
32
32
  "serverless-deploy-scouts": "serverless deploy --config serverless-scouts.yml",
33
33
  "test:oidc:e2e": "node test/oidc-e2e/run.mjs",
34
- "typecheck:oidc:e2e": "tsc --noEmit -p test/oidc-e2e/tsconfig.json"
34
+ "typecheck:oidc:e2e": "tsc --noEmit -p test/oidc-e2e/tsconfig.json",
35
+ "typecheck:bedrock": "tsc --noEmit -p tsconfig.bedrock.json"
35
36
  },
36
37
  "keywords": [],
37
38
  "author": "WeLibrary, LLC (www.welibrary.io)",
@@ -49,29 +50,31 @@
49
50
  "serverless": "3.40.0",
50
51
  "serverless-certificate-creator": "^1.6.0",
51
52
  "serverless-domain-manager": "^6.4.4",
52
- "serverless-esbuild": "1.55.0",
53
+ "serverless-esbuild": "1.57.2",
53
54
  "serverless-http": "3.2.0",
54
55
  "serverless-offline": "^14.4.0",
55
56
  "serverless-prune-plugin": "^2.1.0",
56
57
  "shx": "^0.3.4",
57
- "@playwright/test": "^1.42.0"
58
+ "@playwright/test": "^1.42.0",
59
+ "@types/pngjs": "^6.0.5"
58
60
  },
59
61
  "dependencies": {
62
+ "@aws-sdk/client-bedrock-runtime": "^3.1139.0",
60
63
  "@aws-sdk/client-kms": "^3.1139.0",
61
64
  "@aws-sdk/client-secrets-manager": "^3.1139.0",
62
65
  "@fastify/cors": "^11.2.0",
63
66
  "@fastify/formbody": "^8.0.2",
64
67
  "@fastify/rate-limit": "^10.2.0",
65
68
  "@fastify/static": "^10.1.1",
66
- "@learncard/core": "9.4.40",
67
- "@learncard/did-web-plugin": "1.1.40",
68
- "@learncard/didkit-plugin": "1.10.5",
69
- "@learncard/didkit-plugin-node": "0.3.5",
69
+ "@learncard/core": "9.4.41",
70
+ "@learncard/did-web-plugin": "1.1.41",
71
+ "@learncard/didkit-plugin": "1.10.6",
72
+ "@learncard/didkit-plugin-node": "0.3.6",
70
73
  "@learncard/email-templates": "1.2.1",
71
- "@learncard/helpers": "1.7.0",
72
- "@learncard/init": "2.5.5",
73
- "@learncard/types": "5.23.2",
74
- "@learncard/sss-key-manager": "0.2.3",
74
+ "@learncard/helpers": "1.7.1",
75
+ "@learncard/init": "2.5.6",
76
+ "@learncard/types": "5.24.0",
77
+ "@learncard/sss-key-manager": "0.2.4",
75
78
  "@sentry/serverless": "^7.61.0",
76
79
  "@trpc/server": "11.8.0",
77
80
  "@types/jsonwebtoken": "^9.0.8",
@@ -95,11 +98,13 @@
95
98
  "mongodb": "^6.2.0",
96
99
  "multiformats": "^11.0.1",
97
100
  "node-forge": "^1.4.0",
98
- "openai": "^4.55.7",
101
+ "openai": "6.47.0",
99
102
  "postmark": "^4.0.5",
100
103
  "trpc-to-openapi": "^3.1.0",
101
104
  "uuid": "^14.0.0",
102
- "zod": "4.4.3"
105
+ "zod": "4.6.6",
106
+ "pngjs": "^7.0.0",
107
+ "@aws/bedrock-token-generator": "1.1.0"
103
108
  },
104
109
  "repository": {
105
110
  "type": "git",
@@ -0,0 +1,9 @@
1
+ /* global module */
2
+ // Serverless 3.40 predates Node 24. Extend only LCA's runtime enum, keeping
3
+ // all other configuration validation enabled. AWS supports nodejs24.x.
4
+ module.exports = class Node24Runtime {
5
+ constructor(serverless) {
6
+ const runtimes = serverless.configSchemaHandler.schema.definitions.awsLambdaRuntime.enum;
7
+ if (!runtimes.includes('nodejs24.x')) runtimes.push('nodejs24.x');
8
+ }
9
+ };
@@ -0,0 +1,49 @@
1
+ /* global require, module, process, Buffer, console */
2
+ /* eslint-disable @typescript-eslint/no-require-imports -- Node CommonJS deployment script. */
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { execFileSync } = require('node:child_process');
7
+
8
+ const verifyLayer = (layerRoot, nodeMajor = Number(process.versions.node.split('.')[0])) => {
9
+ assert.equal(nodeMajor, 24, 'LCA deployment must run with Node 24');
10
+ assert.equal(process.platform, 'linux', 'LCA native layer requires Linux');
11
+ assert.equal(process.arch, 'x64', 'LCA native layer requires x86_64');
12
+ const modules = path.join(layerRoot, 'nodejs/node_modules/@learncard');
13
+ const native = path.join(modules, 'didkit-plugin-node');
14
+ const binary = path.join(native, 'index.linux-x64-gnu.node');
15
+ assert.ok(fs.existsSync(binary), 'Missing Lambda Linux x64 GNU DIDKit addon');
16
+ const binaryNames = fs.readdirSync(native).filter(name => name.endsWith('.node'));
17
+ assert.deepEqual(
18
+ binaryNames,
19
+ ['index.linux-x64-gnu.node'],
20
+ 'Layer must contain only the Lambda addon'
21
+ );
22
+ for (const pkg of ['didkit-plugin-node', 'didkit-plugin']) {
23
+ const directory = path.join(modules, pkg);
24
+ const manifest = JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8'));
25
+ assert.ok(
26
+ fs.existsSync(path.resolve(directory, manifest.main)),
27
+ `Missing ${pkg} runtime entry`
28
+ );
29
+ }
30
+ // Node 24 Lambda uses AL2023/glibc 2.34. Reject Ubuntu-built artifacts
31
+ // requiring newer libc symbols before Serverless can change the stack.
32
+ const versions = execFileSync('readelf', ['--version-info', binary], { encoding: 'utf8' });
33
+ for (const match of versions.matchAll(/GLIBC_(\d+)\.(\d+)/g)) {
34
+ assert.ok(
35
+ Number(match[1]) < 2 || (Number(match[1]) === 2 && Number(match[2]) <= 34),
36
+ `Addon requires ${match[0]}, newer than Lambda AL2023`
37
+ );
38
+ }
39
+ // Exercise stable N-API with a deterministic synthetic key; never log key material.
40
+ const addon = require(binary);
41
+ const key = addon.generateEd25519KeyFromBytes(Buffer.alloc(32, 1));
42
+ assert.match(addon.keyToDid('key', key), /^did:key:/);
43
+ };
44
+
45
+ module.exports = { verifyLayer };
46
+ if (require.main === module) {
47
+ verifyLayer(path.resolve('didkit-native-layer'));
48
+ console.log('LCA Node 24 native layer ABI, libc and package checks passed');
49
+ }
@@ -7,6 +7,7 @@ frameworkVersion: '3'
7
7
  useDotenv: true
8
8
 
9
9
  plugins:
10
+ - ./scripts/node24-runtime.cjs
10
11
  - serverless-esbuild
11
12
  - serverless-offline
12
13
 
@@ -19,6 +20,7 @@ custom:
19
20
  packager: pnpm
20
21
  plugins: esbuildPlugins.cjs
21
22
  external: ['snappy', '@learncard/didkit-plugin/dist/didkit_wasm_bg.wasm']
23
+ target: node24
22
24
  sourcemap: external
23
25
  watch:
24
26
  pattern: ['src/**/*.ts', 'dids.ts']
@@ -30,7 +32,7 @@ package:
30
32
 
31
33
  provider:
32
34
  name: aws
33
- runtime: nodejs20.x
35
+ runtime: nodejs24.x
34
36
  memorySize: 2048
35
37
  timeout: 29
36
38
  stage: ${opt:stage, "dev"}
@@ -24,10 +24,8 @@ const API_SECRETS = [
24
24
  'KEYCLOAK_JWKS_URL_OVERRIDES',
25
25
  ];
26
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'];
27
+ // Non-secret operator toggles are forwarded in both bundle and fallback modes.
28
+ const API_TOGGLES = ['ESCROW_ENCLAVE_MODE', 'ESCROW_RELEASE_KILL_SWITCH', 'BEDROCK_BASE_URL'];
31
29
 
32
30
  // Serverless v3 supplies CLI options to file functions and preserves CF intrinsics.
33
31
  // No stage-file values are forwarded from the deployment environment.
package/serverless.yml CHANGED
@@ -7,6 +7,7 @@ frameworkVersion: '3'
7
7
  useDotenv: true
8
8
 
9
9
  plugins:
10
+ - ./scripts/node24-runtime.cjs
10
11
  - serverless-esbuild
11
12
  - serverless-domain-manager
12
13
  - serverless-certificate-creator
@@ -47,7 +48,7 @@ custom:
47
48
  # serverless-esbuild default and must be preserved when overriding.
48
49
  exclude: ['aws-sdk', '@learncard/didkit-plugin-node']
49
50
  sourcemap: external
50
- target: 'node20'
51
+ target: 'node24'
51
52
  watch:
52
53
  pattern: ['src/**/*.ts', 'dids.ts']
53
54
 
@@ -117,13 +118,13 @@ layers:
117
118
  name: ${self:service}-${opt:stage, 'dev'}-didkit-native
118
119
  description: Native DIDKit napi addon and runtime DIDKit dependency
119
120
  compatibleRuntimes:
120
- - nodejs20.x
121
+ - nodejs24.x
121
122
  compatibleArchitectures:
122
123
  - x86_64
123
124
 
124
125
  provider:
125
126
  name: aws
126
- runtime: nodejs20.x
127
+ runtime: nodejs24.x
127
128
  # Attach the native DIDKit layer to every function in this service.
128
129
  layers:
129
130
  - { Ref: DidkitNativeLambdaLayer }
@@ -272,6 +273,14 @@ resources:
272
273
  # ESCROW_ENCLAVE_ENDPOINT_SERVICE_NAME (the enclave account's endpoint
273
274
  # service). TLS trust for the hostname is pinned in enclaveTrustAnchors.ts.
274
275
  Conditions:
276
+ LcaDevBedrockGeneration:
277
+ Fn::And:
278
+ - Fn::Equals:
279
+ - ${sls:stage}
280
+ - dev
281
+ - Fn::Equals:
282
+ - Ref: AWS::Region
283
+ - us-east-1
275
284
  EscrowEnclavePrivateLink:
276
285
  Fn::Not:
277
286
  - Fn::Equals: ['${env:ESCROW_ENCLAVE_ENDPOINT_SERVICE_NAME, ""}', '']
@@ -283,6 +292,70 @@ resources:
283
292
  Value:
284
293
  Ref: SeedMigrationLambdaFunction
285
294
  Resources:
295
+ LcaDevBedrockGenerationPolicy:
296
+ Type: AWS::IAM::Policy
297
+ Condition: LcaDevBedrockGeneration
298
+ Properties:
299
+ PolicyName: lca-api-dev-bedrock-generation
300
+ Roles:
301
+ - Ref: SigningAuthorityExecutionRole
302
+ PolicyDocument:
303
+ Version: '2012-10-17'
304
+ Statement:
305
+ - Sid: ShortTermBearerOnly
306
+ Effect: Allow
307
+ Action: bedrock:CallWithBearerToken
308
+ Resource: '*'
309
+ Condition:
310
+ StringEquals:
311
+ bedrock:bearerTokenType: SHORT_TERM
312
+ - Sid: ApprovedProfiles
313
+ Effect: Allow
314
+ Action:
315
+ - bedrock:GetInferenceProfile
316
+ - bedrock:InvokeModel
317
+ Resource:
318
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1:${AWS::AccountId}:inference-profile/us.openai.gpt-6.1-sol
319
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1:${AWS::AccountId}:inference-profile/us.openai.gpt-6-luna
320
+ - Sid: DefaultProjectForApprovedProfiles
321
+ Effect: Allow
322
+ Action: bedrock:InvokeModel
323
+ Resource:
324
+ Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1:${AWS::AccountId}:project/default
325
+ Condition:
326
+ StringEquals:
327
+ bedrock:ModelArn:
328
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1:${AWS::AccountId}:inference-profile/us.openai.gpt-6.1-sol
329
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1:${AWS::AccountId}:inference-profile/us.openai.gpt-6-luna
330
+ - Sid: SolBackingModelsThroughSolProfile
331
+ Effect: Allow
332
+ Action: bedrock:InvokeModel
333
+ Resource:
334
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1::foundation-model/openai.gpt-6.1-sol
335
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-2::foundation-model/openai.gpt-6.1-sol
336
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-west-2::foundation-model/openai.gpt-6.1-sol
337
+ Condition:
338
+ StringEquals:
339
+ bedrock:InferenceProfileArn:
340
+ Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1:${AWS::AccountId}:inference-profile/us.openai.gpt-6.1-sol
341
+ - Sid: LunaBackingModelsThroughLunaProfile
342
+ Effect: Allow
343
+ Action: bedrock:InvokeModel
344
+ Resource:
345
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1::foundation-model/openai.gpt-6-luna
346
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-2::foundation-model/openai.gpt-6-luna
347
+ - Fn::Sub: arn:${AWS::Partition}:bedrock:us-west-2::foundation-model/openai.gpt-6-luna
348
+ Condition:
349
+ StringEquals:
350
+ bedrock:InferenceProfileArn:
351
+ Fn::Sub: arn:${AWS::Partition}:bedrock:us-east-1:${AWS::AccountId}:inference-profile/us.openai.gpt-6-luna
352
+ # generateImage default provider (docs/image-generation.md): one model,
353
+ # one region, non-streaming InvokeModel only.
354
+ - Sid: StableImageCoreUsWest2
355
+ Effect: Allow
356
+ Action: bedrock:InvokeModel
357
+ Resource:
358
+ Fn::Sub: arn:${AWS::Partition}:bedrock:us-west-2::foundation-model/stability.stable-image-core-v1:1
286
359
  # Only the two signing entry points and the migration worker assume this role.
287
360
  # Keep KMS references out of the role itself to avoid a Role -> Key -> Role cycle.
288
361
  SigningAuthorityExecutionRole:
@@ -31,6 +31,20 @@ export const lcaApiEnvironmentShape = {
31
31
  NETWORK_BRAIN_SERVICE_URL: optionalEnvironmentUrl,
32
32
  GOOGLE_APPLICATION_CREDENTIAL: optionalEnvironmentString,
33
33
  OPENAI_API_KEY: optionalEnvironmentString,
34
+ BEDROCK_BASE_URL: optionalEnvironmentString,
35
+ IMAGE_PROVIDER: optionalEnvironmentString
36
+ .transform(value => value ?? 'bedrock')
37
+ .pipe(z.enum(['openai', 'bedrock'])),
38
+ BEDROCK_IMAGE_REGION: optionalEnvironmentString
39
+ .transform(value => value ?? 'us-west-2')
40
+ .pipe(z.literal('us-west-2')),
41
+ BEDROCK_IMAGE_MODEL_ID: optionalEnvironmentString
42
+ .transform(value => value ?? 'stability.stable-image-core-v1:1')
43
+ .pipe(z.literal('stability.stable-image-core-v1:1')),
44
+ // Must finish inside the 29-second Lambda timeout (same 25-second ceiling as text generation).
45
+ IMAGE_TIMEOUT_MS: optionalEnvironmentString
46
+ .transform(value => (value === undefined ? 25_000 : Number(value)))
47
+ .pipe(z.number().int().min(1_000).max(25_000)),
34
48
  METABASE_SECRET_KEY: optionalEnvironmentString,
35
49
  SCOUTS_SSO_CLIENT_SECRET: optionalEnvironmentString,
36
50
  KEYCLOAK_ISSUERS: optionalEnvironmentString,