@blue-quickjs/quickjs-runtime 0.1.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.
@@ -0,0 +1,397 @@
1
+ import { validateAbiManifest, } from '@blue-quickjs/abi-manifest';
2
+ import { DV_LIMIT_DEFAULTS, DvError, decodeDv, encodeDv, validateDv, } from '@blue-quickjs/dv';
3
+ const UINT32_MAX = 0xffffffff;
4
+ const UTF8 = new TextEncoder();
5
+ export class HostDispatcherError extends Error {
6
+ code;
7
+ constructor(code, message, options) {
8
+ super(message, options);
9
+ this.code = code;
10
+ this.name = 'HostDispatcherError';
11
+ }
12
+ }
13
+ export function createHostDispatcher(manifest, handlers, options) {
14
+ const canonical = validateAbiManifest(manifest);
15
+ const expectedAbiId = options?.expectedAbiId ?? 'Host.v1';
16
+ const expectedAbiVersion = options?.expectedAbiVersion ?? 1;
17
+ if (canonical.abi_id !== expectedAbiId) {
18
+ throw new HostDispatcherError('INVALID_REQUEST', `manifest abi_id mismatch: expected ${expectedAbiId}, received ${canonical.abi_id}`);
19
+ }
20
+ if (canonical.abi_version !== expectedAbiVersion) {
21
+ throw new HostDispatcherError('INVALID_REQUEST', `manifest abi_version mismatch: expected ${expectedAbiVersion}, received ${canonical.abi_version}`);
22
+ }
23
+ const dvLimits = normalizeDvLimits(options?.dvLimits);
24
+ const bindings = buildBindings(canonical.functions, handlers);
25
+ return {
26
+ manifest: canonical,
27
+ dispatch(fnId, requestBytes) {
28
+ const normalizedFnId = toUint32(fnId);
29
+ const binding = bindings.get(normalizedFnId);
30
+ if (!binding) {
31
+ return fatal('UNKNOWN_FUNCTION', `unknown fn_id ${normalizedFnId}`);
32
+ }
33
+ const request = asUint8Array(requestBytes);
34
+ if (request.length > binding.fn.limits.max_request_bytes) {
35
+ if (binding.limitExceededEnvelope) {
36
+ return encodeEnvelope(binding.fn, binding.limitExceededEnvelope, dvLimits);
37
+ }
38
+ return fatalLimitError(binding.fn);
39
+ }
40
+ const decodeLimits = {
41
+ ...dvLimits,
42
+ maxEncodedBytes: Math.min(dvLimits.maxEncodedBytes, binding.fn.limits.max_request_bytes),
43
+ };
44
+ let args;
45
+ try {
46
+ args = decodeDv(request, { limits: decodeLimits });
47
+ }
48
+ catch (err) {
49
+ return fatal('INVALID_REQUEST', `failed to decode request for fn_id=${normalizedFnId}: ${stringifyError(err)}`, err);
50
+ }
51
+ if (!Array.isArray(args)) {
52
+ return fatal('INVALID_ARGUMENTS', `request for fn_id=${normalizedFnId} must be a DV array`);
53
+ }
54
+ if (args.length !== binding.fn.arity) {
55
+ return fatal('INVALID_ARGUMENTS', `fn_id=${normalizedFnId} expected ${binding.fn.arity} args, received ${args.length}`);
56
+ }
57
+ try {
58
+ return binding.dispatch(args, dvLimits);
59
+ }
60
+ catch (err) {
61
+ return fatal('HANDLER_ERROR', `fn_id=${normalizedFnId} handler threw: ${stringifyError(err)}`, err);
62
+ }
63
+ },
64
+ };
65
+ }
66
+ export function createHostCallImport(dispatcher, memory) {
67
+ let inProgress = false;
68
+ return (fnId, reqPtr, reqLen, respPtr, respCap) => {
69
+ if (inProgress) {
70
+ return UINT32_MAX;
71
+ }
72
+ inProgress = true;
73
+ try {
74
+ const mem = new Uint8Array(memory.buffer);
75
+ const reqOffset = toUint32(reqPtr);
76
+ const reqLength = toUint32(reqLen);
77
+ const respOffset = toUint32(respPtr);
78
+ const respCapacity = toUint32(respCap);
79
+ const fn = toUint32(fnId);
80
+ if (!withinBounds(mem, reqOffset, reqLength) ||
81
+ !withinBounds(mem, respOffset, respCapacity)) {
82
+ return UINT32_MAX;
83
+ }
84
+ if (rangesOverlap(reqOffset, reqLength, respOffset, respCapacity)) {
85
+ return UINT32_MAX;
86
+ }
87
+ const request = mem.subarray(reqOffset, reqOffset + reqLength);
88
+ const result = dispatcher.dispatch(fn, request);
89
+ if (result.kind === 'fatal') {
90
+ return UINT32_MAX;
91
+ }
92
+ if (result.envelope.length > respCapacity) {
93
+ return UINT32_MAX;
94
+ }
95
+ mem
96
+ .subarray(respOffset, respOffset + result.envelope.length)
97
+ .set(result.envelope);
98
+ return result.envelope.length >>> 0;
99
+ }
100
+ catch {
101
+ return UINT32_MAX;
102
+ }
103
+ finally {
104
+ inProgress = false;
105
+ }
106
+ };
107
+ }
108
+ function buildBindings(functions, handlers) {
109
+ const bindings = new Map();
110
+ const byPath = new Map();
111
+ for (const fn of functions) {
112
+ const canonical = withErrorTags(fn);
113
+ byPath.set(canonical.js_path.join('.'), canonical);
114
+ }
115
+ const documentGet = byPath.get('document.get');
116
+ const documentGetCanonical = byPath.get('document.getCanonical');
117
+ if (!documentGet || !documentGetCanonical) {
118
+ throw new HostDispatcherError('INVALID_REQUEST', 'Host.v1 manifest must include document.get and document.getCanonical');
119
+ }
120
+ const emitFn = byPath.get('emit');
121
+ if (emitFn && !handlers.emit) {
122
+ throw new HostDispatcherError('INVALID_REQUEST', 'manifest declares emit but no emit handler was provided');
123
+ }
124
+ if (!emitFn && handlers.emit) {
125
+ throw new HostDispatcherError('INVALID_REQUEST', 'emit handler provided but manifest does not declare emit');
126
+ }
127
+ bindings.set(documentGet.fn_id, buildDocumentBinding(documentGet, handlers.document.get));
128
+ bindings.set(documentGetCanonical.fn_id, buildDocumentBinding(documentGetCanonical, handlers.document.getCanonical));
129
+ if (emitFn && handlers.emit) {
130
+ bindings.set(emitFn.fn_id, buildEmitBinding(emitFn, handlers.emit));
131
+ }
132
+ if (functions.length !== bindings.size) {
133
+ const knownIds = [...bindings.keys()].sort((a, b) => a - b).join(', ');
134
+ const manifestIds = functions.map((fn) => fn.fn_id).sort((a, b) => a - b);
135
+ throw new HostDispatcherError('INVALID_REQUEST', `dispatcher does not implement all manifest functions (implemented: ${knownIds}; manifest: ${manifestIds.join(', ')})`);
136
+ }
137
+ return bindings;
138
+ }
139
+ function buildDocumentBinding(fn, handler) {
140
+ assertDocumentShape(fn);
141
+ const limitExceededEnvelope = createLimitEnvelope(fn);
142
+ return {
143
+ fn,
144
+ limitExceededEnvelope,
145
+ dispatch(args, dvLimits) {
146
+ const [path] = args;
147
+ if (typeof path !== 'string') {
148
+ return fatal('INVALID_ARGUMENTS', `fn_id=${fn.fn_id} expected string path argument`);
149
+ }
150
+ const utf8Max = fn.limits.arg_utf8_max?.[0];
151
+ if (utf8Max !== undefined) {
152
+ const byteLen = UTF8.encode(path).byteLength;
153
+ if (byteLen > utf8Max) {
154
+ if (limitExceededEnvelope) {
155
+ return encodeEnvelope(fn, limitExceededEnvelope, dvLimits);
156
+ }
157
+ return fatal('INVALID_ARGUMENTS', `fn_id=${fn.fn_id} path exceeds utf8 limit (${byteLen} > ${utf8Max})`);
158
+ }
159
+ }
160
+ const result = handler(path);
161
+ return encodeResult(fn, result, dvLimits, limitExceededEnvelope);
162
+ },
163
+ };
164
+ }
165
+ function buildEmitBinding(fn, handler) {
166
+ assertEmitShape(fn);
167
+ const limitExceededEnvelope = createLimitEnvelope(fn);
168
+ return {
169
+ fn,
170
+ limitExceededEnvelope,
171
+ dispatch(args, dvLimits) {
172
+ const [value] = args;
173
+ const result = handler(value);
174
+ return encodeResult(fn, result, dvLimits, limitExceededEnvelope);
175
+ },
176
+ };
177
+ }
178
+ function encodeResult(fn, result, dvLimits, limitExceededEnvelope) {
179
+ if (result === null || typeof result !== 'object') {
180
+ return fatal('HANDLER_ERROR', `fn_id=${fn.fn_id} returned invalid result`);
181
+ }
182
+ const hasOk = 'ok' in result;
183
+ const hasErr = 'err' in result;
184
+ if (hasOk === hasErr) {
185
+ return fatal('HANDLER_ERROR', `fn_id=${fn.fn_id} result must contain exactly one of ok or err`);
186
+ }
187
+ if (!('units' in result)) {
188
+ return fatal('HANDLER_ERROR', `fn_id=${fn.fn_id} result.units is required`);
189
+ }
190
+ let units;
191
+ try {
192
+ units = normalizeUint(result.units, fn.limits.max_units, 'result.units', !!limitExceededEnvelope);
193
+ }
194
+ catch (err) {
195
+ if (err instanceof HostDispatcherError) {
196
+ return fatal(err.code, err.message, err);
197
+ }
198
+ throw err;
199
+ }
200
+ if (units === null) {
201
+ if (limitExceededEnvelope) {
202
+ return encodeEnvelope(fn, limitExceededEnvelope, dvLimits);
203
+ }
204
+ return fatal('INVALID_ARGUMENTS', `fn_id=${fn.fn_id} units exceed max_units (${fn.limits.max_units})`);
205
+ }
206
+ if ('ok' in result) {
207
+ if (fn.return_schema.type === 'null' && result.ok !== null) {
208
+ return fatal('HANDLER_ERROR', `fn_id=${fn.fn_id} must return null for return_schema "null"`);
209
+ }
210
+ if (fn.return_schema.type === 'dv') {
211
+ try {
212
+ validateDv(result.ok, {
213
+ limits: cappedDvLimits(dvLimits, fn.limits.max_response_bytes),
214
+ });
215
+ }
216
+ catch (err) {
217
+ return handleDvValidationError(fn, err, limitExceededEnvelope, dvLimits);
218
+ }
219
+ }
220
+ return encodeEnvelope(fn, { ok: result.ok, units }, dvLimits, limitExceededEnvelope);
221
+ }
222
+ if (result.err === null ||
223
+ typeof result.err !== 'object' ||
224
+ typeof result.err.code !== 'string' ||
225
+ typeof result.err.tag !== 'string') {
226
+ return fatal('HANDLER_ERROR', `fn_id=${fn.fn_id} returned malformed err payload`);
227
+ }
228
+ const manifestTag = fn.errorTagMap.get(result.err.code);
229
+ if (!manifestTag) {
230
+ return fatal('HANDLER_ERROR', `fn_id=${fn.fn_id} returned unknown error code ${result.err.code}`);
231
+ }
232
+ if (result.err.tag !== manifestTag) {
233
+ return fatal('HANDLER_ERROR', `fn_id=${fn.fn_id} error tag mismatch for code ${result.err.code}: expected ${manifestTag}, received ${result.err.tag}`);
234
+ }
235
+ if (result.err.details !== undefined) {
236
+ try {
237
+ validateDv(result.err.details, {
238
+ limits: cappedDvLimits(dvLimits, fn.limits.max_response_bytes),
239
+ });
240
+ }
241
+ catch (err) {
242
+ return handleDvValidationError(fn, err, limitExceededEnvelope, dvLimits);
243
+ }
244
+ }
245
+ return encodeEnvelope(fn, {
246
+ err: result.err.details === undefined
247
+ ? { code: result.err.code }
248
+ : { code: result.err.code, details: result.err.details },
249
+ units,
250
+ }, dvLimits, limitExceededEnvelope);
251
+ }
252
+ function encodeEnvelope(fn, envelope, dvLimits, limitExceededEnvelope) {
253
+ const encodeLimits = cappedDvLimits(dvLimits, fn.limits.max_response_bytes);
254
+ try {
255
+ const bytes = encodeDv(envelope, { limits: encodeLimits });
256
+ return { kind: 'response', envelope: bytes };
257
+ }
258
+ catch (err) {
259
+ if (limitExceededEnvelope && isSizeRelatedDvError(err)) {
260
+ try {
261
+ const bytes = encodeDv(limitExceededEnvelope, { limits: encodeLimits });
262
+ return { kind: 'response', envelope: bytes };
263
+ }
264
+ catch (limitErr) {
265
+ return fatal('RESPONSE_LIMIT', `fn_id=${fn.fn_id} failed to encode limit response: ${stringifyError(limitErr)}`, limitErr);
266
+ }
267
+ }
268
+ return fatal('RESPONSE_LIMIT', `fn_id=${fn.fn_id} failed to encode response: ${stringifyError(err)}`, err);
269
+ }
270
+ }
271
+ function createLimitEnvelope(fn) {
272
+ if (!fn.errorTagMap.has('LIMIT_EXCEEDED')) {
273
+ return undefined;
274
+ }
275
+ return { err: { code: 'LIMIT_EXCEEDED' }, units: 0 };
276
+ }
277
+ function assertDocumentShape(fn) {
278
+ if (fn.arity !== 1 || fn.arg_schema.length !== 1) {
279
+ throw new HostDispatcherError('INVALID_REQUEST', `document.* functions must have arity 1 (fn_id=${fn.fn_id})`);
280
+ }
281
+ if (fn.arg_schema[0]?.type !== 'string') {
282
+ throw new HostDispatcherError('INVALID_REQUEST', `document.* functions must take a string argument (fn_id=${fn.fn_id})`);
283
+ }
284
+ if (fn.return_schema.type !== 'dv') {
285
+ throw new HostDispatcherError('INVALID_REQUEST', `document.* functions must return DV (fn_id=${fn.fn_id})`);
286
+ }
287
+ }
288
+ function assertEmitShape(fn) {
289
+ if (fn.arity !== 1 || fn.arg_schema.length !== 1) {
290
+ throw new HostDispatcherError('INVALID_REQUEST', `emit must have arity 1 (fn_id=${fn.fn_id})`);
291
+ }
292
+ if (fn.arg_schema[0]?.type !== 'dv') {
293
+ throw new HostDispatcherError('INVALID_REQUEST', `emit must take a DV argument (fn_id=${fn.fn_id})`);
294
+ }
295
+ if (fn.return_schema.type !== 'null') {
296
+ throw new HostDispatcherError('INVALID_REQUEST', `emit must return null (fn_id=${fn.fn_id})`);
297
+ }
298
+ }
299
+ function handleDvValidationError(fn, err, limitExceededEnvelope, dvLimits) {
300
+ if (limitExceededEnvelope && isSizeRelatedDvError(err)) {
301
+ return encodeEnvelope(fn, limitExceededEnvelope, dvLimits);
302
+ }
303
+ return fatal('HANDLER_ERROR', `fn_id=${fn.fn_id} produced non-DV value: ${stringifyError(err)}`, err);
304
+ }
305
+ function cappedDvLimits(limits, maxBytes) {
306
+ return {
307
+ ...limits,
308
+ maxEncodedBytes: Math.min(limits.maxEncodedBytes, maxBytes),
309
+ };
310
+ }
311
+ function normalizeDvLimits(limits) {
312
+ return {
313
+ maxDepth: limits?.maxDepth ?? DV_LIMIT_DEFAULTS.maxDepth,
314
+ maxEncodedBytes: limits?.maxEncodedBytes ?? DV_LIMIT_DEFAULTS.maxEncodedBytes,
315
+ maxStringBytes: limits?.maxStringBytes ?? DV_LIMIT_DEFAULTS.maxStringBytes,
316
+ maxArrayLength: limits?.maxArrayLength ?? DV_LIMIT_DEFAULTS.maxArrayLength,
317
+ maxMapLength: limits?.maxMapLength ?? DV_LIMIT_DEFAULTS.maxMapLength,
318
+ };
319
+ }
320
+ function toUint32(value) {
321
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
322
+ return 0;
323
+ }
324
+ return value >>> 0;
325
+ }
326
+ function normalizeUint(value, max, path, allowOverflow) {
327
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
328
+ throw new HostDispatcherError('HANDLER_ERROR', `${path} must be an integer`);
329
+ }
330
+ if (Object.is(value, -0)) {
331
+ throw new HostDispatcherError('HANDLER_ERROR', `${path} must not be -0`);
332
+ }
333
+ if (value < 0 || value > UINT32_MAX) {
334
+ throw new HostDispatcherError('HANDLER_ERROR', `${path} must be between 0 and ${UINT32_MAX}`);
335
+ }
336
+ if (value > max) {
337
+ if (allowOverflow) {
338
+ return null;
339
+ }
340
+ throw new HostDispatcherError('HANDLER_ERROR', `${path} exceeds max_units (${value} > ${max})`);
341
+ }
342
+ return value;
343
+ }
344
+ function fatal(code, message, cause) {
345
+ return {
346
+ kind: 'fatal',
347
+ error: new HostDispatcherError(code, message, { cause }),
348
+ };
349
+ }
350
+ function fatalLimitError(fn) {
351
+ return fatal('INVALID_ARGUMENTS', `fn_id=${fn.fn_id} request exceeded max_request_bytes (${fn.limits.max_request_bytes})`);
352
+ }
353
+ function asUint8Array(input) {
354
+ if (input instanceof Uint8Array) {
355
+ return input;
356
+ }
357
+ if (input instanceof ArrayBuffer) {
358
+ return new Uint8Array(input);
359
+ }
360
+ return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
361
+ }
362
+ function isSizeRelatedDvError(err) {
363
+ return (err instanceof DvError &&
364
+ (err.code === 'ENCODED_TOO_LARGE' ||
365
+ err.code === 'STRING_TOO_LONG' ||
366
+ err.code === 'ARRAY_TOO_LONG' ||
367
+ err.code === 'MAP_TOO_LONG' ||
368
+ err.code === 'DEPTH_EXCEEDED'));
369
+ }
370
+ function withErrorTags(fn) {
371
+ const errorTagMap = new Map();
372
+ for (const entry of fn.error_codes) {
373
+ errorTagMap.set(entry.code, entry.tag);
374
+ }
375
+ return { ...fn, errorTagMap };
376
+ }
377
+ function stringifyError(err) {
378
+ if (err instanceof Error) {
379
+ return err.message;
380
+ }
381
+ return String(err);
382
+ }
383
+ function withinBounds(view, offset, length) {
384
+ if (length === 0) {
385
+ return offset <= view.byteLength;
386
+ }
387
+ if (offset > view.byteLength) {
388
+ return false;
389
+ }
390
+ return length <= view.byteLength - offset;
391
+ }
392
+ function rangesOverlap(aOffset, aLength, bOffset, bLength) {
393
+ if (aLength === 0 || bLength === 0) {
394
+ return false;
395
+ }
396
+ return aOffset < bOffset + bLength && bOffset < aOffset + aLength;
397
+ }
@@ -0,0 +1,35 @@
1
+ import { DV, DvLimits } from '@blue-quickjs/dv';
2
+ export interface ProgramArtifact {
3
+ code: string;
4
+ abiId: string;
5
+ abiVersion: number;
6
+ abiManifestHash: string;
7
+ engineBuildHash?: string;
8
+ }
9
+ export interface ProgramArtifactLimits {
10
+ maxCodeUnits: number;
11
+ maxAbiIdLength: number;
12
+ }
13
+ export declare const PROGRAM_LIMIT_DEFAULTS: Readonly<ProgramArtifactLimits>;
14
+ export interface ProgramValidationOptions {
15
+ limits?: Partial<ProgramArtifactLimits>;
16
+ }
17
+ export interface InputEnvelope {
18
+ event: DV;
19
+ eventCanonical: DV;
20
+ steps: DV;
21
+ }
22
+ export interface InputValidationOptions {
23
+ dvLimits?: Partial<DvLimits>;
24
+ }
25
+ export type RuntimeValidationErrorCode = 'INVALID_TYPE' | 'MISSING_FIELD' | 'UNKNOWN_FIELD' | 'EMPTY_STRING' | 'EXCEEDS_LIMIT' | 'INVALID_HEX' | 'OUT_OF_RANGE' | 'DV_INVALID';
26
+ export declare class RuntimeValidationError extends Error {
27
+ readonly code: RuntimeValidationErrorCode;
28
+ readonly path?: string | undefined;
29
+ constructor(code: RuntimeValidationErrorCode, message: string, path?: string | undefined, options?: {
30
+ cause?: unknown;
31
+ });
32
+ }
33
+ export declare function validateProgramArtifact(value: unknown, options?: ProgramValidationOptions): ProgramArtifact;
34
+ export declare function validateInputEnvelope(value: unknown, options?: InputValidationOptions): InputEnvelope;
35
+ //# sourceMappingURL=quickjs-runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quickjs-runtime.d.ts","sourceRoot":"","sources":["../../src/lib/quickjs-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,EAAE,EAGF,QAAQ,EAET,MAAM,kBAAkB,CAAC;AAK1B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAAC,qBAAqB,CAGlE,CAAC;AAEF,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,EAAE,CAAC;IACV,cAAc,EAAE,EAAE,CAAC;IACnB,KAAK,EAAE,EAAE,CAAC;CACX;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC9B;AAED,MAAM,MAAM,0BAA0B,GAClC,cAAc,GACd,eAAe,GACf,eAAe,GACf,cAAc,GACd,eAAe,GACf,aAAa,GACb,cAAc,GACd,YAAY,CAAC;AAEjB,qBAAa,sBAAuB,SAAQ,KAAK;aAE7B,IAAI,EAAE,0BAA0B;aAEhC,IAAI,CAAC,EAAE,MAAM;gBAFb,IAAI,EAAE,0BAA0B,EAChD,OAAO,EAAE,MAAM,EACC,IAAI,CAAC,EAAE,MAAM,YAAA,EAC7B,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKhC;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,wBAAwB,GACjC,eAAe,CAyCjB;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE,sBAAsB,GAC/B,aAAa,CAkBf"}
@@ -0,0 +1,143 @@
1
+ import { DV_LIMIT_DEFAULTS, DvError, validateDv, } from '@blue-quickjs/dv';
2
+ const UINT32_MAX = 0xffffffff;
3
+ const SHA256_HEX_LENGTH = 64;
4
+ const HEX_RE = /^[0-9a-f]+$/;
5
+ export const PROGRAM_LIMIT_DEFAULTS = {
6
+ maxCodeUnits: 1_048_576, // 1 MiB in UTF-16 code units
7
+ maxAbiIdLength: 128,
8
+ };
9
+ export class RuntimeValidationError extends Error {
10
+ code;
11
+ path;
12
+ constructor(code, message, path, options) {
13
+ super(message, options);
14
+ this.code = code;
15
+ this.path = path;
16
+ this.name = 'RuntimeValidationError';
17
+ }
18
+ }
19
+ export function validateProgramArtifact(value, options) {
20
+ const limits = normalizeProgramLimits(options?.limits);
21
+ const program = expectPlainObject(value, 'program');
22
+ enforceExactKeys(program, ['code', 'abiId', 'abiVersion', 'abiManifestHash', 'engineBuildHash'], 'program');
23
+ const code = expectString(program.code, 'program.code', {
24
+ maxLength: limits.maxCodeUnits,
25
+ allowEmpty: true,
26
+ });
27
+ const abiId = expectString(program.abiId, 'program.abiId', {
28
+ maxLength: limits.maxAbiIdLength,
29
+ });
30
+ const abiVersion = expectUint(program.abiVersion, 1, UINT32_MAX, 'program.abiVersion');
31
+ const abiManifestHash = expectHexString(program.abiManifestHash, 'program.abiManifestHash', { exactLength: SHA256_HEX_LENGTH });
32
+ const engineBuildHash = program.engineBuildHash !== undefined
33
+ ? expectHexString(program.engineBuildHash, 'program.engineBuildHash', {
34
+ exactLength: SHA256_HEX_LENGTH,
35
+ })
36
+ : undefined;
37
+ return {
38
+ code,
39
+ abiId,
40
+ abiVersion,
41
+ abiManifestHash,
42
+ engineBuildHash,
43
+ };
44
+ }
45
+ export function validateInputEnvelope(value, options) {
46
+ const dvLimits = normalizeDvLimits(options?.dvLimits);
47
+ const input = expectPlainObject(value, 'input');
48
+ enforceExactKeys(input, ['event', 'eventCanonical', 'steps'], 'input');
49
+ const event = validateDvField(input.event, dvLimits, 'input.event');
50
+ const eventCanonical = validateDvField(input.eventCanonical, dvLimits, 'input.eventCanonical');
51
+ const steps = validateDvField(input.steps, dvLimits, 'input.steps');
52
+ return {
53
+ event,
54
+ eventCanonical,
55
+ steps,
56
+ };
57
+ }
58
+ function validateDvField(value, limits, path) {
59
+ try {
60
+ validateDv(value, { limits });
61
+ return value;
62
+ }
63
+ catch (err) {
64
+ if (err instanceof DvError) {
65
+ throw runtimeError('DV_INVALID', `${path} is not valid DV: ${err.message}`, path, err);
66
+ }
67
+ throw err;
68
+ }
69
+ }
70
+ function expectPlainObject(value, path) {
71
+ if (value === null ||
72
+ typeof value !== 'object' ||
73
+ Array.isArray(value) ||
74
+ Object.getPrototypeOf(value) !== Object.prototype) {
75
+ throw runtimeError('INVALID_TYPE', `${path} must be a plain object`, path);
76
+ }
77
+ return value;
78
+ }
79
+ function enforceExactKeys(value, allowed, path) {
80
+ for (const key of Object.keys(value)) {
81
+ if (!allowed.includes(key)) {
82
+ throw runtimeError('UNKNOWN_FIELD', `${path} contains unknown field "${key}"`, `${path}.${key}`);
83
+ }
84
+ }
85
+ }
86
+ function expectString(value, path, options) {
87
+ if (typeof value !== 'string') {
88
+ throw runtimeError('INVALID_TYPE', `${path} must be a string`, path);
89
+ }
90
+ if (!options?.allowEmpty && value.length === 0) {
91
+ throw runtimeError('EMPTY_STRING', `${path} must not be empty`, path);
92
+ }
93
+ if (options?.maxLength !== undefined && value.length > options.maxLength) {
94
+ throw runtimeError('EXCEEDS_LIMIT', `${path} exceeds maxLength (${value.length} > ${options.maxLength})`, path);
95
+ }
96
+ return value;
97
+ }
98
+ function expectHexString(value, path, options) {
99
+ const hex = expectString(value, path);
100
+ if (options.exactLength !== undefined && hex.length !== options.exactLength) {
101
+ throw runtimeError('INVALID_HEX', `${path} must be ${options.exactLength} hex characters`, path);
102
+ }
103
+ if (options.maxLength !== undefined && hex.length > options.maxLength) {
104
+ throw runtimeError('EXCEEDS_LIMIT', `${path} exceeds maxLength (${hex.length} > ${options.maxLength})`, path);
105
+ }
106
+ if (hex.length % 2 !== 0) {
107
+ throw runtimeError('INVALID_HEX', `${path} must have an even number of hex characters`, path);
108
+ }
109
+ if (!HEX_RE.test(hex)) {
110
+ throw runtimeError('INVALID_HEX', `${path} must be lowercase hex`, path);
111
+ }
112
+ return hex;
113
+ }
114
+ function expectUint(value, min, max, path) {
115
+ if (typeof value !== 'number' || !Number.isInteger(value)) {
116
+ throw runtimeError('INVALID_TYPE', `${path} must be an integer`, path);
117
+ }
118
+ if (Object.is(value, -0)) {
119
+ throw runtimeError('OUT_OF_RANGE', `${path} must not be -0`, path);
120
+ }
121
+ if (value < min || value > max) {
122
+ throw runtimeError('OUT_OF_RANGE', `${path} must be between ${min} and ${max}`, path);
123
+ }
124
+ return value;
125
+ }
126
+ function normalizeProgramLimits(overrides) {
127
+ return {
128
+ maxCodeUnits: overrides?.maxCodeUnits ?? PROGRAM_LIMIT_DEFAULTS.maxCodeUnits,
129
+ maxAbiIdLength: overrides?.maxAbiIdLength ?? PROGRAM_LIMIT_DEFAULTS.maxAbiIdLength,
130
+ };
131
+ }
132
+ function normalizeDvLimits(overrides) {
133
+ return {
134
+ maxDepth: overrides?.maxDepth ?? DV_LIMIT_DEFAULTS.maxDepth,
135
+ maxEncodedBytes: overrides?.maxEncodedBytes ?? DV_LIMIT_DEFAULTS.maxEncodedBytes,
136
+ maxStringBytes: overrides?.maxStringBytes ?? DV_LIMIT_DEFAULTS.maxStringBytes,
137
+ maxArrayLength: overrides?.maxArrayLength ?? DV_LIMIT_DEFAULTS.maxArrayLength,
138
+ maxMapLength: overrides?.maxMapLength ?? DV_LIMIT_DEFAULTS.maxMapLength,
139
+ };
140
+ }
141
+ function runtimeError(code, message, path, cause) {
142
+ return new RuntimeValidationError(code, message, path, { cause });
143
+ }
@@ -0,0 +1,33 @@
1
+ import { type AbiManifest, type CanonicalAbiManifest } from '@blue-quickjs/abi-manifest';
2
+ import { type QuickjsWasmArtifact, type QuickjsWasmBuildMetadata, type QuickjsWasmBuildType, type QuickjsWasmVariant } from '@blue-quickjs/quickjs-wasm';
3
+ import { type HostCallImport, type HostDispatcher, type HostDispatcherHandlers, type HostDispatcherOptions } from './host-dispatcher.js';
4
+ export interface QuickjsWasmModule {
5
+ HEAPU8: Uint8Array;
6
+ cwrap<T extends (...args: unknown[]) => unknown>(ident: string, returnType: string | null, argTypes: Array<string | null>): T;
7
+ UTF8ToString(ptr: number, maxBytesToRead?: number): string;
8
+ _malloc(size: number): number;
9
+ _free(ptr: number): void;
10
+ ready?: Promise<unknown>;
11
+ }
12
+ export interface RuntimeArtifactSelection {
13
+ variant?: QuickjsWasmVariant;
14
+ buildType?: QuickjsWasmBuildType;
15
+ metadata?: QuickjsWasmBuildMetadata;
16
+ wasmBinary?: Uint8Array;
17
+ }
18
+ export interface CreateRuntimeOptions extends HostDispatcherOptions, RuntimeArtifactSelection {
19
+ manifest: AbiManifest;
20
+ handlers: HostDispatcherHandlers;
21
+ }
22
+ export interface RuntimeInstance {
23
+ module: QuickjsWasmModule;
24
+ dispatcher: HostDispatcher;
25
+ hostCall: HostCallImport;
26
+ manifest: CanonicalAbiManifest;
27
+ artifact: QuickjsWasmArtifact;
28
+ metadata: QuickjsWasmBuildMetadata;
29
+ variant: QuickjsWasmVariant;
30
+ buildType: QuickjsWasmBuildType;
31
+ }
32
+ export declare function createRuntime(options: CreateRuntimeOptions): Promise<RuntimeInstance>;
33
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/lib/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,oBAAoB,EAC1B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,EAIxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAGL,KAAK,cAAc,EAEnB,KAAK,cAAc,EACnB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAM9B,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,UAAU,CAAC;IACnB,KAAK,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAC7C,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GAAG,IAAI,EACzB,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,GAC7B,CAAC,CAAC;IACL,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3D,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAEzB,KAAK,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,SAAS,CAAC,EAAE,oBAAoB,CAAC;IACjC,QAAQ,CAAC,EAAE,wBAAwB,CAAC;IACpC,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AAED,MAAM,WAAW,oBACf,SAAQ,qBAAqB,EAAE,wBAAwB;IACvD,QAAQ,EAAE,WAAW,CAAC;IACtB,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,cAAc,CAAC;IAC3B,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,wBAAwB,CAAC;IACnC,OAAO,EAAE,kBAAkB,CAAC;IAC5B,SAAS,EAAE,oBAAoB,CAAC;CACjC;AAQD,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,eAAe,CAAC,CA8D1B"}
@@ -0,0 +1,56 @@
1
+ import { getQuickjsWasmArtifact, loadQuickjsWasmBinary, loadQuickjsWasmMetadata, } from '@blue-quickjs/quickjs-wasm';
2
+ import { createHostCallImport, createHostDispatcher, } from './host-dispatcher.js';
3
+ const UINT32_MAX = 0xffffffff;
4
+ const DEFAULT_VARIANT = 'wasm32';
5
+ const DEFAULT_BUILD_TYPE = 'release';
6
+ export async function createRuntime(options) {
7
+ const variant = options.variant ?? DEFAULT_VARIANT;
8
+ const buildType = options.buildType ?? DEFAULT_BUILD_TYPE;
9
+ if (variant !== 'wasm32') {
10
+ throw new Error(`quickjs-runtime supports wasm32 only; host_call pointers are 32-bit (received variant=${variant})`);
11
+ }
12
+ const metadata = options.metadata ??
13
+ (await loadQuickjsWasmMetadata().catch((error) => {
14
+ throw new Error(`Failed to load QuickJS wasm metadata: ${String(error)}`);
15
+ }));
16
+ const artifact = await getQuickjsWasmArtifact(variant, buildType, metadata);
17
+ const wasmBinary = options.wasmBinary ??
18
+ (await loadQuickjsWasmBinary(variant, buildType, metadata));
19
+ const dispatcher = createHostDispatcher(options.manifest, options.handlers, options);
20
+ const hostMemory = { buffer: new ArrayBuffer(0) };
21
+ const hostCall = createHostCallImport(dispatcher, hostMemory);
22
+ const guardedHostCall = (...args) => {
23
+ if (hostMemory.buffer.byteLength === 0) {
24
+ return UINT32_MAX;
25
+ }
26
+ return hostCall(...args);
27
+ };
28
+ const moduleFactory = (await import(artifact.loaderUrl.href))
29
+ .default;
30
+ const module = await moduleFactory({
31
+ host: { host_call: guardedHostCall },
32
+ wasmBinary: toArrayBuffer(wasmBinary),
33
+ locateFile: (path) => path.endsWith('.wasm') ? artifact.wasmUrl.href : path,
34
+ });
35
+ const buffer = module?.HEAPU8?.buffer;
36
+ if (!buffer) {
37
+ throw new Error('QuickJS wasm module did not expose linear memory');
38
+ }
39
+ hostMemory.buffer = buffer;
40
+ return {
41
+ module,
42
+ dispatcher,
43
+ hostCall,
44
+ manifest: dispatcher.manifest,
45
+ artifact,
46
+ metadata,
47
+ variant,
48
+ buildType,
49
+ };
50
+ }
51
+ function toArrayBuffer(view) {
52
+ if (view.byteOffset === 0 && view.byteLength === view.buffer.byteLength) {
53
+ return view.buffer;
54
+ }
55
+ return view.slice().buffer;
56
+ }