@fairfox/polly 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +55 -1
  2. package/dist/src/elysia/index.js +5 -3
  3. package/dist/src/elysia/index.js.map +3 -3
  4. package/dist/src/elysia/peer-repo-plugin.d.ts +1 -1
  5. package/dist/src/mesh-node.d.ts +89 -0
  6. package/dist/src/mesh-node.js +594 -0
  7. package/dist/src/mesh-node.js.map +14 -0
  8. package/dist/src/mesh.d.ts +10 -0
  9. package/dist/src/mesh.js +926 -24
  10. package/dist/src/mesh.js.map +17 -9
  11. package/dist/src/peer.d.ts +1 -0
  12. package/dist/src/peer.js +108 -85
  13. package/dist/src/peer.js.map +11 -10
  14. package/dist/src/shared/lib/blob-cache.d.ts +58 -0
  15. package/dist/src/shared/lib/blob-store-impl.d.ts +33 -0
  16. package/dist/src/shared/lib/blob-store.d.ts +87 -0
  17. package/dist/src/shared/lib/blob-transfer.d.ts +58 -0
  18. package/dist/src/shared/lib/crdt-specialised.d.ts +1 -1
  19. package/dist/src/shared/lib/crdt-state.d.ts +1 -1
  20. package/dist/src/shared/lib/keyring-storage.d.ts +57 -0
  21. package/dist/src/shared/lib/mesh-client.d.ts +91 -0
  22. package/dist/src/shared/lib/mesh-network-adapter.d.ts +1 -1
  23. package/dist/src/shared/lib/mesh-signaling-client.d.ts +6 -0
  24. package/dist/src/shared/lib/mesh-state.d.ts +1 -1
  25. package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +20 -1
  26. package/dist/src/shared/lib/peer-relay-adapter.d.ts +1 -1
  27. package/dist/src/shared/lib/peer-repo-server.d.ts +1 -1
  28. package/dist/src/shared/lib/peer-state.d.ts +1 -1
  29. package/dist/src/shared/lib/wasm-init.d.ts +17 -0
  30. package/dist/tools/quality/src/cli.js +98 -47
  31. package/dist/tools/quality/src/cli.js.map +4 -4
  32. package/dist/tools/quality/src/index.d.ts +25 -0
  33. package/dist/tools/quality/src/index.js +196 -0
  34. package/dist/tools/quality/src/index.js.map +10 -0
  35. package/dist/tools/quality/src/no-as-casting.d.ts +44 -0
  36. package/package.json +22 -2
package/dist/src/mesh.js CHANGED
@@ -45,19 +45,22 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
45
45
  });
46
46
 
47
47
  // src/shared/lib/encryption.ts
48
+ var exports_encryption = {};
49
+ __export(exports_encryption, {
50
+ sealEnvelope: () => sealEnvelope,
51
+ openEnvelope: () => openEnvelope,
52
+ generateDocumentKey: () => generateDocumentKey,
53
+ encrypt: () => encrypt,
54
+ encodeEncryptedEnvelope: () => encodeEncryptedEnvelope,
55
+ decryptOrThrow: () => decryptOrThrow,
56
+ decrypt: () => decrypt,
57
+ decodeEncryptedEnvelope: () => decodeEncryptedEnvelope,
58
+ TAG_BYTES: () => TAG_BYTES,
59
+ NONCE_BYTES: () => NONCE_BYTES,
60
+ KEY_BYTES: () => KEY_BYTES,
61
+ EncryptionError: () => EncryptionError
62
+ });
48
63
  import nacl from "tweetnacl";
49
- var KEY_BYTES = 32;
50
- var NONCE_BYTES = 24;
51
- var TAG_BYTES = 16;
52
-
53
- class EncryptionError extends Error {
54
- code;
55
- constructor(message, code) {
56
- super(message);
57
- this.name = "EncryptionError";
58
- this.code = code;
59
- }
60
- }
61
64
  function generateDocumentKey() {
62
65
  return nacl.randomBytes(KEY_BYTES);
63
66
  }
@@ -122,10 +125,786 @@ function decodeEncryptedEnvelope(bytes) {
122
125
  const sealed = bytes.slice(4 + idLen);
123
126
  return { documentId, sealed };
124
127
  }
