@eryx-org/eryx 0.0.0 → 0.15.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 (63) hide show
  1. package/README.md +281 -4
  2. package/eryx-sandbox.conditional-host-import-0.wasm +0 -0
  3. package/eryx-sandbox.conditional-host-import-1.wasm +0 -0
  4. package/eryx-sandbox.conditional-suspending-1-i32-to-i32.wasm +0 -0
  5. package/eryx-sandbox.core.wasm +0 -0
  6. package/eryx-sandbox.core10.wasm +0 -0
  7. package/eryx-sandbox.core11.wasm +0 -0
  8. package/eryx-sandbox.core12.wasm +0 -0
  9. package/eryx-sandbox.core13.wasm +0 -0
  10. package/eryx-sandbox.core2.wasm +0 -0
  11. package/eryx-sandbox.core3.wasm +0 -0
  12. package/eryx-sandbox.core4.wasm +0 -0
  13. package/eryx-sandbox.core5.wasm +0 -0
  14. package/eryx-sandbox.core6.wasm +0 -0
  15. package/eryx-sandbox.core7.wasm +0 -0
  16. package/eryx-sandbox.core8.wasm +0 -0
  17. package/eryx-sandbox.core9.wasm +0 -0
  18. package/eryx-sandbox.d.ts +163 -0
  19. package/eryx-sandbox.js +43541 -0
  20. package/index.d.ts +331 -0
  21. package/index.js +698 -0
  22. package/instance.js +174 -0
  23. package/interfaces/eryx-net-tcp.d.ts +30 -0
  24. package/interfaces/eryx-net-tls.d.ts +24 -0
  25. package/interfaces/wasi-cli-environment.d.ts +3 -0
  26. package/interfaces/wasi-cli-exit.d.ts +3 -0
  27. package/interfaces/wasi-cli-stderr.d.ts +3 -0
  28. package/interfaces/wasi-cli-stdin.d.ts +3 -0
  29. package/interfaces/wasi-cli-stdout.d.ts +3 -0
  30. package/interfaces/wasi-cli-terminal-input.d.ts +8 -0
  31. package/interfaces/wasi-cli-terminal-output.d.ts +8 -0
  32. package/interfaces/wasi-cli-terminal-stderr.d.ts +3 -0
  33. package/interfaces/wasi-cli-terminal-stdin.d.ts +3 -0
  34. package/interfaces/wasi-cli-terminal-stdout.d.ts +3 -0
  35. package/interfaces/wasi-clocks-monotonic-clock.d.ts +8 -0
  36. package/interfaces/wasi-clocks-wall-clock.d.ts +7 -0
  37. package/interfaces/wasi-filesystem-preopens.d.ts +3 -0
  38. package/interfaces/wasi-filesystem-types.d.ts +207 -0
  39. package/interfaces/wasi-io-error.d.ts +8 -0
  40. package/interfaces/wasi-io-poll.d.ts +10 -0
  41. package/interfaces/wasi-io-streams.d.ts +33 -0
  42. package/interfaces/wasi-random-random.d.ts +2 -0
  43. package/interfaces/wasi-sockets-instance-network.d.ts +3 -0
  44. package/interfaces/wasi-sockets-ip-name-lookup.d.ts +15 -0
  45. package/interfaces/wasi-sockets-network.d.ts +92 -0
  46. package/interfaces/wasi-sockets-tcp-create-socket.d.ts +5 -0
  47. package/interfaces/wasi-sockets-tcp.d.ts +52 -0
  48. package/interfaces/wasi-sockets-udp-create-socket.d.ts +5 -0
  49. package/interfaces/wasi-sockets-udp.d.ts +51 -0
  50. package/package.json +69 -3
  51. package/python-stdlib.tar.gz +0 -0
  52. package/shims/callbacks.d.ts +45 -0
  53. package/shims/callbacks.js +469 -0
  54. package/shims/ip.js +46 -0
  55. package/shims/net-fetch.js +264 -0
  56. package/shims/net-node.js +236 -0
  57. package/shims/net.d.ts +84 -0
  58. package/shims/net.js +255 -0
  59. package/shims/preview2-node.js +35 -0
  60. package/shims/raw-json.js +111 -0
  61. package/shims/sockets.js +53 -0
  62. package/stdlib-loader.js +174 -0
  63. package/worker.js +100 -0
