@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.
Files changed (56) hide show
  1. package/.env.example +10 -0
  2. package/CHANGELOG.md +44 -0
  3. package/bundle.mjs +1 -1
  4. package/dist/app.d.ts +23 -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/ai.test.d.ts +1 -0
  19. package/dist/routes/escrow.d.ts +19 -0
  20. package/dist/routes/keys.d.ts +1 -0
  21. package/dist/routes/notifications.d.ts +3 -1
  22. package/dist/services/escrow-enclave/logFailure.d.ts +1 -0
  23. package/dist/services/escrow-enclave/logFailure.test.d.ts +1 -0
  24. package/dist/services/escrow-enclave/types.d.ts +17 -1
  25. package/dist/types/notifications.d.ts +3 -1
  26. package/docs/bedrock-generation.md +119 -0
  27. package/docs/image-generation.md +120 -0
  28. package/esbuildPlugins.cjs +9 -6
  29. package/package.json +21 -16
  30. package/scripts/node24-runtime.cjs +9 -0
  31. package/scripts/verify-node24-layer.cjs +49 -0
  32. package/serverless-local.yml +3 -1
  33. package/serverless.function-env.cjs +2 -4
  34. package/serverless.yml +76 -3
  35. package/src/config/environment.ts +14 -0
  36. package/src/helpers/bedrockGeneration.test.ts +222 -0
  37. package/src/helpers/bedrockGeneration.ts +110 -0
  38. package/src/helpers/bedrockSchema.ts +352 -0
  39. package/src/helpers/imageGeneration.test.ts +456 -0
  40. package/src/helpers/imageGeneration.ts +432 -0
  41. package/src/routes/ai.skill-icons.test.ts +92 -0
  42. package/src/routes/ai.test.ts +80 -0
  43. package/src/routes/ai.ts +33 -144
  44. package/src/routes/escrow.ts +15 -2
  45. package/src/routes/keys.ts +18 -0
  46. package/src/services/escrow-enclave/enclaveTrustAnchors.ts +19 -0
  47. package/src/services/escrow-enclave/logFailure.test.ts +72 -0
  48. package/src/services/escrow-enclave/logFailure.ts +46 -0
  49. package/src/services/escrow-enclave/remoteEnclave.test.ts +101 -3
  50. package/src/services/escrow-enclave/remoteEnclave.tls.test.ts +192 -2
  51. package/src/services/escrow-enclave/remoteEnclave.ts +101 -36
  52. package/src/services/escrow-enclave/types.ts +26 -2
  53. package/test/keys-auth-share-escrow.spec.ts +103 -0
  54. package/test/keys-escrow.spec.ts +65 -0
  55. package/test/oidc.spec.ts +41 -2
  56. package/tsconfig.bedrock.json +10 -0
