host.mjs raw

   1  // WASM Loader — instantiates a Moxie WASM binary with all bridge imports.
   2  // Reuses existing jsruntime modules for browser API surface.
   3  
   4  import * as dom from './dom.mjs';
   5  import * as webgl from './webgl.mjs';
   6  import * as app from './app.mjs';
   7  import * as text from './text.mjs';
   8  import * as localstorage from './localstorage.mjs';
   9  import * as ws from './ws.mjs';
  10  import * as schnorr from './schnorr.mjs';
  11  import * as ed25519 from './ed25519.mjs';
  12  import * as x25519 from './x25519.mjs';
  13  import * as p256 from './p256.mjs';
  14  import * as poly1305 from './poly1305.mjs';
  15  import * as crypto from './crypto.mjs';
  16  import * as subtle from './subtle.mjs';
  17  import * as signer from './signer.mjs';
  18  import * as ext from './ext.mjs';
  19  import * as idb from './idb.mjs';
  20  import * as sw from './sw.mjs';
  21  import * as markdown from './markdown.mjs';
  22  import * as node from './node.mjs';
  23  import * as sabChannel from './sab-channel.mjs';
  24  
  25  let memory;
  26  let instance;
  27  const encoder = new TextEncoder();
  28  const decoder = new TextDecoder();
  29  
  30  function readStr(ptr, len) {
  31    if (len <= 0) return '';
  32    return decoder.decode(new Uint8Array(memory.buffer, ptr, len));
  33  }
  34  
  35  function readBytes(ptr, len) {
  36    if (len <= 0) return new Uint8Array(0);
  37    return new Uint8Array(memory.buffer, ptr, len).slice();
  38  }
  39  
  40  function writeStr(s) {
  41    const buf = encoder.encode(s);
  42    const ptr = instance.exports.__alloc(buf.length);
  43    new Uint8Array(memory.buffer, ptr, buf.length).set(buf);
  44    return { ptr, len: buf.length };
  45  }
  46  
  47  function writeBytes(b) {
  48    if (!b || b.length === 0) return { ptr: 0, len: 0 };
  49    const ptr = instance.exports.__alloc(b.length);
  50    new Uint8Array(memory.buffer, ptr, b.length).set(b instanceof Uint8Array ? b : new Uint8Array(b));
  51    return { ptr, len: b.length };
  52  }
  53  
  54  function setOutStr(outPtr, outLen, s) {
  55    const { ptr, len } = writeStr(s);
  56    const dv = new DataView(memory.buffer);
  57    dv.setInt32(outPtr, ptr, true);
  58    dv.setInt32(outLen, len, true);
  59  }
  60  
  61  function setOutBytes(outPtr, outLen, b) {
  62    const { ptr, len } = writeBytes(b);
  63    const dv = new DataView(memory.buffer);
  64    dv.setInt32(outPtr, ptr, true);
  65    dv.setInt32(outLen, len, true);
  66  }
  67  
  68  function setOutI32(addr, val) {
  69    new DataView(memory.buffer).setInt32(addr, val, true);
  70  }
  71  
  72  // Callback dispatch — always deferred via queueMicrotask.
  73  function cb0(id) { queueMicrotask(() => instance.exports.__cb0(id)); }
  74  function cbs(id, s) {
  75    queueMicrotask(() => {
  76      const { ptr, len } = writeStr(s);
  77      instance.exports.__cbs(id, ptr, len);
  78    });
  79  }
  80  function cbb(id, v) { queueMicrotask(() => instance.exports.__cbb(id, v ? 1 : 0)); }
  81  function cbi(id, v) { queueMicrotask(() => instance.exports.__cbi(id, v | 0)); }
  82  function cbis(id, i, s) {
  83    queueMicrotask(() => {
  84      const { ptr, len } = writeStr(s);
  85      instance.exports.__cbis(id, i | 0, ptr, len);
  86    });
  87  }
  88  function cbiis(id, a, b, s) {
  89    queueMicrotask(() => {
  90      const { ptr, len } = writeStr(s);
  91      instance.exports.__cbiis(id, a | 0, b | 0, ptr, len);
  92    });
  93  }
  94  function cbss(id, s1, s2) {
  95    queueMicrotask(() => {
  96      const a = writeStr(s1);
  97      const b = writeStr(s2);
  98      instance.exports.__cbss(id, a.ptr, a.len, b.ptr, b.len);
  99    });
 100  }
 101  function cb6i(id, a, b, c, d, e, f) {
 102    queueMicrotask(() => instance.exports.__cb6i(id, a|0, b|0, c|0, d|0, e|0, f|0));
 103  }
 104  function cbii(id, a, b) { queueMicrotask(() => instance.exports.__cbii(id, a|0, b|0)); }
 105  function cbiii(id, a, b, c) { queueMicrotask(() => instance.exports.__cbiii(id, a|0, b|0, c|0)); }
 106  function cbdata(id, bytes) {
 107    queueMicrotask(() => {
 108      const { ptr, len } = writeBytes(bytes);
 109      instance.exports.__cbdata(id, ptr, len);
 110    });
 111  }
 112  
 113  // Parse null-separated string list.
 114  function splitNull(ptr, len, count) {
 115    if (count <= 0) return [];
 116    const s = readStr(ptr, len);
 117    return s.split('\0');
 118  }
 119  
 120  // Track JS wrappers for WASM event listeners so RemoveEventListener works.
 121  const jsListeners = new Map();
 122  function listenerKey(elId, event, cbID) { return `${elId}:${event}:${cbID}`; }
 123  
 124  // Hook response tracking for nested callbacks.
 125  const hookResponses = new Map();
 126  let hookNextReq = 1;
 127  
 128  // buildBridge returns the import object for the "bridge" module. The same
 129  // shape is used by the page and by a spawned domain's Worker; the two differ
 130  // only in which end of the channel rings they own, so the channel table and
 131  // the worker flag are parameters.
 132  //
 133  // opts.channels: Map<int32, SharedArrayBuffer> of rings bound at spawn. On
 134  //   the page side this starts empty and channel_create fills it; in a worker
 135  //   the handles arrive from the parent and this table is pre-populated so the
 136  //   child reads the far end of the same rings.
 137  // opts.isWorker: 1 inside a spawned domain, 0 in the page.
 138  export function buildBridge(opts = {}) {
 139    const handles = opts.channels || new Map();
 140    let next = 1;
 141    const isWorker = opts.isWorker ? 1 : 0;
 142    return {
 143      // --- localstorage ---
 144      localstorage_get_item(kp, kl, _kc, op, ol) {
 145        setOutStr(op, ol, localstorage.GetItem(readStr(kp, kl)));
 146      },
 147      localstorage_set_item(kp, kl, _kc, vp, vl, _vc) {
 148        localstorage.SetItem(readStr(kp, kl), readStr(vp, vl));
 149      },
 150      localstorage_remove_item(kp, kl, _kc) {
 151        localstorage.RemoveItem(readStr(kp, kl));
 152      },
 153  
 154      // --- crypto ---
 155      crypto_pubkey_from_seckey(p, l, _c, op, ol) {
 156        setOutBytes(op, ol, crypto.PubKeyFromSecKey(readBytes(p, l)));
 157      },
 158      crypto_sign_schnorr(skp, skl, _skc, mp, ml, _mc, ap, al, _ac, op, ol) {
 159        setOutBytes(op, ol, crypto.SignSchnorr(readBytes(skp, skl), readBytes(mp, ml), readBytes(ap, al)));
 160      },
 161      crypto_verify_schnorr(pkp, pkl, _pkc, mp, ml, _mc, sp, sl, _sc) {
 162        return crypto.VerifySchnorr(readBytes(pkp, pkl), readBytes(mp, ml), readBytes(sp, sl)) ? 1 : 0;
 163      },
 164  
 165      // --- schnorr ---
 166      schnorr_pubkey_from_seckey(p, l, _c, op, ol, ok) {
 167        try {
 168          const r = schnorr.PubKeyFromSecKey(readBytes(p, l));
 169          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 170        } catch { setOutI32(ok, 0); }
 171      },
 172      schnorr_sign(skp, skl, _skc, mp, ml, _mc, ap, al, _ac, op, ol, ok) {
 173        try {
 174          const r = schnorr.SignSchnorr(readBytes(skp, skl), readBytes(mp, ml), readBytes(ap, al));
 175          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 176        } catch { setOutI32(ok, 0); }
 177      },
 178      schnorr_verify(pkp, pkl, _pkc, mp, ml, _mc, sp, sl, _sc) {
 179        return schnorr.VerifySchnorr(readBytes(pkp, pkl), readBytes(mp, ml), readBytes(sp, sl)) ? 1 : 0;
 180      },
 181      schnorr_ecdh(skp, skl, _skc, pkp, pkl, _pkc, op, ol, ok) {
 182        try {
 183          const r = schnorr.ECDH(readBytes(skp, skl), readBytes(pkp, pkl));
 184          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 185        } catch { setOutI32(ok, 0); }
 186      },
 187      schnorr_sha256sum(p, l, _c, op, ol) {
 188        setOutBytes(op, ol, schnorr.SHA256Sum(readBytes(p, l)));
 189      },
 190      schnorr_scalar_add_mod_n(ap, al, _ac, bp, bl, _bc, op, ol, ok) {
 191        try {
 192          const r = schnorr.ScalarAddModN(readBytes(ap, al), readBytes(bp, bl));
 193          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 194        } catch { setOutI32(ok, 0); }
 195      },
 196      schnorr_compressed_pubkey(p, l, _c, op, ol, ok) {
 197        try {
 198          const r = schnorr.CompressedPubKey(readBytes(p, l));
 199          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 200        } catch { setOutI32(ok, 0); }
 201      },
 202  
 203      // --- ed25519 ---
 204      ed25519_new_key_from_seed(p, l, _c, op, ol) {
 205        setOutBytes(op, ol, ed25519.NewKeyFromSeed(readBytes(p, l)));
 206      },
 207      ed25519_sign(sp, sl, _sc, mp, ml, _mc, op, ol) {
 208        setOutBytes(op, ol, ed25519.Sign(readBytes(sp, sl), readBytes(mp, ml)));
 209      },
 210      ed25519_verify(pkp, pkl, _pkc, mp, ml, _mc, sp, sl, _sc) {
 211        return ed25519.Verify(readBytes(pkp, pkl), readBytes(mp, ml), readBytes(sp, sl)) ? 1 : 0;
 212      },
 213  
 214      // --- x25519 ---
 215      x25519_scalar_mult(sp, sl, _sc, pp, pl, _pc, op, ol) {
 216        setOutBytes(op, ol, x25519.ScalarMult(readBytes(sp, sl), readBytes(pp, pl)));
 217      },
 218      x25519_scalar_base_mult(sp, sl, _sc, op, ol) {
 219        setOutBytes(op, ol, x25519.ScalarBaseMult(readBytes(sp, sl)));
 220      },
 221  
 222      // --- p256 ---
 223      p256_scalar_base_mult(p, l, _c, op, ol) {
 224        setOutBytes(op, ol, p256.ScalarBaseMult(readBytes(p, l)));
 225      },
 226      p256_scalar_mult(sp, sl, _sc, pp, pl, _pc, op, ol) {
 227        setOutBytes(op, ol, p256.ScalarMult(readBytes(sp, sl), readBytes(pp, pl)));
 228      },
 229      p256_sign(sp, sl, _sc, mp, ml, _mc, op, ol) {
 230        setOutBytes(op, ol, p256.Sign(readBytes(sp, sl), readBytes(mp, ml)));
 231      },
 232      p256_verify(pkp, pkl, _pkc, mp, ml, _mc, sp, sl, _sc) {
 233        return p256.Verify(readBytes(pkp, pkl), readBytes(mp, ml), readBytes(sp, sl)) ? 1 : 0;
 234      },
 235      p256_reduce_scalar(p, l, _c, op, ol) {
 236        setOutBytes(op, ol, p256.ReduceScalar(readBytes(p, l)));
 237      },
 238      p256_valid_scalar(p, l, _c) {
 239        return p256.ValidScalar(readBytes(p, l)) ? 1 : 0;
 240      },
 241  
 242      // --- poly1305 ---
 243      poly1305_mac(kp, kl, _kc, dp, dl, _dc, op, ol) {
 244        setOutBytes(op, ol, poly1305.MAC(readBytes(kp, kl), readBytes(dp, dl)));
 245      },
 246      poly1305_verify(kp, kl, _kc, dp, dl, _dc, tp, tl, _tc) {
 247        return poly1305.Verify(readBytes(kp, kl), readBytes(dp, dl), readBytes(tp, tl)) ? 1 : 0;
 248      },
 249  
 250      // --- markdown ---
 251      markdown_render(p, l, _c, op, ol) {
 252        setOutStr(op, ol, markdown.Render(readStr(p, l)));
 253      },
 254  
 255      // --- dom ---
 256      dom_body() { return dom.Body(); },
 257      dom_create_element(p, l, _c) { return dom.CreateElement(readStr(p, l)); },
 258      dom_create_text_node(p, l, _c) { return dom.CreateTextNode(readStr(p, l)); },
 259      dom_get_element_by_id(p, l, _c) { return dom.GetElementById(readStr(p, l)); },
 260      dom_query_selector(p, l, _c) { return dom.QuerySelector(readStr(p, l)); },
 261      dom_query_selector_from(root, p, l, _c) { return dom.QuerySelectorFrom(root, readStr(p, l)); },
 262      dom_append_child(parent, child) { dom.AppendChild(parent, child); },
 263      dom_remove_child(parent, child) { dom.RemoveChild(parent, child); },
 264      dom_remove(el) { dom.Remove(el); },
 265      dom_insert_before(parent, nc, rc) { dom.InsertBefore(parent, nc, rc); },
 266      dom_first_child(el) { return dom.FirstChild(el); },
 267      dom_first_element_child(el) { return dom.FirstElementChild(el); },
 268      dom_next_sibling(el) { return dom.NextSibling(el); },
 269      dom_release_element(el) { dom.ReleaseElement(el); },
 270      dom_set_attribute(el, np, nl, _nc, vp, vl, _vc) {
 271        dom.SetAttribute(el, readStr(np, nl), readStr(vp, vl));
 272      },
 273      dom_remove_attribute(el, np, nl, _nc) { dom.RemoveAttribute(el, readStr(np, nl)); },
 274      dom_get_attribute(el, np, nl, _nc, op, ol) { setOutStr(op, ol, dom.GetAttribute(el, readStr(np, nl))); },
 275      dom_set_text_content(el, p, l, _c) { dom.SetTextContent(el, readStr(p, l)); },
 276      dom_set_inner_html(el, p, l, _c) { dom.SetInnerHTML(el, readStr(p, l)); },
 277      dom_set_style(el, pp, pl, _pc, vp, vl, _vc) {
 278        dom.SetStyle(el, readStr(pp, pl), readStr(vp, vl));
 279      },
 280      dom_add_class(el, p, l, _c) { dom.AddClass(el, readStr(p, l)); },
 281      dom_remove_class(el, p, l, _c) { dom.RemoveClass(el, readStr(p, l)); },
 282      dom_focus(el) { dom.Focus(el); },
 283      dom_set_property(el, pp, pl, _pc, vp, vl, _vc) {
 284        dom.SetProperty(el, readStr(pp, pl), readStr(vp, vl));
 285      },
 286      dom_get_property(el, pp, pl, _pc, op, ol) {
 287        setOutStr(op, ol, dom.GetProperty(el, readStr(pp, pl)));
 288      },
 289      dom_bounding_client_left(el) { return dom.BoundingClientLeft(el); },
 290      dom_get_bounding_rect(el, cbID) {
 291        const r = dom.GetBoundingRect_sync ? dom.GetBoundingRect_sync(el) : null;
 292        if (r) { cb6i(cbID, r.left|0, r.top|0, r.right|0, r.bottom|0, r.width|0, r.height|0); }
 293        else { dom.GetBoundingRect(el, (l,t,r,b,w,h) => cb6i(cbID, l,t,r,b,w,h)); }
 294      },
 295      dom_get_viewport_height() { return dom.GetViewportHeight(); },
 296      dom_get_viewport_width() { return dom.GetViewportWidth(); },
 297      dom_add_event_listener(elId, ep, el2, _ec, cbID) {
 298        const el = dom.getElement(elId);
 299        if (!el) return;
 300        const event = readStr(ep, el2);
 301        const fn = () => cb0(cbID);
 302        el.addEventListener(event, fn);
 303        jsListeners.set(listenerKey(elId, event, cbID), fn);
 304      },
 305      dom_add_self_event_listener(elId, ep, el2, _ec, cbID) {
 306        const el = dom.getElement(elId);
 307        if (!el) return;
 308        const event = readStr(ep, el2);
 309        const fn = (e) => { if (e.target === e.currentTarget) cb0(cbID); };
 310        el.addEventListener(event, fn);
 311        jsListeners.set(listenerKey(elId, event, cbID), fn);
 312      },
 313      dom_add_enter_key_listener(elId, cbID) {
 314        const el = dom.getElement(elId);
 315        if (!el) return;
 316        const fn = (e) => { if (e.key === 'Enter') { e.preventDefault(); cb0(cbID); } };
 317        el.addEventListener('keydown', fn);
 318        jsListeners.set(listenerKey(elId, 'keydown:enter', cbID), fn);
 319      },
 320      dom_remove_event_listener(elId, ep, el2, _ec, cbID) {
 321        const el = dom.getElement(elId);
 322        if (!el) return;
 323        const event = readStr(ep, el2);
 324        const key = listenerKey(elId, event, cbID);
 325        const fn = jsListeners.get(key);
 326        if (fn) { el.removeEventListener(event, fn); jsListeners.delete(key); }
 327      },
 328      dom_request_animation_frame(cbID) { requestAnimationFrame(() => cb0(cbID)); },
 329      dom_observe_resize(elId, cbID) { dom.ObserveResize(elId, cbID); },
 330      dom_unobserve_resize(elId) { dom.UnobserveResize(elId); },
 331      dom_on_keydown(el, handlerID) {
 332        const element = dom.getElement(el);
 333        if (!element) return;
 334        element.addEventListener('keydown', (e) => {
 335          const { ptr, len } = writeStr(e.key);
 336          const prevented = instance.exports.__hook_keydown(handlerID, ptr, len);
 337          if (prevented) e.preventDefault();
 338        });
 339      },
 340      dom_insert_mention_chip(el, charCount, np, nl, _nc, nmp, nml, _nmc, pp, pl, _pc) {
 341        dom.InsertMentionChip(el, charCount, readStr(np, nl), readStr(nmp, nml), readStr(pp, pl));
 342      },
 343      dom_set_timeout(cbID, ms) {
 344        return setTimeout(() => cb0(cbID), ms);
 345      },
 346      dom_clear_timeout(id) { clearTimeout(id); },
 347      dom_fetch_text(p, l, _c, cbID) {
 348        dom.FetchText(readStr(p, l), (s) => cbs(cbID, s));
 349      },
 350      dom_fetch_relay_info(p, l, _c, cbID) {
 351        dom.FetchRelayInfo(readStr(p, l), (s) => cbs(cbID, s));
 352      },
 353      dom_fetch_put_blob_base64(up, ul, _uc, dp, dl, _dc, cp, cl, _cc, ap, al, _ac, cbID) {
 354        dom.FetchPutBlobBase64(readStr(up, ul), readStr(dp, dl), readStr(cp, cl), readStr(ap, al), (s) => cbs(cbID, s));
 355      },
 356      dom_idb_set_enc_key(p, l, _c) { dom.IDBSetEncKey(readStr(p, l)); },
 357      dom_idb_get(sp, sl, _sc, kp, kl, _kc, cbID) {
 358        dom.IDBGet(readStr(sp, sl), readStr(kp, kl), (s) => cbs(cbID, s));
 359      },
 360      dom_idb_put(sp, sl, _sc, kp, kl, _kc, vp, vl, _vc) {
 361        dom.IDBPut(readStr(sp, sl), readStr(kp, kl), readStr(vp, vl));
 362      },
 363      dom_idb_get_all(sp, sl, _sc, fnID, doneID) {
 364        dom.IDBGetAll(readStr(sp, sl), (k, v) => cbss(fnID, k, v), () => cb0(doneID));
 365      },
 366      dom_prefers_dark() { return dom.PrefersDark() ? 1 : 0; },
 367      dom_console_log(p, l, _c) { console.log(readStr(p, l)); },
 368      dom_confirm(p, l, _c) { return confirm(readStr(p, l)) ? 1 : 0; },
 369      dom_post_to_sw(p, l, _c) { dom.PostToSW(readStr(p, l)); },
 370      dom_on_sw_message(cbID) { dom.OnSWMessage((s) => cbs(cbID, s)); },
 371      dom_push_state(p, l, _c) { dom.PushState(readStr(p, l)); },
 372      dom_replace_state(p, l, _c) { dom.ReplaceState(readStr(p, l)); },
 373      dom_back() { history.back(); },
 374      dom_location_reload() { location.reload(); },
 375      dom_hard_refresh() { dom.HardRefresh(); },
 376      dom_clear_storage_prefix(p, l, _c) { dom.ClearStoragePrefix(readStr(p, l)); },
 377      dom_timezone_offset_seconds() { return -(new Date().getTimezoneOffset()) * 60; },
 378      dom_read_clipboard(cbID) { dom.ReadClipboard((s) => cbs(cbID, s)); },
 379      dom_write_clipboard(p, l, _c, cbID) { dom.WriteClipboard(readStr(p, l), (ok) => cbb(cbID, ok)); },
 380      dom_get_path(op, ol) { setOutStr(op, ol, location.pathname); },
 381      dom_get_hash(op, ol) { setOutStr(op, ol, location.hash); },
 382      dom_hostname(op, ol) { setOutStr(op, ol, location.hostname); },
 383      dom_port(op, ol) { setOutStr(op, ol, location.port); },
 384      dom_user_agent(op, ol) { setOutStr(op, ol, navigator.userAgent); },
 385      dom_on_pop_state(cbID) {
 386        window.addEventListener('popstate', () => cbs(cbID, location.pathname));
 387      },
 388      dom_pick_file_text(p, l, _c, cbID) { dom.PickFileText(readStr(p, l), (s) => cbs(cbID, s)); },
 389      dom_download_text(fp, fl, _fc, cp, cl, _cc, mp, ml, _mc) {
 390        dom.DownloadText(readStr(fp, fl), readStr(cp, cl), readStr(mp, ml));
 391      },
 392      dom_on_pull_refresh(el, ind, cbID) { dom.OnPullRefresh(el, ind, () => cb0(cbID)); },
 393      dom_now_seconds() { return BigInt(Math.floor(Date.now() / 1000)); },
 394      dom_pick_file_base64(p, l, _c, cbID) {
 395        dom.PickFileBase64(readStr(p, l), (d, m) => cbss(cbID, d, m));
 396      },
 397      dom_on_paste_image(el, cbID) { dom.OnPasteImage(el, (d, m) => cbss(cbID, d, m)); },
 398      dom_on_drop_image(el, cbID) { dom.OnDropImage(el, (d, m) => cbss(cbID, d, m)); },
 399  
 400      // --- ws ---
 401      ws_dial(up, ul, _uc, msgID, openID, closeID, errID) {
 402        return ws.Dial(readStr(up, ul),
 403          (conn, s) => cbis(msgID, conn, s),
 404          (conn) => cbi(openID, conn),
 405          (conn, code, reason) => cbiis(closeID, conn, code, reason),
 406          (conn) => cbi(errID, conn),
 407        );
 408      },
 409      ws_send(conn, mp, ml, _mc) { return ws.Send(conn, readStr(mp, ml)) ? 1 : 0; },
 410      ws_close(conn) { ws.Close(conn); },
 411      ws_ready_state(conn) { return ws.ReadyState(conn); },
 412  
 413      // --- bc ---
 414      bc_open(np, nl, _nc, cbID) {
 415        return (typeof BroadcastChannel !== 'undefined')
 416          ? (() => { const bc = new BroadcastChannel(readStr(np, nl)); bc.onmessage = (e) => cbs(cbID, String(e.data)); return 1; })()
 417          : 0;
 418      },
 419      bc_send(handle, mp, ml, _mc) { /* placeholder - needs handle tracking */ },
 420      bc_close(handle) { /* placeholder */ },
 421  
 422      // --- wasm ---
 423      wasm_load_go_wasm(ep, el, _ec, wp, wl, _wc, cbID) {
 424        // Placeholder: loads another Go WASM binary
 425        cb0(cbID);
 426      },
 427  
 428      // --- node ---
 429      node_on_line(cbID) { if (node.OnLine) node.OnLine((s) => cbs(cbID, s)); },
 430      node_on_close(cbID) { if (node.OnClose) node.OnClose(() => cb0(cbID)); },
 431      node_write_line(p, l, _c) { if (node.WriteLine) node.WriteLine(readStr(p, l)); },
 432      node_write_err(p, l, _c) { if (node.WriteErr) node.WriteErr(readStr(p, l)); },
 433      node_exit(code) { if (node.Exit) node.Exit(code); },
 434      node_now_seconds() { return BigInt(Math.floor(Date.now() / 1000)); },
 435  
 436      // --- subtle ---
 437      subtle_random_bytes(p, l, _c) {
 438        const buf = new Uint8Array(memory.buffer, p, l);
 439        globalThis.crypto.getRandomValues(buf);
 440      },
 441      subtle_aes_cbc_encrypt(kp, kl, _kc, ip, il, _ic, pp, pl, _pc, cbID) {
 442        subtle.AESCBCEncrypt(readBytes(kp, kl), readBytes(ip, il), readBytes(pp, pl), (r) => cbdata(cbID, r));
 443      },
 444      subtle_aes_cbc_decrypt(kp, kl, _kc, ip, il, _ic, cp, cl, _cc, cbID) {
 445        subtle.AESCBCDecrypt(readBytes(kp, kl), readBytes(ip, il), readBytes(cp, cl), (r) => cbdata(cbID, r));
 446      },
 447      subtle_aes_gcm_encrypt(kp, kl, _kc, ip, il, _ic, pp, pl, _pc, cbID) {
 448        subtle.AESGCMEncrypt(readBytes(kp, kl), readBytes(ip, il), readBytes(pp, pl), (r) => cbdata(cbID, r));
 449      },
 450      subtle_aes_gcm_decrypt(kp, kl, _kc, ip, il, _ic, cp, cl, _cc, cbID) {
 451        subtle.AESGCMDecrypt(readBytes(kp, kl), readBytes(ip, il), readBytes(cp, cl), (r) => cbdata(cbID, r));
 452      },
 453      subtle_pbkdf2_derive_key(pp, pl, _pc, sp, sl, _sc, iterations, cbID) {
 454        subtle.PBKDF2DeriveKey(readStr(pp, pl), readBytes(sp, sl), iterations, (r) => cbdata(cbID, r));
 455      },
 456      subtle_argon2id_derive_key(pp, pl, _pc, sp, sl, _sc, t, m, p, dkLen, cbID) {
 457        subtle.Argon2idDeriveKey(readStr(pp, pl), readBytes(sp, sl), t, m, p, dkLen, (r) => cbdata(cbID, r));
 458      },
 459      subtle_sha256_hex(p, l, _c, cbID) {
 460        subtle.SHA256Hex(readBytes(p, l), (s) => cbs(cbID, s));
 461      },
 462      subtle_hmac_sha512(kp, kl, _kc, dp, dl, _dc, cbID) {
 463        subtle.HMACSHA512(readBytes(kp, kl), readBytes(dp, dl), (r) => cbdata(cbID, r));
 464      },
 465      subtle_pbkdf2_sha512(pp, pl, _pc, sp, sl, _sc, iterations, dkLen, cbID) {
 466        subtle.PBKDF2SHA512(readStr(pp, pl), readBytes(sp, sl), iterations, dkLen, (r) => cbdata(cbID, r));
 467      },
 468  
 469      // --- signer ---
 470      signer_has_signer() { return signer.HasSigner() ? 1 : 0; },
 471      signer_has_mls() { return signer.HasMLS() ? 1 : 0; },
 472      signer_get_public_key(cbID) { signer.GetPublicKey((s) => cbs(cbID, s)); },
 473      signer_sign_event(p, l, _c, cbID) { signer.SignEvent(readStr(p, l), (s) => cbs(cbID, s)); },
 474      signer_get_shared_secret(p, l, _c, cbID) { signer.GetSharedSecret(readStr(p, l), (s) => cbs(cbID, s)); },
 475      signer_nip04_decrypt(pkp, pkl, _pkc, cp, cl, _cc, cbID) {
 476        signer.Nip04Decrypt(readStr(pkp, pkl), readStr(cp, cl), (s) => cbs(cbID, s));
 477      },
 478      signer_nip04_encrypt(pkp, pkl, _pkc, pp, pl, _pc, cbID) {
 479        signer.Nip04Encrypt(readStr(pkp, pkl), readStr(pp, pl), (s) => cbs(cbID, s));
 480      },
 481      signer_nip44_decrypt(pkp, pkl, _pkc, cp, cl, _cc, cbID) {
 482        signer.Nip44Decrypt(readStr(pkp, pkl), readStr(cp, cl), (s) => cbs(cbID, s));
 483      },
 484      signer_nip44_encrypt(pkp, pkl, _pkc, pp, pl, _pc, cbID) {
 485        signer.Nip44Encrypt(readStr(pkp, pkl), readStr(pp, pl), (s) => cbs(cbID, s));
 486      },
 487      signer_is_installed(cbID) { signer.IsInstalled((v) => cbb(cbID, v)); },
 488      signer_get_vault_status(cbID) { signer.GetVaultStatus((s) => cbs(cbID, s)); },
 489      signer_create_vault(p, l, _c, cbID) { signer.CreateVault(readStr(p, l), (v) => cbb(cbID, v)); },
 490      signer_unlock_vault(p, l, _c, cbID) { signer.UnlockVault(readStr(p, l), (v) => cbb(cbID, v)); },
 491      signer_lock_vault(cbID) { signer.LockVault(() => cb0(cbID)); },
 492      signer_list_identities(cbID) { signer.ListIdentities((s) => cbs(cbID, s)); },
 493      signer_add_identity(p, l, _c, cbID) { signer.AddIdentity(readStr(p, l), (v) => cbb(cbID, v)); },
 494      signer_nsec_login(p, l, _c, cbID) { signer.NsecLogin(readStr(p, l), (v) => cbb(cbID, v)); },
 495      signer_switch_identity(p, l, _c, cbID) { signer.SwitchIdentity(readStr(p, l), (v) => cbb(cbID, v)); },
 496      signer_export_vault(p, l, _c, cbID) { signer.ExportVault(readStr(p, l), (s) => cbs(cbID, s)); },
 497      signer_import_vault(p, l, _c, cbID) { signer.ImportVault(readStr(p, l), (v) => cbb(cbID, v)); },
 498      signer_remove_identity(p, l, _c, cbID) { signer.RemoveIdentity(readStr(p, l), (v) => cbb(cbID, v)); },
 499      signer_generate_mnemonic(cbID) { signer.GenerateMnemonic((s) => cbs(cbID, s)); },
 500      signer_validate_mnemonic(p, l, _c, cbID) { signer.ValidateMnemonic(readStr(p, l), (v) => cbb(cbID, v)); },
 501      signer_create_hd_vault(pp, pl, _pc, np, nl, _nc, cbID) {
 502        signer.CreateHDVault(readStr(pp, pl), readStr(np, nl), (s) => cbs(cbID, s));
 503      },
 504      signer_restore_hd_vault(pp, pl, _pc, mp, ml, _mc, np, nl, _nc, cbID) {
 505        signer.RestoreHDVault(readStr(pp, pl), readStr(mp, ml), readStr(np, nl), (v) => cbb(cbID, v));
 506      },
 507      signer_derive_identity(p, l, _c, cbID) { signer.DeriveIdentity(readStr(p, l), (s) => cbs(cbID, s)); },
 508      signer_get_mnemonic(cbID) { signer.GetMnemonic((s) => cbs(cbID, s)); },
 509      signer_probe_account(index, cbID) { signer.ProbeAccount(index, (s) => cbs(cbID, s)); },
 510      signer_is_hd(cbID) { signer.IsHD((v) => cbb(cbID, v)); },
 511      signer_reset_extension(cbID) { signer.ResetExtension((v) => cbb(cbID, v)); },
 512      signer_nwc_list(cbID) { signer.NwcList((s) => cbs(cbID, s)); },
 513      signer_nwc_add(up, ul, _uc, ap, al, _ac, createdAt, cbID) {
 514        signer.NwcAdd(readStr(up, ul), readStr(ap, al), Number(createdAt), (s) => cbs(cbID, s));
 515      },
 516      signer_nwc_remove(p, l, _c, cbID) { signer.NwcRemove(readStr(p, l), (v) => cbb(cbID, v)); },
 517      signer_nwc_build_request(ip, il, _ic, mp, ml, _mc, pp, pl, _pc, ep, el2, _ec, createdAt, cbID) {
 518        signer.NwcBuildRequest(readStr(ip, il), readStr(mp, ml), readStr(pp, pl), readStr(ep, el2), Number(createdAt), (s) => cbs(cbID, s));
 519      },
 520      signer_nwc_parse_response(ip, il, _ic, cp, cl, _cc, ep, el2, _ec, cbID) {
 521        signer.NwcParseResponse(readStr(ip, il), readStr(cp, cl), readStr(ep, el2), (s) => cbs(cbID, s));
 522      },
 523  
 524      // --- idb ---
 525      idb_set_enc_key(p, l, _c) { idb.SetEncKey(readStr(p, l)); },
 526      idb_open(cbID) { idb.Open(() => cb0(cbID)); },
 527      idb_save_event(p, l, _c, cbID) { idb.SaveEvent(readStr(p, l), (v) => cbb(cbID, v)); },
 528      idb_query_events(p, l, _c, cbID) { idb.QueryEvents(readStr(p, l), (s) => cbs(cbID, s)); },
 529      idb_save_dm(p, l, _c, cbID) { idb.SaveDM(readStr(p, l), (s) => cbs(cbID, s)); },
 530      idb_query_dms(pp, pl, _pc, limit, until, cbID) {
 531        idb.QueryDMs(readStr(pp, pl), limit, Number(until), (s) => cbs(cbID, s));
 532      },
 533      idb_get_conversation_list(cbID) { idb.GetConversationList((s) => cbs(cbID, s)); },
 534      idb_clear_dms_by_peer(p, l, _c, cbID) { idb.ClearDMsByPeer(readStr(p, l), () => cb0(cbID)); },
 535      idb_set_version(p, l, _c) { idb.SetVersion(readStr(p, l)); },
 536      idb_mls_save_group(gp, gl, _gc, sp, sl, _sc, cbID) {
 537        idb.MlsSaveGroup(readStr(gp, gl), readStr(sp, sl), () => cb0(cbID));
 538      },
 539      idb_mls_load_group(gp, gl, _gc, cbID) {
 540        idb.MlsLoadGroup(readStr(gp, gl), (s) => cbs(cbID, s));
 541      },
 542      idb_mls_list_groups(cbID) { idb.MlsListGroups((s) => cbs(cbID, s)); },
 543      idb_mls_save_kpp(p, l, _c, cbID) { idb.MlsSaveKPP(readStr(p, l), () => cb0(cbID)); },
 544      idb_mls_load_kpp(cbID) { idb.MlsLoadKPP((s) => cbs(cbID, s)); },
 545  
 546      // --- ext ---
 547      ext_storage_get(kp, kl, _kc, cbID) { ext.StorageGet(readStr(kp, kl), (s) => cbs(cbID, s)); },
 548      ext_storage_set(kp, kl, _kc, vp, vl, _vc) { ext.StorageSet(readStr(kp, kl), readStr(vp, vl)); },
 549      ext_storage_remove(kp, kl, _kc) { ext.StorageRemove(readStr(kp, kl)); },
 550      ext_on_message(_unused) {
 551        ext.OnMessage((method, params, tabID, respond) => {
 552          const reqID = hookNextReq++;
 553          hookResponses.set(reqID, respond);
 554          const m = writeStr(method);
 555          const p = writeStr(params);
 556          queueMicrotask(() => instance.exports.__hook_ext_on_message(reqID, m.ptr, m.len, p.ptr, p.len, tabID));
 557        });
 558      },
 559      ext_on_message_respond(reqID, p, l, _c) {
 560        const respond = hookResponses.get(reqID);
 561        if (respond) { hookResponses.delete(reqID); respond(readStr(p, l)); }
 562      },
 563      ext_send_message_to_tab(tabID, mp, ml, _mc) { ext.SendMessageToTab(tabID, readStr(mp, ml)); },
 564      ext_get_active_tab(cbID) { ext.GetActiveTab((id, url) => cbis(cbID, id, url)); },
 565      ext_console_log(p, l, _c) { console.log(readStr(p, l)); },
 566      ext_session_get(kp, kl, _kc, cbID) { ext.SessionGet(readStr(kp, kl), (s) => cbs(cbID, s)); },
 567      ext_session_set(kp, kl, _kc, vp, vl, _vc) { ext.SessionSet(readStr(kp, kl), readStr(vp, vl)); },
 568      ext_is_in_page() { return ext.IsInPage() ? 1 : 0; },
 569  
 570      // --- registry ---
 571      registry_set_seckey(p, l, _c) { self.$hooks = self.$hooks || {}; self.$hooks.seckey = readStr(p, l); },
 572      registry_seckey(op, ol) { setOutStr(op, ol, (self.$hooks && self.$hooks.seckey) || ''); },
 573      registry_set_pubkey(p, l, _c) { self.$hooks = self.$hooks || {}; self.$hooks.pubkey = readStr(p, l); },
 574      registry_pubkey(op, ol) { setOutStr(op, ol, (self.$hooks && self.$hooks.pubkey) || ''); },
 575      registry_set_has_key(v) { self.$hooks = self.$hooks || {}; self.$hooks.hasKey = v !== 0; },
 576      registry_has_key() { return (self.$hooks && self.$hooks.hasKey) ? 1 : 0; },
 577  
 578      registry_on_marmot_init(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotInit = (s) => cbs(cbID, s); },
 579      registry_on_marmot_send(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotSend = (r, c) => cbss(cbID, r, c); },
 580      registry_on_marmot_subscribe(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotSubscribe = () => cb0(cbID); },
 581      registry_on_marmot_publish_kp(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotPublishKP = (s) => cbs(cbID, s); },
 582      registry_on_marmot_list_groups(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotListGroups = (s) => cbs(cbID, s); },
 583      registry_on_save_dm_record(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.saveDMRecord = (s) => cbs(cbID, s); },
 584      registry_on_broadcast_to_clients(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.broadcastToClients = (s) => cbs(cbID, s); },
 585      registry_on_send_to_client(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.sendToClient = (id, m) => cbss(cbID, id, m); },
 586  
 587      registry_on_encrypt_nip04() {
 588        self.$hooks = self.$hooks || {};
 589        self.$hooks.encryptNip04 = (pk, ct) => new Promise(resolve => {
 590          const reqID = hookNextReq++;
 591          hookResponses.set(reqID, resolve);
 592          const p = writeStr(pk); const c = writeStr(ct);
 593          queueMicrotask(() => instance.exports.__hook_encrypt_nip04(reqID, p.ptr, p.len, c.ptr, c.len));
 594        });
 595      },
 596      registry_on_encrypt_nip17() {
 597        self.$hooks = self.$hooks || {};
 598        self.$hooks.encryptNip17 = (pk, ct) => new Promise(resolve => {
 599          const reqID = hookNextReq++;
 600          hookResponses.set(reqID, resolve);
 601          const p = writeStr(pk); const c = writeStr(ct);
 602          queueMicrotask(() => instance.exports.__hook_encrypt_nip17(reqID, p.ptr, p.len, c.ptr, c.len));
 603        });
 604      },
 605      registry_on_decrypt_dm() {
 606        self.$hooks = self.$hooks || {};
 607        self.$hooks.decryptDM = (ev) => new Promise(resolve => {
 608          const reqID = hookNextReq++;
 609          hookResponses.set(reqID, resolve);
 610          const e = writeStr(ev);
 611          queueMicrotask(() => instance.exports.__hook_decrypt_dm(reqID, e.ptr, e.len));
 612        });
 613      },
 614      registry_hook_respond_s(reqID, p, l, _c) {
 615        const resolve = hookResponses.get(reqID);
 616        if (resolve) { hookResponses.delete(reqID); resolve(readStr(p, l)); }
 617      },
 618      registry_hook_respond_ss(reqID, p1, l1, _c1, p2, l2, _c2) {
 619        const resolve = hookResponses.get(reqID);
 620        if (resolve) { hookResponses.delete(reqID); resolve([readStr(p1, l1), readStr(p2, l2)]); }
 621      },
 622  
 623      registry_encrypt_nip04(pkp, pkl, _pkc, cp, cl, _cc, cbID) {
 624        if (self.$hooks && self.$hooks.encryptNip04) {
 625          self.$hooks.encryptNip04(readStr(pkp, pkl), readStr(cp, cl)).then((s) => cbs(cbID, s));
 626        }
 627      },
 628      registry_encrypt_nip17(pkp, pkl, _pkc, cp, cl, _cc, cbID) {
 629        if (self.$hooks && self.$hooks.encryptNip17) {
 630          self.$hooks.encryptNip17(readStr(pkp, pkl), readStr(cp, cl)).then(([a, b]) => cbss(cbID, a, b));
 631        }
 632      },
 633      registry_decrypt_dm(p, l, _c, cbID) {
 634        if (self.$hooks && self.$hooks.decryptDM) {
 635          self.$hooks.decryptDM(readStr(p, l)).then((s) => cbs(cbID, s));
 636        }
 637      },
 638      registry_make_dm_record(pp, pl, _pc, fp, fl, _fc, cp, cl, _cc, ts, prp, prl, _prc, ep, el2, _ec, op, ol) {
 639        // Build DM record JSON directly
 640        const peer = readStr(pp, pl), from = readStr(fp, fl), content = readStr(cp, cl);
 641        const proto = readStr(prp, prl), eid = readStr(ep, el2);
 642        const r = JSON.stringify({peer, from, content, ts: Number(ts), proto, eid});
 643        setOutStr(op, ol, r);
 644      },
 645      registry_marmot_init(p, l, _c) { if (self.$hooks && self.$hooks.marmotInit) self.$hooks.marmotInit(readStr(p, l)); },
 646      registry_marmot_send(rp, rl, _rc, cp, cl, _cc) { if (self.$hooks && self.$hooks.marmotSend) self.$hooks.marmotSend(readStr(rp, rl), readStr(cp, cl)); },
 647      registry_marmot_subscribe() { if (self.$hooks && self.$hooks.marmotSubscribe) self.$hooks.marmotSubscribe(); },
 648      registry_marmot_publish_kp(p, l, _c) { if (self.$hooks && self.$hooks.marmotPublishKP) self.$hooks.marmotPublishKP(readStr(p, l)); },
 649      registry_marmot_list_groups(p, l, _c) { if (self.$hooks && self.$hooks.marmotListGroups) self.$hooks.marmotListGroups(readStr(p, l)); },
 650      registry_save_dm_record(p, l, _c) { if (self.$hooks && self.$hooks.saveDMRecord) self.$hooks.saveDMRecord(readStr(p, l)); },
 651      registry_broadcast_to_clients(p, l, _c) { if (self.$hooks && self.$hooks.broadcastToClients) self.$hooks.broadcastToClients(readStr(p, l)); },
 652      registry_send_to_client(ip, il, _ic, mp, ml, _mc) { if (self.$hooks && self.$hooks.sendToClient) self.$hooks.sendToClient(readStr(ip, il), readStr(mp, ml)); },
 653      registry_has_hook(p, l, _c) { return (self.$hooks && self.$hooks[readStr(p, l)]) ? 1 : 0; },
 654      registry_load_module(p, l, _c, cbID) {
 655        // Module loading is moxiejs-specific; in WASM all modules are compiled in
 656        cb0(cbID);
 657      },
 658  
 659      // --- sw ---
 660      sw_on_install(cbID) { self.addEventListener('install', (e) => cbi(cbID, 0)); },
 661      sw_on_activate(cbID) { self.addEventListener('activate', (e) => cbi(cbID, 0)); },
 662      sw_on_fetch(cbID) { self.addEventListener('fetch', (e) => cbi(cbID, 0)); },
 663      sw_on_message(cbID) { self.addEventListener('message', (e) => cbi(cbID, 0)); },
 664      sw_wait_until(event, cbID) { /* simplified - actual impl needs event ref */ cb0(cbID); },
 665      sw_respond_with(event, resp) { /* needs event/response tracking */ },
 666      sw_respond_with_network(event) { /* pass-through */ },
 667      sw_respond_with_cache_first(event) { /* needs event tracking */ },
 668      sw_respond_with_network_first(event) { /* needs event tracking */ },
 669      sw_get_request_url(event, op, ol) { setOutStr(op, ol, ''); },
 670      sw_get_request_path(event, op, ol) { setOutStr(op, ol, ''); },
 671      sw_get_message_data(event, op, ol) { setOutStr(op, ol, ''); },
 672      sw_get_message_client_id(event, op, ol) { setOutStr(op, ol, ''); },
 673      sw_origin(op, ol) { setOutStr(op, ol, self.location ? self.location.origin : ''); },
 674      sw_skip_waiting() { if (self.skipWaiting) self.skipWaiting(); },
 675      sw_claim_clients(cbID) {
 676        if (self.clients && self.clients.claim) self.clients.claim().then(() => cb0(cbID));
 677        else cb0(cbID);
 678      },
 679      sw_match_clients(cbID) { /* simplified */ },
 680      sw_post_message(client, p, l, _c) { /* needs client handle tracking */ },
 681      sw_post_message_json(client, p, l, _c) { /* needs client handle tracking */ },
 682      sw_get_client_by_id(p, l, _c, cbID) { cbii(cbID, 0, 0); },
 683      sw_navigate(client, p, l, _c) { /* needs client handle tracking */ },
 684      sw_cache_open(p, l, _c, cbID) {
 685        caches.open(readStr(p, l)).then(() => cbi(cbID, 1));
 686      },
 687      sw_cache_add_all(cache, up, ul, _uc, count, cbID) {
 688        const urls = splitNull(up, ul, count);
 689        caches.open('v1').then(c => c.addAll(urls)).then(() => cb0(cbID));
 690      },
 691      sw_cache_from_manifests(cache, fp, fl, _fc, count, cbID) { cb0(cbID); },
 692      sw_cache_put(cache, up, ul, _uc, resp, cbID) { cb0(cbID); },
 693      sw_cache_match(p, l, _c, cbID) { cbi(cbID, 0); },
 694      sw_cache_delete(p, l, _c, cbID) {
 695        caches.delete(readStr(p, l)).then(() => cb0(cbID));
 696      },
 697      sw_fetch(p, l, _c, cbID) {
 698        fetch(readStr(p, l)).then(r => cbii(cbID, 1, r.ok ? 1 : 0)).catch(() => cbii(cbID, 0, 0));
 699      },
 700      sw_fetch_all(up, ul, _uc, count, eachID, doneID) {
 701        const urls = splitNull(up, ul, count);
 702        Promise.all(urls.map((u, i) => fetch(u).then(r => cbiii(eachID, i, 1, r.ok ? 1 : 0)).catch(() => cbiii(eachID, i, 0, 0))))
 703          .then(() => cb0(doneID));
 704      },
 705      sw_response_ok(resp) { return 0; },
 706      sw_sse_connect(p, l, _c, cbID) {
 707        const es = new EventSource(readStr(p, l));
 708        es.onmessage = (e) => cbs(cbID, e.data);
 709        return 1;
 710      },
 711      sw_sse_close(id) { /* needs handle tracking */ },
 712      sw_set_timeout(ms, cbID) { return setTimeout(() => cb0(cbID), ms); },
 713      sw_clear_timeout(t) { clearTimeout(t); },
 714      sw_now_seconds() { return BigInt(Math.floor(Date.now() / 1000)); },
 715      sw_now_millis() { return BigInt(Date.now()); },
 716      sw_log(p, l, _c) { console.log(readStr(p, l)); },
 717  
 718      // --- webgl ---
 719      webgl_CreateContext(canvasId) { return webgl.CreateContext(canvasId); },
 720      webgl_CreateContextFromHandle(el) { return webgl.CreateContextFromHandle(el); },
 721      webgl_DeleteContext(ctx) { webgl.DeleteContext(ctx); },
 722      webgl_Enable(ctx, cap) { webgl.Enable(ctx, cap); },
 723      webgl_Disable(ctx, cap) { webgl.Disable(ctx, cap); },
 724      webgl_Viewport(ctx, x, y, w, h) { webgl.Viewport(ctx, x, y, w, h); },
 725      webgl_Scissor(ctx, x, y, w, h) { webgl.Scissor(ctx, x, y, w, h); },
 726      webgl_BlendFunc(ctx, src, dst) { webgl.BlendFunc(ctx, src, dst); },
 727      webgl_BlendFuncSeparate(ctx, sr, dr, sa, da) { webgl.BlendFuncSeparate(ctx, sr, dr, sa, da); },
 728      webgl_BlendEquation(ctx, mode) { webgl.BlendEquation(ctx, mode); },
 729      webgl_DepthFunc(ctx, fn) { webgl.DepthFunc(ctx, fn); },
 730      webgl_DepthMask(ctx, flag) { webgl.DepthMask(ctx, flag); },
 731      webgl_ClearColor(ctx, r, g, b, a) { webgl.ClearColor(ctx, r, g, b, a); },
 732      webgl_ClearDepth(ctx, d) { webgl.ClearDepth(ctx, d); },
 733      webgl_Clear(ctx, mask) { webgl.Clear(ctx, mask); },
 734      webgl_Flush(ctx) { webgl.Flush(ctx); },
 735      webgl_Finish(ctx) { webgl.Finish(ctx); },
 736      webgl_GetError(ctx) { return webgl.GetError(ctx); },
 737      webgl_GetInteger(ctx, pname) { return webgl.GetInteger(ctx, pname); },
 738      webgl_IsEnabled(ctx, cap) { return webgl.IsEnabled(ctx, cap); },
 739      webgl_PixelStorei(ctx, pname, param) { webgl.PixelStorei(ctx, pname, param); },
 740      webgl_ReadPixels(ctx, x, y, w, h, fmt, typ, bufPtr, bufLen) { webgl.ReadPixels(ctx, x, y, w, h, fmt, typ, bufPtr, bufLen); },
 741      webgl_CreateBuffer(ctx) { return webgl.CreateBuffer(ctx); },
 742      webgl_DeleteBuffer(ctx, buf) { webgl.DeleteBuffer(ctx, buf); },
 743      webgl_BindBuffer(ctx, target, buf) { webgl.BindBuffer(ctx, target, buf); },
 744      webgl_BufferData(ctx, target, ptr, len, usage) { webgl.BufferData(ctx, target, ptr, len, usage); },
 745      webgl_BufferSubData(ctx, target, off, ptr, len) { webgl.BufferSubData(ctx, target, off, ptr, len); },
 746      webgl_CreateTexture(ctx) { return webgl.CreateTexture(ctx); },
 747      webgl_DeleteTexture(ctx, tex) { webgl.DeleteTexture(ctx, tex); },
 748      webgl_BindTexture(ctx, target, tex) { webgl.BindTexture(ctx, target, tex); },
 749      webgl_ActiveTexture(ctx, unit) { webgl.ActiveTexture(ctx, unit); },
 750      webgl_TexParameteri(ctx, target, pname, param) { webgl.TexParameteri(ctx, target, pname, param); },
 751      webgl_TexImage2D(ctx, target, lvl, ifmt, w, h, border, fmt, typ, ptr, len) { webgl.TexImage2D(ctx, target, lvl, ifmt, w, h, border, fmt, typ, ptr, len); },
 752      webgl_TexStorage2D(ctx, target, lvls, ifmt, w, h) { webgl.TexStorage2D(ctx, target, lvls, ifmt, w, h); },
 753      webgl_TexSubImage2D(ctx, target, lvl, xo, yo, w, h, fmt, typ, ptr, len) { webgl.TexSubImage2D(ctx, target, lvl, xo, yo, w, h, fmt, typ, ptr, len); },
 754      webgl_CopyTexSubImage2D(ctx, target, lvl, xo, yo, x, y, w, h) { webgl.CopyTexSubImage2D(ctx, target, lvl, xo, yo, x, y, w, h); },
 755      webgl_GenerateMipmap(ctx, target) { webgl.GenerateMipmap(ctx, target); },
 756      webgl_CreateFramebuffer(ctx) { return webgl.CreateFramebuffer(ctx); },
 757      webgl_DeleteFramebuffer(ctx, fbo) { webgl.DeleteFramebuffer(ctx, fbo); },
 758      webgl_BindFramebuffer(ctx, target, fbo) { webgl.BindFramebuffer(ctx, target, fbo); },
 759      webgl_FramebufferTexture2D(ctx, target, att, texTarget, tex, lvl) { webgl.FramebufferTexture2D(ctx, target, att, texTarget, tex, lvl); },
 760      webgl_CheckFramebufferStatus(ctx, target) { return webgl.CheckFramebufferStatus(ctx, target); },
 761      webgl_InvalidateFramebuffer(ctx, target, att) { webgl.InvalidateFramebuffer(ctx, target, att); },
 762      webgl_CreateRenderbuffer(ctx) { return webgl.CreateRenderbuffer(ctx); },
 763      webgl_DeleteRenderbuffer(ctx, rbo) { webgl.DeleteRenderbuffer(ctx, rbo); },
 764      webgl_BindRenderbuffer(ctx, target, rbo) { webgl.BindRenderbuffer(ctx, target, rbo); },
 765      webgl_RenderbufferStorage(ctx, target, ifmt, w, h) { webgl.RenderbufferStorage(ctx, target, ifmt, w, h); },
 766      webgl_FramebufferRenderbuffer(ctx, target, att, rbtarget, rbo) { webgl.FramebufferRenderbuffer(ctx, target, att, rbtarget, rbo); },
 767      webgl_CreateShader(ctx, typ) { return webgl.CreateShader(ctx, typ); },
 768      webgl_DeleteShader(ctx, sh) { webgl.DeleteShader(ctx, sh); },
 769      webgl_ShaderSource(ctx, sh, ptr, len) { webgl.ShaderSource(ctx, sh, ptr, len); },
 770      webgl_CompileShader(ctx, sh) { webgl.CompileShader(ctx, sh); },
 771      webgl_GetShaderParameter(ctx, sh, pname) { return webgl.GetShaderParameter(ctx, sh, pname); },
 772      webgl_GetShaderInfoLog(ctx, sh, ptr, len) { return webgl.GetShaderInfoLog(ctx, sh, ptr, len); },
 773      webgl_CreateProgram(ctx) { return webgl.CreateProgram(ctx); },
 774      webgl_DeleteProgram(ctx, prog) { webgl.DeleteProgram(ctx, prog); },
 775      webgl_AttachShader(ctx, prog, sh) { webgl.AttachShader(ctx, prog, sh); },
 776      webgl_LinkProgram(ctx, prog) { webgl.LinkProgram(ctx, prog); },
 777      webgl_UseProgram(ctx, prog) { webgl.UseProgram(ctx, prog); },
 778      webgl_GetProgramParameter(ctx, prog, pname) { return webgl.GetProgramParameter(ctx, prog, pname); },
 779      webgl_GetProgramInfoLog(ctx, prog, ptr, len) { return webgl.GetProgramInfoLog(ctx, prog, ptr, len); },
 780      webgl_BindAttribLocation(ctx, prog, idx, ptr, len) { webgl.BindAttribLocation(ctx, prog, idx, ptr, len); },
 781      webgl_GetUniformLocation(ctx, prog, ptr, len) { return webgl.GetUniformLocation(ctx, prog, ptr, len); },
 782      webgl_Uniform1f(ctx, loc, v) { webgl.Uniform1f(ctx, loc, v); },
 783      webgl_Uniform2f(ctx, loc, v0, v1) { webgl.Uniform2f(ctx, loc, v0, v1); },
 784      webgl_Uniform3f(ctx, loc, v0, v1, v2) { webgl.Uniform3f(ctx, loc, v0, v1, v2); },
 785      webgl_Uniform4f(ctx, loc, v0, v1, v2, v3) { webgl.Uniform4f(ctx, loc, v0, v1, v2, v3); },
 786      webgl_Uniform1i(ctx, loc, v) { webgl.Uniform1i(ctx, loc, v); },
 787      webgl_EnableVertexAttribArray(ctx, idx) { webgl.EnableVertexAttribArray(ctx, idx); },
 788      webgl_DisableVertexAttribArray(ctx, idx) { webgl.DisableVertexAttribArray(ctx, idx); },
 789      webgl_VertexAttribPointer(ctx, idx, size, typ, norm, stride, off) { webgl.VertexAttribPointer(ctx, idx, size, typ, norm, stride, off); },
 790      webgl_DrawArrays(ctx, mode, first, count) { webgl.DrawArrays(ctx, mode, first, count); },
 791      webgl_DrawElements(ctx, mode, count, typ, off) { webgl.DrawElements(ctx, mode, count, typ, off); },
 792  
 793      // --- text ---
 794      text_Measure(fp, fl, sz, tp, tl, wp, hp) { text.Measure(fp, fl, sz, tp, tl, wp, hp); },
 795      text_Render(fp, fl, sz, tp, tl, mw, dp, dc) { return text.Render(fp, fl, sz, tp, tl, mw, dp, dc); },
 796  
 797      // --- app ---
 798      app_CreateFullscreenCanvas() { app.CreateFullscreenCanvas(); },
 799      app_CreateWebGLContext() { return app.CreateWebGLContext(); },
 800      app_GetDevicePixelRatio() { return app.GetDevicePixelRatio(); },
 801      app_GetCanvasCSSSize(wPtr, hPtr) { app.GetCanvasCSSSize(wPtr, hPtr); },
 802      app_SetCanvasBacking(w, h) { app.SetCanvasBacking(w, h); },
 803      app_RequestRAF() { app.RequestRAF(); },
 804      app_SetCursor(ptr, len) { app.SetCursor(ptr, len); },
 805      app_SetTitle(ptr, len) { app.SetTitle(ptr, len); },
 806      app_SetFullscreen(full) { app.SetFullscreen(full); },
 807      app_RegisterPointerEvents() { app.RegisterPointerEvents(); },
 808      app_RegisterKeyEvents() { app.RegisterKeyEvents(); },
 809      app_RegisterResizeEvents() { app.RegisterResizeEvents(); },
 810      // Spawn bridge: channels are SPSC rings over SharedArrayBuffer, and a
 811      // domain is a dedicated Worker running the same module. The module only
 812      // ever sees an int32 handle, so the host keeps the table.
 813      channel_create(slotSize, slotCount) {
 814        const h = next++;
 815        handles.set(h, sabChannel.create(slotSize, slotCount));
 816        return h;
 817      },
 818      channel_send(h, ptr, len) {
 819        const sab = handles.get(h);
 820        if (sab) sabChannel.send(sab, readBytes(ptr, len));
 821      },
 822      channel_recv(h, ptr, cap) {
 823        const sab = handles.get(h);
 824        if (!sab) return -1;
 825        const dst = new Uint8Array(memory.buffer, ptr, cap);
 826        return sabChannel.recv(sab, dst);
 827      },
 828      channel_close(h) {
 829        const sab = handles.get(h);
 830        if (sab) { sabChannel.close(sab); handles.delete(h); }
 831      },
 832      is_worker_context() { return isWorker; },
 833      spawn_domain(fnIdx, argPtr, argLen, chanHandlesPtr, nChans) {
 834        // Workers are started from the host: the module cannot create one. The
 835        // entry index and the bound channel handles are handed to the child over
 836        // its own message port so it can bind the same rings from the far end.
 837        //
 838        // Every domain runs in a Worker, root and child alike, so being a worker
 839        // is not a reason to refuse: the runtime only requires that spawn is
 840        // reached from a worker context (channel receives block in Atomics.wait,
 841        // which the page thread forbids), and a Worker may start another Worker.
 842        const args = readBytes(argPtr, argLen);
 843        const chans = [];
 844        const dv = new DataView(memory.buffer);
 845        for (let i = 0; i < nChans; i++) {
 846          const h = dv.getInt32(chanHandlesPtr + i * 4, true);
 847          if (handles.has(h)) chans.push({ handle: h, sab: handles.get(h) });
 848        }
 849        spawnWorker(fnIdx, args, chans);
 850      },
 851    };
 852  }
 853  
 854  // buildWorkerBridge returns the bridge for a spawned domain: the same
 855  // functions as the page, over the channel rings the parent handed over.
 856  export function buildWorkerBridge(channels) {
 857    return buildBridge({ channels, isWorker: true });
 858  }
 859  
 860  // buildHostImports returns the non-bridge import object every Moxie wasm
 861  // module needs: the WASI preview1 surface it actually calls, plus the env
 862  // shims the stdlib closure pulls in. mem is a getter for the module's memory,
 863  // not the memory itself, because these imports are built before instantiation
 864  // completes and the buffer only exists once the instance is returned.
 865  export function buildHostImports(mem) {
 866    return {
 867      wasi_snapshot_preview1: {
 868        fd_write(fd, iovs, iovs_len, nwritten) {
 869          const dv = new DataView(mem().buffer);
 870          let totalWritten = 0;
 871          for (let i = 0; i < iovs_len; i++) {
 872            const ptr = dv.getUint32(iovs + i * 8, true);
 873            const len = dv.getUint32(iovs + i * 8 + 4, true);
 874            const s = decoder.decode(new Uint8Array(mem().buffer, ptr, len));
 875            // The module runs in a Worker, so its output cannot reach the page
 876            // through the console alone: forward each write over the port and
 877            // let the page decide where stdout and stderr go.
 878            globalThis.postMessage({ kind: fd === 1 ? 'stdout' : 'stderr', text: s });
 879            totalWritten += len;
 880          }
 881          dv.setUint32(nwritten, totalWritten, true);
 882          return 0;
 883        },
 884        // A domain has no stdin: report EOF so runtime.buffered returns 0 and
 885        // os.Stdin.Read yields instead of blocking on a read that never comes.
 886        fd_read(fd, iovs, iovs_len, nread) {
 887          const dv = new DataView(mem().buffer);
 888          dv.setUint32(nread, 0, true);
 889          return 0;
 890        },
 891        clock_time_get(clockID, precision, time) {
 892          const now = clockID === 1
 893            ? performance.now() * 1_000_000
 894            : Date.now() * 1_000_000;
 895          const dv = new DataView(mem().buffer);
 896          dv.setBigInt64(time, BigInt(Math.floor(now)), true);
 897          return 0;
 898        },
 899      },
 900      // Host shims for pkg/mxutil. Those functions are declared with
 901      // //export, which makes them imports under wasm, and mxutil arrives in
 902      // an application's closure because the standard library's errors
 903      // package uses its GrowCap helper. A browser app has no filesystem, so
 904      // these report failure rather than pretending to work; they are only
 905      // reached if something calls them, which nothing in a page does.
 906      //
 907      // Anything else the module imports from env is a host shim that no page
 908      // code reaches (the whole symbol is pulled in by the stdlib closure).
 909      // Resolving unknown names to a no-op keeps the module loadable; naming
 910      // each one would mean editing this file every time the closure shifts.
 911      env: new Proxy({
 912        mxc_readfile: () => 0,
 913        mxc_filesize: () => -1,
 914        mxc_getenv: () => 0,
 915        mxc_listdir: () => 0,
 916        mxc_writefile: () => -1,
 917        mxc_mkdir: () => -1,
 918        // libc write, pulled in by the compiler-side C shims.
 919        write: () => -1,
 920        // syscall.Exit has no process to terminate in a browser. Report the
 921        // code through the host's error path so it is visible, not silent.
 922        'syscall.Exit': (code) => { throw new Error(`exit ${code | 0}`); },
 923      }, {
 924        get(target, prop) {
 925          return prop in target ? target[prop] : (() => 0);
 926        },
 927      }),
 928    };
 929  }
 930  
 931  // spawnWorker starts a new domain for a Moxie spawn. The child runs the same
 932  // module in a dedicated Worker; the module cannot create one itself, and the
 933  // bound channel rings travel to the child as SharedArrayBuffers, which is the
 934  // only inter-domain state there is.
 935  function spawnWorker(fnIdx, argBytes, chans) {
 936    startDomain({
 937      kind: 'entry',
 938      wasm: globalThis.__moxie_wasm_url,
 939      fnIdx,
 940      args: argBytes,
 941      chans: chans.map(c => ({ handle: c.handle, sab: c.sab })),
 942    }, null, true);
 943  }
 944  
 945  // startDomain creates the Worker that runs a Moxie domain and posts it its
 946  // boot message. A domain always runs in its own Worker: channel receives
 947  // block in Atomics.wait, which the browser forbids on the page thread, and a
 948  // domain that spawns must itself be able to create a Worker.
 949  export function startDomain(boot, onSettled, relayUp) {
 950    const w = new Worker(new URL('./worker-host.mjs', import.meta.url), { type: 'module' });
 951    // A spawned domain reports to the domain that spawned it, not to the page, so
 952    // its output would otherwise stop one hop short and never be seen. relayUp
 953    // forwards it along the chain; in the page the same postMessage has no
 954    // listener and is ignored, so this is safe at every level.
 955    const up = (kind, text) => {
 956      if (relayUp) globalThis.postMessage({ kind, text });
 957    };
 958    w.addEventListener('message', (e) => {
 959      const m = e.data;
 960      if (!m) return;
 961      if (m.kind === 'stdout') { console.log(m.text); up('stdout', m.text); }
 962      else if (m.kind === 'stderr') { console.error(m.text); up('stderr', m.text); }
 963      else if (m.kind === 'done') { w.terminate(); onSettled && onSettled(null); }
 964      else if (m.kind === 'error') {
 965        w.terminate();
 966        if (onSettled) onSettled(new Error(m.error));
 967        // A spawned domain has no caller to reject: the parent reaches it only
 968        // through channels. Relay the failure to the page rather than dropping
 969        // it, or a domain that dies on startup just leaves the parent waiting.
 970        else globalThis.postMessage({ kind: 'stderr', text: '[domain] ' + m.error + '\n' });
 971      }
 972    });
 973    w.addEventListener('error', (e) => {
 974      const msg = String(e.message || e.error || e);
 975      if (onSettled) onSettled(new Error(msg));
 976      else globalThis.postMessage({ kind: 'stderr', text: '[domain] ' + msg + '\n' });
 977    });
 978    w.postMessage(boot);
 979    return w;
 980  }
 981  
 982  // setHostModule points the host-side callbacks at a freshly instantiated
 983  // module. Called by the Worker host once its instance exists, so reverse
 984  // callbacks (__cb*) reach the right instance.
 985  export function setHostModule(inst) {
 986    instance = inst;
 987    memory = inst.exports.memory;
 988  }
 989  
 990  
 991