@fairfox/polly 0.23.0 → 0.25.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 (116) hide show
  1. package/README.md +55 -1
  2. package/dist/cli/polly.js +21 -1
  3. package/dist/cli/polly.js.map +3 -3
  4. package/dist/src/actions/error.d.ts +26 -0
  5. package/dist/src/actions/event-delegation.d.ts +48 -0
  6. package/dist/src/actions/form.d.ts +72 -0
  7. package/dist/src/actions/index.d.ts +13 -0
  8. package/dist/src/actions/index.js +525 -0
  9. package/dist/src/actions/index.js.map +15 -0
  10. package/dist/src/actions/overlay.d.ts +26 -0
  11. package/dist/src/actions/registry.d.ts +25 -0
  12. package/dist/src/actions/store.d.ts +26 -0
  13. package/dist/src/actions/testing.d.ts +26 -0
  14. package/dist/src/background/index.js +26 -1
  15. package/dist/src/background/index.js.map +2 -2
  16. package/dist/src/background/message-router.js +26 -1
  17. package/dist/src/background/message-router.js.map +2 -2
  18. package/dist/src/client/index.js +27 -2
  19. package/dist/src/client/index.js.map +3 -3
  20. package/dist/src/elysia/index.js +27 -2
  21. package/dist/src/elysia/index.js.map +3 -3
  22. package/dist/src/elysia/peer-repo-plugin.d.ts +1 -1
  23. package/dist/src/index.js +26 -1
  24. package/dist/src/index.js.map +2 -2
  25. package/dist/src/mesh-node.d.ts +89 -0
  26. package/dist/src/mesh-node.js +619 -0
  27. package/dist/src/mesh-node.js.map +14 -0
  28. package/dist/src/mesh.d.ts +10 -0
  29. package/dist/src/mesh.js +951 -24
  30. package/dist/src/mesh.js.map +17 -9
  31. package/dist/src/peer.d.ts +1 -0
  32. package/dist/src/peer.js +130 -84
  33. package/dist/src/peer.js.map +11 -10
  34. package/dist/src/polly-ui/ActionForm.d.ts +21 -0
  35. package/dist/src/polly-ui/ActionInput.d.ts +41 -0
  36. package/dist/src/polly-ui/ConfirmDialog.d.ts +24 -0
  37. package/dist/src/polly-ui/Layout.d.ts +51 -0
  38. package/dist/src/polly-ui/Modal.d.ts +52 -0
  39. package/dist/src/polly-ui/OverlayRoot.d.ts +10 -0
  40. package/dist/src/polly-ui/TextInput.d.ts +31 -0
  41. package/dist/src/polly-ui/Toast.d.ts +19 -0
  42. package/dist/src/polly-ui/index.css +319 -0
  43. package/dist/src/polly-ui/index.d.ts +17 -0
  44. package/dist/src/polly-ui/index.js +953 -0
  45. package/dist/src/polly-ui/index.js.map +22 -0
  46. package/dist/src/polly-ui/internal/focus-trap.d.ts +10 -0
  47. package/dist/src/polly-ui/internal/input-base.d.ts +18 -0
  48. package/dist/src/polly-ui/internal/scroll-lock.d.ts +9 -0
  49. package/dist/src/polly-ui/styles.css +70 -0
  50. package/dist/src/polly-ui/theme.css +163 -0
  51. package/dist/src/shared/adapters/index.js +26 -1
  52. package/dist/src/shared/adapters/index.js.map +2 -2
  53. package/dist/src/shared/lib/blob-cache.d.ts +58 -0
  54. package/dist/src/shared/lib/blob-store-impl.d.ts +33 -0
  55. package/dist/src/shared/lib/blob-store.d.ts +87 -0
  56. package/dist/src/shared/lib/blob-transfer.d.ts +58 -0
  57. package/dist/src/shared/lib/context-helpers.js +26 -1
  58. package/dist/src/shared/lib/context-helpers.js.map +2 -2
  59. package/dist/src/shared/lib/crdt-specialised.d.ts +1 -1
  60. package/dist/src/shared/lib/crdt-state.d.ts +1 -1
  61. package/dist/src/shared/lib/errors.js +26 -1
  62. package/dist/src/shared/lib/errors.js.map +2 -2
  63. package/dist/src/shared/lib/keyring-storage.d.ts +57 -0
  64. package/dist/src/shared/lib/mesh-client.d.ts +91 -0
  65. package/dist/src/shared/lib/mesh-network-adapter.d.ts +1 -1
  66. package/dist/src/shared/lib/mesh-signaling-client.d.ts +6 -0
  67. package/dist/src/shared/lib/mesh-state.d.ts +1 -1
  68. package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +20 -1
  69. package/dist/src/shared/lib/message-bus.js +26 -1
  70. package/dist/src/shared/lib/message-bus.js.map +2 -2
  71. package/dist/src/shared/lib/peer-relay-adapter.d.ts +1 -1
  72. package/dist/src/shared/lib/peer-repo-server.d.ts +1 -1
  73. package/dist/src/shared/lib/peer-state.d.ts +1 -1
  74. package/dist/src/shared/lib/resource.js +26 -1
  75. package/dist/src/shared/lib/resource.js.map +2 -2
  76. package/dist/src/shared/lib/state.js +26 -1
  77. package/dist/src/shared/lib/state.js.map +2 -2
  78. package/dist/src/shared/lib/test-helpers.js +26 -1
  79. package/dist/src/shared/lib/test-helpers.js.map +2 -2
  80. package/dist/src/shared/lib/wasm-init.d.ts +17 -0
  81. package/dist/src/shared/state/app-state.js +26 -1
  82. package/dist/src/shared/state/app-state.js.map +2 -2
  83. package/dist/src/shared/types/messages.js +26 -1
  84. package/dist/src/shared/types/messages.js.map +2 -2
  85. package/dist/tools/quality/src/cli.js +647 -28
  86. package/dist/tools/quality/src/cli.js.map +11 -5
  87. package/dist/tools/quality/src/css/check-layout.d.ts +19 -0
  88. package/dist/tools/quality/src/css/check-quality.d.ts +24 -0
  89. package/dist/tools/quality/src/css/check-unused.d.ts +20 -0
  90. package/dist/tools/quality/src/css/check-vars.d.ts +22 -0
  91. package/dist/tools/quality/src/css/shared.d.ts +33 -0
  92. package/dist/tools/quality/src/index.d.ts +37 -0
  93. package/dist/tools/quality/src/index.js +735 -0
  94. package/dist/tools/quality/src/index.js.map +16 -0
  95. package/dist/tools/quality/src/logger.d.ts +26 -0
  96. package/dist/tools/quality/src/no-as-casting.d.ts +44 -0
  97. package/dist/tools/test/src/adapters/index.js +26 -1
  98. package/dist/tools/test/src/adapters/index.js.map +2 -2
  99. package/dist/tools/test/src/browser/index.js +26 -1
  100. package/dist/tools/test/src/browser/index.js.map +2 -2
  101. package/dist/tools/test/src/browser/run.js +238 -0
  102. package/dist/tools/test/src/browser/run.js.map +11 -0
  103. package/dist/tools/test/src/index.js +26 -1
  104. package/dist/tools/test/src/index.js.map +2 -2
  105. package/dist/tools/test/src/test-utils.js +26 -1
  106. package/dist/tools/test/src/test-utils.js.map +2 -2
  107. package/dist/tools/test/src/visual/compare.d.ts +23 -0
  108. package/dist/tools/test/src/visual/harness.d.ts +53 -0
  109. package/dist/tools/test/src/visual/index.d.ts +12 -0
  110. package/dist/tools/test/src/visual/index.js +13968 -0
  111. package/dist/tools/test/src/visual/index.js.map +41 -0
  112. package/dist/tools/verify/src/cli.js +3 -3
  113. package/dist/tools/verify/src/cli.js.map +1 -1
  114. package/dist/tools/verify/src/config.js +26 -1
  115. package/dist/tools/verify/src/config.js.map +2 -2
  116. package/package.json +42 -3