@@ -0,0 +1,352 @@
1
+ import { z } from 'zod';
2
+
3
+ // Reviewed subset: https://docs.aws.amazon.com/bedrock/latest/userguide/structured-output.html
4
+ const basicTypes: Record<string, true> = {
5
+ object: true,
6
+ array: true,
7
+ string: true,
8
+ integer: true,
9
+ number: true,
10
+ boolean: true,
11
+ null: true,
12
+ };
13
+ const formats: Record<string, true> = {
14
+ 'date-time': true,
15
+ time: true,
16
+ date: true,
17
+ duration: true,
18
+ email: true,
19
+ hostname: true,
20
+ uri: true,
21
+ ipv4: true,
22
+ ipv6: true,
23
+ uuid: true,
24
+ };
25
+ const keywords: Record<string, true> = {
26
+ $schema: true,
27
+ $defs: true,
28
+ $def: true,
29
+ definitions: true,
30
+ $ref: true,
31
+ type: true,
32
+ properties: true,
33
+ required: true,
34
+ additionalProperties: true,
35
+ items: true,
36
+ minItems: true,
37
+ enum: true,
38
+ const: true,
39
+ anyOf: true,
40
+ allOf: true,
41
+ format: true,
42
+ title: true,
43
+ description: true,
44
+ };
45
+ const representableZodTypes: Record<string, true> = {
46
+ string: true,
47
+ number: true,
48
+ boolean: true,
49
+ null: true,
50
+ object: true,
51
+ array: true,
52
+ union: true,
53
+ intersection: true,
54
+ enum: true,
55
+ literal: true,
56
+ nullable: true,
57
+ optional: true,
58
+ nonoptional: true,
59
+ pipe: true,
60
+ readonly: true,
61
+ lazy: true,
62
+ transform: true,
63
+ };
64
+ const projectedZodChecks: Record<string, true> = {
65
+ custom: true,
66
+ less_than: true,
67
+ greater_than: true,
68
+ multiple_of: true,
69
+ number_format: true,
70
+ min_length: true,
71
+ max_length: true,
72
+ length_equals: true,
73
+ string_format: true,
74
+ overwrite: true,
75
+ };
76
+ const omittedProviderConstraints = [
77
+ 'minimum',
78
+ 'maximum',
79
+ 'exclusiveMinimum',
80
+ 'exclusiveMaximum',
81
+ 'multipleOf',
82
+ 'minLength',
83
+ 'maxLength',
84
+ 'pattern',
85
+ 'maxItems',
86
+ ] as const;
87
+
88
+ export class BedrockSchemaError extends Error {
89
+ readonly code = 'unsupported_bedrock_schema';
90
+ constructor(
91
+ readonly path: string,
92
+ readonly reason: string
93
+ ) {
94
+ super(`Unsupported Bedrock schema at ${path}: ${reason}`);
95
+ this.name = 'BedrockSchemaError';
96
+ }
97
+ }
98
+ function fail(path: string, reason: string): never {
99
+ throw new BedrockSchemaError(path, reason);
100
+ }
101
+ const record = (value: unknown): value is Record<string, unknown> =>
102
+ value !== null && typeof value === 'object' && !Array.isArray(value);
103
+ const segment = (key: string): string => key.replace(/~/g, '~0').replace(/\//g, '~1');
104
+ const scalar = (value: unknown): boolean =>
105
+ value === null ||
106
+ typeof value === 'string' ||
107
+ typeof value === 'boolean' ||
108
+ (typeof value === 'number' && Number.isFinite(value));
109
+
110
+ /** Validates syntax and the reviewed nonrecursive Bedrock subset, before any egress. */
111
+ export function validateBedrockJsonSchema(schema: unknown): void {
112
+ if (!record(schema)) fail('#', 'schema must be an object');
113
+ const root = schema as Record<string, unknown>;
114
+ const active = new Set<object>();
115
+ const visited = new Set<object>();
116
+ const resolve = (ref: unknown, path: string): Record<string, unknown> => {
117
+ if (typeof ref !== 'string' || !ref.startsWith('#/'))
118
+ fail(path, 'only internal JSON Pointer references are supported');
119
+ let target: unknown = root;
120
+ for (const part of (ref as string).slice(2).split('/')) {
121
+ if (/~(?![01])/u.test(part)) fail(path, 'invalid JSON Pointer escape');
122
+ const key = part.replace(/~1/g, '/').replace(/~0/g, '~');
123
+ if (!record(target) || !Object.hasOwn(target, key))
124
+ fail(path, 'unresolved internal reference');
125
+ target = (target as Record<string, unknown>)[key];
126
+ }
127
+ if (!record(target)) fail(path, 'reference target must be a schema object');
128
+ return target as Record<string, unknown>;
129
+ };
130
+ const visit = (value: unknown, path: string): void => {
131
+ if (!record(value)) fail(path, 'schema must be an object');
132
+ const node = value as Record<string, unknown>;
133
+ if (active.has(node)) fail(path, 'recursive schemas are not supported');
134
+ if (visited.has(node)) return;
135
+ active.add(node);
136
+ for (const key of Object.keys(node)) {
137
+ if (!Object.hasOwn(keywords, key))
138
+ fail(`${path}/${segment(key)}`, 'keyword is not supported');
139
+ }
140
+ for (const key of ['$schema', 'title', 'description']) {
141
+ if (key in node && typeof node[key] !== 'string')
142
+ fail(`${path}/${key}`, 'metadata must be a string');
143
+ }
144
+ if ('$schema' in node && node.$schema !== 'https://json-schema.org/draft/2020-12/schema')
145
+ fail(`${path}/$schema`, 'only Draft 2020-12 is supported');
146
+ for (const key of ['$defs', '$def', 'definitions']) {
147
+ if (!(key in node)) continue;
148
+ if (!record(node[key])) fail(`${path}/${key}`, 'definitions must be an object');
149
+ for (const [name, child] of Object.entries(node[key] as Record<string, unknown>))
150
+ visit(child, `${path}/${key}/${segment(name)}`);
151
+ }
152
+ if ('$ref' in node) {
153
+ if (
154
+ Object.keys(node).some(
155
+ key =>
156
+ ![
157
+ '$ref',
158
+ '$schema',
159
+ '$defs',
160
+ '$def',
161
+ 'definitions',
162
+ 'description',
163
+ 'title',
164
+ ].includes(key)
165
+ )
166
+ )
167
+ fail(`${path}/$ref`, 'reference siblings cannot add constraints');
168
+ visit(resolve(node.$ref, `${path}/$ref`), `${path}/$ref`);
169
+ }
170
+ const types = Array.isArray(node.type) ? node.type : 'type' in node ? [node.type] : [];
171
+ if (
172
+ 'type' in node &&
173
+ (!types.length ||
174
+ types.some(type => typeof type !== 'string' || !Object.hasOwn(basicTypes, type)) ||
175
+ new Set(types).size !== types.length)
176
+ )
177
+ fail(`${path}/type`, 'invalid basic type');
178
+ if (!types.length && !['$ref', 'enum', 'const', 'anyOf', 'allOf'].some(key => key in node))
179
+ fail(path, 'unconstrained schemas are not supported');
180
+ if (
181
+ 'enum' in node &&
182
+ (!Array.isArray(node.enum) ||
183
+ !node.enum.length ||
184
+ node.enum.some(item => !scalar(item)))
185
+ )
186
+ fail(`${path}/enum`, 'enum must contain scalar JSON values');
187
+ if ('const' in node && !scalar(node.const))
188
+ fail(`${path}/const`, 'const must be a scalar JSON value');
189
+ if (
190
+ 'format' in node &&
191
+ (!types.includes('string') ||
192
+ typeof node.format !== 'string' ||
193
+ !Object.hasOwn(formats, node.format))
194
+ )
195
+ fail(`${path}/format`, 'string format is not supported');
196
+ if (types.includes('object')) {
197
+ if (node.additionalProperties !== false)
198
+ fail(`${path}/additionalProperties`, 'objects must reject additional properties');
199
+ if (!record(node.properties))
200
+ fail(`${path}/properties`, 'object properties must be an object');
201
+ for (const [key, child] of Object.entries(node.properties as Record<string, unknown>))
202
+ visit(child, `${path}/properties/${segment(key)}`);
203
+ if (
204
+ 'required' in node &&
205
+ (!Array.isArray(node.required) ||
206
+ new Set(node.required).size !== node.required.length ||
207
+ node.required.some(
208
+ key =>
209
+ typeof key !== 'string' ||
210
+ !Object.hasOwn(node.properties as object, key)
211
+ ))
212
+ )
213
+ fail(`${path}/required`, 'required must name distinct declared properties');
214
+ } else if (['properties', 'required', 'additionalProperties'].some(key => key in node))
215
+ fail(path, 'object keywords require object type');
216
+ if (types.includes('array')) {
217
+ visit(node.items, `${path}/items`);
218
+ if ('minItems' in node && node.minItems !== 0 && node.minItems !== 1)
219
+ fail(`${path}/minItems`, 'only minItems 0 or 1 is supported');
220
+ } else if (['items', 'minItems'].some(key => key in node))
221
+ fail(path, 'array keywords require array type');
222
+ for (const key of ['anyOf', 'allOf']) {
223
+ if (!(key in node)) continue;
224
+ if (!Array.isArray(node[key]) || !(node[key] as unknown[]).length)
225
+ fail(`${path}/${key}`, 'composition must contain schema branches');
226
+ for (const [index, child] of (node[key] as unknown[]).entries()) {
227
+ visit(child, `${path}/${key}/${index}`);
228
+ // Bedrock documents allOf with limitations; only same-type scalar composition is reviewed.
229
+ if (key === 'allOf') {
230
+ const branch =
231
+ record(child) && '$ref' in child
232
+ ? resolve(child.$ref, `${path}/${key}/${index}`)
233
+ : child;
234
+ if (
235
+ !record(branch) ||
236
+ typeof branch.type !== 'string' ||
237
+ ['object', 'array'].includes(branch.type)
238
+ )
239
+ fail(
240
+ `${path}/${key}/${index}`,
241
+ 'allOf is limited to explicitly typed scalar branches'
242
+ );
243
+ const first = (node[key] as Record<string, unknown>[])[0]!;
244
+ const firstType =
245
+ '$ref' in first ? resolve(first.$ref, `${path}/${key}/0`).type : first.type;
246
+ if (branch.type !== firstType)
247
+ fail(
248
+ `${path}/${key}/${index}`,
249
+ 'allOf branches must have the same scalar type'
250
+ );
251
+ }
252
+ }
253
+ }
254
+ active.delete(node);
255
+ visited.add(node);
256
+ };
257
+ visit(root, '#');
258
+ }
259
+
260
+ // Inspect structural input domains before exporting. Nonportable refinements
261
+ // remain authoritative in the unchanged original Zod parser after decoding.
262
+ function inspectZod(schema: z.ZodType, path = '#', active = new Set<z.ZodType>()): void {
263
+ if (active.has(schema)) fail(path, 'recursive schemas are not supported');
264
+ active.add(schema);
265
+ const def = schema._zod.def as z.core.$ZodTypes['_zod']['def'];
266
+ if (def.type === 'custom') fail(path, 'custom schemas have no representable input domain');
267
+ if (def.type === 'pipe' && def.in._zod.def.type === 'transform')
268
+ fail(path, 'preprocessing has no representable input domain');
269
+ if (!Object.hasOwn(representableZodTypes, def.type))
270
+ fail(path, 'Zod input domain cannot be represented as JSON Schema');
271
+ if ('coerce' in def && def.coerce) fail(path, 'coercion has no representable input domain');
272
+ if (def.type === 'object' && def.catchall && def.catchall._zod.def.type !== 'never')
273
+ fail(path, 'objects must reject additional properties');
274
+ const checks = (def.checks ?? []).map(check => check._zod.def);
275
+ if ('check' in def) checks.push(def as z.core.$ZodCheckDef);
276
+ for (const check of checks) {
277
+ const kind = check.check;
278
+ if (!Object.hasOwn(projectedZodChecks, kind)) fail(path, 'Zod check cannot be represented');
279
+ }
280
+ if (def.type === 'transform') {
281
+ active.delete(schema);
282
+ return;
283
+ }
284
+ const visitValue = (value: unknown, childPath: string): void => {
285
+ if (value instanceof z.ZodType) inspectZod(value, childPath, active);
286
+ else if (Array.isArray(value))
287
+ value.forEach((child, index) => visitValue(child, `${childPath}/${index}`));
288
+ else if (record(value))
289
+ for (const [key, child] of Object.entries(value))
290
+ visitValue(child, `${childPath}/${segment(key)}`);
291
+ };
292
+ if (def.type === 'lazy') visitValue((def as z.core.$ZodLazyDef).getter(), `${path}/lazy`);
293
+ else
294
+ for (const [key, value] of Object.entries(def)) {
295
+ if (['checks', 'error'].includes(key) || (def.type === 'object' && key === 'catchall'))
296
+ continue;
297
+ visitValue(value, `${path}/${segment(key)}`);
298
+ }
299
+ active.delete(schema);
300
+ }
301
+
302
+ /** Portable input structure; no provider strictness or omitted-constraint guarantee. */
303
+ export function compileProjectedSchema(schema: z.ZodType): Record<string, unknown> {
304
+ let compiled: Record<string, unknown>;
305
+ try {
306
+ inspectZod(schema);
307
+ compiled = z.toJSONSchema(schema, {
308
+ target: 'draft-2020-12',
309
+ io: 'input',
310
+ unrepresentable: 'throw',
311
+ cycles: 'throw',
312
+ reused: 'inline',
313
+ override: ({ zodSchema, jsonSchema }) => {
314
+ // Default Zod objects strip unknown keys; constrain generated input to their declared fields.
315
+ // Strict and optional semantics remain unchanged; passthrough/catchall objects are rejected.
316
+ if (zodSchema._zod.def.type === 'object' && !zodSchema._zod.def.catchall)
317
+ jsonSchema.additionalProperties = false;
318
+ // Fixed scalar hints only; arbitrary schema descriptions never cross this boundary.
319
+ delete jsonSchema.description;
320
+ if (
321
+ zodSchema instanceof z.ZodString &&
322
+ (zodSchema.minLength !== null || zodSchema.maxLength !== null)
323
+ )
324
+ jsonSchema.description = `Length must be between ${zodSchema.minLength ?? 0} and ${zodSchema.maxLength ?? 'unbounded'} characters. Local validation remains authoritative.`;
325
+ // Modify only the exported provider schema, never the original validator.
326
+ for (const keyword of omittedProviderConstraints) delete jsonSchema[keyword];
327
+ if (
328
+ 'minItems' in jsonSchema &&
329
+ jsonSchema.minItems !== 0 &&
330
+ jsonSchema.minItems !== 1
331
+ )
332
+ delete jsonSchema.minItems;
333
+ if (
334
+ 'format' in jsonSchema &&
335
+ (typeof jsonSchema.format !== 'string' ||
336
+ !Object.hasOwn(formats, jsonSchema.format))
337
+ )
338
+ delete jsonSchema.format;
339
+ },
340
+ }) as Record<string, unknown>;
341
+ } catch (error) {
342
+ if (error instanceof BedrockSchemaError) throw error;
343
+ fail('#', 'Zod input domain cannot be represented as JSON Schema');
344
+ }
345
+ validateBedrockJsonSchema(compiled);
346
+ return compiled;
347
+ }
348
+
349
+ /** Return the original schema's parsed value, including async transformations. */
350
+ export async function parseBedrockOutput(value: unknown, schema: z.ZodType): Promise<unknown> {
351
+ return schema.parseAsync(value);
352
+ }