@muhkoo/connect 0.8.0-alpha.0 → 0.9.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3814,6 +3814,7 @@ class ShardClient {
3814
3814
  fetchFn;
3815
3815
  cache;
3816
3816
  deferUpload;
3817
+ peers;
3817
3818
  constructor(opts) {
3818
3819
  if (!opts?.baseUrl)
3819
3820
  throw new Error("ShardClient: `baseUrl` is required");
@@ -3831,6 +3832,7 @@ class ShardClient {
3831
3832
  this.fetchFn = f.bind(globalThis);
3832
3833
  this.cache = opts.cache;
3833
3834
  this.deferUpload = opts.deferUpload;
3835
+ this.peers = opts.peers;
3834
3836
  }
3835
3837
  /** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
3836
3838
  urlForHash(hash) {
@@ -3849,6 +3851,9 @@ class ShardClient {
3849
3851
  // this is safe to keep regardless of upload outcome.
3850
3852
  if (this.cache)
3851
3853
  await this.cache.put(hash, bytes);
3854
+ // Tell the swarm we now hold this block, so peers can pull it from us.
3855
+ if (this.peers)
3856
+ this.peers.announce(hash);
3852
3857
  let response;
3853
3858
  try {
3854
3859
  response = await this.fetchFn(url, {
@@ -3892,6 +3897,22 @@ class ShardClient {
3892
3897
  if (cached)
3893
3898
  return cached;
3894
3899
  }
3900
+ // Peers next: ask the Space swarm before spending origin egress. Peer
3901
+ // blocks are hash-verified by the block engine; on miss/timeout we fall
3902
+ // through to origin. Best-effort — never let a peer error block a read.
3903
+ if (this.peers) {
3904
+ try {
3905
+ const fromPeer = await this.peers.getBlock(hash);
3906
+ if (fromPeer) {
3907
+ if (this.cache)
3908
+ void this.cache.put(hash, fromPeer);
3909
+ return fromPeer;
3910
+ }
3911
+ }
3912
+ catch {
3913
+ /* peer layer hiccup — fall through to origin */
3914
+ }
3915
+ }
3895
3916
  const url = this.urlForHash(hash);
3896
3917
  let response;