128
+ var KEY_BYTES = 32, NONCE_BYTES = 24, TAG_BYTES = 16, EncryptionError;
129
+ var init_encryption = __esm(() => {
130
+ EncryptionError = class EncryptionError extends Error {
131
+ code;
132
+ constructor(message, code) {
133
+ super(message);
134
+ this.name = "EncryptionError";
135
+ this.code = code;
136
+ }
137
+ };
138
+ });
139
+
140
+ // src/shared/lib/wasm-init.ts
141
+ import wasmPath from "@automerge/automerge/automerge.wasm";
142
+ import { initializeWasm } from "@automerge/automerge-repo/slim";
143
+ var wasmUrl = new URL(wasmPath, import.meta.url).href;
144
+ await initializeWasm(wasmUrl);
145
+
146
+ // src/shared/lib/blob-cache.ts
147
+ class MemoryBlobCache {
148
+ store = new Map;
149
+ pinned = new Set;
150
+ urls = new Map;
151
+ async get(hash) {
152
+ const entry = this.store.get(hash);
153
+ if (!entry)
154
+ return;
155
+ entry.accessedAt = Date.now();
156
+ return entry.bytes;
157
+ }
158
+ async put(hash, bytes) {
159
+ this.store.set(hash, { bytes, accessedAt: Date.now() });
160
+ }
161
+ async has(hash) {
162
+ return this.store.has(hash);
163
+ }
164
+ async delete(hash) {
165
+ this.store.delete(hash);
166
+ this.pinned.delete(hash);
167
+ const url = this.urls.get(hash);
168
+ if (url) {
169
+ URL.revokeObjectURL(url);
170
+ this.urls.delete(hash);
171
+ }
172
+ }
173
+ async pin(hash) {
174
+ this.pinned.add(hash);
175
+ }
176
+ async unpin(hash) {
177
+ this.pinned.delete(hash);
178
+ }
179
+ async size() {
180
+ let total = 0;
181
+ for (const entry of this.store.values()) {
182
+ total += entry.bytes.byteLength;
183
+ }
184
+ return total;
185
+ }
186
+ async evict(maxBytes) {
187
+ let currentSize = await this.size();
188
+ if (currentSize <= maxBytes)
189
+ return 0;
190
+ const freed = currentSize;
191
+ const candidates = [];
192
+ for (const [hash, entry] of this.store) {
193
+ if (!this.pinned.has(hash)) {
194
+ candidates.push({ hash, accessedAt: entry.accessedAt, size: entry.bytes.byteLength });
195
+ }
196
+ }
197
+ candidates.sort((a, b) => a.accessedAt - b.accessedAt);
198
+ for (const c of candidates) {
199
+ if (currentSize <= maxBytes)
200
+ break;
201
+ await this.delete(c.hash);
202
+ currentSize -= c.size;
203
+ }
204
+ return freed - currentSize;
205
+ }
206
+ async url(hash) {
207
+ const cached = this.urls.get(hash);
208
+ if (cached)
209
+ return cached;
210
+ const entry = this.store.get(hash);
211
+ if (!entry)
212
+ return;
213
+ const buffer = new ArrayBuffer(entry.bytes.byteLength);
214
+ new Uint8Array(buffer).set(entry.bytes);
215
+ const objectUrl = URL.createObjectURL(new Blob([buffer]));
216
+ this.urls.set(hash, objectUrl);
217
+ return objectUrl;
218
+ }
219
+ dispose() {
220
+ for (const objectUrl of this.urls.values()) {
221
+ URL.revokeObjectURL(objectUrl);
222
+ }
223
+ this.urls.clear();
224
+ this.store.clear();
225
+ this.pinned.clear();
226
+ }
227
+ }
228
+
229
+ class IndexedDBBlobCache {
230
+ static DB_NAME = "polly-blobs";
231
+ static DB_VERSION = 1;
232
+ static STORE_NAME = "blobs";
233
+ dbPromise = null;
234
+ urls = new Map;
235
+ openDB() {
236
+ if (this.dbPromise)
237
+ return this.dbPromise;
238
+ this.dbPromise = new Promise((resolve, reject) => {
239
+ const request = indexedDB.open(IndexedDBBlobCache.DB_NAME, IndexedDBBlobCache.DB_VERSION);
240
+ request.onerror = () => reject(request.error);
241
+ request.onsuccess = () => resolve(request.result);
242
+ request.onupgradeneeded = (event) => {
243
+ const db = event.target.result;
244
+ if (!db.objectStoreNames.contains(IndexedDBBlobCache.STORE_NAME)) {
245
+ db.createObjectStore(IndexedDBBlobCache.STORE_NAME);
246
+ }
247
+ };
248
+ });
249
+ return this.dbPromise;
250
+ }
251
+ async getRecord(hash) {
252
+ const db = await this.openDB();
253
+ return new Promise((resolve, reject) => {
254
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
255
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
256
+ const request = store.get(hash);
257
+ request.onsuccess = () => resolve(request.result);
258
+ request.onerror = () => reject(request.error);
259
+ });
260
+ }
261
+ async putRecord(hash, record) {
262
+ const db = await this.openDB();
263
+ return new Promise((resolve, reject) => {
264
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readwrite");
265
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
266
+ store.put(record, hash);
267
+ tx.oncomplete = () => resolve();
268
+ tx.onerror = () => reject(tx.error);
269
+ });
270
+ }
271
+ async get(hash) {
272
+ const record = await this.getRecord(hash);
273
+ if (!record)
274
+ return;
275
+ this.putRecord(hash, { ...record, accessedAt: Date.now() });
276
+ return record.bytes;
277
+ }
278
+ async put(hash, bytes) {
279
+ const existing = await this.getRecord(hash);
280
+ await this.putRecord(hash, {
281
+ bytes,
282
+ size: bytes.byteLength,
283
+ accessedAt: Date.now(),
284
+ pinned: existing?.pinned ?? false
285
+ });
286
+ }
287
+ async has(hash) {
288
+ const db = await this.openDB();
289
+ return new Promise((resolve, reject) => {
290
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
291
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
292
+ const request = store.count(hash);
293
+ request.onsuccess = () => resolve(request.result > 0);
294
+ request.onerror = () => reject(request.error);
295
+ });
296
+ }
297
+ async delete(hash) {
298
+ const url = this.urls.get(hash);
299
+ if (url) {
300
+ URL.revokeObjectURL(url);
301
+ this.urls.delete(hash);
302
+ }
303
+ const db = await this.openDB();
304
+ return new Promise((resolve, reject) => {
305
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readwrite");
306
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
307
+ store.delete(hash);
308
+ tx.oncomplete = () => resolve();
309
+ tx.onerror = () => reject(tx.error);
310
+ });
311
+ }
312
+ async pin(hash) {
313
+ const record = await this.getRecord(hash);
314
+ if (!record)
315
+ return;
316
+ await this.putRecord(hash, { ...record, pinned: true });
317
+ }
318
+ async unpin(hash) {
319
+ const record = await this.getRecord(hash);
320
+ if (!record)
321
+ return;
322
+ await this.putRecord(hash, { ...record, pinned: false });
323
+ }
324
+ async size() {
325
+ const db = await this.openDB();
326
+ return new Promise((resolve, reject) => {
327
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
328
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
329
+ const request = store.openCursor();
330
+ let total = 0;
331
+ request.onsuccess = () => {
332
+ const cursor = request.result;
333
+ if (cursor) {
334
+ const value = cursor.value;
335
+ total += value.size;
336
+ cursor.continue();
337
+ } else {
338
+ resolve(total);
339
+ }
340
+ };
341
+ request.onerror = () => reject(request.error);
342
+ });
343
+ }
344
+ async evict(maxBytes) {
345
+ const db = await this.openDB();
346
+ const candidates = [];
347
+ let totalSize = 0;
348
+ await new Promise((resolve, reject) => {
349
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
350
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
351
+ const request = store.openCursor();
352
+ request.onsuccess = () => {
353
+ const cursor = request.result;
354
+ if (cursor) {
355
+ const value = cursor.value;
356
+ totalSize += value.size;
357
+ if (!value.pinned) {
358
+ candidates.push({
359
+ hash: cursor.key,
360
+ accessedAt: value.accessedAt,
361
+ size: value.size
362
+ });
363
+ }
364
+ cursor.continue();
365
+ } else {
366
+ resolve();
367
+ }
368
+ };
369
+ request.onerror = () => reject(request.error);
370
+ });
371
+ if (totalSize <= maxBytes)
372
+ return 0;
373
+ candidates.sort((a, b) => a.accessedAt - b.accessedAt);
374
+ let freed = 0;
375
+ for (const c of candidates) {
376
+ if (totalSize <= maxBytes)
377
+ break;
378
+ await this.delete(c.hash);
379
+ totalSize -= c.size;
380
+ freed += c.size;
381
+ }
382
+ return freed;
383
+ }
384
+ async url(hash) {
385
+ const cached = this.urls.get(hash);
386
+ if (cached)
387
+ return cached;
388
+ const bytes = await this.get(hash);
389
+ if (!bytes)
390
+ return;
391
+ const buffer = new ArrayBuffer(bytes.byteLength);
392
+ new Uint8Array(buffer).set(bytes);
393
+ const objectUrl = URL.createObjectURL(new Blob([buffer]));
394
+ this.urls.set(hash, objectUrl);
395
+ return objectUrl;
396
+ }
397
+ dispose() {
398
+ for (const objectUrl of this.urls.values()) {
399
+ URL.revokeObjectURL(objectUrl);
400
+ }
401
+ this.urls.clear();
402
+ }
403
+ }
404
+ // src/shared/lib/blob-ref.ts
405
+ function isBlobRef(value) {
406
+ if (typeof value !== "object" || value === null)
407
+ return false;
408
+ const v = value;
409
+ return typeof v["hash"] === "string" && /^[0-9a-f]{64}$/.test(v["hash"]) && typeof v["size"] === "number" && Number.isInteger(v["size"]) && v["size"] >= 0 && typeof v["filename"] === "string" && typeof v["mimeType"] === "string";
410
+ }
411
+ async function computeBlobHash(bytes) {
412
+ const buffer = new ArrayBuffer(bytes.byteLength);
413
+ const copy = new Uint8Array(buffer);
414
+ copy.set(bytes);
415
+ const digest = await crypto.subtle.digest("SHA-256", buffer);
416
+ const view = new Uint8Array(digest);
417
+ let hex = "";
418
+ for (const byte of view) {
419
+ hex += byte.toString(16).padStart(2, "0");
420
+ }
421
+ return hex;
422
+ }
423
+ async function createBlobRef({
424
+ bytes,
425
+ filename,
426
+ mimeType
427
+ }) {
428
+ const hash = await computeBlobHash(bytes);
429
+ return {
430
+ hash,
431
+ size: bytes.byteLength,
432
+ filename,
433
+ mimeType
434
+ };
435
+ }
436
+ // src/shared/lib/blob-transfer.ts
437
+ var BLOB_CHUNK_SIZE = 65536;
438
+ var BLOB_BUFFER_HIGH_WATER = 256 * 1024;
439
+ function chunkBlob(bytes, chunkSize = BLOB_CHUNK_SIZE) {
440
+ const chunks = [];
441
+ for (let offset = 0;offset < bytes.length; offset += chunkSize) {
442
+ chunks.push(bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)));
443
+ }
444
+ if (chunks.length === 0) {
445
+ chunks.push(new Uint8Array(0));
446
+ }
447
+ return chunks;
448
+ }
449
+ function reassembleChunks(chunks, total) {
450
+ let totalBytes = 0;
451
+ for (let i = 0;i < total; i++) {
452
+ const chunk = chunks.get(i);
453
+ if (!chunk) {
454
+ throw new Error(`reassembleChunks: missing chunk ${i} of ${total}`);
455
+ }
456
+ totalBytes += chunk.length;
457
+ }
458
+ const out = new Uint8Array(totalBytes);
459
+ let offset = 0;
460
+ for (let i = 0;i < total; i++) {
461
+ const chunk = chunks.get(i);
462
+ out.set(chunk, offset);
463
+ offset += chunk.length;
464
+ }
465
+ return out;
466
+ }
467
+ function missingChunkIndices(chunks, total) {
468
+ const missing = [];
469
+ for (let i = 0;i < total; i++) {
470
+ if (!chunks.has(i)) {
471
+ missing.push(i);
472
+ }
473
+ }
474
+ return missing;
475
+ }
476
+ function serialiseBlobMessage(header, data = new Uint8Array(0)) {
477
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
478
+ const size = 4 + headerBytes.length + data.length;
479
+ const buffer = new ArrayBuffer(size);
480
+ const out = new Uint8Array(buffer);
481
+ const view = new DataView(buffer);
482
+ view.setUint32(0, headerBytes.length, false);
483
+ out.set(headerBytes, 4);
484
+ out.set(data, 4 + headerBytes.length);
485
+ return out;
486
+ }
487
+ function isBlobMessageType(bytes) {
488
+ if (bytes.length < 4)
489
+ return false;
490
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
491
+ const headerLen = view.getUint32(0, false);
492
+ if (bytes.length < 4 + headerLen)
493
+ return false;
494
+ const headerSlice = bytes.subarray(4, 4 + headerLen);
495
+ const needle = new TextEncoder().encode('"type":"blob-');
496
+ return findSubarray(headerSlice, needle) !== -1;
497
+ }
498
+ function findSubarray(haystack, needle) {
499
+ if (needle.length === 0)
500
+ return 0;
501
+ outer:
502
+ for (let i = 0;i <= haystack.length - needle.length; i++) {
503
+ for (let j = 0;j < needle.length; j++) {
504
+ if (haystack[i + j] !== needle[j])
505
+ continue outer;
506
+ }
507
+ return i;
508
+ }
509
+ return -1;
510
+ }
511
+
512
+ // src/shared/lib/blob-store-impl.ts
513
+ var DEFAULT_MAX_BLOB_SIZE = 100 * 1024 * 1024;
514
+ var DOWNLOAD_TIMEOUT_MS = 60000;
515
+ var RE_REQUEST_DELAY_MS = 5000;
516
+ function createBlobStore(adapter, options) {
517
+ const maxBlobSize = options?.maxBlobSize ?? DEFAULT_MAX_BLOB_SIZE;
518
+ const defaultKey = options?.encrypt?.key;
519
+ const cache = options?.cache ?? new MemoryBlobCache;
520
+ const keysByHash = new Map;
521
+ const peerBlobs = new Map;
522
+ const downloads = new Map;
523
+ const localHashes = new Set;
524
+ const urlCache = new Map;
525
+ let disposed = false;
526
+ adapter.onBlobMessage = (peerId, header, data) => {
527
+ if (disposed)
528
+ return;
529
+ const type = header["type"];
530
+ switch (type) {
531
+ case "blob-chunk":
532
+ handleChunk(peerId, header, data);
533
+ break;
534
+ case "blob-request":
535
+ handleRequest(peerId, header);
536
+ break;
537
+ case "blob-have":
538
+ handleHave(peerId, header);
539
+ break;
540
+ }
541
+ };
542
+ const peerCandidateHandler = (event) => {
543
+ if (disposed)
544
+ return;
545
+ const newPeerId = event.peerId;
546
+ for (const hash of localHashes) {
547
+ const msg = serialiseBlobMessage({ type: "blob-have", hash });
548
+ adapter.sendBlobMessage(newPeerId, msg);
549
+ }
550
+ };
551
+ adapter.on("peer-candidate", peerCandidateHandler);
552
+ async function encryptChunk(plaintext, key) {
553
+ const { encrypt: encrypt2 } = await Promise.resolve().then(() => (init_encryption(), exports_encryption));
554
+ return encrypt2(plaintext, key);
555
+ }
556
+ async function decryptChunk(sealed, key) {
557
+ const { decrypt: decrypt2 } = await Promise.resolve().then(() => (init_encryption(), exports_encryption));
558
+ return decrypt2(sealed, key);
559
+ }
560
+ async function handleChunk(peerId, header, data) {
561
+ const download = downloads.get(header.hash);
562
+ if (!download)
563
+ return;
564
+ download.total = header.total;
565
+ if (!download.peersAttempted.includes(peerId)) {
566
+ download.peersAttempted.push(peerId);
567
+ }
568
+ let chunkBytes;
569
+ if (download.key) {
570
+ const plaintext = await decryptChunk(data, download.key);
571
+ if (!plaintext) {
572
+ return;
573
+ }
574
+ chunkBytes = plaintext;
575
+ } else {
576
+ chunkBytes = data.slice();
577
+ }
578
+ download.chunks.set(header.index, chunkBytes);
579
+ reportChunkProgress(download);
580
+ resetReRequestTimer(download);
581
+ if (download.chunks.size >= header.total) {
582
+ finishDownload(header.hash, download);
583
+ } else {
584
+ scheduleReRequest(header.hash, download);
585
+ }
586
+ }
587
+ function reportChunkProgress(download) {
588
+ if (!download.onProgress || download.total <= 0)
589
+ return;
590
+ let loaded = 0;
591
+ for (const chunk of download.chunks.values()) {
592
+ loaded += chunk.length;
593
+ }
594
+ download.onProgress({ loaded, total: undefined, phase: "downloading" });
595
+ }
596
+ function resetReRequestTimer(download) {
597
+ if (download.reRequestId) {
598
+ clearTimeout(download.reRequestId);
599
+ download.reRequestId = undefined;
600
+ }
601
+ }
602
+ function finishDownload(hash, download) {
603
+ clearDownloadTimers(download);
604
+ try {
605
+ const assembled = reassembleChunks(download.chunks, download.total);
606
+ downloads.delete(hash);
607
+ download.resolve(assembled);
608
+ } catch (err) {
609
+ downloads.delete(hash);
610
+ download.reject(err instanceof Error ? err : new Error(String(err)));
611
+ }
612
+ }
613
+ async function handleRequest(peerId, header) {
614
+ const plaintext = await cache.get(header.hash);
615
+ if (!plaintext)
616
+ return;
617
+ const plaintextChunks = chunkBlob(plaintext);
618
+ const requested = header.missing ?? plaintextChunks.map((_, i) => i);
619
+ const chunkKey = keysByHash.get(header.hash);
620
+ for (const index of requested) {
621
+ await sendChunkAtIndex(peerId, header.hash, plaintextChunks, index, chunkKey);
622
+ }
623
+ }
624
+ async function sendChunkAtIndex(peerId, hash, plaintextChunks, index, chunkKey) {
625
+ if (index < 0 || index >= plaintextChunks.length)
626
+ return;
627
+ const plainChunk = plaintextChunks[index];
628
+ if (!plainChunk)
629
+ return;
630
+ const payload = chunkKey ? await encryptChunk(plainChunk, chunkKey) : plainChunk;
631
+ const chunkHeader = {
632
+ type: "blob-chunk",
633
+ hash,
634
+ index,
635
+ total: plaintextChunks.length
636
+ };
637
+ const msg = serialiseBlobMessage(chunkHeader, payload);
638
+ if (!adapter.sendBlobMessage(peerId, msg)) {
639
+ await waitForBufferDrain();
640
+ adapter.sendBlobMessage(peerId, msg);
641
+ }
642
+ }
643
+ function handleHave(peerId, header) {
644
+ let peers = peerBlobs.get(header.hash);
645
+ if (!peers) {
646
+ peers = new Set;
647
+ peerBlobs.set(header.hash, peers);
648
+ }
649
+ peers.add(peerId);
650
+ }
651
+ function announceHave(hash) {
652
+ const msg = serialiseBlobMessage({ type: "blob-have", hash });
653
+ for (const peerId of adapter.connectedPeerIds) {
654
+ adapter.sendBlobMessage(peerId, msg);
655
+ }
656
+ }
657
+ function clearDownloadTimers(download) {
658
+ if (download.timeoutId)
659
+ clearTimeout(download.timeoutId);
660
+ if (download.reRequestId)
661
+ clearTimeout(download.reRequestId);
662
+ }
663
+ function scheduleReRequest(hash, download) {
664
+ download.reRequestId = setTimeout(() => fireReRequest(hash, download), RE_REQUEST_DELAY_MS);
665
+ }
666
+ function fireReRequest(hash, download) {
667
+ if (!downloads.has(hash))
668
+ return;
669
+ const missing = missingChunkIndices(download.chunks, download.total);
670
+ if (missing.length === 0)
671
+ return;
672
+ const peers = peerBlobs.get(hash);
673
+ const pool = peers && peers.size > 0 ? Array.from(peers) : Array.from(adapter.connectedPeerIds);
674
+ if (pool.length === 0)
675
+ return;
676
+ const target = pool[download.peerRotationIndex % pool.length];
677
+ download.peerRotationIndex++;
678
+ if (!target)
679
+ return;
680
+ const reqHeader = { type: "blob-request", hash, missing };
681
+ const msg = serialiseBlobMessage(reqHeader);
682
+ adapter.sendBlobMessage(target, msg);
683
+ scheduleReRequest(hash, download);
684
+ }
685
+ function waitForBufferDrain() {
686
+ return new Promise((resolve) => setTimeout(resolve, 50));
687
+ }
688
+ const store = {
689
+ async put(ref, bytes, options2) {
690
+ if (disposed)
691
+ throw new Error("BlobStore is disposed");
692
+ options2?.signal?.throwIfAborted();
693
+ if (bytes.length > maxBlobSize) {
694
+ throw new Error(`Blob exceeds maximum size (${bytes.length} > ${maxBlobSize})`);
695
+ }
696
+ const hash = await computeBlobHash(bytes);
697
+ if (hash !== ref.hash) {
698
+ throw new Error(`Hash mismatch: expected ${ref.hash}, got ${hash}`);
699
+ }
700
+ options2?.signal?.throwIfAborted();
701
+ const key = options2?.key ?? defaultKey;
702
+ if (key) {
703
+ keysByHash.set(ref.hash, key);
704
+ }
705
+ options2?.onProgress?.({ loaded: bytes.length, total: bytes.length, phase: "uploading" });
706
+ await cache.put(ref.hash, bytes);
707
+ localHashes.add(ref.hash);
708
+ announceHave(ref.hash);
709
+ },
710
+ async get(hash, options2) {
711
+ if (disposed)
712
+ throw new Error("BlobStore is disposed");
713
+ options2?.signal?.throwIfAborted();
714
+ const cached = await cache.get(hash);
715
+ if (cached)
716
+ return cached;
717
+ const key = options2?.key ?? defaultKey;
718
+ const peers = peerBlobs.get(hash);
719
+ const candidates = peers && peers.size > 0 ? Array.from(peers) : Array.from(adapter.connectedPeerIds);
720
+ const targetPeer = candidates[0];
721
+ if (!targetPeer)
722
+ return;
723
+ const requestHeader = { type: "blob-request", hash };
724
+ const msg = serialiseBlobMessage(requestHeader);
725
+ adapter.sendBlobMessage(targetPeer, msg);
726
+ const plaintext = await new Promise((resolve, reject) => {
727
+ const download = {
728
+ total: 0,
729
+ chunks: new Map,
730
+ resolve,
731
+ reject,
732
+ onProgress: options2?.onProgress,
733
+ key,
734
+ peersAttempted: [targetPeer],
735
+ peerRotationIndex: 1
736
+ };
737
+ downloads.set(hash, download);
738
+ options2?.signal?.addEventListener("abort", () => {
739
+ if (downloads.has(hash)) {
740
+ clearDownloadTimers(download);
741
+ downloads.delete(hash);
742
+ reject(new Error("Blob download aborted"));
743
+ }
744
+ }, { once: true });
745
+ download.timeoutId = setTimeout(() => {
746
+ if (downloads.has(hash)) {
747
+ clearDownloadTimers(download);
748
+ downloads.delete(hash);
749
+ reject(new Error("Blob download timed out"));
750
+ }
751
+ }, DOWNLOAD_TIMEOUT_MS);
752
+ });
753
+ const actualHash = await computeBlobHash(plaintext);
754
+ if (actualHash !== hash) {
755
+ throw new Error(`Blob hash mismatch after download: expected ${hash}, got ${actualHash}`);
756
+ }
757
+ await cache.put(hash, plaintext);
758
+ if (key)
759
+ keysByHash.set(hash, key);
760
+ localHashes.add(hash);
761
+ return plaintext;
762
+ },
763
+ async url(hash) {
764
+ if (disposed)
765
+ return;
766
+ const cached = urlCache.get(hash);
767
+ if (cached)
768
+ return cached;
769
+ const bytes = await cache.get(hash);
770
+ if (!bytes)
771
+ return;
772
+ const buffer = new ArrayBuffer(bytes.byteLength);
773
+ new Uint8Array(buffer).set(bytes);
774
+ const objectUrl = URL.createObjectURL(new Blob([buffer]));
775
+ urlCache.set(hash, objectUrl);
776
+ return objectUrl;
777
+ },
778
+ async pin(hash) {
779
+ await cache.pin(hash);
780
+ },
781
+ async unpin(hash) {
782
+ await cache.unpin(hash);
783
+ },
784
+ async size() {
785
+ return cache.size();
786
+ },
787
+ async evict(maxBytes) {
788
+ return cache.evict(maxBytes);
789
+ },
790
+ dispose() {
791
+ disposed = true;
792
+ adapter.onBlobMessage = undefined;
793
+ adapter.off("peer-candidate", peerCandidateHandler);
794
+ for (const [hash, download] of downloads) {
795
+ clearDownloadTimers(download);
796
+ download.reject(new Error("BlobStore disposed"));
797
+ downloads.delete(hash);
798
+ }
799
+ for (const objectUrl of urlCache.values()) {
800
+ URL.revokeObjectURL(objectUrl);
801
+ }
802
+ urlCache.clear();
803
+ keysByHash.clear();
804
+ cache.dispose();
805
+ }
806
+ };
807
+ return store;
808
+ }
809
+
810
+ // src/mesh.ts
811
+ init_encryption();
812
+
813
+ // src/shared/lib/keyring-storage.ts
814
+ function memoryKeyringStorage() {
815
+ let stored = null;
816
+ return {
817
+ load: async () => stored,
818
+ save: async (keyring) => {
819
+ stored = keyring;
820
+ }
821
+ };
822
+ }
823
+ function serialiseKeyring(keyring) {
824
+ const payload = {
825
+ version: 1,
826
+ identity: {
827
+ publicKey: bytesToBase64(keyring.identity.publicKey),
828
+ secretKey: bytesToBase64(keyring.identity.secretKey)
829
+ },
830
+ knownPeers: mapToBase64Record(keyring.knownPeers),
831
+ documentKeys: mapToBase64Record(keyring.documentKeys),
832
+ revokedPeers: [...keyring.revokedPeers]
833
+ };
834
+ if (keyring.revocationAuthority && keyring.revocationAuthority.size > 0) {
835
+ payload.revocationAuthority = [...keyring.revocationAuthority];
836
+ }
837
+ return JSON.stringify(payload, null, 2);
838
+ }
839
+ function deserialiseKeyring(text) {
840
+ let raw;
841
+ try {
842
+ raw = JSON.parse(text);
843
+ } catch (err) {
844
+ throw new Error(`KeyringStorage: keyring payload is not valid JSON: ${err.message}`);
845
+ }
846
+ if (!raw || typeof raw !== "object") {
847
+ throw new Error("KeyringStorage: keyring payload is not an object");
848
+ }
849
+ const r = raw;
850
+ if (r.version !== 1) {
851
+ throw new Error(`KeyringStorage: unsupported keyring version: ${String(r.version)}`);
852
+ }
853
+ if (!r.identity || typeof r.identity !== "object") {
854
+ throw new Error("KeyringStorage: keyring payload is missing identity");
855
+ }
856
+ const identity = {
857
+ publicKey: base64ToBytes(r.identity.publicKey),
858
+ secretKey: base64ToBytes(r.identity.secretKey)
859
+ };
860
+ const keyring = {
861
+ identity,
862
+ knownPeers: base64RecordToMap(r.knownPeers ?? {}),
863
+ documentKeys: base64RecordToMap(r.documentKeys ?? {}),
864
+ revokedPeers: new Set(r.revokedPeers ?? [])
865
+ };
866
+ if (r.revocationAuthority && r.revocationAuthority.length > 0) {
867
+ keyring.revocationAuthority = new Set(r.revocationAuthority);
868
+ }
869
+ return keyring;
870
+ }
871
+ function mapToBase64Record(map) {
872
+ const out = {};
873
+ for (const [key, value] of map) {
874
+ out[key] = bytesToBase64(value);
875
+ }
876
+ return out;
877
+ }
878
+ function base64RecordToMap(record) {
879
+ const out = new Map;
880
+ for (const [key, value] of Object.entries(record)) {
881
+ out.set(key, base64ToBytes(value));
882
+ }
883
+ return out;
884
+ }
885
+ function bytesToBase64(bytes) {
886
+ let binary = "";
887
+ for (const byte of bytes) {
888
+ binary += String.fromCharCode(byte);
889
+ }
890
+ return btoa(binary);
891
+ }
892
+ function base64ToBytes(b64) {
893
+ const binary = atob(b64);
894
+ const bytes = new Uint8Array(binary.length);
895
+ for (let i = 0;i < binary.length; i++) {
896
+ bytes[i] = binary.charCodeAt(i);
897
+ }
898
+ return bytes;
899
+ }
900
+ // src/shared/lib/mesh-client.ts
901
+ import { Repo } from "@automerge/automerge-repo/slim";
902
+
125
903
  // src/shared/lib/mesh-network-adapter.ts
