@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.
- package/README.md +55 -1
- package/dist/cli/polly.js +21 -1
- package/dist/cli/polly.js.map +3 -3
- package/dist/src/actions/error.d.ts +26 -0
- package/dist/src/actions/event-delegation.d.ts +48 -0
- package/dist/src/actions/form.d.ts +72 -0
- package/dist/src/actions/index.d.ts +13 -0
- package/dist/src/actions/index.js +525 -0
- package/dist/src/actions/index.js.map +15 -0
- package/dist/src/actions/overlay.d.ts +26 -0
- package/dist/src/actions/registry.d.ts +25 -0
- package/dist/src/actions/store.d.ts +26 -0
- package/dist/src/actions/testing.d.ts +26 -0
- package/dist/src/background/index.js +26 -1
- package/dist/src/background/index.js.map +2 -2
- package/dist/src/background/message-router.js +26 -1
- package/dist/src/background/message-router.js.map +2 -2
- package/dist/src/client/index.js +27 -2
- package/dist/src/client/index.js.map +3 -3
- package/dist/src/elysia/index.js +27 -2
- package/dist/src/elysia/index.js.map +3 -3
- package/dist/src/elysia/peer-repo-plugin.d.ts +1 -1
- package/dist/src/index.js +26 -1
- package/dist/src/index.js.map +2 -2
- package/dist/src/mesh-node.d.ts +89 -0
- package/dist/src/mesh-node.js +619 -0
- package/dist/src/mesh-node.js.map +14 -0
- package/dist/src/mesh.d.ts +10 -0
- package/dist/src/mesh.js +951 -24
- package/dist/src/mesh.js.map +17 -9
- package/dist/src/peer.d.ts +1 -0
- package/dist/src/peer.js +130 -84
- package/dist/src/peer.js.map +11 -10
- package/dist/src/polly-ui/ActionForm.d.ts +21 -0
- package/dist/src/polly-ui/ActionInput.d.ts +41 -0
- package/dist/src/polly-ui/ConfirmDialog.d.ts +24 -0
- package/dist/src/polly-ui/Layout.d.ts +51 -0
- package/dist/src/polly-ui/Modal.d.ts +52 -0
- package/dist/src/polly-ui/OverlayRoot.d.ts +10 -0
- package/dist/src/polly-ui/TextInput.d.ts +31 -0
- package/dist/src/polly-ui/Toast.d.ts +19 -0
- package/dist/src/polly-ui/index.css +319 -0
- package/dist/src/polly-ui/index.d.ts +17 -0
- package/dist/src/polly-ui/index.js +953 -0
- package/dist/src/polly-ui/index.js.map +22 -0
- package/dist/src/polly-ui/internal/focus-trap.d.ts +10 -0
- package/dist/src/polly-ui/internal/input-base.d.ts +18 -0
- package/dist/src/polly-ui/internal/scroll-lock.d.ts +9 -0
- package/dist/src/polly-ui/styles.css +70 -0
- package/dist/src/polly-ui/theme.css +163 -0
- package/dist/src/shared/adapters/index.js +26 -1
- package/dist/src/shared/adapters/index.js.map +2 -2
- 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/context-helpers.js +26 -1
- package/dist/src/shared/lib/context-helpers.js.map +2 -2
- 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/errors.js +26 -1
- package/dist/src/shared/lib/errors.js.map +2 -2
- 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/message-bus.js +26 -1
- package/dist/src/shared/lib/message-bus.js.map +2 -2
- 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/resource.js +26 -1
- package/dist/src/shared/lib/resource.js.map +2 -2
- package/dist/src/shared/lib/state.js +26 -1
- package/dist/src/shared/lib/state.js.map +2 -2
- package/dist/src/shared/lib/test-helpers.js +26 -1
- package/dist/src/shared/lib/test-helpers.js.map +2 -2
- package/dist/src/shared/lib/wasm-init.d.ts +17 -0
- package/dist/src/shared/state/app-state.js +26 -1
- package/dist/src/shared/state/app-state.js.map +2 -2
- package/dist/src/shared/types/messages.js +26 -1
- package/dist/src/shared/types/messages.js.map +2 -2
- package/dist/tools/quality/src/cli.js +647 -28
- package/dist/tools/quality/src/cli.js.map +11 -5
- package/dist/tools/quality/src/css/check-layout.d.ts +19 -0
- package/dist/tools/quality/src/css/check-quality.d.ts +24 -0
- package/dist/tools/quality/src/css/check-unused.d.ts +20 -0
- package/dist/tools/quality/src/css/check-vars.d.ts +22 -0
- package/dist/tools/quality/src/css/shared.d.ts +33 -0
- package/dist/tools/quality/src/index.d.ts +37 -0
- package/dist/tools/quality/src/index.js +735 -0
- package/dist/tools/quality/src/index.js.map +16 -0
- package/dist/tools/quality/src/logger.d.ts +26 -0
- package/dist/tools/quality/src/no-as-casting.d.ts +44 -0
- package/dist/tools/test/src/adapters/index.js +26 -1
- package/dist/tools/test/src/adapters/index.js.map +2 -2
- package/dist/tools/test/src/browser/index.js +26 -1
- package/dist/tools/test/src/browser/index.js.map +2 -2
- package/dist/tools/test/src/browser/run.js +238 -0
- package/dist/tools/test/src/browser/run.js.map +11 -0
- package/dist/tools/test/src/index.js +26 -1
- package/dist/tools/test/src/index.js.map +2 -2
- package/dist/tools/test/src/test-utils.js +26 -1
- package/dist/tools/test/src/test-utils.js.map +2 -2
- package/dist/tools/test/src/visual/compare.d.ts +23 -0
- package/dist/tools/test/src/visual/harness.d.ts +53 -0
- package/dist/tools/test/src/visual/index.d.ts +12 -0
- package/dist/tools/test/src/visual/index.js +13968 -0
- package/dist/tools/test/src/visual/index.js.map +41 -0
- package/dist/tools/verify/src/cli.js +3 -3
- package/dist/tools/verify/src/cli.js.map +1 -1
- package/dist/tools/verify/src/config.js +26 -1
- package/dist/tools/verify/src/config.js.map +2 -2
- 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
|
|
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 !==
|
|
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 ===
|
|
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
|
|
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
|
|
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=
|
|
2429
|
+
//# debugId=6CA75FA83D35A2A964756E2164756E21
|