@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.
@@ -2976,7 +2976,7 @@ function base64UrlDecode(s) {
2976
2976
  * expect (and isn't serving a stale cached bundle). Keep this in sync with
2977
2977
  * `package.json` `version`.
2978
2978
  */
2979
- const VERSION = "0.8.0-alpha.0";
2979
+ const VERSION = "0.9.0-alpha.0";
2980
2980
 
2981
2981
  /**
2982
2982
  * `HttpClient` — the one place the unified {@link Client} talks to the
@@ -5463,6 +5463,7 @@ class ShardClient {
5463
5463
  fetchFn;
5464
5464
  cache;
5465
5465
  deferUpload;
5466
+ peers;
5466
5467
  constructor(opts) {
5467
5468
  if (!opts?.baseUrl)
5468
5469
  throw new Error("ShardClient: `baseUrl` is required");
@@ -5480,6 +5481,7 @@ class ShardClient {
5480
5481
  this.fetchFn = f.bind(globalThis);
5481
5482
  this.cache = opts.cache;
5482
5483
  this.deferUpload = opts.deferUpload;
5484
+ this.peers = opts.peers;
5483
5485
  }
5484
5486
  /** Build the URL for a given shard hash — exposed for callers that want to log or pre-resolve. */
5485
5487
  urlForHash(hash) {
@@ -5498,6 +5500,9 @@ class ShardClient {
5498
5500
  // this is safe to keep regardless of upload outcome.
5499
5501
  if (this.cache)
5500
5502
  await this.cache.put(hash, bytes);
5503
+ // Tell the swarm we now hold this block, so peers can pull it from us.
5504
+ if (this.peers)
5505
+ this.peers.announce(hash);
5501
5506
  let response;
5502
5507
  try {
5503
5508
  response = await this.fetchFn(url, {
@@ -5541,6 +5546,22 @@ class ShardClient {
5541
5546
  if (cached)
5542
5547
  return cached;
5543
5548
  }
5549
+ // Peers next: ask the Space swarm before spending origin egress. Peer
5550
+ // blocks are hash-verified by the block engine; on miss/timeout we fall
5551
+ // through to origin. Best-effort — never let a peer error block a read.
5552
+ if (this.peers) {
5553
+ try {
5554
+ const fromPeer = await this.peers.getBlock(hash);
5555
+ if (fromPeer) {
5556
+ if (this.cache)
5557
+ void this.cache.put(hash, fromPeer);
5558
+ return fromPeer;
5559
+ }
5560
+ }
5561
+ catch {
5562
+ /* peer layer hiccup — fall through to origin */
5563
+ }
5564
+ }
5544
5565
  const url = this.urlForHash(hash);
5545
5566
  let response;
5546
5567
  try {
@@ -6768,6 +6789,8 @@ class Space {
6768
6789
  events = new EventTarget();
6769
6790
  rawWired = false;
6770
6791
  catchUpUnsub = null;
6792
+ /** Optional P2P swarm for this Space (block exchange among members). */
6793
+ peerNet = null;
6771
6794
  constructor(deps) {
6772
6795
  this.deps = deps;
6773
6796
  this.name = deps.name;
@@ -6816,6 +6839,9 @@ class Space {
6816
6839
  if (this.deps.keyring) {
6817
6840
  this.channel.on(BroadcastChannelEvents.CONNECTED, () => {
6818
6841
  this.channel?.sendRaw({ name: this.deps.myId() });
6842
+ // Kick P2P peer discovery now the signaling channel is live
6843
+ // (re-runs on each reconnect to re-form the mesh).
6844
+ this.peerNet?.start();
6819
6845
  });
6820
6846
  this.wireFanout();
6821
6847
  // Register offline catch-up once: on each reconnect the sync
@@ -6831,6 +6857,27 @@ class Space {
6831
6857
  this.channel?.disconnect();
6832
6858
  this.catchUpUnsub?.();
6833
6859
  this.catchUpUnsub = null;
6860
+ this.peerNet?.close();
6861
+ this.peerNet = null;
6862
+ }
6863
+ /** Attach a P2P swarm so this Space's file reads/writes can use peers. */
6864
+ attachPeerNetwork(net) {
6865
+ this.peerNet = net;
6866
+ }
6867
+ /**
6868
+ * Broadcast an app payload directly to connected Space peers over P2P,
6869
+ * **bypassing the server** — for CRDT ops / high-frequency state (live
6870
+ * cursors, presence, collaborative edits) you don't want to push through the
6871
+ * relay. No-op when no P2P mesh is active (requires `client` `p2p`).
6872
+ * Unlike {@link sendEphemeral} (server-relayed, metered, size-limited), this
6873
+ * is peer-direct and fragments large payloads.
6874
+ */
6875
+ gossipToPeers(data) {
6876
+ this.peerNet?.gossip(data);
6877
+ }
6878
+ /** Subscribe to peer gossip ({@link gossipToPeers}). Returns an unsubscribe fn. */
6879
+ onPeerGossip(handler) {
6880
+ return this.peerNet?.onGossip(handler) ?? (() => { });
6834
6881
  }
6835
6882
  /** Subscribe to a raw channel event. Buffered until `connect()` if early. */
6836
6883
  on(event, handler) {
@@ -7000,10 +7047,12 @@ class Space {
7000
7047
  * indicators, presence, or live cursors are built on top of this. No-op if
7001
7048
  * the space isn't connected (ephemeral signals aren't worth queuing).
7002
7049
  */
7003
- sendEphemeral(subject, data) {
7050
+ sendEphemeral(subject, data, to) {
7004
7051
  if (!this.isConnected())
7005
7052
  return;
7006
- this.sendRaw({ pub: { subject, data } });
7053
+ // `to` (a member id) lets the server unicast a *directed* ephemeral
7054
+ // instead of broadcasting; omitted ⇒ broadcast to the room (unchanged).
7055
+ this.sendRaw({ pub: to ? { subject, data, to } : { subject, data } });
7007
7056
  }
7008
7057
  /** Subscribe to inbound ephemeral signals. `from` is the authenticated sender. */
7009
7058
  onEphemeral(handler) {
@@ -7123,6 +7172,7 @@ class Space {
7123
7172
  fetch: this.deps.fetch,
7124
7173
  cache: this.deps.shardCache,
7125
7174
  deferUpload: this.deps.deferShardUpload,
7175
+ peers: this.peerNet?.exchange,
7126
7176
  });
7127
7177
  return new FileStorage({ shards });
7128
7178
  }
@@ -8041,6 +8091,849 @@ class SpaceCache {
8041
8091
  }
8042
8092
  }
8043
8093
 
8094
+ /**
8095
+ * `ShardCache` — a Cache-API-backed store for file-shard bytes. Shards are
8096
+ * content-addressed (the key IS the SHA-256 of the ciphertext), so caching them
8097
+ * is uniquely safe: a cache hit is self-validating, immutable, and shareable
8098
+ * across files. The Cache API (rather than IndexedDB) is the right home for
8099
+ * large binary blobs — it streams, it's purpose-built for response bodies, and
8100
+ * it has its own eviction.
8101
+ *
8102
+ * Keys are SYNTHETIC absolute URLs (`https://shard.muhkoo.local/<hash>`) rather
8103
+ * than the real shard endpoint, so the cache is addressable by hash alone — the
8104
+ * sync engine can re-PUT a deferred shard knowing only its hash, without
8105
+ * reconstructing the accelerator URL. We never fetch these synthetic URLs; they
8106
+ * exist only as cache keys.
8107
+ *
8108
+ * Every method degrades to a no-op / null where the Cache API is unavailable
8109
+ * (Node, Workers, insecure contexts), so callers don't need to branch.
8110
+ */
8111
+ const CACHE_NAME = "muhkoo-shards-v1";
8112
+ class ShardCache {
8113
+ static available() {
8114
+ try {
8115
+ return typeof caches !== "undefined";
8116
+ }
8117
+ catch {
8118
+ return false;
8119
+ }
8120
+ }
8121
+ key(hash) {
8122
+ return `https://shard.muhkoo.local/${encodeURIComponent(hash)}`;
8123
+ }
8124
+ open() {
8125
+ return caches.open(CACHE_NAME);
8126
+ }
8127
+ /** Cached shard bytes, or `null` on miss / when unavailable. */
8128
+ async get(hash) {
8129
+ if (!ShardCache.available())
8130
+ return null;
8131
+ try {
8132
+ const cache = await this.open();
8133
+ const res = await cache.match(this.key(hash));
8134
+ if (!res)
8135
+ return null;
8136
+ return new Uint8Array(await res.arrayBuffer());
8137
+ }
8138
+ catch {
8139
+ return null;
8140
+ }
8141
+ }
8142
+ /** Store shard bytes under their hash. Best-effort. */
8143
+ async put(hash, bytes) {
8144
+ if (!ShardCache.available())
8145
+ return;
8146
+ try {
8147
+ const cache = await this.open();
8148
+ await cache.put(this.key(hash), new Response(bytes, {
8149
+ headers: { "content-type": "application/octet-stream" },
8150
+ }));
8151
+ }
8152
+ catch {
8153
+ /* quota / unavailable — degrade to online-only for this shard */
8154
+ }
8155
+ }
8156
+ /** Whether a shard is present in the cache. */
8157
+ async has(hash) {
8158
+ if (!ShardCache.available())
8159
+ return false;
8160
+ try {
8161
+ const cache = await this.open();
8162
+ return Boolean(await cache.match(this.key(hash)));
8163
+ }
8164
+ catch {
8165
+ return false;
8166
+ }
8167
+ }
8168
+ }
8169
+
8170
+ /**
8171
+ * `PeerExchange` — the thin façade the storage layer consults to try fetching a
8172
+ * block from peers before falling back to origin. It satisfies the structural
8173
+ * `PeerBlockSource` interface that {@link ../storage/transport/ShardClient}
8174
+ * declares (so storage never imports the p2p module — the dependency arrow stays
8175
+ * one-way, p2p → storage).
8176
+ *
8177
+ * It delegates to an {@link EngineHandle} — either an in-process {@link
8178
+ * ./worker/blockEngine.BlockEngine} or the Web Worker proxy — so the same façade
8179
+ * works whether the block engine runs on the main thread or off it.
8180
+ */
8181
+ class PeerExchange {
8182
+ engine;
8183
+ timeoutMs;
8184
+ constructor(engine, opts = {}) {
8185
+ this.engine = engine;
8186
+ this.timeoutMs = opts.timeoutMs ?? 2500;
8187
+ }
8188
+ getBlock(hash, opts) {
8189
+ return this.engine.want(hash, opts?.timeoutMs ?? this.timeoutMs);
8190
+ }
8191
+ announce(hash) {
8192
+ this.engine.announce(hash);
8193
+ }
8194
+ }
8195
+
8196
+ /**
8197
+ * `SpaceSignaler` — WebRTC signaling carried over the Space's existing ephemeral
8198
+ * relay. No new server channel: `Space.sendEphemeral`/`onEphemeral` already
8199
+ * blind-relay `pub` frames stamped with the authenticated sender, so SDP
8200
+ * offers/answers and ICE candidates ride a reserved subject (`__p2p__`) scoped
8201
+ * to Space membership. The sender (`from`) is server-stamped and can't be
8202
+ * spoofed; the payload is opaque to the server.
8203
+ *
8204
+ * Envelopes carry a `to` (a member id, or `"*"` for discovery) so the
8205
+ * accelerator can optionally unicast peer-pair signaling instead of broadcasting
8206
+ * (a small, backward-compatible `pub` enhancement); the client filters by `to`
8207
+ * regardless.
8208
+ */
8209
+ const P2P_SUBJECT = "__p2p__";
8210
+ class SpaceSignaler {
8211
+ space;
8212
+ myId;
8213
+ constructor(space, myId) {
8214
+ this.space = space;
8215
+ this.myId = myId;
8216
+ }
8217
+ /** Send a signal to one peer (or `"*"`). */
8218
+ send(to, kind, data) {
8219
+ const env = { to, kind, data };
8220
+ // Ask the server to unicast for a directed target; broadcast for "*".
8221
+ // `to` also stays in the envelope so a broadcast-only server still works
8222
+ // (the client filters by it).
8223
+ this.space.sendEphemeral(P2P_SUBJECT, env, to !== "*" ? to : undefined);
8224
+ }
8225
+ /** Announce/withdraw P2P availability to all Space members. */
8226
+ broadcast(kind, data) {
8227
+ this.send("*", kind, data);
8228
+ }
8229
+ /**
8230
+ * Subscribe to inbound signals addressed to us (or broadcast). Skips our own
8231
+ * echoes. Returns an unsubscribe function.
8232
+ */
8233
+ onSignal(cb) {
8234
+ return this.space.onEphemeral((e) => {
8235
+ if (e.subject !== P2P_SUBJECT || e.from === this.myId)
8236
+ return;
8237
+ const env = e.data;
8238
+ if (!env || typeof env.kind !== "string")
8239
+ return;
8240
+ if (env.to !== this.myId && env.to !== "*")
8241
+ return;
8242
+ cb(e.from, env.kind, env.data);
8243
+ });
8244
+ }
8245
+ }
8246
+
8247
+ /**
8248
+ * `WebRtcTransport` — a {@link ./PeerTransport.PeerTransport} over RTCDataChannels,
8249
+ * signaled by {@link ../signaling/SpaceSignaler}. Runs on the **main thread**
8250
+ * (`RTCPeerConnection` is Window-only — not available in Workers), so the block
8251
+ * engine lives in a Worker and this only shuttles bytes.
8252
+ *
8253
+ * Connection setup uses a deterministic initiator (the peer with the smaller id
8254
+ * dials) to avoid offer/answer glare. STUN handles NAT traversal; there is no
8255
+ * TURN — an unreachable peer simply never connects and the caller falls back to
8256
+ * origin (P2P is best-effort).
8257
+ *
8258
+ * Frames (up to a 4 MiB shard) exceed an RTCDataChannel's max message size, so
8259
+ * each frame is fragmented into `CHUNK`-sized pieces with a 12-byte header
8260
+ * `[msgId u32][index u32][total u32]` and reassembled per peer.
8261
+ */
8262
+ const CHUNK = 16 * 1024;
8263
+ // [msgId u32][index u32][total u32][channel u32]
8264
+ const HEADER$1 = 16;
8265
+ /** Pause sends when a channel has this many bytes buffered (backpressure). */
8266
+ const MAX_BUFFERED = 4 * 1024 * 1024;
8267
+ const DEFAULT_ICE = [{ urls: "stun:stun.cloudflare.com:3478" }];
8268
+ class WebRtcTransport {
8269
+ signaler;
8270
+ myId;
8271
+ conns = new Map();
8272
+ msgCbs = new Set();
8273
+ peerCbs = new Set();
8274
+ offSignal;
8275
+ iceServers;
8276
+ maxPeers;
8277
+ nextMsgId = 1;
8278
+ constructor(signaler, myId, opts = {}) {
8279
+ this.signaler = signaler;
8280
+ this.myId = myId;
8281
+ this.iceServers = opts.iceServers ?? DEFAULT_ICE;
8282
+ this.maxPeers = opts.maxPeers ?? 8;
8283
+ this.offSignal = this.signaler.onSignal((from, kind, data) => this.onSignal(from, kind, data));
8284
+ }
8285
+ peers() {
8286
+ return [...this.conns].filter(([, c]) => c.dc?.readyState === "open").map(([id]) => id);
8287
+ }
8288
+ /** Dial a peer (no-op unless we're the deterministic initiator for the pair). */
8289
+ connectTo(peerId) {
8290
+ if (peerId === this.myId || this.conns.has(peerId))
8291
+ return;
8292
+ if (this.conns.size >= this.maxPeers)
8293
+ return; // mesh cap
8294
+ if (this.myId < peerId)
8295
+ void this.initiate(peerId); // smaller id dials
8296
+ }
8297
+ send(peer, data, channel = 0) {
8298
+ const dc = this.conns.get(peer)?.dc;
8299
+ if (dc?.readyState === "open")
8300
+ void this.sendFragmented(dc, data, channel);
8301
+ }
8302
+ broadcast(data, channel = 0) {
8303
+ for (const [, c] of this.conns) {
8304
+ if (c.dc?.readyState === "open")
8305
+ void this.sendFragmented(c.dc, data, channel);
8306
+ }
8307
+ }
8308
+ onMessage(cb) {
8309
+ this.msgCbs.add(cb);
8310
+ return () => this.msgCbs.delete(cb);
8311
+ }
8312
+ onPeerChange(cb) {
8313
+ this.peerCbs.add(cb);
8314
+ return () => this.peerCbs.delete(cb);
8315
+ }
8316
+ close() {
8317
+ this.offSignal();
8318
+ for (const [, c] of this.conns) {
8319
+ try {
8320
+ c.dc?.close();
8321
+ }
8322
+ catch { /* already closing */ }
8323
+ try {
8324
+ c.pc.close();
8325
+ }
8326
+ catch { /* already closing */ }
8327
+ }
8328
+ this.conns.clear();
8329
+ this.msgCbs.clear();
8330
+ this.peerCbs.clear();
8331
+ }
8332
+ // -- internals -----------------------------------------------------------
8333
+ newPc(peerId) {
8334
+ const pc = new RTCPeerConnection({ iceServers: this.iceServers });
8335
+ const conn = { pc, reasm: new Map() };
8336
+ this.conns.set(peerId, conn);
8337
+ pc.onicecandidate = (e) => {
8338
+ if (e.candidate)
8339
+ this.signaler.send(peerId, "ice", e.candidate.toJSON());
8340
+ };
8341
+ pc.onconnectionstatechange = () => {
8342
+ if (["failed", "closed", "disconnected"].includes(pc.connectionState))
8343
+ this.drop(peerId);
8344
+ };
8345
+ pc.ondatachannel = (e) => this.bindChannel(peerId, conn, e.channel);
8346
+ return conn;
8347
+ }
8348
+ async initiate(peerId) {
8349
+ const conn = this.newPc(peerId);
8350
+ const dc = conn.pc.createDataChannel("blocks");
8351
+ this.bindChannel(peerId, conn, dc);
8352
+ const offer = await conn.pc.createOffer();
8353
+ await conn.pc.setLocalDescription(offer);
8354
+ this.signaler.send(peerId, "offer", offer);
8355
+ }
8356
+ bindChannel(peerId, conn, dc) {
8357
+ dc.binaryType = "arraybuffer";
8358
+ conn.dc = dc;
8359
+ dc.onopen = () => this.firePeerChange();
8360
+ dc.onclose = () => this.drop(peerId);
8361
+ dc.onmessage = (e) => this.onChunk(peerId, conn, new Uint8Array(e.data));
8362
+ }
8363
+ async onSignal(from, kind, data) {
8364
+ if (kind === "offer") {
8365
+ if (!this.conns.has(from) && this.conns.size >= this.maxPeers)
8366
+ return; // mesh cap
8367
+ const conn = this.conns.get(from) ?? this.newPc(from);
8368
+ await conn.pc.setRemoteDescription(data);
8369
+ const answer = await conn.pc.createAnswer();
8370
+ await conn.pc.setLocalDescription(answer);
8371
+ this.signaler.send(from, "answer", answer);
8372
+ }
8373
+ else if (kind === "answer") {
8374
+ const conn = this.conns.get(from);
8375
+ if (conn)
8376
+ await conn.pc.setRemoteDescription(data);
8377
+ }
8378
+ else if (kind === "ice") {
8379
+ const conn = this.conns.get(from);
8380
+ if (conn && data) {
8381
+ try {
8382
+ await conn.pc.addIceCandidate(data);
8383
+ }
8384
+ catch { /* pre-SRD race */ }
8385
+ }
8386
+ }
8387
+ else if (kind === "bye") {
8388
+ this.drop(from);
8389
+ }
8390
+ }
8391
+ drop(peerId) {
8392
+ const conn = this.conns.get(peerId);
8393
+ if (!conn)
8394
+ return;
8395
+ this.conns.delete(peerId);
8396
+ try {
8397
+ conn.dc?.close();
8398
+ }
8399
+ catch { /* already closing */ }
8400
+ try {
8401
+ conn.pc.close();
8402
+ }
8403
+ catch { /* already closing */ }
8404
+ this.firePeerChange();
8405
+ }
8406
+ firePeerChange() {
8407
+ const peers = this.peers();
8408
+ for (const cb of this.peerCbs)
8409
+ cb(peers);
8410
+ }
8411
+ // -- fragmentation -------------------------------------------------------
8412
+ async sendFragmented(dc, data, channel) {
8413
+ const msgId = this.nextMsgId++ >>> 0;
8414
+ const total = Math.max(1, Math.ceil(data.length / CHUNK));
8415
+ for (let i = 0; i < total; i++) {
8416
+ const slice = data.subarray(i * CHUNK, (i + 1) * CHUNK);
8417
+ const frame = new Uint8Array(HEADER$1 + slice.length);
8418
+ const dv = new DataView(frame.buffer);
8419
+ dv.setUint32(0, msgId);
8420
+ dv.setUint32(4, i);
8421
+ dv.setUint32(8, total);
8422
+ dv.setUint32(12, channel);
8423
+ frame.set(slice, HEADER$1);
8424
+ if (dc.bufferedAmount > MAX_BUFFERED)
8425
+ await this.drain(dc);
8426
+ dc.send(frame);
8427
+ }
8428
+ }
8429
+ drain(dc) {
8430
+ return new Promise((resolve) => {
8431
+ dc.bufferedAmountLowThreshold = MAX_BUFFERED / 2;
8432
+ const onLow = () => { dc.removeEventListener("bufferedamountlow", onLow); resolve(); };
8433
+ dc.addEventListener("bufferedamountlow", onLow);
8434
+ });
8435
+ }
8436
+ onChunk(peerId, conn, frame) {
8437
+ if (frame.length < HEADER$1)
8438
+ return;
8439
+ const dv = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
8440
+ const msgId = dv.getUint32(0);
8441
+ const index = dv.getUint32(4);
8442
+ const total = dv.getUint32(8);
8443
+ const channel = dv.getUint32(12);
8444
+ const chunk = frame.slice(HEADER$1);
8445
+ let r = conn.reasm.get(msgId);
8446
+ if (!r) {
8447
+ r = { chunks: new Array(total), received: 0, total, size: 0, channel };
8448
+ conn.reasm.set(msgId, r);
8449
+ }
8450
+ if (r.chunks[index])
8451
+ return; // dup
8452
+ r.chunks[index] = chunk;
8453
+ r.received++;
8454
+ r.size += chunk.length;
8455
+ if (r.received < r.total)
8456
+ return;
8457
+ conn.reasm.delete(msgId);
8458
+ const out = new Uint8Array(r.size);
8459
+ let off = 0;
8460
+ for (const c of r.chunks) {
8461
+ if (!c)
8462
+ return; // gap — shouldn't happen on reliable ordered
8463
+ out.set(c, off);
8464
+ off += c.length;
8465
+ }
8466
+ for (const cb of this.msgCbs)
8467
+ cb(peerId, out, r.channel);
8468
+ }
8469
+ }
8470
+
8471
+ /**
8472
+ * `PeerTransport` — the pluggable wire under the block engine. It moves opaque
8473
+ * frames between Space peers; it knows nothing about blocks, hashes, or the
8474
+ * protocol. {@link ./WebRtcTransport} implements it over RTCDataChannels today;
8475
+ * a future libp2p/Helia transport can implement the same interface without the
8476
+ * block engine or `ShardClient` changing.
8477
+ *
8478
+ * Implementations are responsible for fragmenting/ reassembling frames larger
8479
+ * than the underlying channel's max message size (a 4 MiB shard won't fit in one
8480
+ * `RTCDataChannel.send`); the engine always deals in whole frames.
8481
+ */
8482
+ /**
8483
+ * Logical channels multiplexed over the one data link. `0` is block exchange
8484
+ * (the engine), `1` is gossip (app/CRDT data). The tag rides the framing header
8485
+ * so payloads aren't copied to prepend it.
8486
+ */
8487
+ const CHANNEL_BLOCK = 0;
8488
+ const CHANNEL_GOSSIP = 1;
8489
+
8490
+ /**
8491
+ * Bitswap-lite wire protocol for peer block exchange. Four frame types over a
8492
+ * {@link ../transport/PeerTransport} data channel:
8493
+ *
8494
+ * WANT(hash) — "does anyone have this block?" / "send it to me"
8495
+ * HAVE(hash) — "I hold this block" (announce / reply to a WANT)
8496
+ * BLOCK(hash, bytes) — the block itself (verified by hash on receipt)
8497
+ * CANCEL(hash) — "stop, I got it elsewhere"
8498
+ *
8499
+ * Frames are compact binary so a 4 MiB shard isn't base64-bloated through JSON:
8500
+ *
8501
+ * byte 0 : type (0 WANT | 1 HAVE | 2 BLOCK | 3 CANCEL)
8502
+ * bytes 1..33 : 32-byte raw SHA-256 of the block (our shard hash, hex-decoded)
8503
+ * bytes 33.. : payload (BLOCK only)
8504
+ *
8505
+ * Blocks are AES-GCM ciphertext, so a frame carries nothing readable; the hash
8506
+ * is the capability check (the receiver re-hashes the payload and drops a
8507
+ * mismatch). See {@link ./blockEngine}.
8508
+ */
8509
+ var FrameType;
8510
+ (function (FrameType) {
8511
+ FrameType[FrameType["WANT"] = 0] = "WANT";
8512
+ FrameType[FrameType["HAVE"] = 1] = "HAVE";
8513
+ FrameType[FrameType["BLOCK"] = 2] = "BLOCK";
8514
+ FrameType[FrameType["CANCEL"] = 3] = "CANCEL";
8515
+ })(FrameType || (FrameType = {}));
8516
+ const HASH_BYTES = 32;
8517
+ const HEADER = 1 + HASH_BYTES; // type + hash
8518
+ function hexToBytes(hex) {
8519
+ if (hex.length !== HASH_BYTES * 2) {
8520
+ throw new Error(`protocol: expected a ${HASH_BYTES}-byte hex hash, got ${hex.length / 2} bytes`);
8521
+ }
8522
+ const out = new Uint8Array(HASH_BYTES);
8523
+ for (let i = 0; i < HASH_BYTES; i++)
8524
+ out[i] = parseInt(hex.substr(i * 2, 2), 16);
8525
+ return out;
8526
+ }
8527
+ const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
8528
+ function bytesToHex(bytes) {
8529
+ let s = "";
8530
+ for (let i = 0; i < bytes.length; i++)
8531
+ s += HEX[bytes[i]];
8532
+ return s;
8533
+ }
8534
+ function header(type, hash, extra = 0) {
8535
+ const buf = new Uint8Array(HEADER + extra);
8536
+ buf[0] = type;
8537
+ buf.set(hexToBytes(hash), 1);
8538
+ return { buf, offset: HEADER };
8539
+ }
8540
+ function encodeWant(hash) {
8541
+ return header(FrameType.WANT, hash).buf;
8542
+ }
8543
+ function encodeHave(hash) {
8544
+ return header(FrameType.HAVE, hash).buf;
8545
+ }
8546
+ function encodeCancel(hash) {
8547
+ return header(FrameType.CANCEL, hash).buf;
8548
+ }
8549
+ function encodeBlock(hash, payload) {
8550
+ const { buf, offset } = header(FrameType.BLOCK, hash, payload.length);
8551
+ buf.set(payload, offset);
8552
+ return buf;
8553
+ }
8554
+ function decode(frame) {
8555
+ if (frame.length < HEADER)
8556
+ throw new Error("protocol: frame too short");
8557
+ const type = frame[0];
8558
+ const hash = bytesToHex(frame.subarray(1, HEADER));
8559
+ if (type === FrameType.BLOCK) {
8560
+ // Copy the payload out so it doesn't alias a transferred/pooled buffer.
8561
+ return { type, hash, payload: frame.slice(HEADER) };
8562
+ }
8563
+ return { type, hash };
8564
+ }
8565
+
8566
+ var protocol = /*#__PURE__*/Object.freeze({
8567
+ __proto__: null,
8568
+ get FrameType () { return FrameType; },
8569
+ decode: decode,
8570
+ encodeBlock: encodeBlock,
8571
+ encodeCancel: encodeCancel,
8572
+ encodeHave: encodeHave,
8573
+ encodeWant: encodeWant
8574
+ });
8575
+
8576
+ /**
8577
+ * `BlockEngine` — the bitswap-lite want/have ledger. Host-agnostic: it speaks
8578
+ * only to a {@link BlockStore} (get/put/has) and an `out` sink that ships
8579
+ * encoded frames to a peer (or `"*"` to broadcast). The host wires `out` and
8580
+ * `handleFrame` to a real transport — on the main thread or inside the Web
8581
+ * Worker (see {@link ./engineClient}) — so this class is pure and unit-testable.
8582
+ *
8583
+ * It never sees keys: blocks are AES-GCM ciphertext, addressed by SHA-256. The
8584
+ * one trust check is integrity — an inbound BLOCK is re-hashed and dropped on
8585
+ * mismatch, so a malicious peer can't poison the store (the requester just
8586
+ * falls back to origin).
8587
+ */
8588
+ class BlockEngine {
8589
+ store;
8590
+ out;
8591
+ hash;
8592
+ wants = new Map();
8593
+ constructor(store, out, hash) {
8594
+ this.store = store;
8595
+ this.out = out;
8596
+ this.hash = hash;
8597
+ }
8598
+ /**
8599
+ * Ask the swarm for a block. Resolves with the verified bytes, or `null` on
8600
+ * timeout (caller then falls back to origin). Returns immediately if we
8601
+ * already hold it; concurrent wants for the same hash share one request.
8602
+ */
8603
+ want(hash, timeoutMs) {
8604
+ return new Promise((resolve) => {
8605
+ void this.store.get(hash).then((local) => {
8606
+ if (local)
8607
+ return resolve(local);
8608
+ const existing = this.wants.get(hash);
8609
+ if (existing) {
8610
+ existing.resolvers.push(resolve);
8611
+ return;
8612
+ }
8613
+ const timer = setTimeout(() => {
8614
+ const w = this.wants.get(hash);
8615
+ this.wants.delete(hash);
8616
+ this.out("*", encodeCancel(hash));
8617
+ w?.resolvers.forEach((r) => r(null));
8618
+ }, timeoutMs);
8619
+ this.wants.set(hash, { resolvers: [resolve], timer });
8620
+ this.out("*", encodeWant(hash));
8621
+ });
8622
+ });
8623
+ }
8624
+ /** Tell the swarm we now hold `hash` (so peers wanting it can pull from us). */
8625
+ announce(hash) {
8626
+ this.out("*", encodeHave(hash));
8627
+ }
8628
+ /** Handle one inbound frame from `peer`. */
8629
+ async handleFrame(peer, frame) {
8630
+ let f;
8631
+ try {
8632
+ f = decode(frame);
8633
+ }
8634
+ catch {
8635
+ return; // malformed — ignore
8636
+ }
8637
+ switch (f.type) {
8638
+ case FrameType.WANT: {
8639
+ const bytes = await this.store.get(f.hash);
8640
+ if (bytes)
8641
+ this.out(peer, encodeBlock(f.hash, bytes));
8642
+ return;
8643
+ }
8644
+ case FrameType.HAVE: {
8645
+ // A peer announced a block we're still waiting for → pull from them.
8646
+ if (this.wants.has(f.hash))
8647
+ this.out(peer, encodeWant(f.hash));
8648
+ return;
8649
+ }
8650
+ case FrameType.BLOCK: {
8651
+ const pending = this.wants.get(f.hash);
8652
+ if (!pending || !f.payload)
8653
+ return; // unsolicited / empty — ignore
8654
+ // Integrity: a peer can't poison the store. Drop a mismatch and
8655
+ // keep waiting (the timeout will fall back to origin).
8656
+ const got = await this.hash(f.payload);
8657
+ if (got !== f.hash)
8658
+ return;
8659
+ await this.store.put(f.hash, f.payload);
8660
+ this.wants.delete(f.hash);
8661
+ clearTimeout(pending.timer);
8662
+ this.out("*", encodeCancel(f.hash)); // others can stop sending
8663
+ pending.resolvers.forEach((r) => r(f.payload));
8664
+ return;
8665
+ }
8666
+ case FrameType.CANCEL:
8667
+ // We serve WANTs synchronously, so there's nothing in flight to
8668
+ // cancel yet. Reserved for chunked-send cancellation later.
8669
+ return;
8670
+ }
8671
+ }
8672
+ /** Drop all pending waits (resolve null) — on teardown. */
8673
+ close() {
8674
+ for (const [, w] of this.wants) {
8675
+ clearTimeout(w.timer);
8676
+ w.resolvers.forEach((r) => r(null));
8677
+ }
8678
+ this.wants.clear();
8679
+ }
8680
+ }
8681
+
8682
+ /**
8683
+ * `EngineHost` — the boundary {@link ../PeerNetwork} wires a transport to. It
8684
+ * abstracts *where* the {@link ./blockEngine.BlockEngine} runs:
8685
+ * - {@link LocalEngineHost} runs it on the calling thread (tests, or a
8686
+ * fallback when the Web Worker can't be built by a consumer's bundler);
8687
+ * - the Worker proxy ({@link ./engineClient}) runs it off-thread.
8688
+ *
8689
+ * Either way the host exposes the same surface: drive `want`/`announce`, feed it
8690
+ * inbound frames via `handleFrame`, and receive its outbound frames via
8691
+ * `onOutbound` (which the network forwards to the transport).
8692
+ */
8693
+ /** Runs the engine in-process (main thread or test). */
8694
+ class LocalEngineHost {
8695
+ engine;
8696
+ cbs = new Set();
8697
+ constructor(store, hasher) {
8698
+ this.engine = new BlockEngine(store, (target, frame) => {
8699
+ for (const cb of this.cbs)
8700
+ cb(target, frame);
8701
+ }, hasher);
8702
+ }
8703
+ want(hash, timeoutMs) {
8704
+ return this.engine.want(hash, timeoutMs);
8705
+ }
8706
+ announce(hash) {
8707
+ this.engine.announce(hash);
8708
+ }
8709
+ handleFrame(peer, frame) {
8710
+ void this.engine.handleFrame(peer, frame);
8711
+ }
8712
+ onOutbound(cb) {
8713
+ this.cbs.add(cb);
8714
+ return () => this.cbs.delete(cb);
8715
+ }
8716
+ close() {
8717
+ this.engine.close();
8718
+ this.cbs.clear();
8719
+ }
8720
+ }
8721
+
8722
+ /**
8723
+ * `WorkerEngineHost` — the main-thread proxy to a {@link ./blockEngine.BlockEngine}
8724
+ * running inside a dedicated Web Worker (so hashing, blockstore I/O, and the
8725
+ * protocol codec stay off the main thread). It speaks the small message protocol
8726
+ * in {@link ./blockEngine.worker} and satisfies {@link ./engineHost.EngineHost},
8727
+ * so {@link ../PeerNetwork} treats it identically to the in-process host.
8728
+ *
8729
+ * Block bytes cross the worker boundary as **transferable** ArrayBuffers
8730
+ * (zero-copy). Only encrypted blocks ever cross — keys never enter the worker.
8731
+ */
8732
+ class WorkerEngineHost {
8733
+ worker;
8734
+ wants = new Map();
8735
+ cbs = new Set();
8736
+ nextId = 1;
8737
+ constructor(worker) {
8738
+ this.worker = worker;
8739
+ this.worker.onmessage = (e) => {
8740
+ const m = e.data;
8741
+ if (m.t === "out") {
8742
+ const frame = new Uint8Array(m.data);
8743
+ for (const cb of this.cbs)
8744
+ cb(m.target, frame);
8745
+ }
8746
+ else if (m.t === "want-result") {
8747
+ const resolve = this.wants.get(m.id);
8748
+ if (resolve) {
8749
+ this.wants.delete(m.id);
8750
+ resolve(m.data ? new Uint8Array(m.data) : null);
8751
+ }
8752
+ }
8753
+ };
8754
+ }
8755
+ want(hash, timeoutMs) {
8756
+ return new Promise((resolve) => {
8757
+ const id = this.nextId++;
8758
+ this.wants.set(id, resolve);
8759
+ this.worker.postMessage({ t: "want", id, hash, timeout: timeoutMs });
8760
+ });
8761
+ }
8762
+ announce(hash) {
8763
+ this.worker.postMessage({ t: "announce", hash });
8764
+ }
8765
+ handleFrame(peer, frame) {
8766
+ // Transfer an exact-size buffer; copy if the view doesn't own the whole buffer.
8767
+ const buf = frame.byteOffset === 0 && frame.byteLength === frame.buffer.byteLength
8768
+ ? frame.buffer
8769
+ : frame.slice().buffer;
8770
+ this.worker.postMessage({ t: "frame", peer, data: buf }, [buf]);
8771
+ }
8772
+ onOutbound(cb) {
8773
+ this.cbs.add(cb);
8774
+ return () => this.cbs.delete(cb);
8775
+ }
8776
+ close() {
8777
+ try {
8778
+ this.worker.postMessage({ t: "close" });
8779
+ }
8780
+ catch { /* already gone */ }
8781
+ this.worker.terminate();
8782
+ this.wants.forEach((r) => r(null));
8783
+ this.wants.clear();
8784
+ this.cbs.clear();
8785
+ }
8786
+ }
8787
+
8788
+ /**
8789
+ * `PeerNetwork` — the per-Space orchestrator that ties the pieces together on
8790
+ * the main thread:
8791
+ *
8792
+ * transport (WebRTC) ⇄ engine host (Worker or in-process) → PeerExchange
8793
+ * ▲ │
8794
+ * └────────────── signaling over the Space relay ─────────────┘
8795
+ *
8796
+ * It owns one {@link ./transport/WebRtcTransport} + one {@link
8797
+ * ./worker/engineHost.EngineHost}, wires inbound frames to the engine and the
8798
+ * engine's outbound frames back to the transport, runs peer discovery over the
8799
+ * Space's ephemeral channel, and exposes a {@link ./PeerExchange.PeerExchange}
8800
+ * for `ShardClient` to consult. Scope is one Space (private swarm).
8801
+ *
8802
+ * `engineHost` defaults to in-process; pass `workerFactory` to run the block
8803
+ * engine off the main thread (the consumer's bundler resolves the worker URL).
8804
+ */
8805
+ /**
8806
+ * Build the engine host, preferring an off-thread Web Worker:
8807
+ * 1. an explicit `workerFactory` (most reliable in app bundlers), else
8808
+ * 2. the bundled worker resolved relative to this module (works where the
8809
+ * bundler supports `new Worker(new URL(...))`, e.g. Vite builds), else
8810
+ * 3. the in-process engine (always works; tests + non-worker runtimes).
8811
+ * Any worker construction failure degrades to in-process.
8812
+ */
8813
+ function buildEngineHost(opts) {
8814
+ try {
8815
+ const worker = opts.workerFactory ? opts.workerFactory() : defaultWorker();
8816
+ if (worker)
8817
+ return new WorkerEngineHost(worker);
8818
+ }
8819
+ catch {
8820
+ /* no Worker global / bundler can't resolve it → in-process */
8821
+ }
8822
+ return new LocalEngineHost(opts.store ?? new ShardCache(), shardHash);
8823
+ }
8824
+ /** Construct the bundled worker if the runtime + bundler support it, else null. */
8825
+ function defaultWorker() {
8826
+ try {
8827
+ if (typeof Worker === "undefined")
8828
+ return null;
8829
+ // The string is resolved by the consumer's bundler relative to the
8830
+ // built connect module (sibling of dist/browser/index.js).
8831
+ return new Worker(new URL("./blockEngine.worker.js", import.meta.url), { type: "module" });
8832
+ }
8833
+ catch {
8834
+ return null;
8835
+ }
8836
+ }
8837
+ class PeerNetwork {
8838
+ /** Pass this to a `ShardClient` as `peers`. */
8839
+ exchange;
8840
+ transport;
8841
+ host;
8842
+ signaler;
8843
+ offs = [];
8844
+ gossipCbs = new Set();
8845
+ closed = false;
8846
+ constructor(opts) {
8847
+ this.signaler = new SpaceSignaler(opts.space, opts.myId);
8848
+ this.transport =
8849
+ opts.transport ??
8850
+ new WebRtcTransport(this.signaler, opts.myId, {
8851
+ iceServers: opts.iceServers,
8852
+ maxPeers: opts.maxPeers,
8853
+ });
8854
+ this.host = buildEngineHost(opts);
8855
+ // Wire transport ⇄ engine, demuxing block (engine) from gossip (app).
8856
+ this.offs.push(this.transport.onMessage((peer, data, channel) => {
8857
+ if (channel === CHANNEL_GOSSIP) {
8858
+ for (const cb of this.gossipCbs)
8859
+ cb(peer, data);
8860
+ }
8861
+ else {
8862
+ this.host.handleFrame(peer, data);
8863
+ }
8864
+ }));
8865
+ this.offs.push(this.host.onOutbound((target, frame) => target === "*"
8866
+ ? this.transport.broadcast(frame, CHANNEL_BLOCK)
8867
+ : this.transport.send(target, frame, CHANNEL_BLOCK)));
8868
+ // Discovery: a broadcast hello triggers a unicast hello back (so a
8869
+ // newcomer learns existing members); both sides connectTo (the smaller
8870
+ // id actually dials). Deterministic + idempotent.
8871
+ this.offs.push(this.signaler.onSignal((from, kind, data) => {
8872
+ if (kind !== "hello")
8873
+ return;
8874
+ this.transport.connectTo?.(from);
8875
+ if (data?.broadcast) {
8876
+ this.signaler.send(from, "hello", { broadcast: false });
8877
+ }
8878
+ }));
8879
+ this.exchange = new PeerExchange(this.host, { timeoutMs: opts.timeoutMs });
8880
+ }
8881
+ /**
8882
+ * Begin peer discovery — broadcast a hello so existing members dial us and
8883
+ * we learn them. Call once the signaling channel is live (the Space fires
8884
+ * this on CONNECTED); safe to call again on reconnect.
8885
+ */
8886
+ start() {
8887
+ if (this.closed)
8888
+ return;
8889
+ this.signaler.broadcast("hello", { broadcast: true });
8890
+ }
8891
+ /**
8892
+ * Broadcast an app payload to all connected peers over the gossip channel —
8893
+ * a direct, server-bypassing data path for CRDT ops / high-frequency state
8894
+ * (cursors, presence, collaborative edits). Single-hop (no re-forwarding).
8895
+ */
8896
+ gossip(data) {
8897
+ this.transport.broadcast(data, CHANNEL_GOSSIP);
8898
+ }
8899
+ /** Subscribe to gossip from peers. Returns an unsubscribe function. */
8900
+ onGossip(cb) {
8901
+ this.gossipCbs.add(cb);
8902
+ return () => this.gossipCbs.delete(cb);
8903
+ }
8904
+ /** Connected peer count (open data channels). */
8905
+ peerCount() {
8906
+ return this.transport.peers().length;
8907
+ }
8908
+ close() {
8909
+ if (this.closed)
8910
+ return;
8911
+ this.closed = true;
8912
+ try {
8913
+ this.signaler.broadcast("bye");
8914
+ }
8915
+ catch { /* socket may be gone */ }
8916
+ for (const off of this.offs)
8917
+ off();
8918
+ this.transport.close();
8919
+ this.host.close();
8920
+ }
8921
+ }
8922
+
8923
+ /**
8924
+ * Whether this environment can do WebRTC peering. `RTCPeerConnection` is
8925
+ * Window-only (absent in Node/Workers/SSR), so this gates whether a
8926
+ * {@link ./PeerNetwork} can be built at all.
8927
+ */
8928
+ function isP2pCapable() {
8929
+ try {
8930
+ return typeof RTCPeerConnection !== "undefined";
8931
+ }
8932
+ catch {
8933
+ return false;
8934
+ }
8935
+ }
8936
+
8044
8937
  /**
8045
8938
  * `client.space` — the fan-out group-encryption surface.
8046
8939
  *
@@ -8272,6 +9165,17 @@ class SpaceNamespace {
8272
9165
  : undefined,
8273
9166
  });
8274
9167
  this.openSpaces.set(spaceId, space);
9168
+ // Attach a private P2P swarm scoped to this Space (block exchange among
9169
+ // members; signaled over the Space relay). Discovery starts on connect.
9170
+ if (this.deps.p2p?.enabled && isP2pCapable()) {
9171
+ space.attachPeerNetwork(new PeerNetwork({
9172
+ space,
9173
+ myId: memberId,
9174
+ workerFactory: this.deps.p2p.workerFactory,
9175
+ iceServers: this.deps.p2p.iceServers,
9176
+ maxPeers: this.deps.p2p.maxPeers,
9177
+ }));
9178
+ }
8275
9179
  return space;
8276
9180
  }
8277
9181
  /** Ensure the member has an identity keypair; return its public ECDH + ECDSA keys. */
@@ -9035,82 +9939,6 @@ class SnapshotNamespace {
9035
9939
  }
9036
9940
  }
9037
9941
 
9038
- /**
9039
- * `ShardCache` — a Cache-API-backed store for file-shard bytes. Shards are
9040
- * content-addressed (the key IS the SHA-256 of the ciphertext), so caching them
9041
- * is uniquely safe: a cache hit is self-validating, immutable, and shareable
9042
- * across files. The Cache API (rather than IndexedDB) is the right home for
9043
- * large binary blobs — it streams, it's purpose-built for response bodies, and
9044
- * it has its own eviction.
9045
- *
9046
- * Keys are SYNTHETIC absolute URLs (`https://shard.muhkoo.local/<hash>`) rather
9047
- * than the real shard endpoint, so the cache is addressable by hash alone — the
9048
- * sync engine can re-PUT a deferred shard knowing only its hash, without
9049
- * reconstructing the accelerator URL. We never fetch these synthetic URLs; they
9050
- * exist only as cache keys.
9051
- *
9052
- * Every method degrades to a no-op / null where the Cache API is unavailable
9053
- * (Node, Workers, insecure contexts), so callers don't need to branch.
9054
- */
9055
- const CACHE_NAME = "muhkoo-shards-v1";
9056
- class ShardCache {
9057
- static available() {
9058
- try {
9059
- return typeof caches !== "undefined";
9060
- }
9061
- catch {
9062
- return false;
9063
- }
9064
- }
9065
- key(hash) {
9066
- return `https://shard.muhkoo.local/${encodeURIComponent(hash)}`;
9067
- }
9068
- open() {
9069
- return caches.open(CACHE_NAME);
9070
- }
9071
- /** Cached shard bytes, or `null` on miss / when unavailable. */
9072
- async get(hash) {
9073
- if (!ShardCache.available())
9074
- return null;
9075
- try {
9076
- const cache = await this.open();
9077
- const res = await cache.match(this.key(hash));
9078
- if (!res)
9079
- return null;
9080
- return new Uint8Array(await res.arrayBuffer());
9081
- }
9082
- catch {
9083
- return null;
9084
- }
9085
- }
9086
- /** Store shard bytes under their hash. Best-effort. */
9087
- async put(hash, bytes) {
9088
- if (!ShardCache.available())
9089
- return;
9090
- try {
9091
- const cache = await this.open();
9092
- await cache.put(this.key(hash), new Response(bytes, {
9093
- headers: { "content-type": "application/octet-stream" },
9094
- }));
9095
- }
9096
- catch {
9097
- /* quota / unavailable — degrade to online-only for this shard */
9098
- }
9099
- }
9100
- /** Whether a shard is present in the cache. */
9101
- async has(hash) {
9102
- if (!ShardCache.available())
9103
- return false;
9104
- try {
9105
- const cache = await this.open();
9106
- return Boolean(await cache.match(this.key(hash)));
9107
- }
9108
- catch {
9109
- return false;
9110
- }
9111
- }
9112
- }
9113
-
9114
9942
  /**
9115
9943
  * IndexedDB schema for the offline layer — the single source of truth for the
9116
9944
  * database name, version, and object stores. Bump {@link DB_VERSION} whenever
@@ -9833,6 +10661,7 @@ class Client {
9833
10661
  wsBaseUrl,
9834
10662
  cache: spaceKeyCache,
9835
10663
  offline: this.offline,
10664
+ p2p: options.p2p,
9836
10665
  });
9837
10666
  this.agents = new AgentsNamespace({ http: this.http });
9838
10667
  this.functions = new FunctionsNamespace({ http: this.http, fnHostSuffix: options.functionsHostSuffix });
@@ -10724,5 +11553,5 @@ var bundledLoader = /*#__PURE__*/Object.freeze({
10724
11553
  default: loadRsWasm
10725
11554
  });
10726
11555
 
10727
- 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_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DbCache, DbNamespace, DbTable, 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, Logger, 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, Ready, ReedSolomonCodec, Retry, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, StorageNamespace, SyncEngine, VERSION, VaultUnavailableError, WSTransport, ZERO_HLC, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger$1 as appLogger, assertType, base58Decode, base58Encode, buildCommitment, compareHlc, compilePrograms, concatBytes, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deserialize, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, fromBase64$1 as fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateId, generatePkce, getIPAddress, getId, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isNumber, isOfflineCapable, isProvisional, log, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, poseidonHash, provisionalHandle, quickVerify, randomBytes, randomState, sealSeed, serialize, shardHash, signMessage, stableStringify, toBase64$1 as toBase64, toBase64Url, toField, toHex, tombstoneRow, unpack, unsealSeed, unwrapWithPassphrase, utf8Decode, utf8Encode, verify, verifyChecksum, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
11556
+ 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_BASE_URL, DEFAULT_FN_HOST_SUFFIX, DbCache, DbNamespace, DbTable, 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, Logger, 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, Ready, ReedSolomonCodec, Retry, Space as Room, SerializeMessage, SessionState, ShardCache, ShardClient, SharedSpaceClient, SnapshotNamespace, Space, SpaceCache, SpaceKeyring, SpaceNamespace, SpacePacketCipher, SpaceSignaler, StorageNamespace, SyncEngine, VERSION, VaultUnavailableError, WSTransport, WebRtcTransport, WorkerEngineHost, ZERO_HLC, ZkAuth, _accountId, _formatBytes, _messageId, _objectId, _packetId, _socketId, _userId, appLogger$1 as appLogger, assertType, base58Decode, base58Encode, buildCommitment, compareHlc, compilePrograms, concatBytes, decodeFromHex, decorators, decryptAesGcm, defaultCircuitUrls, defaultSessionStore, deleteBeatsWrite, deleteRegister, deriveAesKeyFromPassphrase, deriveBitsHkdf, deriveIdentity, deriveIdentityFromSeed, deriveMasterSeedFromPassword, deserialize, ejectAgentPrompt, ejectAgentTools, emptyMap, emptyRegister, encodeToHex, encryptAesGcm, exportPublicKeyBase64, exportPublicKeyHex, fromBase64$1 as fromBase64, fromBase64Url, fromHex, generateAuthProof, generateChecksum, generateId, generatePkce, getIPAddress, getId, getMuhkooAppDescriptor, getSubtle, initBn128Wasm, initRsWasm, initializeCircuits, isNewer, isNumber, isOfflineCapable, isP2pCapable, isProvisional, log, manifestToStat, materializeRow, mergeMap, mergeMessage, mergeRegister, messageKey, pack, padHandle, poseidonHash, protocol, provisionalHandle, quickVerify, randomBytes, randomState, sealSeed, serialize, shardHash, signMessage, stableStringify, toBase64$1 as toBase64, toBase64Url, toField, toHex, tombstoneRow, unpack, unsealSeed, unwrapWithPassphrase, utf8Decode, utf8Encode, verify, verifyChecksum, verifyGroth16, verifyHashKnowledge, verifyPreimagePoK, wrapWithPassphrase, writeBeatsTombstone, writeColumns, writeRegister };
10728
11557
  //# sourceMappingURL=index.js.map