904
+ init_encryption();
126
905
  import {
127
906
  NetworkAdapter
128
- } from "@automerge/automerge-repo";
907
+ } from "@automerge/automerge-repo/slim";
129
908
 
130
909
  // src/shared/lib/signing.ts
131
910
  import nacl2 from "tweetnacl";
@@ -355,6 +1134,7 @@ function deserialiseMessage(bytes) {
355
1134
  const data = bytes.slice(4 + headerLen);
356
1135
  return { ...header, data };
357
1136
  }
1137
+
358
1138
  // src/shared/lib/mesh-signaling-client.ts
359
1139
  class MeshSignalingClient {
360
1140
  url;
@@ -365,6 +1145,7 @@ class MeshSignalingClient {
365
1145
  onClose;
366
1146
  socket;
367
1147
  joined = false;
1148
+ WebSocketCtor;
368
1149
  constructor(options) {
369
1150
  this.url = options.url;
370
1151
  this.peerId = options.peerId;
@@ -375,10 +1156,15 @@ class MeshSignalingClient {
375
1156
  this.onOpen = options.onOpen;
376
1157
  if (options.onClose !== undefined)
377
1158
  this.onClose = options.onClose;
1159
+ const WS = options.WebSocket ?? globalThis.WebSocket;
1160
+ if (typeof WS !== "function") {
1161
+ throw new Error("MeshSignalingClient: no WebSocket implementation found. Pass one via options.WebSocket, or run in an environment where `globalThis.WebSocket` exists (Node 21+, Bun, browsers).");
1162
+ }
1163
+ this.WebSocketCtor = WS;
378
1164
  }
379
1165
  async connect() {
380
1166
  return new Promise((resolve, reject) => {
381
- const ws = new WebSocket(this.url);
1167
+ const ws = new this.WebSocketCtor(this.url);
382
1168
  this.socket = ws;
383
1169
  ws.addEventListener("open", () => {
384
1170
  ws.send(JSON.stringify({ type: "join", peerId: this.peerId }));
@@ -411,7 +1197,7 @@ class MeshSignalingClient {
411
1197
  });
412
1198
  }
413
1199
  sendSignal(targetPeerId, payload) {
414
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN || !this.joined) {
1200
+ if (!this.socket || this.socket.readyState !== this.WebSocketCtor.OPEN || !this.joined) {
415
1201
  return false;
416
1202
  }
417
1203
  const msg = {
@@ -429,11 +1215,12 @@ class MeshSignalingClient {
429
1215
  this.joined = false;
430
1216
  }
431
1217
  get isConnected() {
432
- return this.joined && this.socket?.readyState === WebSocket.OPEN;
1218
+ return this.joined && this.socket?.readyState === this.WebSocketCtor.OPEN;
433
1219
  }
434
1220
  }
1221
+
435
1222
  // src/shared/lib/crdt-specialised.ts
436
- import { Counter, updateText } from "@automerge/automerge-repo";
1223
+ import { Counter, updateText } from "@automerge/automerge-repo/slim";
437
1224
  import { effect, signal } from "@preact/signals";
438
1225
 
439
1226
  // src/shared/lib/migrate-primitive.ts
@@ -886,10 +1673,11 @@ function $meshList(key, initialValue, options = {}) {
886
1673
  access: options.access
887
1674
  });
888
1675
  }
1676
+
889
1677
  // src/shared/lib/mesh-webrtc-adapter.ts
890
1678
  import {
891
1679
  NetworkAdapter as NetworkAdapter2
892
- } from "@automerge/automerge-repo";
1680
+ } from "@automerge/automerge-repo/slim";
893
1681
  var DEFAULT_ICE_SERVERS = [
894
1682
  { urls: "stun:stun.l.google.com:19302" },
895
1683
  { urls: "stun:stun1.l.google.com:19302" }
@@ -900,15 +1688,22 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
900
1688
  iceServers;
901
1689
  dataChannelLabel;
902
1690
  knownPeerIds;
1691
+ RTCPeerConnectionCtor;
903
1692
  slots = new Map;
904
1693
  ready = false;
905
1694
  readyResolver;
1695
+ onBlobMessage;
906
1696
  constructor(options) {
907
1697
  super();
908
1698
  this.signaling = options.signaling;
909
1699
  this.iceServers = options.iceServers ?? DEFAULT_ICE_SERVERS;
910
1700
  this.dataChannelLabel = options.dataChannelLabel ?? "polly-mesh";
911
1701
  this.knownPeerIds = options.knownPeerIds ?? [];
1702
+ const PC = options.RTCPeerConnection ?? globalThis.RTCPeerConnection;
1703
+ if (typeof PC !== "function") {
1704
+ throw new Error("MeshWebRTCAdapter: no RTCPeerConnection implementation found. Pass one via options.RTCPeerConnection (e.g. from `werift` or `@roamhq/wrtc`), or run in a browser where `globalThis.RTCPeerConnection` exists.");
1705
+ }
1706
+ this.RTCPeerConnectionCtor = PC;
912
1707
  }
913
1708
  isReady() {
914
1709
  return this.ready;
@@ -974,7 +1769,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
974
1769
  }
975
1770
  }
976
1771
  createInitiatingSlot(targetId) {
977
- const connection = new RTCPeerConnection({ iceServers: this.iceServers });
1772
+ const connection = new this.RTCPeerConnectionCtor({ iceServers: this.iceServers });
978
1773
  const channel = connection.createDataChannel(this.dataChannelLabel, { ordered: true });
979
1774
  const slot = { connection, channel, pendingSends: [] };
980
1775
  this.slots.set(targetId, slot);
@@ -999,7 +1794,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
999
1794
  existing.connection.close();
1000
1795
  this.slots.delete(fromPeerId);
1001
1796
  }
1002
- const connection = new RTCPeerConnection({ iceServers: this.iceServers });
1797
+ const connection = new this.RTCPeerConnectionCtor({ iceServers: this.iceServers });
1003
1798
  const slot = { connection, channel: undefined, pendingSends: [] };
1004
1799
  this.slots.set(fromPeerId, slot);
1005
1800
  this.wireConnection(fromPeerId, connection);
@@ -1064,9 +1859,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1064
1859
  channel.onmessage = (event) => {
1065
1860
  const data = event.data;
1066
1861
  if (data instanceof ArrayBuffer) {
1067
- this.dispatchMessage(new Uint8Array(data));
1862
+ this.dispatchMessage(peerId, new Uint8Array(data));
1068
1863
  } else if (data instanceof Uint8Array) {
1069
- this.dispatchMessage(data);
1864
+ this.dispatchMessage(peerId, data);
1070
1865
  }
1071
1866
  };
1072
1867
  channel.onclose = () => {
@@ -1076,12 +1871,41 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1076
1871
  }
1077
1872
  };
1078
1873
  }
1079
- dispatchMessage(bytes) {
1874
+ dispatchMessage(fromPeerId, bytes) {
1080
1875
  try {
1876
+ if (this.onBlobMessage && isBlobMessageType(bytes)) {
1877
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1878
+ const headerLen = view.getUint32(0, false);
1879
+ const header = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + headerLen)));
1880
+ const data = bytes.subarray(4 + headerLen);
1881
+ this.onBlobMessage(fromPeerId, header, data);
1882
+ return;
1883
+ }
1081
1884
  const message = this.deserialiseMessage(bytes);
1082
1885
  this.emit("message", message);
1083
1886
  } catch {}
1084
1887
  }