package/dist/src/mesh.js CHANGED
@@ -1,3 +1,5 @@
1
+ var __create = Object.create;
2
+ var __getProtoOf = Object.getPrototypeOf;
1
3
  var __defProp = Object.defineProperty;
2
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -5,6 +7,28 @@ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
7
  function __accessProp(key) {
6
8
  return this[key];
7
9
  }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ for (let key of __getOwnPropNames(mod))
23
+ if (!__hasOwnProp.call(to, key))
24
+ __defProp(to, key, {
25
+ get: __accessProp.bind(mod, key),
26
+ enumerable: true
27
+ });
28
+ if (canCache)
29
+ cache.set(mod, to);
30
+ return to;
31
+ };
8
32
  var __toCommonJS = (from) => {
9
33
  var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
34
  if (entry)
@@ -22,6 +46,7 @@ var __toCommonJS = (from) => {
22
46
  return entry;
23
47
  };
24
48
  var __moduleCache;
49
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
25
50
  var __returnValue = (v) => v;
26
51
  function __exportSetter(name, newValue) {
27
52
  this[name] = __returnValue.bind(null, newValue);
@@ -45,19 +70,22 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
45
70
  });
46
71
 
47
72
  // src/shared/lib/encryption.ts
73
+ var exports_encryption = {};
74
+ __export(exports_encryption, {
75
+ sealEnvelope: () => sealEnvelope,
76
+ openEnvelope: () => openEnvelope,
77
+ generateDocumentKey: () => generateDocumentKey,
78
+ encrypt: () => encrypt,
79
+ encodeEncryptedEnvelope: () => encodeEncryptedEnvelope,
80
+ decryptOrThrow: () => decryptOrThrow,
81
+ decrypt: () => decrypt,
82
+ decodeEncryptedEnvelope: () => decodeEncryptedEnvelope,
83
+ TAG_BYTES: () => TAG_BYTES,
84
+ NONCE_BYTES: () => NONCE_BYTES,
85
+ KEY_BYTES: () => KEY_BYTES,
86
+ EncryptionError: () => EncryptionError
87
+ });
48
88
  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
89
  function generateDocumentKey() {
62
90
  return nacl.randomBytes(KEY_BYTES);
63
91
  }
@@ -122,10 +150,786 @@ function decodeEncryptedEnvelope(bytes) {
122
150
  const sealed = bytes.slice(4 + idLen);
123
151
  return { documentId, sealed };
124
152
  }
