@fairfox/polly 0.23.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.
- package/README.md +55 -1
- package/dist/src/elysia/index.js +2 -2
- package/dist/src/elysia/index.js.map +2 -2
- package/dist/src/elysia/peer-repo-plugin.d.ts +1 -1
- package/dist/src/mesh-node.d.ts +89 -0
- package/dist/src/mesh-node.js +594 -0
- package/dist/src/mesh-node.js.map +14 -0
- package/dist/src/mesh.d.ts +10 -0
- package/dist/src/mesh.js +926 -24
- package/dist/src/mesh.js.map +17 -9
- package/dist/src/peer.d.ts +1 -0
- package/dist/src/peer.js +105 -84
- package/dist/src/peer.js.map +11 -10
- package/dist/src/shared/lib/blob-cache.d.ts +58 -0
- package/dist/src/shared/lib/blob-store-impl.d.ts +33 -0
- package/dist/src/shared/lib/blob-store.d.ts +87 -0
- package/dist/src/shared/lib/blob-transfer.d.ts +58 -0
- package/dist/src/shared/lib/crdt-specialised.d.ts +1 -1
- package/dist/src/shared/lib/crdt-state.d.ts +1 -1
- package/dist/src/shared/lib/keyring-storage.d.ts +57 -0
- package/dist/src/shared/lib/mesh-client.d.ts +91 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +1 -1
- package/dist/src/shared/lib/mesh-signaling-client.d.ts +6 -0
- package/dist/src/shared/lib/mesh-state.d.ts +1 -1
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +20 -1
- package/dist/src/shared/lib/peer-relay-adapter.d.ts +1 -1
- package/dist/src/shared/lib/peer-repo-server.d.ts +1 -1
- package/dist/src/shared/lib/peer-state.d.ts +1 -1
- package/dist/src/shared/lib/wasm-init.d.ts +17 -0
- package/dist/tools/quality/src/cli.js +8 -1
- package/dist/tools/quality/src/cli.js.map +3 -3
- package/dist/tools/quality/src/index.d.ts +25 -0
- package/dist/tools/quality/src/index.js +196 -0
- package/dist/tools/quality/src/index.js.map +10 -0
- package/dist/tools/quality/src/no-as-casting.d.ts +44 -0
- 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
|
|
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 !==
|
|
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 ===
|
|
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
|
|
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
|
|
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=
|
|
2404
|
+
//# debugId=EBE34B8E4170302164756E2164756E21
|