@aresdefencelabs/wasm-http-runtime 0.1.6 → 0.1.8

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,597 @@
1
+ export function createWasmHttpRuntime(config) {
2
+ const {
3
+ wasmModule,
4
+ createModule,
5
+ debug = false,
6
+ responseTtlMs = 60_000,
7
+ maxHttpResponseHandles = 128,
8
+ maxTotalBridgeBytes = 8 * 1024 * 1024,
9
+ maxResponseBodyBytes = 2 * 1024 * 1024,
10
+ onLog = null,
11
+ onError = null,
12
+ beforeOutboundFetch = null,
13
+ afterOutboundFetch = null,
14
+ } = config ?? {};
15
+
16
+ if (!wasmModule) {
17
+ throw new Error("createWasmHttpRuntime: wasmModule is required");
18
+ }
19
+
20
+ if (typeof createModule !== "function") {
21
+ throw new Error("createWasmHttpRuntime: createModule is required");
22
+ }
23
+
24
+ const encoder = new TextEncoder();
25
+ const decoder = new TextDecoder();
26
+
27
+ const state = {
28
+ instance: null,
29
+ requestContext: null,
30
+ nextHttpResponseId: 1,
31
+ httpResponses: new Map(),
32
+ options: {
33
+ debug,
34
+ responseTtlMs,
35
+ maxHttpResponseHandles,
36
+ maxTotalBridgeBytes,
37
+ maxResponseBodyBytes,
38
+ onLog,
39
+ onError,
40
+ beforeOutboundFetch,
41
+ afterOutboundFetch,
42
+ },
43
+ };
44
+
45
+ let instancePromise = null;
46
+ let currentInstance = null;
47
+
48
+ function log(...args) {
49
+ if (state.options.debug) {
50
+ console.log(...args);
51
+ }
52
+ }
53
+
54
+ function pickExport(mod, names) {
55
+ for (const name of names) {
56
+ const fn = mod?.[name];
57
+ if (typeof fn === "function") {
58
+ return fn;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+
64
+ function getModule(instance) {
65
+ if (!instance) {
66
+ throw new Error("WASM module is not available");
67
+ }
68
+ return instance;
69
+ }
70
+
71
+ function getHeapU8(mod) {
72
+ if (mod?.HEAPU8 instanceof Uint8Array) {
73
+ return mod.HEAPU8;
74
+ }
75
+ throw new Error("HEAPU8 is not exported on the Emscripten module");
76
+ }
77
+
78
+ function getBuffer(mod) {
79
+ return getHeapU8(mod).buffer;
80
+ }
81
+
82
+ function getCurrentBuffer() {
83
+ if (!currentInstance) {
84
+ throw new Error("WASM instance not ready");
85
+ }
86
+ return getBuffer(currentInstance);
87
+ }
88
+
89
+ function safeSetU32(ptr, value) {
90
+ if (!ptr) return;
91
+ const buffer = getCurrentBuffer();
92
+ const start = ptr >>> 0;
93
+ if (start + 4 > buffer.byteLength) return;
94
+ new DataView(buffer).setUint32(start, value >>> 0, true);
95
+ }
96
+
97
+ function safeSetU64Zero(ptr) {
98
+ if (!ptr) return;
99
+ const buffer = getCurrentBuffer();
100
+ const start = ptr >>> 0;
101
+ if (start + 8 > buffer.byteLength) return;
102
+ const view = new DataView(buffer);
103
+ view.setUint32(start, 0, true);
104
+ view.setUint32(start + 4, 0, true);
105
+ }
106
+
107
+ function utf8ByteLength(value) {
108
+ return encoder.encode(value).length;
109
+ }
110
+
111
+ function normalizeHeaders(headers) {
112
+ const out = {};
113
+ for (const [key, value] of headers.entries()) {
114
+ out[key.toLowerCase()] = value;
115
+ }
116
+ return out;
117
+ }
118
+
119
+ function searchParamsToObject(searchParams) {
120
+ const out = {};
121
+ for (const [key, value] of searchParams.entries()) {
122
+ out[key] = value;
123
+ }
124
+ return out;
125
+ }
126
+
127
+ async function readRequestBodyAsText(request) {
128
+ if (request.method === "GET" || request.method === "HEAD") {
129
+ return "";
130
+ }
131
+ return await request.text();
132
+ }
133
+
134
+ async function buildInboundEnvelope(request) {
135
+ const url = new URL(request.url);
136
+ const bodyText = await readRequestBodyAsText(request);
137
+
138
+ return {
139
+ method: request.method,
140
+ url: request.url,
141
+ path: url.pathname,
142
+ body: bodyText,
143
+ headers: normalizeHeaders(request.headers),
144
+ query: searchParamsToObject(url.searchParams),
145
+ };
146
+ }
147
+
148
+ function estimateResponseBytes(resp) {
149
+ let total = utf8ByteLength(resp.bodyText);
150
+ for (const [k, v] of Object.entries(resp.headers)) {
151
+ total += utf8ByteLength(k);
152
+ total += utf8ByteLength(v);
153
+ }
154
+ return total >>> 0;
155
+ }
156
+
157
+ function sweepExpiredResponses() {
158
+ const now = Date.now();
159
+ const ttl = state.options.responseTtlMs;
160
+
161
+ for (const [id, entry] of state.httpResponses.entries()) {
162
+ if (now - entry.createdAtMs > ttl) {
163
+ state.httpResponses.delete(id);
164
+ }
165
+ }
166
+ }
167
+
168
+ function getCurrentBridgeBytes() {
169
+ let total = 0;
170
+ for (const entry of state.httpResponses.values()) {
171
+ total += entry.estimatedBytes;
172
+ }
173
+ return total >>> 0;
174
+ }
175
+
176
+ function assertBridgeCapacity(nextEntryBytes) {
177
+ sweepExpiredResponses();
178
+
179
+ if (state.httpResponses.size >= state.options.maxHttpResponseHandles) {
180
+ throw new Error("Too many outstanding HTTP response handles");
181
+ }
182
+
183
+ const totalAfterInsert = getCurrentBridgeBytes() + nextEntryBytes;
184
+ if (totalAfterInsert > state.options.maxTotalBridgeBytes) {
185
+ throw new Error("HTTP response bridge memory budget exceeded");
186
+ }
187
+ }
188
+
189
+ function getResponseByIdOrEmpty(responseId) {
190
+ sweepExpiredResponses();
191
+
192
+ return (
193
+ state.httpResponses.get(responseId) ?? {
194
+ status: 0,
195
+ bodyText: "",
196
+ headers: {},
197
+ createdAtMs: 0,
198
+ estimatedBytes: 0,
199
+ }
200
+ );
201
+ }
202
+
203
+ function getAlloc(mod) {
204
+ const alloc = pickExport(mod, ["_alloc", "alloc", "_malloc", "malloc"]);
205
+ if (!alloc) {
206
+ throw new Error("No alloc/malloc export found");
207
+ }
208
+ return alloc;
209
+ }
210
+
211
+ function getFree(mod) {
212
+ const freeMem = pickExport(mod, ["_free_mem", "free_mem"]);
213
+ if (freeMem) {
214
+ return { fn: freeMem, takesSize: true };
215
+ }
216
+
217
+ const free = pickExport(mod, ["_free", "free"]);
218
+ if (free) {
219
+ return { fn: free, takesSize: false };
220
+ }
221
+
222
+ throw new Error("No free export found");
223
+ }
224
+
225
+ function writeCString(mod, value) {
226
+ const encoded = encoder.encode(value);
227
+ const alloc = getAlloc(mod);
228
+ const ptr = alloc(encoded.length + 1) >>> 0;
229
+
230
+ if (!ptr) {
231
+ throw new Error(`alloc failed for ${encoded.length + 1} bytes`);
232
+ }
233
+
234
+ const heap = getHeapU8(mod);
235
+ if (ptr + encoded.length + 1 > heap.length) {
236
+ throw new Error("Allocated CString buffer exceeds wasm memory bounds");
237
+ }
238
+
239
+ heap.set(encoded, ptr);
240
+ heap[ptr + encoded.length] = 0;
241
+
242
+ return {
243
+ ptr,
244
+ size: encoded.length + 1,
245
+ };
246
+ }
247
+
248
+ function freePtr(mod, ptr, size = 0) {
249
+ if (!ptr) return;
250
+
251
+ const free = getFree(mod);
252
+ if (free.takesSize) {
253
+ free.fn(ptr >>> 0, size >>> 0);
254
+ } else {
255
+ free.fn(ptr >>> 0);
256
+ }
257
+ }
258
+
259
+ function readCStringFromModule(mod, ptr, maxLen = 10_000_000) {
260
+ if (!ptr) return "";
261
+
262
+ if (typeof mod.UTF8ToString === "function") {
263
+ return mod.UTF8ToString(ptr);
264
+ }
265
+
266
+ const bytes = getHeapU8(mod);
267
+ const start = ptr >>> 0;
268
+
269
+ if (start >= bytes.length) {
270
+ throw new Error(`CString pointer out of bounds: ${ptr}`);
271
+ }
272
+
273
+ let end = start;
274
+ const limit = Math.min(bytes.length, start + maxLen);
275
+
276
+ while (end < limit && bytes[end] !== 0) {
277
+ end++;
278
+ }
279
+
280
+ if (end >= limit) {
281
+ throw new Error("CString terminator not found within safe bounds");
282
+ }
283
+
284
+ return decoder.decode(bytes.subarray(start, end));
285
+ }
286
+
287
+ function copyUtf8ToHeap(mod, value, outPtr, maxLen) {
288
+ const bytes = encoder.encode(value);
289
+ const count = Math.min(bytes.length, maxLen >>> 0);
290
+ getHeapU8(mod).set(bytes.subarray(0, count), outPtr >>> 0);
291
+ return count >>> 0;
292
+ }
293
+
294
+ function jsonResponse(status, body) {
295
+ return new Response(JSON.stringify(body), {
296
+ status,
297
+ headers: { "content-type": "application/json; charset=utf-8" },
298
+ });
299
+ }
300
+
301
+ function normalizeResponseEnvelope(responseEnvelope) {
302
+ const status = Number.isInteger(responseEnvelope?.status)
303
+ ? responseEnvelope.status
304
+ : 200;
305
+
306
+ const headers =
307
+ responseEnvelope?.headers &&
308
+ typeof responseEnvelope.headers === "object" &&
309
+ !Array.isArray(responseEnvelope.headers)
310
+ ? responseEnvelope.headers
311
+ : { "content-type": "application/json; charset=utf-8" };
312
+
313
+ let body = responseEnvelope?.body ?? "";
314
+
315
+ if (typeof body !== "string") {
316
+ body = JSON.stringify(body);
317
+ }
318
+
319
+ return { status, headers, body };
320
+ }
321
+
322
+ function makeBaseImports() {
323
+ return {
324
+ env: {
325
+ emscripten_notify_memory_growth: () => {},
326
+ emscripten_date_now: () => Date.now(),
327
+ emscripten_get_now: () => performance.now(),
328
+
329
+ abort: (msg, file, line, col) => {
330
+ throw new Error(
331
+ `abort: ${String(msg)} @ ${String(file)}:${String(line)}:${String(col)}`
332
+ );
333
+ },
334
+ },
335
+
336
+ wasi_snapshot_preview1: {
337
+ fd_write(_fd, _iovs, _iovsLen, nwritten) {
338
+ safeSetU32(nwritten, 0);
339
+ return 0;
340
+ },
341
+
342
+ fd_read(_fd, _iovs, _iovsLen, nread) {
343
+ safeSetU32(nread, 0);
344
+ return 0;
345
+ },
346
+
347
+ fd_seek(_fd, _offsetLow, _offsetHigh, _whence, newOffset) {
348
+ safeSetU64Zero(newOffset);
349
+ return 0;
350
+ },
351
+
352
+ fd_close(_fd) {
353
+ return 0;
354
+ },
355
+ },
356
+ };
357
+ }
358
+
359
+ async function bootWasm() {
360
+ if (instancePromise) {
361
+ return instancePromise;
362
+ }
363
+
364
+ instancePromise = (async () => {
365
+ const baseImports = makeBaseImports();
366
+ let mod = null;
367
+
368
+ mod = await createModule({
369
+ ...baseImports,
370
+
371
+ instantiateWasm(wasmImports, successCallback) {
372
+ const finalImports = {
373
+ ...wasmImports,
374
+ env: {
375
+ ...(wasmImports.env || {}),
376
+ ...(baseImports.env || {}),
377
+ },
378
+ wasi_snapshot_preview1: {
379
+ ...(wasmImports.wasi_snapshot_preview1 || {}),
380
+ ...(baseImports.wasi_snapshot_preview1 || {}),
381
+ },
382
+ };
383
+
384
+ const instance = new WebAssembly.Instance(wasmModule, finalImports);
385
+ successCallback(instance);
386
+ return instance.exports;
387
+ },
388
+
389
+ __aresAbiLog(messagePtr) {
390
+ const message = readCStringFromModule(mod, messagePtr);
391
+
392
+ if (typeof state.options.onLog === "function") {
393
+ state.options.onLog(message);
394
+ } else if (state.options.debug) {
395
+ console.log("[AresWasm]", message);
396
+ }
397
+ },
398
+
399
+ __aresAbiHttpGetUserAgentName() {
400
+ const ctx = state.requestContext;
401
+ const userAgent =
402
+ ctx?.request?.headers.get("user-agent") ?? "Cloudflare-Worker";
403
+
404
+ const written = writeCString(mod, userAgent);
405
+ return written.ptr >>> 0;
406
+ },
407
+
408
+ __aresAbiHttpFetchBlockingAsync: async (requestJsonCstrPtr) => {
409
+ log("[AresHost] __aresAbiHttpFetchBlockingAsync entered");
410
+
411
+ try {
412
+ const requestJson = readCStringFromModule(mod, requestJsonCstrPtr);
413
+ const outbound = JSON.parse(requestJson);
414
+
415
+ if (typeof state.options.beforeOutboundFetch === "function") {
416
+ state.options.beforeOutboundFetch(outbound);
417
+ }
418
+
419
+ const response = await fetch(outbound.url, {
420
+ method: outbound.method ?? "GET",
421
+ headers: outbound.headers ?? {},
422
+ body: outbound.body ?? undefined,
423
+ });
424
+
425
+ const bodyText = await response.text();
426
+
427
+ if (typeof state.options.afterOutboundFetch === "function") {
428
+ state.options.afterOutboundFetch(response, bodyText, outbound);
429
+ }
430
+
431
+ const bodyBytes = utf8ByteLength(bodyText);
432
+ if (bodyBytes > state.options.maxResponseBodyBytes) {
433
+ throw new Error(
434
+ `HTTP response body too large: ${bodyBytes} bytes exceeds maxResponseBodyBytes=${state.options.maxResponseBodyBytes}`
435
+ );
436
+ }
437
+
438
+ const headers = normalizeHeaders(response.headers);
439
+ const estimatedBytes = estimateResponseBytes({
440
+ bodyText,
441
+ headers,
442
+ });
443
+
444
+ assertBridgeCapacity(estimatedBytes);
445
+
446
+ const responseId = state.nextHttpResponseId++;
447
+ state.httpResponses.set(responseId, {
448
+ status: response.status,
449
+ bodyText,
450
+ headers,
451
+ createdAtMs: Date.now(),
452
+ estimatedBytes,
453
+ });
454
+
455
+ return responseId >>> 0;
456
+ } catch (error) {
457
+ console.error("[AresHost] __aresAbiHttpFetchBlockingAsync failed:", error);
458
+ throw error;
459
+ }
460
+ },
461
+
462
+ __aresAbiHttpResponseGetStatus(responseId) {
463
+ return getResponseByIdOrEmpty(responseId).status >>> 0;
464
+ },
465
+
466
+ __aresAbiHttpResponseGetBodyLen(responseId) {
467
+ return utf8ByteLength(getResponseByIdOrEmpty(responseId).bodyText);
468
+ },
469
+
470
+ __aresAbiHttpResponseCopyBody(responseId, outPtr, maxLen) {
471
+ return copyUtf8ToHeap(
472
+ mod,
473
+ getResponseByIdOrEmpty(responseId).bodyText,
474
+ outPtr,
475
+ maxLen
476
+ );
477
+ },
478
+
479
+ __aresAbiHttpResponseCopyHeader(responseId, keyPtr, outPtr, maxLen) {
480
+ const key = readCStringFromModule(mod, keyPtr).toLowerCase();
481
+ const value = getResponseByIdOrEmpty(responseId).headers[key] ?? "";
482
+ return copyUtf8ToHeap(mod, value, outPtr, maxLen);
483
+ },
484
+
485
+ __aresAbiHttpResponseFree(responseId) {
486
+ state.httpResponses.delete(responseId);
487
+ },
488
+ });
489
+
490
+ currentInstance = mod;
491
+ state.instance = mod;
492
+
493
+ log("WASM module keys:", Object.keys(mod));
494
+ log("HEAPU8 exported:", mod.HEAPU8 instanceof Uint8Array);
495
+ log("UTF8ToString exported:", typeof mod.UTF8ToString === "function");
496
+ log("ccall exported:", typeof mod.ccall === "function");
497
+
498
+ getHeapU8(mod);
499
+
500
+ const appInit = pickExport(mod, ["_app_init", "app_init"]);
501
+ if (appInit) {
502
+ const rc = await Promise.resolve(appInit());
503
+ if (rc !== 0) {
504
+ throw new Error(`WASM app_init failed rc=${rc}`);
505
+ }
506
+ }
507
+
508
+ return mod;
509
+ })();
510
+
511
+ return instancePromise;
512
+ }
513
+
514
+ const ready = bootWasm();
515
+
516
+ async function handleFetch(request, env, ctx) {
517
+ let requestPtr = 0;
518
+ let requestSize = 0;
519
+ let responsePtr = 0;
520
+
521
+ try {
522
+ const inst = await ready;
523
+ const mod = getModule(inst);
524
+
525
+ state.requestContext = { request, env, ctx };
526
+
527
+ if (typeof mod.ccall !== "function") {
528
+ throw new Error("WASM runtime method ccall() is not available");
529
+ }
530
+
531
+ const inboundEnvelope = await buildInboundEnvelope(request);
532
+ const requestJson = JSON.stringify(inboundEnvelope);
533
+
534
+ log("[AresHost] inbound envelope =", inboundEnvelope);
535
+
536
+ const written = writeCString(mod, requestJson);
537
+ requestPtr = written.ptr;
538
+ requestSize = written.size;
539
+
540
+ responsePtr = await mod.ccall(
541
+ "handle_http",
542
+ "number",
543
+ ["number"],
544
+ [requestPtr],
545
+ { async: true }
546
+ );
547
+
548
+ if (!responsePtr) {
549
+ throw new Error("WASM returned null response pointer");
550
+ }
551
+
552
+ const responseJson = readCStringFromModule(mod, responsePtr);
553
+ const responseEnvelope = JSON.parse(responseJson);
554
+ const normalized = normalizeResponseEnvelope(responseEnvelope);
555
+
556
+ log("[AresHost] response envelope =", normalized);
557
+
558
+ return new Response(normalized.body, {
559
+ status: normalized.status,
560
+ headers: normalized.headers,
561
+ });
562
+ } catch (err) {
563
+ if (typeof state.options.onError === "function") {
564
+ try {
565
+ state.options.onError(err);
566
+ } catch {}
567
+ }
568
+
569
+ return jsonResponse(500, {
570
+ error: "worker_wasm_failure",
571
+ message: err instanceof Error ? err.message : String(err),
572
+ });
573
+ } finally {
574
+ state.requestContext = null;
575
+
576
+ try {
577
+ const inst = instancePromise
578
+ ? await instancePromise.catch(() => null)
579
+ : null;
580
+
581
+ if (inst) {
582
+ freePtr(inst, requestPtr, requestSize);
583
+ freePtr(inst, responsePtr);
584
+ }
585
+ } catch {
586
+ // ignore cleanup failures
587
+ }
588
+ }
589
+ }
590
+
591
+ return {
592
+ fetch: handleFetch,
593
+ ready: () => ready,
594
+ getModule: () => currentInstance,
595
+ getState: () => state,
596
+ };
597
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aresdefencelabs/wasm-http-runtime",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Runtime adapter that connects C++ WebAssembly workers to the Cloudflare Workers runtime via an ABI bridge.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -36,5 +36,5 @@
36
36
  },
37
37
  "main": "./dist/index.js",
38
38
  "types": "./dist/index.d.ts",
39
- "files": ["dist", "emscripten"]
39
+ "files": ["dist", "emscripten", "hostingEngine"]
40
40
  }