153
+ var KEY_BYTES = 32, NONCE_BYTES = 24, TAG_BYTES = 16, EncryptionError;
154
+ var init_encryption = __esm(() => {
155
+ EncryptionError = class EncryptionError extends Error {
156
+ code;
157
+ constructor(message, code) {
158
+ super(message);
159
+ this.name = "EncryptionError";
160
+ this.code = code;
161
+ }
162
+ };
163
+ });
164
+
165
+ // src/shared/lib/wasm-init.ts
166
+ import wasmPath from "@automerge/automerge/automerge.wasm";
167
+ import { initializeWasm } from "@automerge/automerge-repo/slim";
168
+ var wasmUrl = new URL(wasmPath, import.meta.url).href;
169
+ await initializeWasm(wasmUrl);
170
+
171
+ // src/shared/lib/blob-cache.ts
172
+ class MemoryBlobCache {
173
+ store = new Map;
174
+ pinned = new Set;
175
+ urls = new Map;
176
+ async get(hash) {
177
+ const entry = this.store.get(hash);
178
+ if (!entry)
179
+ return;
180
+ entry.accessedAt = Date.now();
181
+ return entry.bytes;
182
+ }
183
+ async put(hash, bytes) {
184
+ this.store.set(hash, { bytes, accessedAt: Date.now() });
185
+ }
186
+ async has(hash) {
187
+ return this.store.has(hash);
188
+ }
189
+ async delete(hash) {
190
+ this.store.delete(hash);
191
+ this.pinned.delete(hash);
192
+ const url = this.urls.get(hash);
193
+ if (url) {
194
+ URL.revokeObjectURL(url);
195
+ this.urls.delete(hash);
196
+ }
197
+ }
198
+ async pin(hash) {
199
+ this.pinned.add(hash);
200
+ }
201
+ async unpin(hash) {
202
+ this.pinned.delete(hash);
203
+ }
204
+ async size() {
205
+ let total = 0;
206
+ for (const entry of this.store.values()) {
207
+ total += entry.bytes.byteLength;
208
+ }
209
+ return total;
210
+ }
211
+ async evict(maxBytes) {
212
+ let currentSize = await this.size();
213
+ if (currentSize <= maxBytes)
214
+ return 0;
215
+ const freed = currentSize;
216
+ const candidates = [];
217
+ for (const [hash, entry] of this.store) {
218
+ if (!this.pinned.has(hash)) {
219
+ candidates.push({ hash, accessedAt: entry.accessedAt, size: entry.bytes.byteLength });
220
+ }
221
+ }
222
+ candidates.sort((a, b) => a.accessedAt - b.accessedAt);
223
+ for (const c of candidates) {
224
+ if (currentSize <= maxBytes)
225
+ break;
226
+ await this.delete(c.hash);
227
+ currentSize -= c.size;
228
+ }
229
+ return freed - currentSize;
230
+ }
231
+ async url(hash) {
232
+ const cached = this.urls.get(hash);
233
+ if (cached)
234
+ return cached;
235
+ const entry = this.store.get(hash);
236
+ if (!entry)
237
+ return;
238
+ const buffer = new ArrayBuffer(entry.bytes.byteLength);
239
+ new Uint8Array(buffer).set(entry.bytes);
240
+ const objectUrl = URL.createObjectURL(new Blob([buffer]));
241
+ this.urls.set(hash, objectUrl);
242
+ return objectUrl;
243
+ }
244
+ dispose() {
245
+ for (const objectUrl of this.urls.values()) {
246
+ URL.revokeObjectURL(objectUrl);
247
+ }
248
+ this.urls.clear();
249
+ this.store.clear();
250
+ this.pinned.clear();
251
+ }
252
+ }
253
+
254
+ class IndexedDBBlobCache {
255
+ static DB_NAME = "polly-blobs";
256
+ static DB_VERSION = 1;
257
+ static STORE_NAME = "blobs";
258
+ dbPromise = null;
259
+ urls = new Map;
260
+ openDB() {
261
+ if (this.dbPromise)
262
+ return this.dbPromise;
263
+ this.dbPromise = new Promise((resolve, reject) => {
264
+ const request = indexedDB.open(IndexedDBBlobCache.DB_NAME, IndexedDBBlobCache.DB_VERSION);
265
+ request.onerror = () => reject(request.error);
266
+ request.onsuccess = () => resolve(request.result);
267
+ request.onupgradeneeded = (event) => {
268
+ const db = event.target.result;
269
+ if (!db.objectStoreNames.contains(IndexedDBBlobCache.STORE_NAME)) {
270
+ db.createObjectStore(IndexedDBBlobCache.STORE_NAME);
271
+ }
272
+ };
273
+ });
274
+ return this.dbPromise;
275
+ }
276
+ async getRecord(hash) {
277
+ const db = await this.openDB();
278
+ return new Promise((resolve, reject) => {
279
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
280
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
281
+ const request = store.get(hash);
282
+ request.onsuccess = () => resolve(request.result);
283
+ request.onerror = () => reject(request.error);
284
+ });
285
+ }
286
+ async putRecord(hash, record) {
287
+ const db = await this.openDB();
288
+ return new Promise((resolve, reject) => {
289
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readwrite");
290
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
291
+ store.put(record, hash);
292
+ tx.oncomplete = () => resolve();
293
+ tx.onerror = () => reject(tx.error);
294
+ });
295
+ }
296
+ async get(hash) {
297
+ const record = await this.getRecord(hash);
298
+ if (!record)
299
+ return;
300
+ this.putRecord(hash, { ...record, accessedAt: Date.now() });
301
+ return record.bytes;
302
+ }
303
+ async put(hash, bytes) {
304
+ const existing = await this.getRecord(hash);
305
+ await this.putRecord(hash, {
306
+ bytes,
307
+ size: bytes.byteLength,
308
+ accessedAt: Date.now(),
309
+ pinned: existing?.pinned ?? false
310
+ });
311
+ }
312
+ async has(hash) {
313
+ const db = await this.openDB();
314
+ return new Promise((resolve, reject) => {
315
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
316
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
317
+ const request = store.count(hash);
318
+ request.onsuccess = () => resolve(request.result > 0);
319
+ request.onerror = () => reject(request.error);
320
+ });
321
+ }
322
+ async delete(hash) {
323
+ const url = this.urls.get(hash);
324
+ if (url) {
325
+ URL.revokeObjectURL(url);
326
+ this.urls.delete(hash);
327
+ }
328
+ const db = await this.openDB();
329
+ return new Promise((resolve, reject) => {
330
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readwrite");
331
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
332
+ store.delete(hash);
333
+ tx.oncomplete = () => resolve();
334
+ tx.onerror = () => reject(tx.error);
335
+ });
336
+ }
337
+ async pin(hash) {
338
+ const record = await this.getRecord(hash);
339
+ if (!record)
340
+ return;
341
+ await this.putRecord(hash, { ...record, pinned: true });
342
+ }
343
+ async unpin(hash) {
344
+ const record = await this.getRecord(hash);
345
+ if (!record)
346
+ return;
347
+ await this.putRecord(hash, { ...record, pinned: false });
348
+ }
349
+ async size() {
350
+ const db = await this.openDB();
351
+ return new Promise((resolve, reject) => {
352
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
353
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
354
+ const request = store.openCursor();
355
+ let total = 0;
356
+ request.onsuccess = () => {
357
+ const cursor = request.result;
358
+ if (cursor) {
359
+ const value = cursor.value;
360
+ total += value.size;
361
+ cursor.continue();
362
+ } else {
363
+ resolve(total);
364
+ }
365
+ };
366
+ request.onerror = () => reject(request.error);
367
+ });
368
+ }
369
+ async evict(maxBytes) {
370
+ const db = await this.openDB();
371
+ const candidates = [];
372
+ let totalSize = 0;
373
+ await new Promise((resolve, reject) => {
374
+ const tx = db.transaction(IndexedDBBlobCache.STORE_NAME, "readonly");
375
+ const store = tx.objectStore(IndexedDBBlobCache.STORE_NAME);
376
+ const request = store.openCursor();
377
+ request.onsuccess = () => {
378
+ const cursor = request.result;
379
+ if (cursor) {
380
+ const value = cursor.value;
381
+ totalSize += value.size;
382
+ if (!value.pinned) {
383
+ candidates.push({
384
+ hash: cursor.key,
385
+ accessedAt: value.accessedAt,
386
+ size: value.size
387
+ });
388
+ }
389
+ cursor.continue();
390
+ } else {
391
+ resolve();
392
+ }
393
+ };
394
+ request.onerror = () => reject(request.error);
395
+ });
396
+ if (totalSize <= maxBytes)
397
+ return 0;
398
+ candidates.sort((a, b) => a.accessedAt - b.accessedAt);
399
+ let freed = 0;
400
+ for (const c of candidates) {
401
+ if (totalSize <= maxBytes)
402
+ break;
403
+ await this.delete(c.hash);
404
+ totalSize -= c.size;
405
+ freed += c.size;
406
+ }
407
+ return freed;
408
+ }
409
+ async url(hash) {
410
+ const cached = this.urls.get(hash);
411
+ if (cached)
412
+ return cached;
413
+ const bytes = await this.get(hash);
414
+ if (!bytes)
415
+ return;
416
+ const buffer = new ArrayBuffer(bytes.byteLength);
417
+ new Uint8Array(buffer).set(bytes);
418
+ const objectUrl = URL.createObjectURL(new Blob([buffer]));
419
+ this.urls.set(hash, objectUrl);
420
+ return objectUrl;
421
+ }
422
+ dispose() {
423
+ for (const objectUrl of this.urls.values()) {
424
+ URL.revokeObjectURL(objectUrl);
425
+ }
426
+ this.urls.clear();
427
+ }
428
+ }
429
+ // src/shared/lib/blob-ref.ts
430
+ function isBlobRef(value) {
431
+ if (typeof value !== "object" || value === null)
432
+ return false;
433
+ const v = value;
434
+ 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";
435
+ }
436
+ async function computeBlobHash(bytes) {
437
+ const buffer = new ArrayBuffer(bytes.byteLength);
438
+ const copy = new Uint8Array(buffer);
439
+ copy.set(bytes);
440
+ const digest = await crypto.subtle.digest("SHA-256", buffer);
441
+ const view = new Uint8Array(digest);
442
+ let hex = "";
443
+ for (const byte of view) {
444
+ hex += byte.toString(16).padStart(2, "0");
445
+ }
446
+ return hex;
447
+ }
448
+ async function createBlobRef({
449
+ bytes,
450
+ filename,
451
+ mimeType
452
+ }) {
453
+ const hash = await computeBlobHash(bytes);
454
+ return {
455
+ hash,
456
+ size: bytes.byteLength,
457
+ filename,
458
+ mimeType
459
+ };
460
+ }
461
+ // src/shared/lib/blob-transfer.ts
462
+ var BLOB_CHUNK_SIZE = 65536;
463
+ var BLOB_BUFFER_HIGH_WATER = 256 * 1024;
464
+ function chunkBlob(bytes, chunkSize = BLOB_CHUNK_SIZE) {
465
+ const chunks = [];
466
+ for (let offset = 0;offset < bytes.length; offset += chunkSize) {
467
+ chunks.push(bytes.subarray(offset, Math.min(offset + chunkSize, bytes.length)));
468
+ }
469
+ if (chunks.length === 0) {
470
+ chunks.push(new Uint8Array(0));
471
+ }
472
+ return chunks;
473
+ }
474
+ function reassembleChunks(chunks, total) {
475
+ let totalBytes = 0;
476
+ for (let i = 0;i < total; i++) {
477
+ const chunk = chunks.get(i);
478
+ if (!chunk) {
479
+ throw new Error(`reassembleChunks: missing chunk ${i} of ${total}`);
480
+ }
481
+ totalBytes += chunk.length;
482
+ }
483
+ const out = new Uint8Array(totalBytes);
484
+ let offset = 0;
485
+ for (let i = 0;i < total; i++) {
486
+ const chunk = chunks.get(i);
487
+ out.set(chunk, offset);
488
+ offset += chunk.length;
489
+ }
490
+ return out;
491
+ }
492
+ function missingChunkIndices(chunks, total) {
493
+ const missing = [];
494
+ for (let i = 0;i < total; i++) {
495
+ if (!chunks.has(i)) {
496
+ missing.push(i);
497
+ }
498
+ }
499
+ return missing;
500
+ }
501
+ function serialiseBlobMessage(header, data = new Uint8Array(0)) {
502
+ const headerBytes = new TextEncoder().encode(JSON.stringify(header));
503
+ const size = 4 + headerBytes.length + data.length;
504
+ const buffer = new ArrayBuffer(size);
505
+ const out = new Uint8Array(buffer);
506
+ const view = new DataView(buffer);
507
+ view.setUint32(0, headerBytes.length, false);
508
+ out.set(headerBytes, 4);
509
+ out.set(data, 4 + headerBytes.length);
510
+ return out;
511
+ }
512
+ function isBlobMessageType(bytes) {
513
+ if (bytes.length < 4)
514
+ return false;
515
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
516
+ const headerLen = view.getUint32(0, false);
517
+ if (bytes.length < 4 + headerLen)
518
+ return false;
519
+ const headerSlice = bytes.subarray(4, 4 + headerLen);
520
+ const needle = new TextEncoder().encode('"type":"blob-');
521
+ return findSubarray(headerSlice, needle) !== -1;
522
+ }
523
+ function findSubarray(haystack, needle) {
524
+ if (needle.length === 0)
525
+ return 0;
526
+ outer:
527
+ for (let i = 0;i <= haystack.length - needle.length; i++) {
528
+ for (let j = 0;j < needle.length; j++) {
529
+ if (haystack[i + j] !== needle[j])
530
+ continue outer;
531
+ }
532
+ return i;
533
+ }
534
+ return -1;
535
+ }
536
+
537
+ // src/shared/lib/blob-store-impl.ts
538
+ var DEFAULT_MAX_BLOB_SIZE = 100 * 1024 * 1024;
539
+ var DOWNLOAD_TIMEOUT_MS = 60000;
540
+ var RE_REQUEST_DELAY_MS = 5000;
541
+ function createBlobStore(adapter, options) {
542
+ const maxBlobSize = options?.maxBlobSize ?? DEFAULT_MAX_BLOB_SIZE;
543
+ const defaultKey = options?.encrypt?.key;
544
+ const cache = options?.cache ?? new MemoryBlobCache;
545
+ const keysByHash = new Map;
546
+ const peerBlobs = new Map;
547
+ const downloads = new Map;
548
+ const localHashes = new Set;
549
+ const urlCache = new Map;
550
+ let disposed = false;
551
+ adapter.onBlobMessage = (peerId, header, data) => {
552
+ if (disposed)
553
+ return;
554
+ const type = header["type"];
555
+ switch (type) {
556
+ case "blob-chunk":
557
+ handleChunk(peerId, header, data);
558
+ break;
559
+ case "blob-request":
560
+ handleRequest(peerId, header);
561
+ break;
562
+ case "blob-have":
563
+ handleHave(peerId, header);
564
+ break;
565
+ }
566
+ };
567
+ const peerCandidateHandler = (event) => {
568
+ if (disposed)
569
+ return;
570
+ const newPeerId = event.peerId;
571
+ for (const hash of localHashes) {
572
+ const msg = serialiseBlobMessage({ type: "blob-have", hash });
573
+ adapter.sendBlobMessage(newPeerId, msg);
574
+ }
575
+ };
576
+ adapter.on("peer-candidate", peerCandidateHandler);
577
+ async function encryptChunk(plaintext, key) {
578
+ const { encrypt: encrypt2 } = await Promise.resolve().then(() => (init_encryption(), exports_encryption));
579
+ return encrypt2(plaintext, key);
580
+ }
581
+ async function decryptChunk(sealed, key) {
582
+ const { decrypt: decrypt2 } = await Promise.resolve().then(() => (init_encryption(), exports_encryption));
583
+ return decrypt2(sealed, key);
584
+ }
585
+ async function handleChunk(peerId, header, data) {
586
+ const download = downloads.get(header.hash);
587
+ if (!download)
588
+ return;
589
+ download.total = header.total;
590
+ if (!download.peersAttempted.includes(peerId)) {
591
+ download.peersAttempted.push(peerId);
592
+ }
593
+ let chunkBytes;
594
+ if (download.key) {
595
+ const plaintext = await decryptChunk(data, download.key);
596
+ if (!plaintext) {
597
+ return;
598
+ }
599
+ chunkBytes = plaintext;
600
+ } else {
601
+ chunkBytes = data.slice();
602
+ }
603
+ download.chunks.set(header.index, chunkBytes);
604
+ reportChunkProgress(download);
605
+ resetReRequestTimer(download);
606
+ if (download.chunks.size >= header.total) {
607
+ finishDownload(header.hash, download);
608
+ } else {
609
+ scheduleReRequest(header.hash, download);
610
+ }
611
+ }
612
+ function reportChunkProgress(download) {
613
+ if (!download.onProgress || download.total <= 0)
614
+ return;
615
+ let loaded = 0;
616
+ for (const chunk of download.chunks.values()) {
617
+ loaded += chunk.length;
618
+ }
619
+ download.onProgress({ loaded, total: undefined, phase: "downloading" });
620
+ }
621
+ function resetReRequestTimer(download) {
622
+ if (download.reRequestId) {
623
+ clearTimeout(download.reRequestId);
624
+ download.reRequestId = undefined;
625
+ }
626
+ }
627
+ function finishDownload(hash, download) {
628
+ clearDownloadTimers(download);
629
+ try {
630
+ const assembled = reassembleChunks(download.chunks, download.total);
631
+ downloads.delete(hash);
632
+ download.resolve(assembled);
633
+ } catch (err) {
634
+ downloads.delete(hash);
635
+ download.reject(err instanceof Error ? err : new Error(String(err)));
636
+ }
637
+ }
638
+ async function handleRequest(peerId, header) {
639
+ const plaintext = await cache.get(header.hash);
640
+ if (!plaintext)
641
+ return;
642
+ const plaintextChunks = chunkBlob(plaintext);
643
+ const requested = header.missing ?? plaintextChunks.map((_, i) => i);
644
+ const chunkKey = keysByHash.get(header.hash);
645
+ for (const index of requested) {
646
+ await sendChunkAtIndex(peerId, header.hash, plaintextChunks, index, chunkKey);
647
+ }
648
+ }
649
+ async function sendChunkAtIndex(peerId, hash, plaintextChunks, index, chunkKey) {
650
+ if (index < 0 || index >= plaintextChunks.length)
651
+ return;
652
+ const plainChunk = plaintextChunks[index];
653
+ if (!plainChunk)
654
+ return;
655
+ const payload = chunkKey ? await encryptChunk(plainChunk, chunkKey) : plainChunk;
656
+ const chunkHeader = {
657
+ type: "blob-chunk",
658
+ hash,
659
+ index,
660
+ total: plaintextChunks.length
661
+ };
662
+ const msg = serialiseBlobMessage(chunkHeader, payload);
663
+ if (!adapter.sendBlobMessage(peerId, msg)) {
664
+ await waitForBufferDrain();
665
+ adapter.sendBlobMessage(peerId, msg);
666
+ }
667
+ }
668
+ function handleHave(peerId, header) {
669
+ let peers = peerBlobs.get(header.hash);
670
+ if (!peers) {
671
+ peers = new Set;
672
+ peerBlobs.set(header.hash, peers);
673
+ }
674
+ peers.add(peerId);
675
+ }
676
+ function announceHave(hash) {
677
+ const msg = serialiseBlobMessage({ type: "blob-have", hash });
678
+ for (const peerId of adapter.connectedPeerIds) {
679
+ adapter.sendBlobMessage(peerId, msg);
680
+ }
681
+ }
682
+ function clearDownloadTimers(download) {
683
+ if (download.timeoutId)
684
+ clearTimeout(download.timeoutId);
685
+ if (download.reRequestId)
686
+ clearTimeout(download.reRequestId);
687
+ }
688
+ function scheduleReRequest(hash, download) {
689
+ download.reRequestId = setTimeout(() => fireReRequest(hash, download), RE_REQUEST_DELAY_MS);
690
+ }
691
+ function fireReRequest(hash, download) {
692
+ if (!downloads.has(hash))
693
+ return;
694
+ const missing = missingChunkIndices(download.chunks, download.total);
695
+ if (missing.length === 0)
696
+ return;
697
+ const peers = peerBlobs.get(hash);
698
+ const pool = peers && peers.size > 0 ? Array.from(peers) : Array.from(adapter.connectedPeerIds);
699
+ if (pool.length === 0)
700
+ return;
701
+ const target = pool[download.peerRotationIndex % pool.length];
702
+ download.peerRotationIndex++;
703
+ if (!target)
704
+ return;
705
+ const reqHeader = { type: "blob-request", hash, missing };
706
+ const msg = serialiseBlobMessage(reqHeader);
707
+ adapter.sendBlobMessage(target, msg);
708
+ scheduleReRequest(hash, download);
709
+ }
710
+ function waitForBufferDrain() {
711
+ return new Promise((resolve) => setTimeout(resolve, 50));
712
+ }
713
+ const store = {
714
+ async put(ref, bytes, options2) {
715
+ if (disposed)
716
+ throw new Error("BlobStore is disposed");
717
+ options2?.signal?.throwIfAborted();
718
+ if (bytes.length > maxBlobSize) {
719
+ throw new Error(`Blob exceeds maximum size (${bytes.length} > ${maxBlobSize})`);
720
+ }
721
+ const hash = await computeBlobHash(bytes);
722
+ if (hash !== ref.hash) {
723
+ throw new Error(`Hash mismatch: expected ${ref.hash}, got ${hash}`);
724
+ }
725
+ options2?.signal?.throwIfAborted();
726
+ const key = options2?.key ?? defaultKey;
727
+ if (key) {
728
+ keysByHash.set(ref.hash, key);
729
+ }
730
+ options2?.onProgress?.({ loaded: bytes.length, total: bytes.length, phase: "uploading" });
731
+ await cache.put(ref.hash, bytes);
732
+ localHashes.add(ref.hash);
733
+ announceHave(ref.hash);
734
+ },
735
+ async get(hash, options2) {
736
+ if (disposed)
737
+ throw new Error("BlobStore is disposed");
738
+ options2?.signal?.throwIfAborted();
739
+ const cached = await cache.get(hash);
740
+ if (cached)
741
+ return cached;
742
+ const key = options2?.key ?? defaultKey;
743
+ const peers = peerBlobs.get(hash);
744
+ const candidates = peers && peers.size > 0 ? Array.from(peers) : Array.from(adapter.connectedPeerIds);
745
+ const targetPeer = candidates[0];
746
+ if (!targetPeer)
747
+ return;
748
+ const requestHeader = { type: "blob-request", hash };
749
+ const msg = serialiseBlobMessage(requestHeader);
750
+ adapter.sendBlobMessage(targetPeer, msg);
751
+ const plaintext = await new Promise((resolve, reject) => {
752
+ const download = {
753
+ total: 0,
754
+ chunks: new Map,
755
+ resolve,
756
+ reject,
757
+ onProgress: options2?.onProgress,
758
+ key,
759
+ peersAttempted: [targetPeer],
760
+ peerRotationIndex: 1
761
+ };
762
+ downloads.set(hash, download);
763
+ options2?.signal?.addEventListener("abort", () => {
764
+ if (downloads.has(hash)) {
765
+ clearDownloadTimers(download);
766
+ downloads.delete(hash);
767
+ reject(new Error("Blob download aborted"));
768
+ }
769
+ }, { once: true });
770
+ download.timeoutId = setTimeout(() => {
771
+ if (downloads.has(hash)) {
772
+ clearDownloadTimers(download);
773
+ downloads.delete(hash);
774
+ reject(new Error("Blob download timed out"));
775
+ }
776
+ }, DOWNLOAD_TIMEOUT_MS);
777
+ });
778
+ const actualHash = await computeBlobHash(plaintext);
779
+ if (actualHash !== hash) {
780
+ throw new Error(`Blob hash mismatch after download: expected ${hash}, got ${actualHash}`);
781
+ }
782
+ await cache.put(hash, plaintext);
783
+ if (key)
784
+ keysByHash.set(hash, key);
785
+ localHashes.add(hash);
786
+ return plaintext;
787
+ },
788
+ async url(hash) {
789
+ if (disposed)
790
+ return;
791
+ const cached = urlCache.get(hash);
792
+ if (cached)
793
+ return cached;
794
+ const bytes = await cache.get(hash);
795
+ if (!bytes)
796
+ return;
797
+ const buffer = new ArrayBuffer(bytes.byteLength);
798
+ new Uint8Array(buffer).set(bytes);
799
+ const objectUrl = URL.createObjectURL(new Blob([buffer]));
800
+ urlCache.set(hash, objectUrl);
801
+ return objectUrl;
802
+ },
803
+ async pin(hash) {
804
+ await cache.pin(hash);
805
+ },
806
+ async unpin(hash) {
807
+ await cache.unpin(hash);
808
+ },
809
+ async size() {
810
+ return cache.size();
811
+ },
812
+ async evict(maxBytes) {
813
+ return cache.evict(maxBytes);
814
+ },
815
+ dispose() {
816
+ disposed = true;
817
+ adapter.onBlobMessage = undefined;
818
+ adapter.off("peer-candidate", peerCandidateHandler);
819
+ for (const [hash, download] of downloads) {
820
+ clearDownloadTimers(download);
821
+ download.reject(new Error("BlobStore disposed"));
822
+ downloads.delete(hash);
823
+ }
824
+ for (const objectUrl of urlCache.values()) {
825
+ URL.revokeObjectURL(objectUrl);
826
+ }
827
+ urlCache.clear();
828
+ keysByHash.clear();
829
+ cache.dispose();
830
+ }
831
+ };
832
+ return store;
833
+ }
834
+
835
+ // src/mesh.ts
836
+ init_encryption();
837
+
838
+ // src/shared/lib/keyring-storage.ts
839
+ function memoryKeyringStorage() {
840
+ let stored = null;
841
+ return {
842
+ load: async () => stored,
843
+ save: async (keyring) => {
844
+ stored = keyring;
845
+ }
846
+ };
847
+ }
848
+ function serialiseKeyring(keyring) {
849
+ const payload = {
850
+ version: 1,
851
+ identity: {
852
+ publicKey: bytesToBase64(keyring.identity.publicKey),
853
+ secretKey: bytesToBase64(keyring.identity.secretKey)
854
+ },
855
+ knownPeers: mapToBase64Record(keyring.knownPeers),
856
+ documentKeys: mapToBase64Record(keyring.documentKeys),
857
+ revokedPeers: [...keyring.revokedPeers]
858
+ };
859
+ if (keyring.revocationAuthority && keyring.revocationAuthority.size > 0) {
860
+ payload.revocationAuthority = [...keyring.revocationAuthority];
861
+ }
862
+ return JSON.stringify(payload, null, 2);
863
+ }
864
+ function deserialiseKeyring(text) {
865
+ let raw;
866
+ try {
867
+ raw = JSON.parse(text);
868
+ } catch (err) {
869
+ throw new Error(`KeyringStorage: keyring payload is not valid JSON: ${err.message}`);
870
+ }
871
+ if (!raw || typeof raw !== "object") {
872
+ throw new Error("KeyringStorage: keyring payload is not an object");
873
+ }
874
+ const r = raw;
875
+ if (r.version !== 1) {
876
+ throw new Error(`KeyringStorage: unsupported keyring version: ${String(r.version)}`);
877
+ }
878
+ if (!r.identity || typeof r.identity !== "object") {
879
+ throw new Error("KeyringStorage: keyring payload is missing identity");
880
+ }
881
+ const identity = {
882
+ publicKey: base64ToBytes(r.identity.publicKey),
883
+ secretKey: base64ToBytes(r.identity.secretKey)
884
+ };
885
+ const keyring = {
886
+ identity,
887
+ knownPeers: base64RecordToMap(r.knownPeers ?? {}),
888
+ documentKeys: base64RecordToMap(r.documentKeys ?? {}),
889
+ revokedPeers: new Set(r.revokedPeers ?? [])
890
+ };
891
+ if (r.revocationAuthority && r.revocationAuthority.length > 0) {
892
+ keyring.revocationAuthority = new Set(r.revocationAuthority);
893
+ }
894
+ return keyring;
895
+ }
896
+ function mapToBase64Record(map) {
897
+ const out = {};
898
+ for (const [key, value] of map) {
899
+ out[key] = bytesToBase64(value);
900
+ }
901
+ return out;
902
+ }
903
+ function base64RecordToMap(record) {
904
+ const out = new Map;
905
+ for (const [key, value] of Object.entries(record)) {
906
+ out.set(key, base64ToBytes(value));
907
+ }
908
+ return out;
909
+ }
910
+ function bytesToBase64(bytes) {
911
+ let binary = "";
912
+ for (const byte of bytes) {
913
+ binary += String.fromCharCode(byte);
914
+ }
915
+ return btoa(binary);
916
+ }
917
+ function base64ToBytes(b64) {
918
+ const binary = atob(b64);
919
+ const bytes = new Uint8Array(binary.length);
920
+ for (let i = 0;i < binary.length; i++) {
921
+ bytes[i] = binary.charCodeAt(i);
922
+ }
923
+ return bytes;
924
+ }
925
+ // src/shared/lib/mesh-client.ts
926
+ import { Repo } from "@automerge/automerge-repo/slim";
927
+
125
928
  // src/shared/lib/mesh-network-adapter.ts
