loader.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  
  24  let memory;
  25  let instance;
  26  const encoder = new TextEncoder();
  27  const decoder = new TextDecoder();
  28  
  29  function readStr(ptr, len) {
  30    if (len <= 0) return '';
  31    return decoder.decode(new Uint8Array(memory.buffer, ptr, len));
  32  }
  33  
  34  function readBytes(ptr, len) {
  35    if (len <= 0) return new Uint8Array(0);
  36    return new Uint8Array(memory.buffer, ptr, len).slice();
  37  }
  38  
  39  function writeStr(s) {
  40    const buf = encoder.encode(s);
  41    const ptr = instance.exports.__alloc(buf.length);
  42    new Uint8Array(memory.buffer, ptr, buf.length).set(buf);
  43    return { ptr, len: buf.length };
  44  }
  45  
  46  function writeBytes(b) {
  47    if (!b || b.length === 0) return { ptr: 0, len: 0 };
  48    const ptr = instance.exports.__alloc(b.length);
  49    new Uint8Array(memory.buffer, ptr, b.length).set(b instanceof Uint8Array ? b : new Uint8Array(b));
  50    return { ptr, len: b.length };
  51  }
  52  
  53  function setOutStr(outPtr, outLen, s) {
  54    const { ptr, len } = writeStr(s);
  55    const dv = new DataView(memory.buffer);
  56    dv.setInt32(outPtr, ptr, true);
  57    dv.setInt32(outLen, len, true);
  58  }
  59  
  60  function setOutBytes(outPtr, outLen, b) {
  61    const { ptr, len } = writeBytes(b);
  62    const dv = new DataView(memory.buffer);
  63    dv.setInt32(outPtr, ptr, true);
  64    dv.setInt32(outLen, len, true);
  65  }
  66  
  67  function setOutI32(addr, val) {
  68    new DataView(memory.buffer).setInt32(addr, val, true);
  69  }
  70  
  71  // Callback dispatch — always deferred via queueMicrotask.
  72  function cb0(id) { queueMicrotask(() => instance.exports.__cb0(id)); }
  73  function cbs(id, s) {
  74    queueMicrotask(() => {
  75      const { ptr, len } = writeStr(s);
  76      instance.exports.__cbs(id, ptr, len);
  77    });
  78  }
  79  function cbb(id, v) { queueMicrotask(() => instance.exports.__cbb(id, v ? 1 : 0)); }
  80  function cbi(id, v) { queueMicrotask(() => instance.exports.__cbi(id, v | 0)); }
  81  function cbis(id, i, s) {
  82    queueMicrotask(() => {
  83      const { ptr, len } = writeStr(s);
  84      instance.exports.__cbis(id, i | 0, ptr, len);
  85    });
  86  }
  87  function cbiis(id, a, b, s) {
  88    queueMicrotask(() => {
  89      const { ptr, len } = writeStr(s);
  90      instance.exports.__cbiis(id, a | 0, b | 0, ptr, len);
  91    });
  92  }
  93  function cbss(id, s1, s2) {
  94    queueMicrotask(() => {
  95      const a = writeStr(s1);
  96      const b = writeStr(s2);
  97      instance.exports.__cbss(id, a.ptr, a.len, b.ptr, b.len);
  98    });
  99  }
 100  function cb6i(id, a, b, c, d, e, f) {
 101    queueMicrotask(() => instance.exports.__cb6i(id, a|0, b|0, c|0, d|0, e|0, f|0));
 102  }
 103  function cbii(id, a, b) { queueMicrotask(() => instance.exports.__cbii(id, a|0, b|0)); }
 104  function cbiii(id, a, b, c) { queueMicrotask(() => instance.exports.__cbiii(id, a|0, b|0, c|0)); }
 105  function cbdata(id, bytes) {
 106    queueMicrotask(() => {
 107      const { ptr, len } = writeBytes(bytes);
 108      instance.exports.__cbdata(id, ptr, len);
 109    });
 110  }
 111  
 112  // Parse null-separated string list.
 113  function splitNull(ptr, len, count) {
 114    if (count <= 0) return [];
 115    const s = readStr(ptr, len);
 116    return s.split('\0');
 117  }
 118  
 119  // Track JS wrappers for WASM event listeners so RemoveEventListener works.
 120  const jsListeners = new Map();
 121  function listenerKey(elId, event, cbID) { return `${elId}:${event}:${cbID}`; }
 122  
 123  // Hook response tracking for nested callbacks.
 124  const hookResponses = new Map();
 125  let hookNextReq = 1;
 126  
 127  function buildBridge() {
 128    return {
 129      // --- localstorage ---
 130      localstorage_get_item(kp, kl, _kc, op, ol) {
 131        setOutStr(op, ol, localstorage.GetItem(readStr(kp, kl)));
 132      },
 133      localstorage_set_item(kp, kl, _kc, vp, vl, _vc) {
 134        localstorage.SetItem(readStr(kp, kl), readStr(vp, vl));
 135      },
 136      localstorage_remove_item(kp, kl, _kc) {
 137        localstorage.RemoveItem(readStr(kp, kl));
 138      },
 139  
 140      // --- crypto ---
 141      crypto_pubkey_from_seckey(p, l, _c, op, ol) {
 142        setOutBytes(op, ol, crypto.PubKeyFromSecKey(readBytes(p, l)));
 143      },
 144      crypto_sign_schnorr(skp, skl, _skc, mp, ml, _mc, ap, al, _ac, op, ol) {
 145        setOutBytes(op, ol, crypto.SignSchnorr(readBytes(skp, skl), readBytes(mp, ml), readBytes(ap, al)));
 146      },
 147      crypto_verify_schnorr(pkp, pkl, _pkc, mp, ml, _mc, sp, sl, _sc) {
 148        return crypto.VerifySchnorr(readBytes(pkp, pkl), readBytes(mp, ml), readBytes(sp, sl)) ? 1 : 0;
 149      },
 150  
 151      // --- schnorr ---
 152      schnorr_pubkey_from_seckey(p, l, _c, op, ol, ok) {
 153        try {
 154          const r = schnorr.PubKeyFromSecKey(readBytes(p, l));
 155          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 156        } catch { setOutI32(ok, 0); }
 157      },
 158      schnorr_sign(skp, skl, _skc, mp, ml, _mc, ap, al, _ac, op, ol, ok) {
 159        try {
 160          const r = schnorr.SignSchnorr(readBytes(skp, skl), readBytes(mp, ml), readBytes(ap, al));
 161          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 162        } catch { setOutI32(ok, 0); }
 163      },
 164      schnorr_verify(pkp, pkl, _pkc, mp, ml, _mc, sp, sl, _sc) {
 165        return schnorr.VerifySchnorr(readBytes(pkp, pkl), readBytes(mp, ml), readBytes(sp, sl)) ? 1 : 0;
 166      },
 167      schnorr_ecdh(skp, skl, _skc, pkp, pkl, _pkc, op, ol, ok) {
 168        try {
 169          const r = schnorr.ECDH(readBytes(skp, skl), readBytes(pkp, pkl));
 170          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 171        } catch { setOutI32(ok, 0); }
 172      },
 173      schnorr_sha256sum(p, l, _c, op, ol) {
 174        setOutBytes(op, ol, schnorr.SHA256Sum(readBytes(p, l)));
 175      },
 176      schnorr_scalar_add_mod_n(ap, al, _ac, bp, bl, _bc, op, ol, ok) {
 177        try {
 178          const r = schnorr.ScalarAddModN(readBytes(ap, al), readBytes(bp, bl));
 179          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 180        } catch { setOutI32(ok, 0); }
 181      },
 182      schnorr_compressed_pubkey(p, l, _c, op, ol, ok) {
 183        try {
 184          const r = schnorr.CompressedPubKey(readBytes(p, l));
 185          setOutBytes(op, ol, r); setOutI32(ok, r ? 1 : 0);
 186        } catch { setOutI32(ok, 0); }
 187      },
 188  
 189      // --- ed25519 ---
 190      ed25519_new_key_from_seed(p, l, _c, op, ol) {
 191        setOutBytes(op, ol, ed25519.NewKeyFromSeed(readBytes(p, l)));
 192      },
 193      ed25519_sign(sp, sl, _sc, mp, ml, _mc, op, ol) {
 194        setOutBytes(op, ol, ed25519.Sign(readBytes(sp, sl), readBytes(mp, ml)));
 195      },
 196      ed25519_verify(pkp, pkl, _pkc, mp, ml, _mc, sp, sl, _sc) {
 197        return ed25519.Verify(readBytes(pkp, pkl), readBytes(mp, ml), readBytes(sp, sl)) ? 1 : 0;
 198      },
 199  
 200      // --- x25519 ---
 201      x25519_scalar_mult(sp, sl, _sc, pp, pl, _pc, op, ol) {
 202        setOutBytes(op, ol, x25519.ScalarMult(readBytes(sp, sl), readBytes(pp, pl)));
 203      },
 204      x25519_scalar_base_mult(sp, sl, _sc, op, ol) {
 205        setOutBytes(op, ol, x25519.ScalarBaseMult(readBytes(sp, sl)));
 206      },
 207  
 208      // --- p256 ---
 209      p256_scalar_base_mult(p, l, _c, op, ol) {
 210        setOutBytes(op, ol, p256.ScalarBaseMult(readBytes(p, l)));
 211      },
 212      p256_scalar_mult(sp, sl, _sc, pp, pl, _pc, op, ol) {
 213        setOutBytes(op, ol, p256.ScalarMult(readBytes(sp, sl), readBytes(pp, pl)));
 214      },
 215      p256_sign(sp, sl, _sc, mp, ml, _mc, op, ol) {
 216        setOutBytes(op, ol, p256.Sign(readBytes(sp, sl), readBytes(mp, ml)));
 217      },
 218      p256_verify(pkp, pkl, _pkc, mp, ml, _mc, sp, sl, _sc) {
 219        return p256.Verify(readBytes(pkp, pkl), readBytes(mp, ml), readBytes(sp, sl)) ? 1 : 0;
 220      },
 221      p256_reduce_scalar(p, l, _c, op, ol) {
 222        setOutBytes(op, ol, p256.ReduceScalar(readBytes(p, l)));
 223      },
 224      p256_valid_scalar(p, l, _c) {
 225        return p256.ValidScalar(readBytes(p, l)) ? 1 : 0;
 226      },
 227  
 228      // --- poly1305 ---
 229      poly1305_mac(kp, kl, _kc, dp, dl, _dc, op, ol) {
 230        setOutBytes(op, ol, poly1305.MAC(readBytes(kp, kl), readBytes(dp, dl)));
 231      },
 232      poly1305_verify(kp, kl, _kc, dp, dl, _dc, tp, tl, _tc) {
 233        return poly1305.Verify(readBytes(kp, kl), readBytes(dp, dl), readBytes(tp, tl)) ? 1 : 0;
 234      },
 235  
 236      // --- markdown ---
 237      markdown_render(p, l, _c, op, ol) {
 238        setOutStr(op, ol, markdown.Render(readStr(p, l)));
 239      },
 240  
 241      // --- dom ---
 242      dom_body() { return dom.Body(); },
 243      dom_create_element(p, l, _c) { return dom.CreateElement(readStr(p, l)); },
 244      dom_create_text_node(p, l, _c) { return dom.CreateTextNode(readStr(p, l)); },
 245      dom_get_element_by_id(p, l, _c) { return dom.GetElementById(readStr(p, l)); },
 246      dom_query_selector(p, l, _c) { return dom.QuerySelector(readStr(p, l)); },
 247      dom_query_selector_from(root, p, l, _c) { return dom.QuerySelectorFrom(root, readStr(p, l)); },
 248      dom_append_child(parent, child) { dom.AppendChild(parent, child); },
 249      dom_remove_child(parent, child) { dom.RemoveChild(parent, child); },
 250      dom_remove(el) { dom.Remove(el); },
 251      dom_insert_before(parent, nc, rc) { dom.InsertBefore(parent, nc, rc); },
 252      dom_first_child(el) { return dom.FirstChild(el); },
 253      dom_first_element_child(el) { return dom.FirstElementChild(el); },
 254      dom_next_sibling(el) { return dom.NextSibling(el); },
 255      dom_release_element(el) { dom.ReleaseElement(el); },
 256      dom_set_attribute(el, np, nl, _nc, vp, vl, _vc) {
 257        dom.SetAttribute(el, readStr(np, nl), readStr(vp, vl));
 258      },
 259      dom_remove_attribute(el, np, nl, _nc) { dom.RemoveAttribute(el, readStr(np, nl)); },
 260      dom_get_attribute(el, np, nl, _nc, op, ol) { setOutStr(op, ol, dom.GetAttribute(el, readStr(np, nl))); },
 261      dom_set_text_content(el, p, l, _c) { dom.SetTextContent(el, readStr(p, l)); },
 262      dom_set_inner_html(el, p, l, _c) { dom.SetInnerHTML(el, readStr(p, l)); },
 263      dom_set_style(el, pp, pl, _pc, vp, vl, _vc) {
 264        dom.SetStyle(el, readStr(pp, pl), readStr(vp, vl));
 265      },
 266      dom_add_class(el, p, l, _c) { dom.AddClass(el, readStr(p, l)); },
 267      dom_remove_class(el, p, l, _c) { dom.RemoveClass(el, readStr(p, l)); },
 268      dom_focus(el) { dom.Focus(el); },
 269      dom_set_property(el, pp, pl, _pc, vp, vl, _vc) {
 270        dom.SetProperty(el, readStr(pp, pl), readStr(vp, vl));
 271      },
 272      dom_get_property(el, pp, pl, _pc, op, ol) {
 273        setOutStr(op, ol, dom.GetProperty(el, readStr(pp, pl)));
 274      },
 275      dom_bounding_client_left(el) { return dom.BoundingClientLeft(el); },
 276      dom_get_bounding_rect(el, cbID) {
 277        const r = dom.GetBoundingRect_sync ? dom.GetBoundingRect_sync(el) : null;
 278        if (r) { cb6i(cbID, r.left|0, r.top|0, r.right|0, r.bottom|0, r.width|0, r.height|0); }
 279        else { dom.GetBoundingRect(el, (l,t,r,b,w,h) => cb6i(cbID, l,t,r,b,w,h)); }
 280      },
 281      dom_get_viewport_height() { return dom.GetViewportHeight(); },
 282      dom_get_viewport_width() { return dom.GetViewportWidth(); },
 283      dom_add_event_listener(elId, ep, el2, _ec, cbID) {
 284        const el = dom.getElement(elId);
 285        if (!el) return;
 286        const event = readStr(ep, el2);
 287        const fn = () => cb0(cbID);
 288        el.addEventListener(event, fn);
 289        jsListeners.set(listenerKey(elId, event, cbID), fn);
 290      },
 291      dom_add_self_event_listener(elId, ep, el2, _ec, cbID) {
 292        const el = dom.getElement(elId);
 293        if (!el) return;
 294        const event = readStr(ep, el2);
 295        const fn = (e) => { if (e.target === e.currentTarget) cb0(cbID); };
 296        el.addEventListener(event, fn);
 297        jsListeners.set(listenerKey(elId, event, cbID), fn);
 298      },
 299      dom_add_enter_key_listener(elId, cbID) {
 300        const el = dom.getElement(elId);
 301        if (!el) return;
 302        const fn = (e) => { if (e.key === 'Enter') { e.preventDefault(); cb0(cbID); } };
 303        el.addEventListener('keydown', fn);
 304        jsListeners.set(listenerKey(elId, 'keydown:enter', cbID), fn);
 305      },
 306      dom_remove_event_listener(elId, ep, el2, _ec, cbID) {
 307        const el = dom.getElement(elId);
 308        if (!el) return;
 309        const event = readStr(ep, el2);
 310        const key = listenerKey(elId, event, cbID);
 311        const fn = jsListeners.get(key);
 312        if (fn) { el.removeEventListener(event, fn); jsListeners.delete(key); }
 313      },
 314      dom_request_animation_frame(cbID) { requestAnimationFrame(() => cb0(cbID)); },
 315      dom_observe_resize(elId, cbID) { dom.ObserveResize(elId, cbID); },
 316      dom_unobserve_resize(elId) { dom.UnobserveResize(elId); },
 317      dom_on_keydown(el, handlerID) {
 318        const element = dom.getElement(el);
 319        if (!element) return;
 320        element.addEventListener('keydown', (e) => {
 321          const { ptr, len } = writeStr(e.key);
 322          const prevented = instance.exports.__hook_keydown(handlerID, ptr, len);
 323          if (prevented) e.preventDefault();
 324        });
 325      },
 326      dom_insert_mention_chip(el, charCount, np, nl, _nc, nmp, nml, _nmc, pp, pl, _pc) {
 327        dom.InsertMentionChip(el, charCount, readStr(np, nl), readStr(nmp, nml), readStr(pp, pl));
 328      },
 329      dom_set_timeout(cbID, ms) {
 330        return setTimeout(() => cb0(cbID), ms);
 331      },
 332      dom_clear_timeout(id) { clearTimeout(id); },
 333      dom_fetch_text(p, l, _c, cbID) {
 334        dom.FetchText(readStr(p, l), (s) => cbs(cbID, s));
 335      },
 336      dom_fetch_relay_info(p, l, _c, cbID) {
 337        dom.FetchRelayInfo(readStr(p, l), (s) => cbs(cbID, s));
 338      },
 339      dom_fetch_put_blob_base64(up, ul, _uc, dp, dl, _dc, cp, cl, _cc, ap, al, _ac, cbID) {
 340        dom.FetchPutBlobBase64(readStr(up, ul), readStr(dp, dl), readStr(cp, cl), readStr(ap, al), (s) => cbs(cbID, s));
 341      },
 342      dom_idb_set_enc_key(p, l, _c) { dom.IDBSetEncKey(readStr(p, l)); },
 343      dom_idb_get(sp, sl, _sc, kp, kl, _kc, cbID) {
 344        dom.IDBGet(readStr(sp, sl), readStr(kp, kl), (s) => cbs(cbID, s));
 345      },
 346      dom_idb_put(sp, sl, _sc, kp, kl, _kc, vp, vl, _vc) {
 347        dom.IDBPut(readStr(sp, sl), readStr(kp, kl), readStr(vp, vl));
 348      },
 349      dom_idb_get_all(sp, sl, _sc, fnID, doneID) {
 350        dom.IDBGetAll(readStr(sp, sl), (k, v) => cbss(fnID, k, v), () => cb0(doneID));
 351      },
 352      dom_prefers_dark() { return dom.PrefersDark() ? 1 : 0; },
 353      dom_console_log(p, l, _c) { console.log(readStr(p, l)); },
 354      dom_confirm(p, l, _c) { return confirm(readStr(p, l)) ? 1 : 0; },
 355      dom_post_to_sw(p, l, _c) { dom.PostToSW(readStr(p, l)); },
 356      dom_on_sw_message(cbID) { dom.OnSWMessage((s) => cbs(cbID, s)); },
 357      dom_push_state(p, l, _c) { dom.PushState(readStr(p, l)); },
 358      dom_replace_state(p, l, _c) { dom.ReplaceState(readStr(p, l)); },
 359      dom_back() { history.back(); },
 360      dom_location_reload() { location.reload(); },
 361      dom_hard_refresh() { dom.HardRefresh(); },
 362      dom_clear_storage_prefix(p, l, _c) { dom.ClearStoragePrefix(readStr(p, l)); },
 363      dom_timezone_offset_seconds() { return -(new Date().getTimezoneOffset()) * 60; },
 364      dom_read_clipboard(cbID) { dom.ReadClipboard((s) => cbs(cbID, s)); },
 365      dom_write_clipboard(p, l, _c, cbID) { dom.WriteClipboard(readStr(p, l), (ok) => cbb(cbID, ok)); },
 366      dom_get_path(op, ol) { setOutStr(op, ol, location.pathname); },
 367      dom_get_hash(op, ol) { setOutStr(op, ol, location.hash); },
 368      dom_hostname(op, ol) { setOutStr(op, ol, location.hostname); },
 369      dom_port(op, ol) { setOutStr(op, ol, location.port); },
 370      dom_user_agent(op, ol) { setOutStr(op, ol, navigator.userAgent); },
 371      dom_on_pop_state(cbID) {
 372        window.addEventListener('popstate', () => cbs(cbID, location.pathname));
 373      },
 374      dom_pick_file_text(p, l, _c, cbID) { dom.PickFileText(readStr(p, l), (s) => cbs(cbID, s)); },
 375      dom_download_text(fp, fl, _fc, cp, cl, _cc, mp, ml, _mc) {
 376        dom.DownloadText(readStr(fp, fl), readStr(cp, cl), readStr(mp, ml));
 377      },
 378      dom_on_pull_refresh(el, ind, cbID) { dom.OnPullRefresh(el, ind, () => cb0(cbID)); },
 379      dom_now_seconds() { return BigInt(Math.floor(Date.now() / 1000)); },
 380      dom_pick_file_base64(p, l, _c, cbID) {
 381        dom.PickFileBase64(readStr(p, l), (d, m) => cbss(cbID, d, m));
 382      },
 383      dom_on_paste_image(el, cbID) { dom.OnPasteImage(el, (d, m) => cbss(cbID, d, m)); },
 384      dom_on_drop_image(el, cbID) { dom.OnDropImage(el, (d, m) => cbss(cbID, d, m)); },
 385  
 386      // --- ws ---
 387      ws_dial(up, ul, _uc, msgID, openID, closeID, errID) {
 388        return ws.Dial(readStr(up, ul),
 389          (conn, s) => cbis(msgID, conn, s),
 390          (conn) => cbi(openID, conn),
 391          (conn, code, reason) => cbiis(closeID, conn, code, reason),
 392          (conn) => cbi(errID, conn),
 393        );
 394      },
 395      ws_send(conn, mp, ml, _mc) { return ws.Send(conn, readStr(mp, ml)) ? 1 : 0; },
 396      ws_close(conn) { ws.Close(conn); },
 397      ws_ready_state(conn) { return ws.ReadyState(conn); },
 398  
 399      // --- bc ---
 400      bc_open(np, nl, _nc, cbID) {
 401        return (typeof BroadcastChannel !== 'undefined')
 402          ? (() => { const bc = new BroadcastChannel(readStr(np, nl)); bc.onmessage = (e) => cbs(cbID, String(e.data)); return 1; })()
 403          : 0;
 404      },
 405      bc_send(handle, mp, ml, _mc) { /* placeholder - needs handle tracking */ },
 406      bc_close(handle) { /* placeholder */ },
 407  
 408      // --- wasm ---
 409      wasm_load_go_wasm(ep, el, _ec, wp, wl, _wc, cbID) {
 410        // Placeholder: loads another Go WASM binary
 411        cb0(cbID);
 412      },
 413  
 414      // --- node ---
 415      node_on_line(cbID) { if (node.OnLine) node.OnLine((s) => cbs(cbID, s)); },
 416      node_on_close(cbID) { if (node.OnClose) node.OnClose(() => cb0(cbID)); },
 417      node_write_line(p, l, _c) { if (node.WriteLine) node.WriteLine(readStr(p, l)); },
 418      node_write_err(p, l, _c) { if (node.WriteErr) node.WriteErr(readStr(p, l)); },
 419      node_exit(code) { if (node.Exit) node.Exit(code); },
 420      node_now_seconds() { return BigInt(Math.floor(Date.now() / 1000)); },
 421  
 422      // --- subtle ---
 423      subtle_random_bytes(p, l, _c) {
 424        const buf = new Uint8Array(memory.buffer, p, l);
 425        globalThis.crypto.getRandomValues(buf);
 426      },
 427      subtle_aes_cbc_encrypt(kp, kl, _kc, ip, il, _ic, pp, pl, _pc, cbID) {
 428        subtle.AESCBCEncrypt(readBytes(kp, kl), readBytes(ip, il), readBytes(pp, pl), (r) => cbdata(cbID, r));
 429      },
 430      subtle_aes_cbc_decrypt(kp, kl, _kc, ip, il, _ic, cp, cl, _cc, cbID) {
 431        subtle.AESCBCDecrypt(readBytes(kp, kl), readBytes(ip, il), readBytes(cp, cl), (r) => cbdata(cbID, r));
 432      },
 433      subtle_aes_gcm_encrypt(kp, kl, _kc, ip, il, _ic, pp, pl, _pc, cbID) {
 434        subtle.AESGCMEncrypt(readBytes(kp, kl), readBytes(ip, il), readBytes(pp, pl), (r) => cbdata(cbID, r));
 435      },
 436      subtle_aes_gcm_decrypt(kp, kl, _kc, ip, il, _ic, cp, cl, _cc, cbID) {
 437        subtle.AESGCMDecrypt(readBytes(kp, kl), readBytes(ip, il), readBytes(cp, cl), (r) => cbdata(cbID, r));
 438      },
 439      subtle_pbkdf2_derive_key(pp, pl, _pc, sp, sl, _sc, iterations, cbID) {
 440        subtle.PBKDF2DeriveKey(readStr(pp, pl), readBytes(sp, sl), iterations, (r) => cbdata(cbID, r));
 441      },
 442      subtle_argon2id_derive_key(pp, pl, _pc, sp, sl, _sc, t, m, p, dkLen, cbID) {
 443        subtle.Argon2idDeriveKey(readStr(pp, pl), readBytes(sp, sl), t, m, p, dkLen, (r) => cbdata(cbID, r));
 444      },
 445      subtle_sha256_hex(p, l, _c, cbID) {
 446        subtle.SHA256Hex(readBytes(p, l), (s) => cbs(cbID, s));
 447      },
 448      subtle_hmac_sha512(kp, kl, _kc, dp, dl, _dc, cbID) {
 449        subtle.HMACSHA512(readBytes(kp, kl), readBytes(dp, dl), (r) => cbdata(cbID, r));
 450      },
 451      subtle_pbkdf2_sha512(pp, pl, _pc, sp, sl, _sc, iterations, dkLen, cbID) {
 452        subtle.PBKDF2SHA512(readStr(pp, pl), readBytes(sp, sl), iterations, dkLen, (r) => cbdata(cbID, r));
 453      },
 454  
 455      // --- signer ---
 456      signer_has_signer() { return signer.HasSigner() ? 1 : 0; },
 457      signer_has_mls() { return signer.HasMLS() ? 1 : 0; },
 458      signer_get_public_key(cbID) { signer.GetPublicKey((s) => cbs(cbID, s)); },
 459      signer_sign_event(p, l, _c, cbID) { signer.SignEvent(readStr(p, l), (s) => cbs(cbID, s)); },
 460      signer_get_shared_secret(p, l, _c, cbID) { signer.GetSharedSecret(readStr(p, l), (s) => cbs(cbID, s)); },
 461      signer_nip04_decrypt(pkp, pkl, _pkc, cp, cl, _cc, cbID) {
 462        signer.Nip04Decrypt(readStr(pkp, pkl), readStr(cp, cl), (s) => cbs(cbID, s));
 463      },
 464      signer_nip04_encrypt(pkp, pkl, _pkc, pp, pl, _pc, cbID) {
 465        signer.Nip04Encrypt(readStr(pkp, pkl), readStr(pp, pl), (s) => cbs(cbID, s));
 466      },
 467      signer_nip44_decrypt(pkp, pkl, _pkc, cp, cl, _cc, cbID) {
 468        signer.Nip44Decrypt(readStr(pkp, pkl), readStr(cp, cl), (s) => cbs(cbID, s));
 469      },
 470      signer_nip44_encrypt(pkp, pkl, _pkc, pp, pl, _pc, cbID) {
 471        signer.Nip44Encrypt(readStr(pkp, pkl), readStr(pp, pl), (s) => cbs(cbID, s));
 472      },
 473      signer_is_installed(cbID) { signer.IsInstalled((v) => cbb(cbID, v)); },
 474      signer_get_vault_status(cbID) { signer.GetVaultStatus((s) => cbs(cbID, s)); },
 475      signer_create_vault(p, l, _c, cbID) { signer.CreateVault(readStr(p, l), (v) => cbb(cbID, v)); },
 476      signer_unlock_vault(p, l, _c, cbID) { signer.UnlockVault(readStr(p, l), (v) => cbb(cbID, v)); },
 477      signer_lock_vault(cbID) { signer.LockVault(() => cb0(cbID)); },
 478      signer_list_identities(cbID) { signer.ListIdentities((s) => cbs(cbID, s)); },
 479      signer_add_identity(p, l, _c, cbID) { signer.AddIdentity(readStr(p, l), (v) => cbb(cbID, v)); },
 480      signer_nsec_login(p, l, _c, cbID) { signer.NsecLogin(readStr(p, l), (v) => cbb(cbID, v)); },
 481      signer_switch_identity(p, l, _c, cbID) { signer.SwitchIdentity(readStr(p, l), (v) => cbb(cbID, v)); },
 482      signer_export_vault(p, l, _c, cbID) { signer.ExportVault(readStr(p, l), (s) => cbs(cbID, s)); },
 483      signer_import_vault(p, l, _c, cbID) { signer.ImportVault(readStr(p, l), (v) => cbb(cbID, v)); },
 484      signer_remove_identity(p, l, _c, cbID) { signer.RemoveIdentity(readStr(p, l), (v) => cbb(cbID, v)); },
 485      signer_generate_mnemonic(cbID) { signer.GenerateMnemonic((s) => cbs(cbID, s)); },
 486      signer_validate_mnemonic(p, l, _c, cbID) { signer.ValidateMnemonic(readStr(p, l), (v) => cbb(cbID, v)); },
 487      signer_create_hd_vault(pp, pl, _pc, np, nl, _nc, cbID) {
 488        signer.CreateHDVault(readStr(pp, pl), readStr(np, nl), (s) => cbs(cbID, s));
 489      },
 490      signer_restore_hd_vault(pp, pl, _pc, mp, ml, _mc, np, nl, _nc, cbID) {
 491        signer.RestoreHDVault(readStr(pp, pl), readStr(mp, ml), readStr(np, nl), (v) => cbb(cbID, v));
 492      },
 493      signer_derive_identity(p, l, _c, cbID) { signer.DeriveIdentity(readStr(p, l), (s) => cbs(cbID, s)); },
 494      signer_get_mnemonic(cbID) { signer.GetMnemonic((s) => cbs(cbID, s)); },
 495      signer_probe_account(index, cbID) { signer.ProbeAccount(index, (s) => cbs(cbID, s)); },
 496      signer_is_hd(cbID) { signer.IsHD((v) => cbb(cbID, v)); },
 497      signer_reset_extension(cbID) { signer.ResetExtension((v) => cbb(cbID, v)); },
 498      signer_nwc_list(cbID) { signer.NwcList((s) => cbs(cbID, s)); },
 499      signer_nwc_add(up, ul, _uc, ap, al, _ac, createdAt, cbID) {
 500        signer.NwcAdd(readStr(up, ul), readStr(ap, al), Number(createdAt), (s) => cbs(cbID, s));
 501      },
 502      signer_nwc_remove(p, l, _c, cbID) { signer.NwcRemove(readStr(p, l), (v) => cbb(cbID, v)); },
 503      signer_nwc_build_request(ip, il, _ic, mp, ml, _mc, pp, pl, _pc, ep, el2, _ec, createdAt, cbID) {
 504        signer.NwcBuildRequest(readStr(ip, il), readStr(mp, ml), readStr(pp, pl), readStr(ep, el2), Number(createdAt), (s) => cbs(cbID, s));
 505      },
 506      signer_nwc_parse_response(ip, il, _ic, cp, cl, _cc, ep, el2, _ec, cbID) {
 507        signer.NwcParseResponse(readStr(ip, il), readStr(cp, cl), readStr(ep, el2), (s) => cbs(cbID, s));
 508      },
 509  
 510      // --- idb ---
 511      idb_set_enc_key(p, l, _c) { idb.SetEncKey(readStr(p, l)); },
 512      idb_open(cbID) { idb.Open(() => cb0(cbID)); },
 513      idb_save_event(p, l, _c, cbID) { idb.SaveEvent(readStr(p, l), (v) => cbb(cbID, v)); },
 514      idb_query_events(p, l, _c, cbID) { idb.QueryEvents(readStr(p, l), (s) => cbs(cbID, s)); },
 515      idb_save_dm(p, l, _c, cbID) { idb.SaveDM(readStr(p, l), (s) => cbs(cbID, s)); },
 516      idb_query_dms(pp, pl, _pc, limit, until, cbID) {
 517        idb.QueryDMs(readStr(pp, pl), limit, Number(until), (s) => cbs(cbID, s));
 518      },
 519      idb_get_conversation_list(cbID) { idb.GetConversationList((s) => cbs(cbID, s)); },
 520      idb_clear_dms_by_peer(p, l, _c, cbID) { idb.ClearDMsByPeer(readStr(p, l), () => cb0(cbID)); },
 521      idb_set_version(p, l, _c) { idb.SetVersion(readStr(p, l)); },
 522      idb_mls_save_group(gp, gl, _gc, sp, sl, _sc, cbID) {
 523        idb.MlsSaveGroup(readStr(gp, gl), readStr(sp, sl), () => cb0(cbID));
 524      },
 525      idb_mls_load_group(gp, gl, _gc, cbID) {
 526        idb.MlsLoadGroup(readStr(gp, gl), (s) => cbs(cbID, s));
 527      },
 528      idb_mls_list_groups(cbID) { idb.MlsListGroups((s) => cbs(cbID, s)); },
 529      idb_mls_save_kpp(p, l, _c, cbID) { idb.MlsSaveKPP(readStr(p, l), () => cb0(cbID)); },
 530      idb_mls_load_kpp(cbID) { idb.MlsLoadKPP((s) => cbs(cbID, s)); },
 531  
 532      // --- ext ---
 533      ext_storage_get(kp, kl, _kc, cbID) { ext.StorageGet(readStr(kp, kl), (s) => cbs(cbID, s)); },
 534      ext_storage_set(kp, kl, _kc, vp, vl, _vc) { ext.StorageSet(readStr(kp, kl), readStr(vp, vl)); },
 535      ext_storage_remove(kp, kl, _kc) { ext.StorageRemove(readStr(kp, kl)); },
 536      ext_on_message(_unused) {
 537        ext.OnMessage((method, params, tabID, respond) => {
 538          const reqID = hookNextReq++;
 539          hookResponses.set(reqID, respond);
 540          const m = writeStr(method);
 541          const p = writeStr(params);
 542          queueMicrotask(() => instance.exports.__hook_ext_on_message(reqID, m.ptr, m.len, p.ptr, p.len, tabID));
 543        });
 544      },
 545      ext_on_message_respond(reqID, p, l, _c) {
 546        const respond = hookResponses.get(reqID);
 547        if (respond) { hookResponses.delete(reqID); respond(readStr(p, l)); }
 548      },
 549      ext_send_message_to_tab(tabID, mp, ml, _mc) { ext.SendMessageToTab(tabID, readStr(mp, ml)); },
 550      ext_get_active_tab(cbID) { ext.GetActiveTab((id, url) => cbis(cbID, id, url)); },
 551      ext_console_log(p, l, _c) { console.log(readStr(p, l)); },
 552      ext_session_get(kp, kl, _kc, cbID) { ext.SessionGet(readStr(kp, kl), (s) => cbs(cbID, s)); },
 553      ext_session_set(kp, kl, _kc, vp, vl, _vc) { ext.SessionSet(readStr(kp, kl), readStr(vp, vl)); },
 554      ext_is_in_page() { return ext.IsInPage() ? 1 : 0; },
 555  
 556      // --- registry ---
 557      registry_set_seckey(p, l, _c) { self.$hooks = self.$hooks || {}; self.$hooks.seckey = readStr(p, l); },
 558      registry_seckey(op, ol) { setOutStr(op, ol, (self.$hooks && self.$hooks.seckey) || ''); },
 559      registry_set_pubkey(p, l, _c) { self.$hooks = self.$hooks || {}; self.$hooks.pubkey = readStr(p, l); },
 560      registry_pubkey(op, ol) { setOutStr(op, ol, (self.$hooks && self.$hooks.pubkey) || ''); },
 561      registry_set_has_key(v) { self.$hooks = self.$hooks || {}; self.$hooks.hasKey = v !== 0; },
 562      registry_has_key() { return (self.$hooks && self.$hooks.hasKey) ? 1 : 0; },
 563  
 564      registry_on_marmot_init(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotInit = (s) => cbs(cbID, s); },
 565      registry_on_marmot_send(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotSend = (r, c) => cbss(cbID, r, c); },
 566      registry_on_marmot_subscribe(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotSubscribe = () => cb0(cbID); },
 567      registry_on_marmot_publish_kp(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotPublishKP = (s) => cbs(cbID, s); },
 568      registry_on_marmot_list_groups(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.marmotListGroups = (s) => cbs(cbID, s); },
 569      registry_on_save_dm_record(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.saveDMRecord = (s) => cbs(cbID, s); },
 570      registry_on_broadcast_to_clients(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.broadcastToClients = (s) => cbs(cbID, s); },
 571      registry_on_send_to_client(cbID) { self.$hooks = self.$hooks || {}; self.$hooks.sendToClient = (id, m) => cbss(cbID, id, m); },
 572  
 573      registry_on_encrypt_nip04() {
 574        self.$hooks = self.$hooks || {};
 575        self.$hooks.encryptNip04 = (pk, ct) => new Promise(resolve => {
 576          const reqID = hookNextReq++;
 577          hookResponses.set(reqID, resolve);
 578          const p = writeStr(pk); const c = writeStr(ct);
 579          queueMicrotask(() => instance.exports.__hook_encrypt_nip04(reqID, p.ptr, p.len, c.ptr, c.len));
 580        });
 581      },
 582      registry_on_encrypt_nip17() {
 583        self.$hooks = self.$hooks || {};
 584        self.$hooks.encryptNip17 = (pk, ct) => new Promise(resolve => {
 585          const reqID = hookNextReq++;
 586          hookResponses.set(reqID, resolve);
 587          const p = writeStr(pk); const c = writeStr(ct);
 588          queueMicrotask(() => instance.exports.__hook_encrypt_nip17(reqID, p.ptr, p.len, c.ptr, c.len));
 589        });
 590      },
 591      registry_on_decrypt_dm() {
 592        self.$hooks = self.$hooks || {};
 593        self.$hooks.decryptDM = (ev) => new Promise(resolve => {
 594          const reqID = hookNextReq++;
 595          hookResponses.set(reqID, resolve);
 596          const e = writeStr(ev);
 597          queueMicrotask(() => instance.exports.__hook_decrypt_dm(reqID, e.ptr, e.len));
 598        });
 599      },
 600      registry_hook_respond_s(reqID, p, l, _c) {
 601        const resolve = hookResponses.get(reqID);
 602        if (resolve) { hookResponses.delete(reqID); resolve(readStr(p, l)); }
 603      },
 604      registry_hook_respond_ss(reqID, p1, l1, _c1, p2, l2, _c2) {
 605        const resolve = hookResponses.get(reqID);
 606        if (resolve) { hookResponses.delete(reqID); resolve([readStr(p1, l1), readStr(p2, l2)]); }
 607      },
 608  
 609      registry_encrypt_nip04(pkp, pkl, _pkc, cp, cl, _cc, cbID) {
 610        if (self.$hooks && self.$hooks.encryptNip04) {
 611          self.$hooks.encryptNip04(readStr(pkp, pkl), readStr(cp, cl)).then((s) => cbs(cbID, s));
 612        }
 613      },
 614      registry_encrypt_nip17(pkp, pkl, _pkc, cp, cl, _cc, cbID) {
 615        if (self.$hooks && self.$hooks.encryptNip17) {
 616          self.$hooks.encryptNip17(readStr(pkp, pkl), readStr(cp, cl)).then(([a, b]) => cbss(cbID, a, b));
 617        }
 618      },
 619      registry_decrypt_dm(p, l, _c, cbID) {
 620        if (self.$hooks && self.$hooks.decryptDM) {
 621          self.$hooks.decryptDM(readStr(p, l)).then((s) => cbs(cbID, s));
 622        }
 623      },
 624      registry_make_dm_record(pp, pl, _pc, fp, fl, _fc, cp, cl, _cc, ts, prp, prl, _prc, ep, el2, _ec, op, ol) {
 625        // Build DM record JSON directly
 626        const peer = readStr(pp, pl), from = readStr(fp, fl), content = readStr(cp, cl);
 627        const proto = readStr(prp, prl), eid = readStr(ep, el2);
 628        const r = JSON.stringify({peer, from, content, ts: Number(ts), proto, eid});
 629        setOutStr(op, ol, r);
 630      },
 631      registry_marmot_init(p, l, _c) { if (self.$hooks && self.$hooks.marmotInit) self.$hooks.marmotInit(readStr(p, l)); },
 632      registry_marmot_send(rp, rl, _rc, cp, cl, _cc) { if (self.$hooks && self.$hooks.marmotSend) self.$hooks.marmotSend(readStr(rp, rl), readStr(cp, cl)); },
 633      registry_marmot_subscribe() { if (self.$hooks && self.$hooks.marmotSubscribe) self.$hooks.marmotSubscribe(); },
 634      registry_marmot_publish_kp(p, l, _c) { if (self.$hooks && self.$hooks.marmotPublishKP) self.$hooks.marmotPublishKP(readStr(p, l)); },
 635      registry_marmot_list_groups(p, l, _c) { if (self.$hooks && self.$hooks.marmotListGroups) self.$hooks.marmotListGroups(readStr(p, l)); },
 636      registry_save_dm_record(p, l, _c) { if (self.$hooks && self.$hooks.saveDMRecord) self.$hooks.saveDMRecord(readStr(p, l)); },
 637      registry_broadcast_to_clients(p, l, _c) { if (self.$hooks && self.$hooks.broadcastToClients) self.$hooks.broadcastToClients(readStr(p, l)); },
 638      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)); },
 639      registry_has_hook(p, l, _c) { return (self.$hooks && self.$hooks[readStr(p, l)]) ? 1 : 0; },
 640      registry_load_module(p, l, _c, cbID) {
 641        // Module loading is moxiejs-specific; in WASM all modules are compiled in
 642        cb0(cbID);
 643      },
 644  
 645      // --- sw ---
 646      sw_on_install(cbID) { self.addEventListener('install', (e) => cbi(cbID, 0)); },
 647      sw_on_activate(cbID) { self.addEventListener('activate', (e) => cbi(cbID, 0)); },
 648      sw_on_fetch(cbID) { self.addEventListener('fetch', (e) => cbi(cbID, 0)); },
 649      sw_on_message(cbID) { self.addEventListener('message', (e) => cbi(cbID, 0)); },
 650      sw_wait_until(event, cbID) { /* simplified - actual impl needs event ref */ cb0(cbID); },
 651      sw_respond_with(event, resp) { /* needs event/response tracking */ },
 652      sw_respond_with_network(event) { /* pass-through */ },
 653      sw_respond_with_cache_first(event) { /* needs event tracking */ },
 654      sw_respond_with_network_first(event) { /* needs event tracking */ },
 655      sw_get_request_url(event, op, ol) { setOutStr(op, ol, ''); },
 656      sw_get_request_path(event, op, ol) { setOutStr(op, ol, ''); },
 657      sw_get_message_data(event, op, ol) { setOutStr(op, ol, ''); },
 658      sw_get_message_client_id(event, op, ol) { setOutStr(op, ol, ''); },
 659      sw_origin(op, ol) { setOutStr(op, ol, self.location ? self.location.origin : ''); },
 660      sw_skip_waiting() { if (self.skipWaiting) self.skipWaiting(); },
 661      sw_claim_clients(cbID) {
 662        if (self.clients && self.clients.claim) self.clients.claim().then(() => cb0(cbID));
 663        else cb0(cbID);
 664      },
 665      sw_match_clients(cbID) { /* simplified */ },
 666      sw_post_message(client, p, l, _c) { /* needs client handle tracking */ },
 667      sw_post_message_json(client, p, l, _c) { /* needs client handle tracking */ },
 668      sw_get_client_by_id(p, l, _c, cbID) { cbii(cbID, 0, 0); },
 669      sw_navigate(client, p, l, _c) { /* needs client handle tracking */ },
 670      sw_cache_open(p, l, _c, cbID) {
 671        caches.open(readStr(p, l)).then(() => cbi(cbID, 1));
 672      },
 673      sw_cache_add_all(cache, up, ul, _uc, count, cbID) {
 674        const urls = splitNull(up, ul, count);
 675        caches.open('v1').then(c => c.addAll(urls)).then(() => cb0(cbID));
 676      },
 677      sw_cache_from_manifests(cache, fp, fl, _fc, count, cbID) { cb0(cbID); },
 678      sw_cache_put(cache, up, ul, _uc, resp, cbID) { cb0(cbID); },
 679      sw_cache_match(p, l, _c, cbID) { cbi(cbID, 0); },
 680      sw_cache_delete(p, l, _c, cbID) {
 681        caches.delete(readStr(p, l)).then(() => cb0(cbID));
 682      },
 683      sw_fetch(p, l, _c, cbID) {
 684        fetch(readStr(p, l)).then(r => cbii(cbID, 1, r.ok ? 1 : 0)).catch(() => cbii(cbID, 0, 0));
 685      },
 686      sw_fetch_all(up, ul, _uc, count, eachID, doneID) {
 687        const urls = splitNull(up, ul, count);
 688        Promise.all(urls.map((u, i) => fetch(u).then(r => cbiii(eachID, i, 1, r.ok ? 1 : 0)).catch(() => cbiii(eachID, i, 0, 0))))
 689          .then(() => cb0(doneID));
 690      },
 691      sw_response_ok(resp) { return 0; },
 692      sw_sse_connect(p, l, _c, cbID) {
 693        const es = new EventSource(readStr(p, l));
 694        es.onmessage = (e) => cbs(cbID, e.data);
 695        return 1;
 696      },
 697      sw_sse_close(id) { /* needs handle tracking */ },
 698      sw_set_timeout(ms, cbID) { return setTimeout(() => cb0(cbID), ms); },
 699      sw_clear_timeout(t) { clearTimeout(t); },
 700      sw_now_seconds() { return BigInt(Math.floor(Date.now() / 1000)); },
 701      sw_now_millis() { return BigInt(Date.now()); },
 702      sw_log(p, l, _c) { console.log(readStr(p, l)); },
 703  
 704      // --- webgl ---
 705      webgl_CreateContext(canvasId) { return webgl.CreateContext(canvasId); },
 706      webgl_CreateContextFromHandle(el) { return webgl.CreateContextFromHandle(el); },
 707      webgl_DeleteContext(ctx) { webgl.DeleteContext(ctx); },
 708      webgl_Enable(ctx, cap) { webgl.Enable(ctx, cap); },
 709      webgl_Disable(ctx, cap) { webgl.Disable(ctx, cap); },
 710      webgl_Viewport(ctx, x, y, w, h) { webgl.Viewport(ctx, x, y, w, h); },
 711      webgl_Scissor(ctx, x, y, w, h) { webgl.Scissor(ctx, x, y, w, h); },
 712      webgl_BlendFunc(ctx, src, dst) { webgl.BlendFunc(ctx, src, dst); },
 713      webgl_BlendFuncSeparate(ctx, sr, dr, sa, da) { webgl.BlendFuncSeparate(ctx, sr, dr, sa, da); },
 714      webgl_BlendEquation(ctx, mode) { webgl.BlendEquation(ctx, mode); },
 715      webgl_DepthFunc(ctx, fn) { webgl.DepthFunc(ctx, fn); },
 716      webgl_DepthMask(ctx, flag) { webgl.DepthMask(ctx, flag); },
 717      webgl_ClearColor(ctx, r, g, b, a) { webgl.ClearColor(ctx, r, g, b, a); },
 718      webgl_ClearDepth(ctx, d) { webgl.ClearDepth(ctx, d); },
 719      webgl_Clear(ctx, mask) { webgl.Clear(ctx, mask); },
 720      webgl_Flush(ctx) { webgl.Flush(ctx); },
 721      webgl_Finish(ctx) { webgl.Finish(ctx); },
 722      webgl_GetError(ctx) { return webgl.GetError(ctx); },
 723      webgl_GetInteger(ctx, pname) { return webgl.GetInteger(ctx, pname); },
 724      webgl_IsEnabled(ctx, cap) { return webgl.IsEnabled(ctx, cap); },
 725      webgl_PixelStorei(ctx, pname, param) { webgl.PixelStorei(ctx, pname, param); },
 726      webgl_ReadPixels(ctx, x, y, w, h, fmt, typ, bufPtr, bufLen) { webgl.ReadPixels(ctx, x, y, w, h, fmt, typ, bufPtr, bufLen); },
 727      webgl_CreateBuffer(ctx) { return webgl.CreateBuffer(ctx); },
 728      webgl_DeleteBuffer(ctx, buf) { webgl.DeleteBuffer(ctx, buf); },
 729      webgl_BindBuffer(ctx, target, buf) { webgl.BindBuffer(ctx, target, buf); },
 730      webgl_BufferData(ctx, target, ptr, len, usage) { webgl.BufferData(ctx, target, ptr, len, usage); },
 731      webgl_BufferSubData(ctx, target, off, ptr, len) { webgl.BufferSubData(ctx, target, off, ptr, len); },
 732      webgl_CreateTexture(ctx) { return webgl.CreateTexture(ctx); },
 733      webgl_DeleteTexture(ctx, tex) { webgl.DeleteTexture(ctx, tex); },
 734      webgl_BindTexture(ctx, target, tex) { webgl.BindTexture(ctx, target, tex); },
 735      webgl_ActiveTexture(ctx, unit) { webgl.ActiveTexture(ctx, unit); },
 736      webgl_TexParameteri(ctx, target, pname, param) { webgl.TexParameteri(ctx, target, pname, param); },
 737      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); },
 738      webgl_TexStorage2D(ctx, target, lvls, ifmt, w, h) { webgl.TexStorage2D(ctx, target, lvls, ifmt, w, h); },
 739      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); },
 740      webgl_CopyTexSubImage2D(ctx, target, lvl, xo, yo, x, y, w, h) { webgl.CopyTexSubImage2D(ctx, target, lvl, xo, yo, x, y, w, h); },
 741      webgl_GenerateMipmap(ctx, target) { webgl.GenerateMipmap(ctx, target); },
 742      webgl_CreateFramebuffer(ctx) { return webgl.CreateFramebuffer(ctx); },
 743      webgl_DeleteFramebuffer(ctx, fbo) { webgl.DeleteFramebuffer(ctx, fbo); },
 744      webgl_BindFramebuffer(ctx, target, fbo) { webgl.BindFramebuffer(ctx, target, fbo); },
 745      webgl_FramebufferTexture2D(ctx, target, att, texTarget, tex, lvl) { webgl.FramebufferTexture2D(ctx, target, att, texTarget, tex, lvl); },
 746      webgl_CheckFramebufferStatus(ctx, target) { return webgl.CheckFramebufferStatus(ctx, target); },
 747      webgl_InvalidateFramebuffer(ctx, target, att) { webgl.InvalidateFramebuffer(ctx, target, att); },
 748      webgl_CreateRenderbuffer(ctx) { return webgl.CreateRenderbuffer(ctx); },
 749      webgl_DeleteRenderbuffer(ctx, rbo) { webgl.DeleteRenderbuffer(ctx, rbo); },
 750      webgl_BindRenderbuffer(ctx, target, rbo) { webgl.BindRenderbuffer(ctx, target, rbo); },
 751      webgl_RenderbufferStorage(ctx, target, ifmt, w, h) { webgl.RenderbufferStorage(ctx, target, ifmt, w, h); },
 752      webgl_FramebufferRenderbuffer(ctx, target, att, rbtarget, rbo) { webgl.FramebufferRenderbuffer(ctx, target, att, rbtarget, rbo); },
 753      webgl_CreateShader(ctx, typ) { return webgl.CreateShader(ctx, typ); },
 754      webgl_DeleteShader(ctx, sh) { webgl.DeleteShader(ctx, sh); },
 755      webgl_ShaderSource(ctx, sh, ptr, len) { webgl.ShaderSource(ctx, sh, ptr, len); },
 756      webgl_CompileShader(ctx, sh) { webgl.CompileShader(ctx, sh); },
 757      webgl_GetShaderParameter(ctx, sh, pname) { return webgl.GetShaderParameter(ctx, sh, pname); },
 758      webgl_GetShaderInfoLog(ctx, sh, ptr, len) { return webgl.GetShaderInfoLog(ctx, sh, ptr, len); },
 759      webgl_CreateProgram(ctx) { return webgl.CreateProgram(ctx); },
 760      webgl_DeleteProgram(ctx, prog) { webgl.DeleteProgram(ctx, prog); },
 761      webgl_AttachShader(ctx, prog, sh) { webgl.AttachShader(ctx, prog, sh); },
 762      webgl_LinkProgram(ctx, prog) { webgl.LinkProgram(ctx, prog); },
 763      webgl_UseProgram(ctx, prog) { webgl.UseProgram(ctx, prog); },
 764      webgl_GetProgramParameter(ctx, prog, pname) { return webgl.GetProgramParameter(ctx, prog, pname); },
 765      webgl_GetProgramInfoLog(ctx, prog, ptr, len) { return webgl.GetProgramInfoLog(ctx, prog, ptr, len); },
 766      webgl_BindAttribLocation(ctx, prog, idx, ptr, len) { webgl.BindAttribLocation(ctx, prog, idx, ptr, len); },
 767      webgl_GetUniformLocation(ctx, prog, ptr, len) { return webgl.GetUniformLocation(ctx, prog, ptr, len); },
 768      webgl_Uniform1f(ctx, loc, v) { webgl.Uniform1f(ctx, loc, v); },
 769      webgl_Uniform2f(ctx, loc, v0, v1) { webgl.Uniform2f(ctx, loc, v0, v1); },
 770      webgl_Uniform3f(ctx, loc, v0, v1, v2) { webgl.Uniform3f(ctx, loc, v0, v1, v2); },
 771      webgl_Uniform4f(ctx, loc, v0, v1, v2, v3) { webgl.Uniform4f(ctx, loc, v0, v1, v2, v3); },
 772      webgl_Uniform1i(ctx, loc, v) { webgl.Uniform1i(ctx, loc, v); },
 773      webgl_EnableVertexAttribArray(ctx, idx) { webgl.EnableVertexAttribArray(ctx, idx); },
 774      webgl_DisableVertexAttribArray(ctx, idx) { webgl.DisableVertexAttribArray(ctx, idx); },
 775      webgl_VertexAttribPointer(ctx, idx, size, typ, norm, stride, off) { webgl.VertexAttribPointer(ctx, idx, size, typ, norm, stride, off); },
 776      webgl_DrawArrays(ctx, mode, first, count) { webgl.DrawArrays(ctx, mode, first, count); },
 777      webgl_DrawElements(ctx, mode, count, typ, off) { webgl.DrawElements(ctx, mode, count, typ, off); },
 778  
 779      // --- text ---
 780      text_Measure(fp, fl, sz, tp, tl, wp, hp) { text.Measure(fp, fl, sz, tp, tl, wp, hp); },
 781      text_Render(fp, fl, sz, tp, tl, mw, dp, dc) { return text.Render(fp, fl, sz, tp, tl, mw, dp, dc); },
 782  
 783      // --- app ---
 784      app_CreateFullscreenCanvas() { app.CreateFullscreenCanvas(); },
 785      app_CreateWebGLContext() { return app.CreateWebGLContext(); },
 786      app_GetDevicePixelRatio() { return app.GetDevicePixelRatio(); },
 787      app_GetCanvasCSSSize(wPtr, hPtr) { app.GetCanvasCSSSize(wPtr, hPtr); },
 788      app_SetCanvasBacking(w, h) { app.SetCanvasBacking(w, h); },
 789      app_RequestRAF() { app.RequestRAF(); },
 790      app_SetCursor(ptr, len) { app.SetCursor(ptr, len); },
 791      app_SetTitle(ptr, len) { app.SetTitle(ptr, len); },
 792      app_SetFullscreen(full) { app.SetFullscreen(full); },
 793      app_RegisterPointerEvents() { app.RegisterPointerEvents(); },
 794      app_RegisterKeyEvents() { app.RegisterKeyEvents(); },
 795      app_RegisterResizeEvents() { app.RegisterResizeEvents(); },
 796    };
 797  }
 798  
 799  export async function load(wasmPath) {
 800    const response = await fetch(wasmPath);
 801    const bytes = await response.arrayBuffer();
 802  
 803    const bridge = buildBridge();
 804  
 805    const result = await WebAssembly.instantiate(bytes, {
 806      wasi_snapshot_preview1: {
 807        fd_write(fd, iovs, iovs_len, nwritten) {
 808          const dv = new DataView(memory.buffer);
 809          let totalWritten = 0;
 810          for (let i = 0; i < iovs_len; i++) {
 811            const ptr = dv.getUint32(iovs + i * 8, true);
 812            const len = dv.getUint32(iovs + i * 8 + 4, true);
 813            const s = decoder.decode(new Uint8Array(memory.buffer, ptr, len));
 814            if (fd === 1) console.log(s);
 815            else if (fd === 2) console.error(s);
 816            totalWritten += len;
 817          }
 818          dv.setUint32(nwritten, totalWritten, true);
 819          return 0;
 820        },
 821        clock_time_get(clockID, precision, time) {
 822          const now = clockID === 1
 823            ? performance.now() * 1_000_000
 824            : Date.now() * 1_000_000;
 825          const dv = new DataView(memory.buffer);
 826          dv.setBigInt64(time, BigInt(Math.floor(now)), true);
 827          return 0;
 828        },
 829      },
 830      bridge,
 831    });
 832  
 833    instance = result.instance;
 834    memory = instance.exports.memory;
 835    // Wire up reverse callbacks for app and text bridges.
 836    app.SetWASM(instance);
 837    text.SetWASM(instance);
 838    globalThis.__moxie_wasm_memory = instance.exports.memory;
 839    instance.exports._start();
 840    // Mark WASM as ready for event dispatch now that initialization is complete.
 841    app.SetWASMReady();
 842    return instance;
 843  }
 844