3897
3918
  try {
@@ -4940,7 +4961,7 @@ function base64UrlDecode(s) {
4940
4961
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
4941
4962
  * `package.json` `version`.
4942
4963
  */
4943
- const VERSION = "0.8.0-alpha.0";
4964
+ const VERSION = "0.9.0-alpha.0";
4944
4965
 
4945
4966
  /**
4946
4967
  * `HttpClient` — the one place the unified {@link Client} talks to the
@@ -7037,6 +7058,8 @@ class Space {
7037
7058
  events = new EventTarget();
7038
7059
  rawWired = false;
7039
7060
  catchUpUnsub = null;
7061
+ /** Optional P2P swarm for this Space (block exchange among members). */
7062
+ peerNet = null;
7040
7063
  constructor(deps) {
7041
7064
  this.deps = deps;
7042
7065
  this.name = deps.name;
@@ -7085,6 +7108,9 @@ class Space {
7085
7108
  if (this.deps.keyring) {
7086
7109
  this.channel.on(BroadcastChannelEvents.CONNECTED, () => {
7087
7110
  this.channel?.sendRaw({ name: this.deps.myId() });
7111
+ // Kick P2P peer discovery now the signaling channel is live
7112
+ // (re-runs on each reconnect to re-form the mesh).
7113
+ this.peerNet?.start();
7088
7114
  });
7089
7115
  this.wireFanout();
7090
7116
  // Register offline catch-up once: on each reconnect the sync
@@ -7100,6 +7126,27 @@ class Space {
7100
7126
  this.channel?.disconnect();
7101
7127
  this.catchUpUnsub?.();
7102
7128
  this.catchUpUnsub = null;
7129
+ this.peerNet?.close();
7130
+ this.peerNet = null;
7131
+ }
7132
+ /** Attach a P2P swarm so this Space's file reads/writes can use peers. */
7133
+ attachPeerNetwork(net) {
7134
+ this.peerNet = net;
7135
+ }
7136
+ /**
7137
+ * Broadcast an app payload directly to connected Space peers over P2P,
7138
+ * **bypassing the server** — for CRDT ops / high-frequency state (live
7139
+ * cursors, presence, collaborative edits) you don't want to push through the
7140
+ * relay. No-op when no P2P mesh is active (requires `client` `p2p`).
7141
+ * Unlike {@link sendEphemeral} (server-relayed, metered, size-limited), this
7142
+ * is peer-direct and fragments large payloads.
7143
+ */
7144
+ gossipToPeers(data) {
7145
+ this.peerNet?.gossip(data);
7146
+ }
7147
+ /** Subscribe to peer gossip ({@link gossipToPeers}). Returns an unsubscribe fn. */
7148
+ onPeerGossip(handler) {
7149
+ return this.peerNet?.onGossip(handler) ?? (() => { });
7103
7150
  }
7104
7151
  /** Subscribe to a raw channel event. Buffered until `connect()` if early. */
7105
7152
  on(event, handler) {
@@ -7269,10 +7316,12 @@ class Space {
7269
7316
  * indicators, presence, or live cursors are built on top of this. No-op if
7270
7317
  * the space isn't connected (ephemeral signals aren't worth queuing).
7271
7318
  */
7272
- sendEphemeral(subject, data) {
7319
+ sendEphemeral(subject, data, to) {
7273
7320
  if (!this.isConnected())
7274
7321
  return;
7275
- this.sendRaw({ pub: { subject, data } });
7322
+ // `to` (a member id) lets the server unicast a *directed* ephemeral
7323
+ // instead of broadcasting; omitted ⇒ broadcast to the room (unchanged).
7324
+ this.sendRaw({ pub: to ? { subject, data, to } : { subject, data } });
7276
7325
  }
7277
7326
  /** Subscribe to inbound ephemeral signals. `from` is the authenticated sender. */
7278
7327
  onEphemeral(handler) {
@@ -7392,6 +7441,7 @@ class Space {
7392
7441
  fetch: this.deps.fetch,
7393
7442
  cache: this.deps.shardCache,
7394
7443
  deferUpload: this.deps.deferShardUpload,
7444
+ peers: this.peerNet?.exchange,
7395
7445
  });
7396
7446
  return new FileStorage({ shards });
7397
7447
  }
@@ -8310,6 +8360,849 @@ class SpaceCache {
8310
8360
  }
8311
8361
  }
8312
8362
 
8363
+ /**
8364
+ * `ShardCache` — a Cache-API-backed store for file-shard bytes. Shards are
8365
+ * content-addressed (the key IS the SHA-256 of the ciphertext), so caching them
8366
+ * is uniquely safe: a cache hit is self-validating, immutable, and shareable
8367
+ * across files. The Cache API (rather than IndexedDB) is the right home for
8368
+ * large binary blobs — it streams, it's purpose-built for response bodies, and
8369
+ * it has its own eviction.
8370
+ *
8371
+ * Keys are SYNTHETIC absolute URLs (`https://shard.muhkoo.local/<hash>`) rather
8372
+ * than the real shard endpoint, so the cache is addressable by hash alone — the
8373
+ * sync engine can re-PUT a deferred shard knowing only its hash, without
8374
+ * reconstructing the accelerator URL. We never fetch these synthetic URLs; they
8375
+ * exist only as cache keys.
8376
+ *
8377
+ * Every method degrades to a no-op / null where the Cache API is unavailable
8378
+ * (Node, Workers, insecure contexts), so callers don't need to branch.
8379
+ */
8380
+ const CACHE_NAME = "muhkoo-shards-v1";
8381
+ class ShardCache {
8382
+ static available() {
8383
+ try {
8384
+ return typeof caches !== "undefined";
8385
+ }
8386
+ catch {
8387
+ return false;
8388
+ }
8389
+ }
8390
+ key(hash) {
8391
+ return `https://shard.muhkoo.local/${encodeURIComponent(hash)}`;
8392
+ }
8393
+ open() {
8394
+ return caches.open(CACHE_NAME);
8395
+ }
8396
+ /** Cached shard bytes, or `null` on miss / when unavailable. */
8397
+ async get(hash) {
8398
+ if (!ShardCache.available())
8399
+ return null;
8400
+ try {
8401
+ const cache = await this.open();
8402
+ const res = await cache.match(this.key(hash));
8403
+ if (!res)
8404
+ return null;
8405
+ return new Uint8Array(await res.arrayBuffer());
8406
+ }
8407
+ catch {
8408
+ return null;
8409
+ }
8410
+ }
8411
+ /** Store shard bytes under their hash. Best-effort. */
8412
+ async put(hash, bytes) {
8413
+ if (!ShardCache.available())
8414
+ return;
8415
+ try {
8416
+ const cache = await this.open();
8417
+ await cache.put(this.key(hash), new Response(bytes, {
8418
+ headers: { "content-type": "application/octet-stream" },
8419
+ }));
8420
+ }
8421
+ catch {
8422
+ /* quota / unavailable — degrade to online-only for this shard */
8423
+ }
8424
+ }
8425
+ /** Whether a shard is present in the cache. */
8426
+ async has(hash) {
8427
+ if (!ShardCache.available())
8428
+ return false;
8429
+ try {
8430
+ const cache = await this.open();
8431
+ return Boolean(await cache.match(this.key(hash)));
8432
+ }
8433
+ catch {
8434
+ return false;
8435
+ }
8436
+ }
8437
+ }
8438
+
8439
+ /**
8440
+ * `PeerExchange` — the thin façade the storage layer consults to try fetching a
8441
+ * block from peers before falling back to origin. It satisfies the structural
8442
+ * `PeerBlockSource` interface that {@link ../storage/transport/ShardClient}
8443
+ * declares (so storage never imports the p2p module — the dependency arrow stays
8444
+ * one-way, p2p → storage).
8445
+ *
8446
+ * It delegates to an {@link EngineHandle} — either an in-process {@link
8447
+ * ./worker/blockEngine.BlockEngine} or the Web Worker proxy — so the same façade
8448
+ * works whether the block engine runs on the main thread or off it.
8449
+ */
8450
+ class PeerExchange {
8451
+ engine;
8452
+ timeoutMs;
8453
+ constructor(engine, opts = {}) {
8454
+ this.engine = engine;
8455
+ this.timeoutMs = opts.timeoutMs ?? 2500;
8456
+ }
8457
+ getBlock(hash, opts) {
8458
+ return this.engine.want(hash, opts?.timeoutMs ?? this.timeoutMs);
8459
+ }
8460
+ announce(hash) {
8461
+ this.engine.announce(hash);
8462
+ }
8463
+ }
8464
+
8465
+ /**
8466
+ * `SpaceSignaler` — WebRTC signaling carried over the Space's existing ephemeral
8467
+ * relay. No new server channel: `Space.sendEphemeral`/`onEphemeral` already
8468
+ * blind-relay `pub` frames stamped with the authenticated sender, so SDP
8469
+ * offers/answers and ICE candidates ride a reserved subject (`__p2p__`) scoped
8470
+ * to Space membership. The sender (`from`) is server-stamped and can't be
8471
+ * spoofed; the payload is opaque to the server.
8472
+ *
8473
+ * Envelopes carry a `to` (a member id, or `"*"` for discovery) so the
8474
+ * accelerator can optionally unicast peer-pair signaling instead of broadcasting
8475
+ * (a small, backward-compatible `pub` enhancement); the client filters by `to`
8476
+ * regardless.
8477
+ */
8478
+ const P2P_SUBJECT = "__p2p__";
8479
+ class SpaceSignaler {
8480
+ space;
8481
+ myId;
8482
+ constructor(space, myId) {
8483
+ this.space = space;
8484
+ this.myId = myId;
8485
+ }
8486
+ /** Send a signal to one peer (or `"*"`). */
8487
+ send(to, kind, data) {
8488
+ const env = { to, kind, data };
8489
+ // Ask the server to unicast for a directed target; broadcast for "*".
8490
+ // `to` also stays in the envelope so a broadcast-only server still works
8491
+ // (the client filters by it).
8492
+ this.space.sendEphemeral(P2P_SUBJECT, env, to !== "*" ? to : undefined);
8493
+ }
8494
+ /** Announce/withdraw P2P availability to all Space members. */
8495
+ broadcast(kind, data) {
8496
+ this.send("*", kind, data);
8497
+ }
8498
+ /**
8499
+ * Subscribe to inbound signals addressed to us (or broadcast). Skips our own
8500
+ * echoes. Returns an unsubscribe function.
8501
+ */
8502
+ onSignal(cb) {
8503
+ return this.space.onEphemeral((e) => {
8504
+ if (e.subject !== P2P_SUBJECT || e.from === this.myId)
8505
+ return;
8506
+ const env = e.data;
8507
+ if (!env || typeof env.kind !== "string")
8508
+ return;
8509
+ if (env.to !== this.myId && env.to !== "*")
8510
+ return;
8511
+ cb(e.from, env.kind, env.data);
8512
+ });
8513
+ }
8514
+ }
8515
+
8516
+ /**
8517
+ * `WebRtcTransport` — a {@link ./PeerTransport.PeerTransport} over RTCDataChannels,
8518
+ * signaled by {@link ../signaling/SpaceSignaler}. Runs on the **main thread**
8519
+ * (`RTCPeerConnection` is Window-only — not available in Workers), so the block
8520
+ * engine lives in a Worker and this only shuttles bytes.
8521
+ *
8522
+ * Connection setup uses a deterministic initiator (the peer with the smaller id
8523
+ * dials) to avoid offer/answer glare. STUN handles NAT traversal; there is no
8524
+ * TURN — an unreachable peer simply never connects and the caller falls back to
8525
+ * origin (P2P is best-effort).
8526
+ *
8527
+ * Frames (up to a 4 MiB shard) exceed an RTCDataChannel's max message size, so
8528
+ * each frame is fragmented into `CHUNK`-sized pieces with a 12-byte header
8529
+ * `[msgId u32][index u32][total u32]` and reassembled per peer.
8530
+ */
8531
+ const CHUNK = 16 * 1024;
8532
+ // [msgId u32][index u32][total u32][channel u32]
8533
+ const HEADER$1 = 16;
8534
+ /** Pause sends when a channel has this many bytes buffered (backpressure). */
8535
+ const MAX_BUFFERED = 4 * 1024 * 1024;
8536
+ const DEFAULT_ICE = [{ urls: "stun:stun.cloudflare.com:3478" }];
8537
+ class WebRtcTransport {
8538
+ signaler;
8539
+ myId;
8540
+ conns = new Map();
8541
+ msgCbs = new Set();
8542
+ peerCbs = new Set();
8543
+ offSignal;
8544
+ iceServers;
8545
+ maxPeers;
8546
+ nextMsgId = 1;
8547
+ constructor(signaler, myId, opts = {}) {
8548
+ this.signaler = signaler;
8549
+ this.myId = myId;
8550
+ this.iceServers = opts.iceServers ?? DEFAULT_ICE;
8551
+ this.maxPeers = opts.maxPeers ?? 8;
8552
+ this.offSignal = this.signaler.onSignal((from, kind, data) => this.onSignal(from, kind, data));
8553
+ }
8554
+ peers() {
8555
+ return [...this.conns].filter(([, c]) => c.dc?.readyState === "open").map(([id]) => id);
8556
+ }
8557
+ /** Dial a peer (no-op unless we're the deterministic initiator for the pair). */
8558
+ connectTo(peerId) {
8559
+ if (peerId === this.myId || this.conns.has(peerId))
8560
+ return;
8561
+ if (this.conns.size >= this.maxPeers)
8562
+ return; // mesh cap
8563
+ if (this.myId < peerId)
8564
+ void this.initiate(peerId); // smaller id dials
8565
+ }
8566
+ send(peer, data, channel = 0) {
8567
+ const dc = this.conns.get(peer)?.dc;
8568
+ if (dc?.readyState === "open")
8569
+ void this.sendFragmented(dc, data, channel);
8570
+ }
8571
+ broadcast(data, channel = 0) {
8572
+ for (const [, c] of this.conns) {
8573
+ if (c.dc?.readyState === "open")
8574
+ void this.sendFragmented(c.dc, data, channel);
8575
+ }
8576
+ }
8577
+ onMessage(cb) {
8578
+ this.msgCbs.add(cb);
8579
+ return () => this.msgCbs.delete(cb);
8580
+ }
8581
+ onPeerChange(cb) {
8582
+ this.peerCbs.add(cb);
8583
+ return () => this.peerCbs.delete(cb);
8584
+ }
8585
+ close() {
8586
+ this.offSignal();
8587
+ for (const [, c] of this.conns) {
8588
+ try {
8589
+ c.dc?.close();
8590
+ }
8591
+ catch { /* already closing */ }
8592
+ try {
8593
+ c.pc.close();
8594
+ }
8595
+ catch { /* already closing */ }
8596
+ }
8597
+ this.conns.clear();
8598
+ this.msgCbs.clear();
8599
+ this.peerCbs.clear();
8600
+ }
8601
+ // -- internals -----------------------------------------------------------
8602
+ newPc(peerId) {
8603
+ const pc = new RTCPeerConnection({ iceServers: this.iceServers });
8604
+ const conn = { pc, reasm: new Map() };
8605
+ this.conns.set(peerId, conn);
8606
+ pc.onicecandidate = (e) => {
8607
+ if (e.candidate)
8608
+ this.signaler.send(peerId, "ice", e.candidate.toJSON());
8609
+ };
8610
+ pc.onconnectionstatechange = () => {
8611
+ if (["failed", "closed", "disconnected"].includes(pc.connectionState))
8612
+ this.drop(peerId);
8613
+ };
8614
+ pc.ondatachannel = (e) => this.bindChannel(peerId, conn, e.channel);
8615
+ return conn;
8616
+ }
8617
+ async initiate(peerId) {
8618
+ const conn = this.newPc(peerId);
8619
+ const dc = conn.pc.createDataChannel("blocks");
8620
+ this.bindChannel(peerId, conn, dc);
8621
+ const offer = await conn.pc.createOffer();
8622
+ await conn.pc.setLocalDescription(offer);
8623
+ this.signaler.send(peerId, "offer", offer);
8624
+ }
8625
+ bindChannel(peerId, conn, dc) {
8626
+ dc.binaryType = "arraybuffer";
8627
+ conn.dc = dc;
8628
+ dc.onopen = () => this.firePeerChange();
8629
+ dc.onclose = () => this.drop(peerId);
8630
+ dc.onmessage = (e) => this.onChunk(peerId, conn, new Uint8Array(e.data));
8631
+ }
8632
+ async onSignal(from, kind, data) {
8633
+ if (kind === "offer") {
8634
+ if (!this.conns.has(from) && this.conns.size >= this.maxPeers)
8635
+ return; // mesh cap
8636
+ const conn = this.conns.get(from) ?? this.newPc(from);
8637
+ await conn.pc.setRemoteDescription(data);
8638
+ const answer = await conn.pc.createAnswer();
8639
+ await conn.pc.setLocalDescription(answer);
8640
+ this.signaler.send(from, "answer", answer);
8641
+ }
8642
+ else if (kind === "answer") {
8643
+ const conn = this.conns.get(from);
8644
+ if (conn)
8645
+ await conn.pc.setRemoteDescription(data);
8646
+ }
8647
+ else if (kind === "ice") {
8648
+ const conn = this.conns.get(from);
8649
+ if (conn && data) {
8650
+ try {
8651
+ await conn.pc.addIceCandidate(data);
8652
+ }
8653
+ catch { /* pre-SRD race */ }
8654
+ }
8655
+ }
8656
+ else if (kind === "bye") {
8657
+ this.drop(from);
8658
+ }
8659
+ }
8660
+ drop(peerId) {
8661
+ const conn = this.conns.get(peerId);
8662
+ if (!conn)
8663
+ return;
8664
+ this.conns.delete(peerId);
8665
+ try {
8666
+ conn.dc?.close();
8667
+ }
8668
+ catch { /* already closing */ }
8669
+ try {
8670
+ conn.pc.close();
8671
+ }
8672
+ catch { /* already closing */ }
8673
+ this.firePeerChange();
8674
+ }
8675
+ firePeerChange() {
8676
+ const peers = this.peers();
8677
+ for (const cb of this.peerCbs)
8678
+ cb(peers);
8679
+ }
8680
+ // -- fragmentation -------------------------------------------------------
8681
+ async sendFragmented(dc, data, channel) {
8682
+ const msgId = this.nextMsgId++ >>> 0;
8683
+ const total = Math.max(1, Math.ceil(data.length / CHUNK));
8684
+ for (let i = 0; i < total; i++) {
8685
+ const slice = data.subarray(i * CHUNK, (i + 1) * CHUNK);
8686
+ const frame = new Uint8Array(HEADER$1 + slice.length);
8687
+ const dv = new DataView(frame.buffer);
8688
+ dv.setUint32(0, msgId);
8689
+ dv.setUint32(4, i);
8690
+ dv.setUint32(8, total);
8691
+ dv.setUint32(12, channel);
8692
+ frame.set(slice, HEADER$1);
8693
+ if (dc.bufferedAmount > MAX_BUFFERED)
8694
+ await this.drain(dc);
8695
+ dc.send(frame);
8696
+ }
8697
+ }
8698
+ drain(dc) {
8699
+ return new Promise((resolve) => {
8700
+ dc.bufferedAmountLowThreshold = MAX_BUFFERED / 2;
8701
+ const onLow = () => { dc.removeEventListener("bufferedamountlow", onLow); resolve(); };
8702
+ dc.addEventListener("bufferedamountlow", onLow);
8703
+ });
8704
+ }
8705
+ onChunk(peerId, conn, frame) {
8706
+ if (frame.length < HEADER$1)
8707
+ return;
8708
+ const dv = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
8709
+ const msgId = dv.getUint32(0);
8710
+ const index = dv.getUint32(4);
8711
+ const total = dv.getUint32(8);
8712
+ const channel = dv.getUint32(12);
8713
+ const chunk = frame.slice(HEADER$1);
8714
+ let r = conn.reasm.get(msgId);
8715
+ if (!r) {
8716
+ r = { chunks: new Array(total), received: 0, total, size: 0, channel };
8717
+ conn.reasm.set(msgId, r);
8718
+ }
8719
+ if (r.chunks[index])
8720
+ return; // dup
8721
+ r.chunks[index] = chunk;
8722
+ r.received++;
8723
+ r.size += chunk.length;
8724
+ if (r.received < r.total)
8725
+ return;
8726
+ conn.reasm.delete(msgId);
8727
+ const out = new Uint8Array(r.size);
8728
+ let off = 0;
8729
+ for (const c of r.chunks) {
8730
+ if (!c)
8731
+ return; // gap — shouldn't happen on reliable ordered
8732
+ out.set(c, off);
8733
+ off += c.length;
8734
+ }
8735
+ for (const cb of this.msgCbs)
8736
+ cb(peerId, out, r.channel);
8737
+ }
8738
+ }
8739
+
8740
+ /**
8741
+ * `PeerTransport` — the pluggable wire under the block engine. It moves opaque
8742
+ * frames between Space peers; it knows nothing about blocks, hashes, or the
8743
+ * protocol. {@link ./WebRtcTransport} implements it over RTCDataChannels today;
8744
+ * a future libp2p/Helia transport can implement the same interface without the
8745
+ * block engine or `ShardClient` changing.
8746
+ *
8747
+ * Implementations are responsible for fragmenting/ reassembling frames larger
8748
+ * than the underlying channel's max message size (a 4 MiB shard won't fit in one
8749
+ * `RTCDataChannel.send`); the engine always deals in whole frames.
8750
+ */
8751
+ /**
8752
+ * Logical channels multiplexed over the one data link. `0` is block exchange
8753
+ * (the engine), `1` is gossip (app/CRDT data). The tag rides the framing header
8754
+ * so payloads aren't copied to prepend it.
8755
+ */
8756
+ const CHANNEL_BLOCK = 0;
8757
+ const CHANNEL_GOSSIP = 1;
8758
+
8759
+ /**
8760
+ * Bitswap-lite wire protocol for peer block exchange. Four frame types over a
8761
+ * {@link ../transport/PeerTransport} data channel:
8762
+ *
8763
+ * WANT(hash) — "does anyone have this block?" / "send it to me"
8764
+ * HAVE(hash) — "I hold this block" (announce / reply to a WANT)
8765
+ * BLOCK(hash, bytes) — the block itself (verified by hash on receipt)
8766
+ * CANCEL(hash) — "stop, I got it elsewhere"
8767
+ *
8768
+ * Frames are compact binary so a 4 MiB shard isn't base64-bloated through JSON:
8769
+ *
8770
+ * byte 0 : type (0 WANT | 1 HAVE | 2 BLOCK | 3 CANCEL)
8771
+ * bytes 1..33 : 32-byte raw SHA-256 of the block (our shard hash, hex-decoded)
8772
+ * bytes 33.. : payload (BLOCK only)
8773
+ *
8774
+ * Blocks are AES-GCM ciphertext, so a frame carries nothing readable; the hash
8775
+ * is the capability check (the receiver re-hashes the payload and drops a
8776
+ * mismatch). See {@link ./blockEngine}.
8777
+ */
8778
+ var FrameType;
8779
+ (function (FrameType) {
8780
+ FrameType[FrameType["WANT"] = 0] = "WANT";
8781
+ FrameType[FrameType["HAVE"] = 1] = "HAVE";
8782
+ FrameType[FrameType["BLOCK"] = 2] = "BLOCK";
8783
+ FrameType[FrameType["CANCEL"] = 3] = "CANCEL";
8784
+ })(FrameType || (FrameType = {}));
8785
+ const HASH_BYTES = 32;
8786
+ const HEADER = 1 + HASH_BYTES; // type + hash
8787
+ function hexToBytes(hex) {
8788
+ if (hex.length !== HASH_BYTES * 2) {
8789
+ throw new Error(`protocol: expected a ${HASH_BYTES}-byte hex hash, got ${hex.length / 2} bytes`);
8790
+ }
8791
+ const out = new Uint8Array(HASH_BYTES);
8792
+ for (let i = 0; i < HASH_BYTES; i++)
8793
+ out[i] = parseInt(hex.substr(i * 2, 2), 16);
8794
+ return out;
8795
+ }
8796
+ const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
8797
+ function bytesToHex(bytes) {
8798
+ let s = "";
8799
+ for (let i = 0; i < bytes.length; i++)
8800
+ s += HEX[bytes[i]];
8801
+ return s;
8802
+ }
8803
+ function header(type, hash, extra = 0) {
8804
+ const buf = new Uint8Array(HEADER + extra);
8805
+ buf[0] = type;
8806
+ buf.set(hexToBytes(hash), 1);
8807
+ return { buf, offset: HEADER };
8808
+ }
8809
+ function encodeWant(hash) {
8810
+ return header(FrameType.WANT, hash).buf;
8811
+ }
8812
+ function encodeHave(hash) {
8813
+ return header(FrameType.HAVE, hash).buf;
8814
+ }
8815
+ function encodeCancel(hash) {
8816
+ return header(FrameType.CANCEL, hash).buf;
8817
+ }
8818
+ function encodeBlock(hash, payload) {
8819
+ const { buf, offset } = header(FrameType.BLOCK, hash, payload.length);
8820
+ buf.set(payload, offset);
8821
+ return buf;
8822
+ }
8823
+ function decode(frame) {
8824
+ if (frame.length < HEADER)
8825
+ throw new Error("protocol: frame too short");
8826
+ const type = frame[0];
8827
+ const hash = bytesToHex(frame.subarray(1, HEADER));
8828
+ if (type === FrameType.BLOCK) {
8829
+ // Copy the payload out so it doesn't alias a transferred/pooled buffer.
8830
+ return { type, hash, payload: frame.slice(HEADER) };
8831
+ }
8832
+ return { type, hash };
8833
+ }
8834
+
8835
+ var protocol = /*#__PURE__*/Object.freeze({
8836
+ __proto__: null,
8837
+ get FrameType () { return FrameType; },
8838
+ decode: decode,
8839
+ encodeBlock: encodeBlock,
8840
+ encodeCancel: encodeCancel,
8841
+ encodeHave: encodeHave,
8842
+ encodeWant: encodeWant
8843
+ });
8844
+
8845
+ /**
8846
+ * `BlockEngine` — the bitswap-lite want/have ledger. Host-agnostic: it speaks
8847
+ * only to a {@link BlockStore} (get/put/has) and an `out` sink that ships
8848
+ * encoded frames to a peer (or `"*"` to broadcast). The host wires `out` and
8849
+ * `handleFrame` to a real transport — on the main thread or inside the Web
8850
+ * Worker (see {@link ./engineClient}) — so this class is pure and unit-testable.
8851
+ *
8852
+ * It never sees keys: blocks are AES-GCM ciphertext, addressed by SHA-256. The
8853
+ * one trust check is integrity — an inbound BLOCK is re-hashed and dropped on
8854
+ * mismatch, so a malicious peer can't poison the store (the requester just
8855
+ * falls back to origin).
8856
+ */
8857
+ class BlockEngine {
8858
+ store;
8859
+ out;
8860
+ hash;
8861
+ wants = new Map();
8862
+ constructor(store, out, hash) {
8863
+ this.store = store;
8864
+ this.out = out;
8865
+ this.hash = hash;
8866
+ }
8867
+ /**
8868
+ * Ask the swarm for a block. Resolves with the verified bytes, or `null` on
8869
+ * timeout (caller then falls back to origin). Returns immediately if we
8870
+ * already hold it; concurrent wants for the same hash share one request.
8871
+ */
8872
+ want(hash, timeoutMs) {
8873
+ return new Promise((resolve) => {
8874
+ void this.store.get(hash).then((local) => {
8875
+ if (local)
8876
+ return resolve(local);
8877
+ const existing = this.wants.get(hash);
8878
+ if (existing) {
8879
+ existing.resolvers.push(resolve);
8880
+ return;
8881
+ }
8882
+ const timer = setTimeout(() => {
8883
+ const w = this.wants.get(hash);
8884
+ this.wants.delete(hash);
8885
+ this.out("*", encodeCancel(hash));
8886
+ w?.resolvers.forEach((r) => r(null));
8887
+ }, timeoutMs);
8888
+ this.wants.set(hash, { resolvers: [resolve], timer });
8889
+ this.out("*", encodeWant(hash));
8890
+ });
8891
+ });
8892
+ }
8893
+ /** Tell the swarm we now hold `hash` (so peers wanting it can pull from us). */
8894
+ announce(hash) {
8895
+ this.out("*", encodeHave(hash));
8896
+ }
8897
+ /** Handle one inbound frame from `peer`. */
8898
+ async handleFrame(peer, frame) {
8899
+ let f;
8900
+ try {
8901
+ f = decode(frame);
8902
+ }
8903
+ catch {
8904
+ return; // malformed — ignore
8905
+ }
8906
+ switch (f.type) {
8907
+ case FrameType.WANT: {
8908
+ const bytes = await this.store.get(f.hash);
8909
+ if (bytes)
8910
+ this.out(peer, encodeBlock(f.hash, bytes));
8911
+ return;
8912
+ }
8913
+ case FrameType.HAVE: {
8914
+ // A peer announced a block we're still waiting for → pull from them.
8915
+ if (this.wants.has(f.hash))
8916
+ this.out(peer, encodeWant(f.hash));
8917
+ return;
8918
+ }
8919
+ case FrameType.BLOCK: {
8920
+ const pending = this.wants.get(f.hash);
8921
+ if (!pending || !f.payload)
8922
+ return; // unsolicited / empty — ignore
8923
+ // Integrity: a peer can't poison the store. Drop a mismatch and
8924
+ // keep waiting (the timeout will fall back to origin).
8925
+ const got = await this.hash(f.payload);
8926
+ if (got !== f.hash)
8927
+ return;
8928
+ await this.store.put(f.hash, f.payload);
8929
+ this.wants.delete(f.hash);
8930
+ clearTimeout(pending.timer);
8931
+ this.out("*", encodeCancel(f.hash)); // others can stop sending
8932
+ pending.resolvers.forEach((r) => r(f.payload));
8933
+ return;
8934
+ }
8935
+ case FrameType.CANCEL:
8936
+ // We serve WANTs synchronously, so there's nothing in flight to
8937
+ // cancel yet. Reserved for chunked-send cancellation later.
8938
+ return;
8939
+ }
8940
+ }
8941
+ /** Drop all pending waits (resolve null) — on teardown. */
8942
+ close() {
8943
+ for (const [, w] of this.wants) {
8944
+ clearTimeout(w.timer);
8945
+ w.resolvers.forEach((r) => r(null));
8946
+ }
8947
+ this.wants.clear();
8948
+ }
8949
+ }
8950
+
8951
+ /**
8952
+ * `EngineHost` — the boundary {@link ../PeerNetwork} wires a transport to. It
8953
+ * abstracts *where* the {@link ./blockEngine.BlockEngine} runs:
8954
+ * - {@link LocalEngineHost} runs it on the calling thread (tests, or a
8955
+ * fallback when the Web Worker can't be built by a consumer's bundler);
8956
+ * - the Worker proxy ({@link ./engineClient}) runs it off-thread.
8957
+ *
8958
+ * Either way the host exposes the same surface: drive `want`/`announce`, feed it
8959
+ * inbound frames via `handleFrame`, and receive its outbound frames via
8960
+ * `onOutbound` (which the network forwards to the transport).
8961
+ */
8962
+ /** Runs the engine in-process (main thread or test). */
8963
+ class LocalEngineHost {
8964
+ engine;
8965
+ cbs = new Set();
8966
+ constructor(store, hasher) {
8967
+ this.engine = new BlockEngine(store, (target, frame) => {
8968
+ for (const cb of this.cbs)
8969
+ cb(target, frame);
8970
+ }, hasher);
8971
+ }
8972
+ want(hash, timeoutMs) {
8973
+ return this.engine.want(hash, timeoutMs);
8974
+ }
8975
+ announce(hash) {
8976
+ this.engine.announce(hash);
8977
+ }
8978
+ handleFrame(peer, frame) {
8979
+ void this.engine.handleFrame(peer, frame);
8980
+ }
8981
+ onOutbound(cb) {
8982
+ this.cbs.add(cb);
8983
+ return () => this.cbs.delete(cb);
8984
+ }
8985
+ close() {
8986
+ this.engine.close();
8987
+ this.cbs.clear();
8988
+ }
8989
+ }
8990
+
8991
+ /**
8992
+ * `WorkerEngineHost` — the main-thread proxy to a {@link ./blockEngine.BlockEngine}
8993
+ * running inside a dedicated Web Worker (so hashing, blockstore I/O, and the
8994
+ * protocol codec stay off the main thread). It speaks the small message protocol
8995
+ * in {@link ./blockEngine.worker} and satisfies {@link ./engineHost.EngineHost},
8996
+ * so {@link ../PeerNetwork} treats it identically to the in-process host.
8997
+ *
8998
+ * Block bytes cross the worker boundary as **transferable** ArrayBuffers
8999
+ * (zero-copy). Only encrypted blocks ever cross — keys never enter the worker.
9000
+ */
9001
+ class WorkerEngineHost {
9002
+ worker;
9003
+ wants = new Map();
9004
+ cbs = new Set();
9005
+ nextId = 1;
9006
+ constructor(worker) {
9007
+ this.worker = worker;
9008
+ this.worker.onmessage = (e) => {
9009
+ const m = e.data;
9010
+ if (m.t === "out") {
9011
+ const frame = new Uint8Array(m.data);
9012
+ for (const cb of this.cbs)
9013
+ cb(m.target, frame);
9014
+ }
9015
+ else if (m.t === "want-result") {
9016
+ const resolve = this.wants.get(m.id);
9017
+ if (resolve) {
9018
+ this.wants.delete(m.id);
9019
+ resolve(m.data ? new Uint8Array(m.data) : null);
9020
+ }
9021
+ }
9022
+ };
9023
+ }
9024
+ want(hash, timeoutMs) {
9025
+ return new Promise((resolve) => {
9026
+ const id = this.nextId++;
9027
+ this.wants.set(id, resolve);
9028
+ this.worker.postMessage({ t: "want", id, hash, timeout: timeoutMs });
9029
+ });
9030
+ }
9031
+ announce(hash) {
9032
+ this.worker.postMessage({ t: "announce", hash });
9033
+ }
9034
+ handleFrame(peer, frame) {
9035
+ // Transfer an exact-size buffer; copy if the view doesn't own the whole buffer.
9036
+ const buf = frame.byteOffset === 0 && frame.byteLength === frame.buffer.byteLength
9037
+ ? frame.buffer
9038
+ : frame.slice().buffer;
9039
+ this.worker.postMessage({ t: "frame", peer, data: buf }, [buf]);
9040
+ }
9041
+ onOutbound(cb) {
9042
+ this.cbs.add(cb);
9043
+ return () => this.cbs.delete(cb);
9044
+ }
9045
+ close() {
9046
+ try {
9047
+ this.worker.postMessage({ t: "close" });
9048
+ }
9049
+ catch { /* already gone */ }
9050
+ this.worker.terminate();
9051
+ this.wants.forEach((r) => r(null));
9052
+ this.wants.clear();
9053
+ this.cbs.clear();
9054
+ }
9055
+ }
9056
+
9057
+ /**
9058
+ * `PeerNetwork` — the per-Space orchestrator that ties the pieces together on
9059
+ * the main thread:
9060
+ *
9061
+ * transport (WebRTC) ⇄ engine host (Worker or in-process) → PeerExchange
9062
+ * ▲ │
9063
+ * └────────────── signaling over the Space relay ─────────────┘
9064
+ *
9065
+ * It owns one {@link ./transport/WebRtcTransport} + one {@link
9066
+ * ./worker/engineHost.EngineHost}, wires inbound frames to the engine and the
9067
+ * engine's outbound frames back to the transport, runs peer discovery over the
9068
+ * Space's ephemeral channel, and exposes a {@link ./PeerExchange.PeerExchange}
9069
+ * for `ShardClient` to consult. Scope is one Space (private swarm).
9070
+ *
9071
+ * `engineHost` defaults to in-process; pass `workerFactory` to run the block
9072
+ * engine off the main thread (the consumer's bundler resolves the worker URL).
9073
+ */
9074
+ /**
9075
+ * Build the engine host, preferring an off-thread Web Worker:
9076
+ * 1. an explicit `workerFactory` (most reliable in app bundlers), else
9077
+ * 2. the bundled worker resolved relative to this module (works where the
9078
+ * bundler supports `new Worker(new URL(...))`, e.g. Vite builds), else
9079
+ * 3. the in-process engine (always works; tests + non-worker runtimes).
9080
+ * Any worker construction failure degrades to in-process.
9081
+ */
9082
+ function buildEngineHost(opts) {
9083
+ try {
9084
+ const worker = opts.workerFactory ? opts.workerFactory() : defaultWorker();
9085
+ if (worker)
9086
+ return new WorkerEngineHost(worker);
9087
+ }
9088
+ catch {
9089
+ /* no Worker global / bundler can't resolve it → in-process */
9090
+ }
9091
+ return new LocalEngineHost(opts.store ?? new ShardCache(), shardHash);
9092
+ }
9093
+ /** Construct the bundled worker if the runtime + bundler support it, else null. */
9094
+ function defaultWorker() {
9095
+ try {
9096
+ if (typeof Worker === "undefined")
9097
+ return null;
9098
+ // The string is resolved by the consumer's bundler relative to the
9099
+ // built connect module (sibling of dist/browser/index.js).
9100
+ return new Worker(new URL("./blockEngine.worker.js", import.meta.url), { type: "module" });
9101
+ }
9102
+ catch {
9103
+ return null;
9104
+ }
9105
+ }
9106
+ class PeerNetwork {
9107
+ /** Pass this to a `ShardClient` as `peers`. */
9108
+ exchange;
9109
+ transport;
9110
+ host;
9111
+ signaler;
9112
+ offs = [];
9113
+ gossipCbs = new Set();
9114
+ closed = false;
9115
+ constructor(opts) {
9116
+ this.signaler = new SpaceSignaler(opts.space, opts.myId);
9117
+ this.transport =
9118
+ opts.transport ??
9119
+ new WebRtcTransport(this.signaler, opts.myId, {
9120
+ iceServers: opts.iceServers,
9121
+ maxPeers: opts.maxPeers,
9122
+ });
9123
+ this.host = buildEngineHost(opts);
9124
+ // Wire transport ⇄ engine, demuxing block (engine) from gossip (app).
9125
+ this.offs.push(this.transport.onMessage((peer, data, channel) => {
9126
+ if (channel === CHANNEL_GOSSIP) {
9127
+ for (const cb of this.gossipCbs)
9128
+ cb(peer, data);
9129
+ }
9130
+ else {
9131
+ this.host.handleFrame(peer, data);
9132
+ }
9133
+ }));
9134
+ this.offs.push(this.host.onOutbound((target, frame) => target === "*"
9135
+ ? this.transport.broadcast(frame, CHANNEL_BLOCK)
9136
+ : this.transport.send(target, frame, CHANNEL_BLOCK)));
9137
+ // Discovery: a broadcast hello triggers a unicast hello back (so a
9138
+ // newcomer learns existing members); both sides connectTo (the smaller
9139
+ // id actually dials). Deterministic + idempotent.
9140
+ this.offs.push(this.signaler.onSignal((from, kind, data) => {
9141
+ if (kind !== "hello")
9142
+ return;
9143
+ this.transport.connectTo?.(from);
9144
+ if (data?.broadcast) {
9145
+ this.signaler.send(from, "hello", { broadcast: false });
9146
+ }
9147
+ }));
9148
+ this.exchange = new PeerExchange(this.host, { timeoutMs: opts.timeoutMs });
9149
+ }
9150
+ /**
9151
+ * Begin peer discovery — broadcast a hello so existing members dial us and
9152
+ * we learn them. Call once the signaling channel is live (the Space fires
9153
+ * this on CONNECTED); safe to call again on reconnect.
9154
+ */
9155
+ start() {
9156
+ if (this.closed)
9157
+ return;
9158
+ this.signaler.broadcast("hello", { broadcast: true });
9159
+ }
9160
+ /**
9161
+ * Broadcast an app payload to all connected peers over the gossip channel —
9162
+ * a direct, server-bypassing data path for CRDT ops / high-frequency state
9163
+ * (cursors, presence, collaborative edits). Single-hop (no re-forwarding).
9164
+ */
9165
+ gossip(data) {
9166
+ this.transport.broadcast(data, CHANNEL_GOSSIP);
9167
+ }
9168
+ /** Subscribe to gossip from peers. Returns an unsubscribe function. */
9169
+ onGossip(cb) {
9170
+ this.gossipCbs.add(cb);
9171
+ return () => this.gossipCbs.delete(cb);
9172
+ }
9173
+ /** Connected peer count (open data channels). */
9174
+ peerCount() {
9175
+ return this.transport.peers().length;
9176
+ }
9177
+ close() {
9178
+ if (this.closed)
9179
+ return;
9180
+ this.closed = true;
9181
+ try {
9182
+ this.signaler.broadcast("bye");
9183
+ }
9184
+ catch { /* socket may be gone */ }
9185
+ for (const off of this.offs)
9186
+ off();
9187
+ this.transport.close();
9188
+ this.host.close();
9189
+ }
9190
+ }
9191
+
9192
+ /**
9193
+ * Whether this environment can do WebRTC peering. `RTCPeerConnection` is
9194
+ * Window-only (absent in Node/Workers/SSR), so this gates whether a
9195
+ * {@link ./PeerNetwork} can be built at all.
9196
+ */
9197
+ function isP2pCapable() {
9198
+ try {
9199
+ return typeof RTCPeerConnection !== "undefined";
9200
+ }
9201
+ catch {
9202
+ return false;
9203
+ }
9204
+ }
9205
+
8313
9206
  /**
8314
9207
  * `client.space` — the fan-out group-encryption surface.
8315
9208
  *
@@ -8541,6 +9434,17 @@ class SpaceNamespace {
8541
9434
  : undefined,
8542
9435
  });
8543
9436
  this.openSpaces.set(spaceId, space);
9437
+ // Attach a private P2P swarm scoped to this Space (block exchange among
9438
+ // members; signaled over the Space relay). Discovery starts on connect.
9439
+ if (this.deps.p2p?.enabled && isP2pCapable()) {
9440
+ space.attachPeerNetwork(new PeerNetwork({
9441
+ space,
9442
+ myId: memberId,
9443
+ workerFactory: this.deps.p2p.workerFactory,
9444
+ iceServers: this.deps.p2p.iceServers,
9445
+ maxPeers: this.deps.p2p.maxPeers,
9446
+ }));
9447
+ }
8544
9448
  return space;
8545
9449
  }
8546
9450
  /** Ensure the member has an identity keypair; return its public ECDH + ECDSA keys. */
@@ -9304,82 +10208,6 @@ class SnapshotNamespace {
9304
10208
  }
9305
10209
  }
9306
10210
 
9307
- /**
9308
- * `ShardCache` — a Cache-API-backed store for file-shard bytes. Shards are
9309
- * content-addressed (the key IS the SHA-256 of the ciphertext), so caching them
9310
- * is uniquely safe: a cache hit is self-validating, immutable, and shareable
9311
- * across files. The Cache API (rather than IndexedDB) is the right home for
9312
- * large binary blobs — it streams, it's purpose-built for response bodies, and
9313
- * it has its own eviction.
9314
- *
9315
- * Keys are SYNTHETIC absolute URLs (`https://shard.muhkoo.local/<hash>`) rather
9316
- * than the real shard endpoint, so the cache is addressable by hash alone — the
9317
- * sync engine can re-PUT a deferred shard knowing only its hash, without
9318
- * reconstructing the accelerator URL. We never fetch these synthetic URLs; they
9319
- * exist only as cache keys.
9320
- *
9321
- * Every method degrades to a no-op / null where the Cache API is unavailable
9322
- * (Node, Workers, insecure contexts), so callers don't need to branch.
9323
- */
9324
- const CACHE_NAME = "muhkoo-shards-v1";
9325
- class ShardCache {
9326
- static available() {
9327
- try {
9328
- return typeof caches !== "undefined";
9329
- }
9330
- catch {
9331
- return false;
9332
- }
9333
- }
9334
- key(hash) {
9335
- return `https://shard.muhkoo.local/${encodeURIComponent(hash)}`;
9336
- }
9337
- open() {
9338
- return caches.open(CACHE_NAME);
9339
- }
9340
- /** Cached shard bytes, or `null` on miss / when unavailable. */
9341
- async get(hash) {
9342
- if (!ShardCache.available())
9343
- return null;
9344
- try {
9345
- const cache = await this.open();
9346
- const res = await cache.match(this.key(hash));
9347
- if (!res)
9348
- return null;
9349
- return new Uint8Array(await res.arrayBuffer());
9350
- }
9351
- catch {
9352
- return null;
9353
- }
9354
- }
9355
- /** Store shard bytes under their hash. Best-effort. */
9356
- async put(hash, bytes) {
9357
- if (!ShardCache.available())
9358
- return;
9359
- try {
9360
- const cache = await this.open();
9361
- await cache.put(this.key(hash), new Response(bytes, {
9362
- headers: { "content-type": "application/octet-stream" },
9363
- }));
9364
- }
9365
- catch {
9366
- /* quota / unavailable — degrade to online-only for this shard */
9367
- }
9368
- }
9369
- /** Whether a shard is present in the cache. */
9370
- async has(hash) {
9371
- if (!ShardCache.available())
9372
- return false;
9373
- try {
9374
- const cache = await this.open();
9375
- return Boolean(await cache.match(this.key(hash)));
9376
- }
9377
- catch {
9378
- return false;
9379
- }
9380
- }
9381
- }
9382
-
9383
10211
  /**
9384
10212
  * IndexedDB schema for the offline layer — the single source of truth for the
9385
10213
  * database name, version, and object stores. Bump {@link DB_VERSION} whenever
@@ -10102,6 +10930,7 @@ class Client {
10102
10930
  wsBaseUrl,
10103
10931
  cache: spaceKeyCache,
10104
10932
  offline: this.offline,
10933
+ p2p: options.p2p,
10105
10934
  });
10106
10935
  this.agents = new AgentsNamespace({ http: this.http });
10107
10936
  this.functions = new FunctionsNamespace({ http: this.http, fnHostSuffix: options.functionsHostSuffix });
@@ -10523,5 +11352,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
10523
11352
  default: loadRsWasm
10524
11353
  });
10525
11354
 
10526
- export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DbCache, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HLC, HashKnowledge, HttpClient, HttpError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvCache, KvNamespace, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, SyncEngine, VaultUnavailableError, WSTransport, ZERO_HLC, ZkAuth, buildCommitment, compareHlc, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, generatePkce, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isOfflineCapable, isProvisional, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, poseidonHash, provisionalHandle, quickVerify, randomBytes, randomState, sealSeed, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
11355
+ export { AES_GCM_IV_BYTES, AES_GCM_KEY_BYTES, AES_GCM_TAG_BYTES, AgentsNamespace, AuthClient, AuthNamespace, AuthPublicInput, Authenticator, BlockEngine, BroadcastChannel, BroadcastChannelEvents, ChannelExistsError, ChannelNotFoundError, ChunkCipher, Client, ConnectivityManager, DEFAULT_AUTH_BASE_URL, DEFAULT_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DbCache, DoubleRatchet, DoubleRatchetCipher, DoubleRatchetManager, EncryptedSession, EventCore, EventCoreEvents, Field, Field as FieldElement, FileStorage, FunctionsNamespace, HLC, HashKnowledge, HttpClient, HttpError, IndexedDbStore, KEEPER_MEMBER_ID, KeyStore, KeyringClient, KvCache, KvNamespace, LocalEngineHost, LocalStorageSessionStore, MemorySessionStore, Message, Message as MessageDefault, MessageNamespace, MuhkooAgent, MuhkooDB, MuhkooFunction, MuhkooSpace, NoopStore, OfflineLockedError, OfflineManager, OutboundQueue, P2P_SUBJECT, PBKDF2_DEFAULT_ITERATIONS, PREIMAGE_POK_VERIFICATION_KEY, Packet, PeerExchange, PeerNetwork, PersonalSpaceClient, Poseidon, PreimagePoK, ReedSolomonCodec, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VaultUnavailableError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, buildCommitment, compareHlc, compilePrograms, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, generateAuthProof, generatePkce, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isOfflineCapable, isP2pCapable, isProvisional, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, sealSeed, shardHash, signMessage, stableStringify, toField, tombstoneRow, unpack, unsealSeed, unwrapWithPassphrase, verify, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
10527
11356
  //# sourceMappingURL=index.js.map