929
+ init_encryption();
126
930
  import {
127
931
  NetworkAdapter
128
- } from "@automerge/automerge-repo";
932
+ } from "@automerge/automerge-repo/slim";
129
933
 
130
934
  // src/shared/lib/signing.ts
131
935
  import nacl2 from "tweetnacl";
@@ -355,6 +1159,7 @@ function deserialiseMessage(bytes) {
355
1159
  const data = bytes.slice(4 + headerLen);
356
1160
  return { ...header, data };
357
1161
  }
1162
+
358
1163
  // src/shared/lib/mesh-signaling-client.ts
359
1164
  class MeshSignalingClient {
360
1165
  url;
@@ -365,6 +1170,7 @@ class MeshSignalingClient {
365
1170
  onClose;
366
1171
  socket;
367
1172
  joined = false;
1173
+ WebSocketCtor;
368
1174
  constructor(options) {
369
1175
  this.url = options.url;
370
1176
  this.peerId = options.peerId;
@@ -375,10 +1181,15 @@ class MeshSignalingClient {
375
1181
  this.onOpen = options.onOpen;
376
1182
  if (options.onClose !== undefined)
377
1183
  this.onClose = options.onClose;
1184
+ const WS = options.WebSocket ?? globalThis.WebSocket;
1185
+ if (typeof WS !== "function") {
1186
+ 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).");
1187
+ }
1188
+ this.WebSocketCtor = WS;
378
1189
  }
379
1190
  async connect() {
380
1191
  return new Promise((resolve, reject) => {
381
- const ws = new WebSocket(this.url);
1192
+ const ws = new this.WebSocketCtor(this.url);
382
1193
  this.socket = ws;
383
1194
  ws.addEventListener("open", () => {
384
1195
  ws.send(JSON.stringify({ type: "join", peerId: this.peerId }));
@@ -411,7 +1222,7 @@ class MeshSignalingClient {
411
1222
  });
412
1223
  }
413
1224
  sendSignal(targetPeerId, payload) {
414
- if (!this.socket || this.socket.readyState !== WebSocket.OPEN || !this.joined) {
1225
+ if (!this.socket || this.socket.readyState !== this.WebSocketCtor.OPEN || !this.joined) {
415
1226
  return false;
416
1227
  }
417
1228
  const msg = {
@@ -429,11 +1240,12 @@ class MeshSignalingClient {
429
1240
  this.joined = false;
430
1241
  }
431
1242
  get isConnected() {
432
- return this.joined && this.socket?.readyState === WebSocket.OPEN;
1243
+ return this.joined && this.socket?.readyState === this.WebSocketCtor.OPEN;
433
1244
  }
434
1245
  }
1246
+
435
1247
  // src/shared/lib/crdt-specialised.ts
436
- import { Counter, updateText } from "@automerge/automerge-repo";
1248
+ import { Counter, updateText } from "@automerge/automerge-repo/slim";
437
1249
  import { effect, signal } from "@preact/signals";
438
1250
 
439
1251
  // src/shared/lib/migrate-primitive.ts
@@ -886,10 +1698,11 @@ function $meshList(key, initialValue, options = {}) {
886
1698
  access: options.access
887
1699
  });
888
1700
  }
1701
+
889
1702
  // src/shared/lib/mesh-webrtc-adapter.ts
890
1703
  import {
891
1704
  NetworkAdapter as NetworkAdapter2
892
- } from "@automerge/automerge-repo";
1705
+ } from "@automerge/automerge-repo/slim";
893
1706
  var DEFAULT_ICE_SERVERS = [
894
1707
  { urls: "stun:stun.l.google.com:19302" },
895
1708
  { urls: "stun:stun1.l.google.com:19302" }
@@ -900,15 +1713,22 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
900
1713
  iceServers;
901
1714
  dataChannelLabel;
902
1715
  knownPeerIds;
1716
+ RTCPeerConnectionCtor;
903
1717
  slots = new Map;
904
1718
  ready = false;
905
1719
  readyResolver;
1720
+ onBlobMessage;
906
1721
  constructor(options) {
907
1722
  super();
908
1723
  this.signaling = options.signaling;
909
1724
  this.iceServers = options.iceServers ?? DEFAULT_ICE_SERVERS;
910
1725
  this.dataChannelLabel = options.dataChannelLabel ?? "polly-mesh";
911
1726
  this.knownPeerIds = options.knownPeerIds ?? [];
1727
+ const PC = options.RTCPeerConnection ?? globalThis.RTCPeerConnection;
1728
+ if (typeof PC !== "function") {
1729
+ 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.");
1730
+ }
1731
+ this.RTCPeerConnectionCtor = PC;
912
1732
  }
913
1733
  isReady() {
914
1734
  return this.ready;
@@ -974,7 +1794,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
974
1794
  }
975
1795
  }
976
1796
  createInitiatingSlot(targetId) {
977
- const connection = new RTCPeerConnection({ iceServers: this.iceServers });
1797
+ const connection = new this.RTCPeerConnectionCtor({ iceServers: this.iceServers });
978
1798
  const channel = connection.createDataChannel(this.dataChannelLabel, { ordered: true });
979
1799
  const slot = { connection, channel, pendingSends: [] };
980
1800
  this.slots.set(targetId, slot);
@@ -999,7 +1819,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
999
1819
  existing.connection.close();
1000
1820
  this.slots.delete(fromPeerId);
1001
1821
  }
1002
- const connection = new RTCPeerConnection({ iceServers: this.iceServers });
1822
+ const connection = new this.RTCPeerConnectionCtor({ iceServers: this.iceServers });
1003
1823
  const slot = { connection, channel: undefined, pendingSends: [] };
1004
1824
  this.slots.set(fromPeerId, slot);
1005
1825
  this.wireConnection(fromPeerId, connection);
@@ -1064,9 +1884,9 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1064
1884
  channel.onmessage = (event) => {
1065
1885
  const data = event.data;
1066
1886
  if (data instanceof ArrayBuffer) {
1067
- this.dispatchMessage(new Uint8Array(data));
1887
+ this.dispatchMessage(peerId, new Uint8Array(data));
1068
1888
  } else if (data instanceof Uint8Array) {
1069
- this.dispatchMessage(data);
1889
+ this.dispatchMessage(peerId, data);
1070
1890
  }
1071
1891
  };
1072
1892
  channel.onclose = () => {
@@ -1076,12 +1896,41 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1076
1896
  }
1077
1897
  };
1078
1898
  }
1079
- dispatchMessage(bytes) {
1899
+ dispatchMessage(fromPeerId, bytes) {
1080
1900
  try {
1901
+ if (this.onBlobMessage && isBlobMessageType(bytes)) {
1902
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1903
+ const headerLen = view.getUint32(0, false);
1904
+ const header = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + headerLen)));
1905
+ const data = bytes.subarray(4 + headerLen);
1906
+ this.onBlobMessage(fromPeerId, header, data);
1907
+ return;
1908
+ }
1081
1909
  const message = this.deserialiseMessage(bytes);
1082
1910
  this.emit("message", message);
1083
1911
  } catch {}
1084
1912
  }