1888
+ get connectedPeerIds() {
1889
+ const ids = [];
1890
+ for (const [peerId, slot] of this.slots) {
1891
+ if (slot.channel && slot.channel.readyState === "open") {
1892
+ ids.push(peerId);
1893
+ }
1894
+ }
1895
+ return ids;
1896
+ }
1897
+ sendBlobMessage(peerId, bytes) {
1898
+ const slot = this.slots.get(peerId);
1899
+ if (!slot?.channel || slot.channel.readyState !== "open")
1900
+ return false;
1901
+ return this.trySendOnChannel(slot.channel, bytes);
1902
+ }
1903
+ trySendOnChannel(channel, bytes) {
1904
+ if (channel.bufferedAmount > 256 * 1024)
1905
+ return false;
1906
+ channel.send(bytes);
1907
+ return true;
1908
+ }
1085
1909
  serialiseMessage(message) {
1086
1910
  const headerObj = {
1087
1911
  type: message.type,
@@ -1116,7 +1940,75 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1116
1940
  return { ...header, data };
1117
1941
  }
1118
1942
  }
1943
+
1944
+ // src/shared/lib/mesh-client.ts
1945
+ async function createMeshClient(options) {
1946
+ const keyring = await resolveKeyring(options.keyring);
1947
+ const encryptionEnabled = options.encryptionEnabled ?? true;
1948
+ if (encryptionEnabled && !keyring.documentKeys.has(DEFAULT_MESH_KEY_ID)) {
1949
+ throw new Error(`createMeshClient: encryption is enabled but the keyring has no document key for "${DEFAULT_MESH_KEY_ID}". Bootstrap or apply a pairing token that carries the document key before connecting.`);
1950
+ }
1951
+ const knownPeerIds = [...keyring.knownPeers.keys()].filter((id) => id !== options.signaling.peerId);
1952
+ const webrtcAdapterOptions = {
1953
+ signaling: undefined,
1954
+ peerId: options.signaling.peerId,
1955
+ knownPeerIds,
1956
+ ...options.rtc?.iceServers !== undefined && { iceServers: options.rtc.iceServers },
1957
+ ...options.rtc?.dataChannelLabel !== undefined && {
1958
+ dataChannelLabel: options.rtc.dataChannelLabel
1959
+ },
1960
+ ...options.rtc?.RTCPeerConnection !== undefined && {
1961
+ RTCPeerConnection: options.rtc.RTCPeerConnection
1962
+ }
1963
+ };
1964
+ let webrtcAdapter;
1965
+ const signaling = new MeshSignalingClient({
1966
+ url: options.signaling.url,
1967
+ peerId: options.signaling.peerId,
1968
+ ...options.signaling.WebSocket !== undefined && { WebSocket: options.signaling.WebSocket },
1969
+ ...options.signaling.onError !== undefined && { onError: options.signaling.onError },
1970
+ onSignal: (fromPeerId, payload) => {
1971
+ webrtcAdapter?.handleSignal(fromPeerId, payload);
1972
+ }
1973
+ });
1974
+ webrtcAdapterOptions.signaling = signaling;
1975
+ webrtcAdapter = new MeshWebRTCAdapter(webrtcAdapterOptions);
1976
+ const networkAdapter = new MeshNetworkAdapter({
1977
+ base: webrtcAdapter,
1978
+ keyring,
1979
+ encryptionEnabled
1980
+ });
1981
+ const repo = new Repo({
1982
+ network: [networkAdapter],
1983
+ ...options.repoStorage !== undefined && { storage: options.repoStorage }
1984
+ });
1985
+ configureMeshState(repo);
1986
+ await signaling.connect();
1987
+ return {
1988
+ repo,
1989
+ keyring,
1990
+ signaling,
1991
+ networkAdapter,
1992
+ webrtcAdapter,
1993
+ close: async () => {
1994
+ signaling.close();
1995
+ webrtcAdapter?.disconnect();
1996
+ await repo.shutdown();
1997
+ }
1998
+ };
1999
+ }
2000
+ async function resolveKeyring(source) {
2001
+ if ("storage" in source) {
2002
+ const loaded = await source.storage.load();
2003
+ if (loaded === null) {
2004
+ throw new Error("createMeshClient: keyring storage returned null (no saved keyring). In a Node CLI, bootstrap with `bootstrapCliKeyring` from `@fairfox/polly/mesh/node`; in a browser, run your pairing flow first and save the keyring through the storage adapter before constructing the client.");
2005
+ }
2006
+ return loaded;
2007
+ }
2008
+ return source;
2009
+ }
1119
2010
  // src/shared/lib/pairing.ts
