@learncard/lca-api-service 1.5.2 → 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.
- package/.env.example +10 -0
- package/CHANGELOG.md +44 -0
- package/bundle.mjs +1 -1
- package/dist/app.d.ts +23 -1
- package/dist/config/cacheEnvironment.d.ts +4 -4
- package/dist/config/environment.d.ts +145 -114
- package/dist/config/oidcEnvironment.d.ts +22 -22
- package/dist/didkit_wasm_bg.wasm +0 -0
- package/dist/helpers/bedrockGeneration.d.ts +17 -0
- package/dist/helpers/bedrockGeneration.test.d.ts +1 -0
- package/dist/helpers/bedrockSchema.d.ts +10 -0
- package/dist/helpers/imageGeneration.d.ts +42 -0
- package/dist/helpers/imageGeneration.test.d.ts +1 -0
- package/dist/index.js +2 -2
- package/dist/models/Notification.d.ts +3 -1
- package/dist/publish-metafile.json +1 -1
- package/dist/routes/ai.skill-icons.test.d.ts +1 -0
- package/dist/routes/ai.test.d.ts +1 -0
- package/dist/routes/escrow.d.ts +19 -0
- package/dist/routes/keys.d.ts +1 -0
- package/dist/routes/notifications.d.ts +3 -1
- package/dist/services/escrow-enclave/logFailure.d.ts +1 -0
- package/dist/services/escrow-enclave/logFailure.test.d.ts +1 -0
- package/dist/services/escrow-enclave/types.d.ts +17 -1
- package/dist/types/notifications.d.ts +3 -1
- package/docs/bedrock-generation.md +119 -0
- package/docs/image-generation.md +120 -0
- package/esbuildPlugins.cjs +9 -6
- package/package.json +21 -16
- package/scripts/node24-runtime.cjs +9 -0
- package/scripts/verify-node24-layer.cjs +49 -0
- package/serverless-local.yml +3 -1
- package/serverless.function-env.cjs +2 -4
- package/serverless.yml +76 -3
- package/src/config/environment.ts +14 -0
- package/src/helpers/bedrockGeneration.test.ts +222 -0
- package/src/helpers/bedrockGeneration.ts +110 -0
- package/src/helpers/bedrockSchema.ts +352 -0
- package/src/helpers/imageGeneration.test.ts +456 -0
- package/src/helpers/imageGeneration.ts +432 -0
- package/src/routes/ai.skill-icons.test.ts +92 -0
- package/src/routes/ai.test.ts +80 -0
- package/src/routes/ai.ts +33 -144
- package/src/routes/escrow.ts +15 -2
- package/src/routes/keys.ts +18 -0
- package/src/services/escrow-enclave/enclaveTrustAnchors.ts +19 -0
- package/src/services/escrow-enclave/logFailure.test.ts +72 -0
- package/src/services/escrow-enclave/logFailure.ts +46 -0
- package/src/services/escrow-enclave/remoteEnclave.test.ts +101 -3
- package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +192 -2
- package/src/services/escrow-enclave/remoteEnclave.ts +101 -36
- package/src/services/escrow-enclave/types.ts +26 -2
- package/test/keys-auth-share-escrow.spec.ts +103 -0
- package/test/keys-escrow.spec.ts +65 -0
- package/test/oidc.spec.ts +41 -2
- package/tsconfig.bedrock.json +10 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/routes/escrow.d.ts
CHANGED
|
@@ -251,6 +251,25 @@ export declare const escrowRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
251
251
|
} | undefined;
|
|
252
252
|
requiresConfirmation?: boolean | undefined;
|
|
253
253
|
pinSalt?: string | undefined;
|
|
254
|
+
codeSent?: boolean | undefined;
|
|
255
|
+
delivery?: {
|
|
256
|
+
codeSent: boolean;
|
|
257
|
+
proof: {
|
|
258
|
+
version: 1;
|
|
259
|
+
keyId: string;
|
|
260
|
+
measurement: number[];
|
|
261
|
+
signature: string;
|
|
262
|
+
kind: "commit";
|
|
263
|
+
payload: string;
|
|
264
|
+
} | {
|
|
265
|
+
version: 1;
|
|
266
|
+
keyId: string;
|
|
267
|
+
measurement: number[];
|
|
268
|
+
payload: string;
|
|
269
|
+
signature: string;
|
|
270
|
+
kind: "snapshot";
|
|
271
|
+
};
|
|
272
|
+
} | undefined;
|
|
254
273
|
};
|
|
255
274
|
meta: import("trpc-to-openapi").OpenApiMeta;
|
|
256
275
|
}>;
|
package/dist/routes/keys.d.ts
CHANGED
|
@@ -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" | "
|
|
179
|
+
action: "write" | "update" | "consent" | "sync" | "withdraw";
|
|
178
180
|
date: string;
|
|
179
181
|
expiresAt?: string | undefined;
|
|
180
182
|
oneTime?: boolean | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const logEscrowFailure: (error: unknown) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -117,6 +117,17 @@ export interface EscrowEnclave {
|
|
|
117
117
|
releaseEscrow(input: ReleaseRequest): Promise<ReleaseResult>;
|
|
118
118
|
createHold(input: EnclaveCreateHoldInput): Promise<{
|
|
119
119
|
holdRecord: EscrowHoldRecord;
|
|
120
|
+
delivery?: {
|
|
121
|
+
codeSent: boolean;
|
|
122
|
+
proof: AuthorityHoldReceipt;
|
|
123
|
+
};
|
|
124
|
+
}>;
|
|
125
|
+
getHoldState?(input: {
|
|
126
|
+
expectedDid: string;
|
|
127
|
+
hold: EscrowHoldRecord;
|
|
128
|
+
nonce: string;
|
|
129
|
+
}): Promise<{
|
|
130
|
+
proof: AuthorityRecord;
|
|
120
131
|
}>;
|
|
121
132
|
cancelHold(input: CancelHoldRequest): Promise<void | {
|
|
122
133
|
receipt: AuthorityRecord;
|
|
@@ -136,6 +147,10 @@ export interface EscrowEnclave {
|
|
|
136
147
|
export declare class EscrowPolicyError extends Error {
|
|
137
148
|
constructor();
|
|
138
149
|
}
|
|
150
|
+
export declare class EscrowCodeError extends Error {
|
|
151
|
+
readonly reason: 'codeMismatch' | 'codeExpired' | 'codeLocked';
|
|
152
|
+
constructor(reason: 'codeMismatch' | 'codeExpired' | 'codeLocked');
|
|
153
|
+
}
|
|
139
154
|
export declare class EscrowPinMismatchError extends Error {
|
|
140
155
|
constructor();
|
|
141
156
|
}
|
|
@@ -143,5 +158,6 @@ export declare class EscrowBlobError extends Error {
|
|
|
143
158
|
constructor();
|
|
144
159
|
}
|
|
145
160
|
export declare class EscrowUnavailableError extends Error {
|
|
146
|
-
|
|
161
|
+
readonly reason: 'unavailable' | 'ledger' | 'time' | 'transport' | 'invalid_response' | 'invalid_hold' | 'http_refusal' | 'rate_limit';
|
|
162
|
+
constructor(reason?: 'unavailable' | 'ledger' | 'time' | 'transport' | 'invalid_response' | 'invalid_hold' | 'http_refusal' | 'rate_limit');
|
|
147
163
|
}
|
|
@@ -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/esbuildPlugins.cjs
CHANGED
|
@@ -2,6 +2,7 @@ const path = require('node:path');
|
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
|
|
4
4
|
const { sentryEsbuildPlugin } = require('@sentry/esbuild-plugin');
|
|
5
|
+
const { withSentryRateLimitRetry } = require('../../../scripts/sentry-rate-limit-retry.cjs');
|
|
5
6
|
|
|
6
7
|
const forceCjsSodiumWrapperPlugin = {
|
|
7
8
|
name: 'force-cjs-sodium-wrapper',
|
|
@@ -40,10 +41,12 @@ const copyDidkitWasmPlugin = {
|
|
|
40
41
|
module.exports = [
|
|
41
42
|
forceCjsSodiumWrapperPlugin,
|
|
42
43
|
copyDidkitWasmPlugin,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
withSentryRateLimitRetry(
|
|
45
|
+
sentryEsbuildPlugin({
|
|
46
|
+
telemetry: process.env.SENTRY_BUILD_TELEMETRY !== 'false',
|
|
47
|
+
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
48
|
+
org: process.env.SENTRY_ORG,
|
|
49
|
+
project: process.env.SENTRY_PROJECT,
|
|
50
|
+
})
|
|
51
|
+
),
|
|
49
52
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/lca-api-service",
|
|
3
|
-
"version": "1.
|
|
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,35 +50,37 @@
|
|
|
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.
|
|
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.
|
|
67
|
-
"@learncard/did-web-plugin": "1.1.
|
|
68
|
-
"@learncard/didkit-plugin": "1.10.
|
|
69
|
-
"@learncard/didkit-plugin-node": "0.3.
|
|
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.
|
|
72
|
-
"@learncard/init": "2.5.
|
|
73
|
-
"@learncard/types": "5.
|
|
74
|
-
"@learncard/sss-key-manager": "0.2.
|
|
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",
|
|
78
81
|
"@types/lodash": "^4.14.191",
|
|
79
82
|
"@types/node-forge": "^1.3.11",
|
|
80
|
-
"axios": "^0.
|
|
83
|
+
"axios": "^0.34.0",
|
|
81
84
|
"dag-jose-utils": "2.0.0",
|
|
82
85
|
"did-jwt": "5.9.0",
|
|
83
86
|
"dotenv": "^16.0.3",
|
|
@@ -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": "
|
|
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.
|
|
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
|
+
}
|
package/serverless-local.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-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:
|
|
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
|
|
28
|
-
|
|
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.
|