1913
+ get connectedPeerIds() {
1914
+ const ids = [];
1915
+ for (const [peerId, slot] of this.slots) {
1916
+ if (slot.channel && slot.channel.readyState === "open") {
1917
+ ids.push(peerId);
1918
+ }
1919
+ }
1920
+ return ids;
1921
+ }
1922
+ sendBlobMessage(peerId, bytes) {
1923
+ const slot = this.slots.get(peerId);
1924
+ if (!slot?.channel || slot.channel.readyState !== "open")
1925
+ return false;
1926
+ return this.trySendOnChannel(slot.channel, bytes);
1927
+ }
1928
+ trySendOnChannel(channel, bytes) {
1929
+ if (channel.bufferedAmount > 256 * 1024)
1930
+ return false;
1931
+ channel.send(bytes);
1932
+ return true;
1933
+ }
1085
1934
  serialiseMessage(message) {
1086
1935
  const headerObj = {
1087
1936
  type: message.type,
@@ -1116,7 +1965,75 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
1116
1965
  return { ...header, data };
1117
1966
  }
1118
1967
  }
1968
+
1969
+ // src/shared/lib/mesh-client.ts
1970
+ async function createMeshClient(options) {
1971
+ const keyring = await resolveKeyring(options.keyring);
1972
+ const encryptionEnabled = options.encryptionEnabled ?? true;
1973
+ if (encryptionEnabled && !keyring.documentKeys.has(DEFAULT_MESH_KEY_ID)) {
1974
+ 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.`);
1975
+ }
1976
+ const knownPeerIds = [...keyring.knownPeers.keys()].filter((id) => id !== options.signaling.peerId);
1977
+ const webrtcAdapterOptions = {
1978
+ signaling: undefined,
1979
+ peerId: options.signaling.peerId,
1980
+ knownPeerIds,
1981
+ ...options.rtc?.iceServers !== undefined && { iceServers: options.rtc.iceServers },
1982
+ ...options.rtc?.dataChannelLabel !== undefined && {
1983
+ dataChannelLabel: options.rtc.dataChannelLabel
1984
+ },
1985
+ ...options.rtc?.RTCPeerConnection !== undefined && {
1986
+ RTCPeerConnection: options.rtc.RTCPeerConnection
1987
+ }
1988
+ };
1989
+ let webrtcAdapter;
1990
+ const signaling = new MeshSignalingClient({
1991
+ url: options.signaling.url,
1992
+ peerId: options.signaling.peerId,
1993
+ ...options.signaling.WebSocket !== undefined && { WebSocket: options.signaling.WebSocket },
1994
+ ...options.signaling.onError !== undefined && { onError: options.signaling.onError },
1995
+ onSignal: (fromPeerId, payload) => {
1996
+ webrtcAdapter?.handleSignal(fromPeerId, payload);
1997
+ }
1998
+ });
1999
+ webrtcAdapterOptions.signaling = signaling;
2000
+ webrtcAdapter = new MeshWebRTCAdapter(webrtcAdapterOptions);
2001
+ const networkAdapter = new MeshNetworkAdapter({
2002
+ base: webrtcAdapter,
2003
+ keyring,
2004
+ encryptionEnabled
2005
+ });
2006
+ const repo = new Repo({
2007
+ network: [networkAdapter],
2008
+ ...options.repoStorage !== undefined && { storage: options.repoStorage }
2009
+ });
2010
+ configureMeshState(repo);
2011
+ await signaling.connect();
2012
+ return {
2013
+ repo,
2014
+ keyring,
2015
+ signaling,
2016
+ networkAdapter,
2017
+ webrtcAdapter,
2018
+ close: async () => {
2019
+ signaling.close();
2020
+ webrtcAdapter?.disconnect();
2021
+ await repo.shutdown();
2022
+ }
2023
+ };
2024
+ }
2025
+ async function resolveKeyring(source) {
2026
+ if ("storage" in source) {
2027
+ const loaded = await source.storage.load();
2028
+ if (loaded === null) {
2029
+ 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.");
2030
+ }
2031
+ return loaded;
2032
+ }
2033
+ return source;
2034
+ }
1119
2035
  // src/shared/lib/pairing.ts
2036
+ init_encryption();
1120
2037
  var PAIRING_TOKEN_VERSION = 1;
1121
2038
  var PAIRING_TOKEN_MAGIC = new Uint8Array([80, 80, 84, 49]);
1122
2039
  var PAIRING_NONCE_BYTES = 16;
@@ -1454,15 +2371,19 @@ export {
1454
2371
  signingKeyPairFromSecret,
1455
2372
  sign,
1456
2373
  serialisePairingToken,
2374
+ serialiseKeyring,
1457
2375
  revokePeerLocally,
1458
2376
  resetMeshState,
1459
2377
  parsePairingToken,
2378
+ memoryKeyringStorage,
1460
2379
  isPairingTokenExpired,
2380
+ isBlobRef,
1461
2381
  generateSigningKeyPair,
1462
2382
  generateDocumentKey,
1463
2383
  encrypt,
1464
2384
  encodeRevocation,
1465
2385
  encodePairingToken,
2386
+ deserialiseKeyring,
1466
2387
  decryptOrThrow,
1467
2388
  decrypt,
1468
2389
  decodeRevocation,
@@ -1470,7 +2391,11 @@ export {
1470
2391
  createRevocation,
1471
2392
  createPairingTokenWithFreshIdentity,
1472
2393
  createPairingToken,
2394
+ createMeshClient,
2395
+ createBlobStore,
2396
+ createBlobRef,
1473
2397
  configureMeshState,
2398
+ computeBlobHash,
1474
2399
  applyRevocation,
1475
2400
  applyPairingToken,
1476
2401
  SigningError,
@@ -1486,6 +2411,8 @@ export {
1486
2411
  MeshWebRTCAdapter,
1487
2412
  MeshSignalingClient,
1488
2413
  MeshNetworkAdapter,
2414
+ MemoryBlobCache,
2415
+ IndexedDBBlobCache,
1489
2416
  EncryptionError,
1490
2417
  TAG_BYTES as ENCRYPTION_TAG_BYTES,
1491
2418
  NONCE_BYTES as ENCRYPTION_NONCE_BYTES,
@@ -1499,4 +2426,4 @@ export {
1499
2426
  $meshCounter
1500
2427
  };
1501
2428
 
1502
- //# debugId=D8E43AFC4B30DF7364756E2164756E21
2429
+ //# debugId=6CA75FA83D35A2A964756E2164756E21