2011
+ init_encryption();
1120
2012
  var PAIRING_TOKEN_VERSION = 1;
1121
2013
  var PAIRING_TOKEN_MAGIC = new Uint8Array([80, 80, 84, 49]);
1122
2014
  var PAIRING_NONCE_BYTES = 16;
@@ -1454,15 +2346,19 @@ export {
1454
2346
  signingKeyPairFromSecret,
1455
2347
  sign,
1456
2348
  serialisePairingToken,
2349
+ serialiseKeyring,
1457
2350
  revokePeerLocally,
1458
2351
  resetMeshState,
1459
2352
  parsePairingToken,
2353
+ memoryKeyringStorage,
1460
2354
  isPairingTokenExpired,
2355
+ isBlobRef,
1461
2356
  generateSigningKeyPair,
1462
2357
  generateDocumentKey,
1463
2358
  encrypt,
1464
2359
  encodeRevocation,
1465
2360
  encodePairingToken,
2361
+ deserialiseKeyring,
1466
2362
  decryptOrThrow,
1467
2363
  decrypt,
1468
2364
  decodeRevocation,
@@ -1470,7 +2366,11 @@ export {
1470
2366
  createRevocation,
1471
2367
  createPairingTokenWithFreshIdentity,
1472
2368
  createPairingToken,
2369
+ createMeshClient,
2370
+ createBlobStore,
2371
+ createBlobRef,
1473
2372
  configureMeshState,
2373
+ computeBlobHash,
1474
2374
  applyRevocation,
1475
2375
  applyPairingToken,
1476
2376
  SigningError,
@@ -1486,6 +2386,8 @@ export {
1486
2386
  MeshWebRTCAdapter,
1487
2387
  MeshSignalingClient,
1488
2388
  MeshNetworkAdapter,
2389
+ MemoryBlobCache,
2390
+ IndexedDBBlobCache,
1489
2391
  EncryptionError,
1490
2392
  TAG_BYTES as ENCRYPTION_TAG_BYTES,
1491
2393
  NONCE_BYTES as ENCRYPTION_NONCE_BYTES,
@@ -1499,4 +2401,4 @@ export {
1499
2401
  $meshCounter
1500
2402
  };
1501
2403
 
1502
- //# debugId=D8E43AFC4B30DF7364756E2164756E21
2404
+ //# debugId=EBE34B8E4170302164756E2164756E21