@@ -0,0 +1,469 @@
1
+ /**
2
+ * Callback shims for the eryx sandbox.
3
+ *
4
+ * These provide the host-side implementations of the sandbox's callback imports:
5
+ * - invoke: call a registered callback by name with JSON arguments
6
+ * - sleep: back the sandbox event loop's timers (asyncio.sleep, wait_for)
7
+ * - listCallbacks: list all registered callbacks
8
+ * - getExecutionOptions: return per-execution behavior options
9
+ * - reportTrace: receive trace events from the Python runtime
10
+ * - reportOutput: receive streaming stdout/stderr output from the Python runtime
11
+ *
12
+ * Each sandbox instance gets its own set from createCallbackHost(), with its
13
+ * own replay state and output decoders. Its callbacks come from that
14
+ * sandbox's `callbacks` option; trace and output handlers from its options,
15
+ * falling back to the module-wide defaults set by setTraceHandler() and
16
+ * setOutputHandler().
17
+ */
18
+
19
+ import { parseJson } from "./raw-json.js";
20
+
21
+ /**
22
+ * Module-wide handlers, used by every sandbox that doesn't set its own.
23
+ * @type {{traceHandler: ((lineno: number, eventJson: string, contextJson: string) => void) | null, outputHandler: ((stream: number, data: string) => void) | null}}
24
+ */
25
+ const defaults = {
26
+ traceHandler: null,
27
+ outputHandler: null,
28
+ };
29
+
30
+ /** A JSON.stringify replacer writing BigInts as exact integers */
31
+ const exactBigInts = (_key, value) =>
32
+ typeof value === "bigint" && JSON.rawJSON ? JSON.rawJSON(value.toString()) : value;
33
+
34
+ /**
35
+ * A sandbox's callbacks, from the `callbacks` option: what `list_callbacks()`
36
+ * shows the code, and the handler the host calls with each call's name and
37
+ * arguments JSON, which answers with the result's JSON.
38
+ * @param {import("@eryx-org/eryx-run/shared").Callbacks} callbacks
39
+ */
40
+ function fromCallbacks(callbacks) {
41
+ if (Array.isArray(callbacks) || typeof callbacks !== "object" || callbacks === null) {
42
+ throw new TypeError(
43
+ "callbacks is an object of functions by name, e.g. { get_user: async ({ id }) => ... } " +
44
+ "(the list of {name, description} with a callbackHandler is gone)",
45
+ );
46
+ }
47
+ const handlers = new Map();
48
+ const infos = [];
49
+ for (const [name, callback] of Object.entries(callbacks)) {
50
+ const fn = typeof callback === "function" ? callback : callback?.handler;
51
+ if (typeof fn !== "function") {
52
+ throw new TypeError(`callback ${JSON.stringify(name)} isn't a function or { handler }`);
53
+ }
54
+ handlers.set(name, fn);
55
+ const description = typeof callback === "function" ? "" : (callback.description ?? "");
56
+ infos.push({ name, description, parametersSchemaJson: "{}" });
57
+ }
58
+ /** @param {string} name @param {string} argsJson */
59
+ const handler = async (name, argsJson) => {
60
+ try {
61
+ const fn = handlers.get(name);
62
+ if (!fn) throw new Error(`no callback ${name}`);
63
+ // undefined isn't JSON. A BigInt is written exactly, and so is what a
64
+ // handler wraps in JSON.rawJSON (e.g. "1.0", to stay a float)
65
+ return JSON.stringify((await fn(JSON.parse(argsJson))) ?? null, exactBigInts);
66
+ } catch (e) {
67
+ if (e instanceof SuspendCallback) throw e;
68
+ // What the handler threw, or a result JSON can't hold (a cycle), is an
69
+ // exception in Python, as on eryx.run: `RuntimeError("RangeError: ...")`
70
+ return { tag: "err", val: e instanceof Error ? `${e.name}: ${e.message}` : String(e) };
71
+ }
72
+ };
73
+ return { infos, handler };
74
+ }
75
+
76
+ /**
77
+ * Set the default handler for trace events from the Python runtime.
78
+ *
79
+ * @param {((lineno: number, eventJson: string, contextJson: string) => void) | null} handler
80
+ */
81
+ export function setTraceHandler(handler) {
82
+ defaults.traceHandler = handler;
83
+ }
84
+
85
+ /**
86
+ * Set the default handler for streaming output (stdout/stderr) from the Python
87
+ * runtime.
88
+ *
89
+ * The handler is called in real-time as Python code writes to stdout or stderr,
90
+ * rather than waiting for execution to complete.
91
+ *
92
+ * @param {((stream: number, data: string) => void) | null} handler
93
+ * stream: 0 = stdout, 1 = stderr
94
+ * data: the text written
95
+ */
96
+ export function setOutputHandler(handler) {
97
+ defaults.outputHandler = handler;
98
+ }
99
+
100
+ /**
101
+ * Thrown by a callback handler to suspend execution ("retry later").
102
+ *
103
+ * The guest halts immediately: no further Python runs and no further callbacks
104
+ * dispatch. Under executeWithJournal() the suspension is surfaced as
105
+ * `outcome.suspended` and the call is not journaled, so it re-runs live when the
106
+ * recorded journal is replayed. Under plain execute() it rejects like any error.
107
+ */
108
+ export class SuspendCallback extends Error {
109
+ /** @param {string} reason - Opaque reason, surfaced to the caller */
110
+ constructor(reason) {
111
+ super(reason);
112
+ this.name = "SuspendCallback";
113
+ this.reason = reason;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * A recorded callback outcome. `ok` is the handler's exact JSON result text (not
119
+ * re-parsed, so replay returns byte-for-byte what Python saw live); `err` is a
120
+ * Python-visible error message, or the message of a thrown Error when `thrown`.
121
+ * @typedef {{ok: string} | {err: string, thrown?: boolean}} RecordedResult
122
+ */
123
+
124
+ /**
125
+ * Replay state for an executeWithJournal() run. Mirrors the Rust `ReplayState`
126
+ * (crates/eryx/src/replay.rs): cached results bucketed by (name, canonical
127
+ * args) as a FIFO multiset, a sticky divergence guard, and a gate that rejects
128
+ * every call after a suspension.
129
+ * @typedef {{cached: Map<string, RecordedResult[]>, liveMode: boolean, nextSeq: number, entries: Array<{name: string, argsJson: string, result: RecordedResult}|undefined>, suspended: Object|null, suspendSeq: number|null, replayedCount: number}} ReplayState
130
+ */
131
+
132
+ /**
133
+ * Parse a serialized journal into its entries.
134
+ *
135
+ * Parsed with {@link parseJson} rather than `JSON.parse` so each `Ok` value
136
+ * keeps its exact text (`1.0` stays a float, large integers stay exact).
137
+ * @param {string} journal
138
+ * @returns {Array<{name: string, argsJson: string, result: RecordedResult}>}
139
+ */
140
+ function _parseJournal(journal) {
141
+ if (typeof journal !== "string") {
142
+ throw new TypeError(
143
+ "journal must be the JSON string from a previous executeWithJournal() outcome",
144
+ );
145
+ }
146
+ const invalid = () =>
147
+ new TypeError("journal is not a valid callback journal");
148
+ const field = (node, key, kind) => {
149
+ const value = node?.kind === "object" ? node.value.get(key) : undefined;
150
+ if (value?.kind !== kind) throw invalid();
151
+ return value;
152
+ };
153
+ const entries = field(parseJson(journal), "entries", "array").value;
154
+ return entries.map((entry) => {
155
+ const result = field(entry, "result", "object").value;
156
+ const ok = result.get("Ok");
157
+ const err = result.get("Err");
158
+ if (!(ok || err?.kind === "string")) throw invalid();
159
+ return {
160
+ name: field(entry, "name", "string").value,
161
+ argsJson: field(entry, "args_json", "string").value,
162
+ result: ok
163
+ ? { ok: ok.raw }
164
+ : { err: err.value, thrown: entry.value.get("thrown")?.raw === "true" },
165
+ };
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Build the replay state for a run, replaying results from `journal` if given.
171
+ * @param {string} [journal] - A journal string from a previous outcome
172
+ * @returns {ReplayState}
173
+ */
174
+ function _newReplay(journal) {
175
+ const cached = new Map();
176
+ for (const entry of journal === undefined ? [] : _parseJournal(journal)) {
177
+ const key = `${entry.name}\0${entry.argsJson}`;
178
+ if (!cached.has(key)) cached.set(key, []);
179
+ cached.get(key).push(entry.result);
180
+ }
181
+ return {
182
+ cached,
183
+ liveMode: false,
184
+ nextSeq: 0,
185
+ entries: [],
186
+ suspended: null,
187
+ suspendSeq: null,
188
+ replayedCount: 0,
189
+ };
190
+ }
191
+
192
+ /** 64-bit FNV-1a, matching the Rust journal's `args_hash`. */
193
+ function _fnv1a64(text) {
194
+ let hash = 0xcbf29ce484222325n;
195
+ for (const byte of new TextEncoder().encode(text)) {
196
+ hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n);
197
+ }
198
+ return hash;
199
+ }
200
+
201
+ /**
202
+ * Serialize what a run recorded.
203
+ *
204
+ * The journal is serialized in the Rust `CallbackJournal` serde shape. Entries
205
+ * keep dispatch order and, if a callback suspended, everything dispatched at or
206
+ * after the suspending call is dropped so the journal is a clean prefix ending
207
+ * before the suspension point.
208
+ * @param {ReplayState} state
209
+ * @param {string} code - The script that produced the journal
210
+ */
211
+ function _finishReplay(state, code) {
212
+ const entries = [];
213
+ state.entries.forEach((entry, index) => {
214
+ if (entry === undefined) return;
215
+ if (state.suspendSeq !== null && index >= state.suspendSeq) return;
216
+ const { name, argsJson, result } = entry;
217
+ const recorded =
218
+ "ok" in result
219
+ ? `{"Ok":${result.ok}}`
220
+ : `{"Err":${JSON.stringify(result.err)}}`;
221
+ // `thrown` is a JS-only extension (serde ignores unknown fields): the Error
222
+ // halted the guest instead of reaching Python, so replay throws it again.
223
+ const thrown = result.thrown ? `,"thrown":true` : "";
224
+ entries.push(
225
+ `{"index":${index},"name":${JSON.stringify(name)},"args_hash":${_fnv1a64(argsJson)},` +
226
+ `"args_json":${JSON.stringify(argsJson)},"result":${recorded}${thrown}}`,
227
+ );
228
+ });
229
+ return {
230
+ journal: `{"code":${JSON.stringify(code)},"entries":[${entries.join(",")}]}`,
231
+ replayedCallbacks: state.replayedCount,
232
+ suspended: state.suspended ?? undefined,
233
+ };
234
+ }
235
+
236
+ /** Deliver a recorded result to the guest exactly as it was delivered live. */
237
+ function _replayResult(result) {
238
+ if ("ok" in result) return result.ok;
239
+ if (result.thrown) throw new Error(result.err);
240
+ return { tag: "err", val: result.err };
241
+ }
242
+
243
+ /**
244
+ * Run a live callback, recording its outcome into `state` at `seq`.
245
+ *
246
+ * Every outcome of a call that actually ran is journaled, so replay never runs
247
+ * it twice: a JSON result as `Ok` (its exact text), an `{tag: "err"}` result or
248
+ * thrown non-Error as `Err`, and a thrown Error as `Err` marked `thrown` (it
249
+ * halts the guest; replay halts it the same way). Only a SuspendCallback is not
250
+ * journaled: the call re-runs live on resume, as in Rust.
251
+ */
252
+ async function _invokeLive(handler, state, seq, name, argsJson) {
253
+ const record = (result) => {
254
+ state.entries[seq] = { name, argsJson, result };
255
+ };
256
+ let ret;
257
+ try {
258
+ ret = await handler(name, argsJson);
259
+ } catch (e) {
260
+ if (e instanceof SuspendCallback) {
261
+ if (state.suspended === null) {
262
+ state.suspended = { name, argsJson, reason: e.reason };
263
+ state.suspendSeq = seq;
264
+ }
265
+ } else if (e instanceof Error) {
266
+ record({ err: e.message, thrown: true });
267
+ } else {
268
+ record({ err: String(e) });
269
+ }
270
+ throw e;
271
+ }
272
+ if (ret !== null && typeof ret === "object" && ret.tag === "err") {
273
+ record({ err: String(ret.val) });
274
+ } else {
275
+ const json = ret !== null && typeof ret === "object" ? ret.val : ret;
276
+ try {
277
+ parseJson(json);
278
+ record({ ok: json });
279
+ } catch {
280
+ // ponytail: a non-JSON result breaks the handler contract and can't be
281
+ // stored as an `Ok` value, so it stays unjournaled (re-runs on replay).
282
+ }
283
+ }
284
+ return ret;
285
+ }
286
+
287
+ /** Longest delay setTimeout accepts; larger values fire immediately. */
288
+ const MAX_TIMEOUT_MS = 2 ** 31 - 1;
289
+
290
+ /**
291
+ * Complete after the given number of nanoseconds.
292
+ * This backs the sandbox event loop's timers (asyncio.sleep, wait_for, ...).
293
+ *
294
+ * @param {bigint} durationNs - Delay in nanoseconds
295
+ * @returns {Promise<void>}
296
+ */
297
+ export function sleep(durationNs) {
298
+ // Round up so a timer never fires early.
299
+ const ms = Number((BigInt(durationNs) + 999_999n) / 1_000_000n);
300
+ return new Promise((resolve) =>
301
+ setTimeout(resolve, Math.min(ms, MAX_TIMEOUT_MS)),
302
+ );
303
+ }
304
+
305
+ const _decoderOpts = { fatal: false, ignoreBOM: true };
306
+
307
+ /**
308
+ * Create the callback imports for one sandbox instance.
309
+ *
310
+ * Each handler option overrides the module-wide default from the matching
311
+ * setter; an omitted (undefined) option follows the default, including later
312
+ * changes to it. Replay state and streaming output decoders belong to the
313
+ * returned host alone.
314
+ *
315
+ * @internal Used by index.js for each Sandbox.
316
+ * @param {{callbacks?: import("@eryx-org/eryx-run/shared").Callbacks, traceHandler?: ((lineno: number, eventJson: string, contextJson: string) => void) | null, outputHandler?: ((stream: number, data: string) => void) | null}} [options]
317
+ */
318
+ export function createCallbackHost(options = {}) {
319
+ const { infos: callbacks, handler } = fromCallbacks(options.callbacks ?? {});
320
+ /** Calls the code made since takeInvocations() last ran */
321
+ let invocations = 0;
322
+ const pick = (key) => (options[key] === undefined ? defaults[key] : options[key]);
323
+
324
+ /** @type {ReplayState | null} */
325
+ let replay = null;
326
+
327
+ // Streaming decoders, tied to the output handler they were created under so
328
+ // an incomplete multi-byte sequence written under one handler isn't carried
329
+ // into the next.
330
+ let decodersFor = null;
331
+ let stdoutDecoder = new TextDecoder("utf-8", _decoderOpts);
332
+ let stderrDecoder = new TextDecoder("utf-8", _decoderOpts);
333
+ const outputHandler = () => {
334
+ const handler = pick("outputHandler");
335
+ if (handler !== decodersFor) {
336
+ decodersFor = handler;
337
+ stdoutDecoder = new TextDecoder("utf-8", _decoderOpts);
338
+ stderrDecoder = new TextDecoder("utf-8", _decoderOpts);
339
+ }
340
+ return handler;
341
+ };
342
+
343
+ /**
344
+ * Invoke a callback by name with JSON arguments.
345
+ * This is called by the sandbox runtime when Python code calls invoke().
346
+ *
347
+ * @param {string} name - Callback name
348
+ * @param {string} argumentsJson - JSON-encoded arguments
349
+ * @returns {Promise<string>} JSON-encoded result
350
+ */
351
+ // Always async: jco 1.41 doesn't turn a synchronous throw from an async-lowered
352
+ // import into an `err` result, leaving the guest stuck mid-invoke so the next
353
+ // execute panics ("RefCell already borrowed"). A rejection is handled correctly.
354
+ async function invoke(name, argumentsJson) {
355
+ invocations++;
356
+ const state = replay;
357
+ if (!state) {
358
+ return handler(name, argumentsJson);
359
+ }
360
+
361
+ // Decide synchronously, before any await, exactly as the Rust ReplayCallback.
362
+ if (state.suspended !== null) {
363
+ throw new SuspendCallback(
364
+ "execution already suspended by a previous callback",
365
+ );
366
+ }
367
+ const seq = state.nextSeq++;
368
+ if (!state.liveMode) {
369
+ // Key on the guest's canonical args text verbatim, as the Rust host does.
370
+ const result = state.cached.get(`${name}\0${argumentsJson}`)?.shift();
371
+ if (result !== undefined) {
372
+ state.replayedCount++;
373
+ state.entries[seq] = { name, argsJson: argumentsJson, result };
374
+ return _replayResult(result);
375
+ }
376
+ // First miss: the run has diverged from the journal, so this call and every
377
+ // later one runs live (prevents replaying a now-stale cached result).
378
+ state.liveMode = true;
379
+ }
380
+ return _invokeLive(handler, state, seq, name, argumentsJson);
381
+ }
382
+
383
+ return {
384
+ imports: {
385
+ invoke,
386
+ sleep,
387
+ /**
388
+ * List all registered callbacks.
389
+ * This is called by the sandbox runtime when Python code calls list_callbacks().
390
+ */
391
+ listCallbacks: () => callbacks,
392
+ /**
393
+ * Return execution behavior options for the JavaScript host.
394
+ *
395
+ * JavaScript instances may execute repeatedly with different callbacks, so
396
+ * callback setup is not reused. Tracing is enabled only while a trace
397
+ * handler is registered.
398
+ */
399
+ getExecutionOptions: () => ({
400
+ pythonTracing: pick("traceHandler") !== null,
401
+ reuseEmptyCallbacks: false,
402
+ }),
403
+ /** Report a trace event from the Python runtime's sys.settrace hook. */
404
+ reportTrace(lineno, eventJson, contextJson) {
405
+ pick("traceHandler")?.(lineno, eventJson, contextJson);
406
+ },
407
+ /**
408
+ * Report streaming output from the Python runtime.
409
+ * This is called by the sandbox runtime on every sys.stdout/stderr.write().
410
+ *
411
+ * @param {number} stream - 0 = stdout, 1 = stderr
412
+ * @param {Uint8Array} data - The raw bytes written
413
+ */
414
+ reportOutput(stream, data) {
415
+ const handler = outputHandler();
416
+ if (handler) {
417
+ const decoder = stream === 0 ? stdoutDecoder : stderrDecoder;
418
+ handler(stream, decoder.decode(data, { stream: true }));
419
+ }
420
+ },
421
+ },
422
+
423
+ /**
424
+ * Start journaling callbacks, replaying results from `journal` if given.
425
+ * @param {string} [journal] - A journal string from a previous outcome
426
+ */
427
+ beginReplay(journal) {
428
+ replay = _newReplay(journal);
429
+ },
430
+
431
+ /**
432
+ * Stop journaling and return what was recorded.
433
+ * @param {string} code - The script that produced the journal
434
+ */
435
+ endReplay(code) {
436
+ const state = replay;
437
+ replay = null;
438
+ return _finishReplay(state, code);
439
+ },
440
+
441
+ /**
442
+ * Deliver any incomplete multi-byte sequence left at the end of an
443
+ * execution (as U+FFFD) and reset the decoders, so it can't merge with the
444
+ * next execution's output. Only call it between executions: mid-run it
445
+ * would cut a character the guest is still writing.
446
+ */
447
+ /** How many callback calls the code made since this was last called. */
448
+ takeInvocations() {
449
+ const count = invocations;
450
+ invocations = 0;
451
+ return count;
452
+ },
453
+
454
+ flushOutput() {
455
+ const handler = outputHandler();
456
+ const stdoutTail = stdoutDecoder.decode();
457
+ const stderrTail = stderrDecoder.decode();
458
+ if (!handler) return;
459
+ if (stdoutTail) handler(0, stdoutTail);
460
+ if (stderrTail) handler(1, stderrTail);
461
+ },
462
+ };
463
+ }
464
+
465
+ // Imports bound to the module-wide defaults, for embedders that instantiate the
466
+ // low-level component themselves.
467
+ const _defaultHost = createCallbackHost();
468
+ export const { invoke, listCallbacks, getExecutionOptions, reportTrace, reportOutput } =
469
+ _defaultHost.imports;
package/shims/ip.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * IP address parsing shared by the network transports.
3
+ */
4
+
5
+ /** Bytes of an IPv4 address in dotted decimal, or null. */
6
+ export function ipv4Bytes(addr) {
7
+ const parts = addr.split(".");
8
+ if (parts.length !== 4 || !parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255)) return null;
9
+ return parts.map(Number);
10
+ }
11
+
12
+ /** Bytes of an IPv6 address (with an optional dotted IPv4 tail), or null. */
13
+ export function ipv6Bytes(addr) {
14
+ const tail = /^(.*:)(\d+\.\d+\.\d+\.\d+)$/.exec(addr);
15
+ if (tail) {
16
+ const b = ipv4Bytes(tail[2]);
17
+ if (!b) return null;
18
+ addr = `${tail[1]}${((b[0] << 8) | b[1]).toString(16)}:${((b[2] << 8) | b[3]).toString(16)}`;
19
+ }
20
+ const halves = addr.split("::");
21
+ if (halves.length > 2) return null;
22
+ const groups = (s) => (s ? s.split(":") : []);
23
+ const head = groups(halves[0]);
24
+ const rest = halves.length === 2 ? groups(halves[1]) : [];
25
+ const fill = 8 - head.length - rest.length;
26
+ if (halves.length === 2 ? fill < 1 : fill !== 0) return null;
27
+ const all = [...head, ...Array(halves.length === 2 ? fill : 0).fill("0"), ...rest];
28
+ if (!all.every((g) => /^[0-9a-f]{1,4}$/i.test(g))) return null;
29
+ return all.flatMap((g) => [parseInt(g, 16) >> 8, parseInt(g, 16) & 255]);
30
+ }
31
+
32
+ /**
33
+ * The bytes of the IPv4 address that IPv6 address bytes `b` stand for on the
34
+ * wire, or null, like the Rust crate's `embedded_ipv4`: IPv4-mapped
35
+ * (`::ffff:a.b.c.d`), IPv4-compatible (`::a.b.c.d`), well-known-prefix NAT64
36
+ * (`64:ff9b::a.b.c.d`, RFC 6052), or the client address of a Teredo address
37
+ * (`2001::/32`, RFC 4380: the last 32 bits, inverted).
38
+ */
39
+ export function embeddedIPv4(b) {
40
+ const zero = (from, to) => b.slice(from, to).every((x) => x === 0);
41
+ if (zero(0, 10) && b[10] === 255 && b[11] === 255) return b.slice(12);
42
+ const nat64 = b[0] === 0 && b[1] === 0x64 && b[2] === 0xff && b[3] === 0x9b;
43
+ if ((nat64 || zero(0, 4)) && zero(4, 12)) return b.slice(12);
44
+ if (b[0] === 0x20 && b[1] === 0x01 && b[2] === 0 && b[3] === 0) return b.slice(12).map((x) => ~x & 255);
45
+ return null;
46
+ }