@fairfox/polly 0.49.0 → 0.51.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/dist/src/mesh.js +160 -18
- package/dist/src/mesh.js.map +7 -6
- package/dist/src/peer.js +16 -10
- package/dist/src/peer.js.map +4 -4
- package/dist/src/shared/lib/mesh-client.d.ts +31 -9
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +22 -5
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +9 -0
- package/dist/src/shared/lib/sync-fragment.d.ts +60 -0
- package/dist/tools/quality/src/cli.js +3 -3
- package/dist/tools/quality/src/cli.js.map +4 -4
- package/dist/tools/quality/src/index.js +3 -3
- package/dist/tools/quality/src/index.js.map +4 -4
- package/package.json +1 -1
package/dist/src/mesh.js
CHANGED
|
@@ -1019,12 +1019,15 @@ var DEFAULT_MESH_KEY_ID = "polly-mesh-default";
|
|
|
1019
1019
|
|
|
1020
1020
|
class MeshNetworkAdapter extends NetworkAdapter {
|
|
1021
1021
|
base;
|
|
1022
|
-
|
|
1022
|
+
keyringSource;
|
|
1023
1023
|
encryptionEnabled;
|
|
1024
|
+
get keyring() {
|
|
1025
|
+
return this.keyringSource();
|
|
1026
|
+
}
|
|
1024
1027
|
constructor(options) {
|
|
1025
1028
|
super();
|
|
1026
1029
|
this.base = options.base;
|
|
1027
|
-
this.
|
|
1030
|
+
this.keyringSource = options.keyringSource;
|
|
1028
1031
|
this.encryptionEnabled = options.encryptionEnabled ?? true;
|
|
1029
1032
|
this.base.on("close", () => this.emit("close"));
|
|
1030
1033
|
this.base.on("peer-candidate", (payload) => this.emit("peer-candidate", payload));
|
|
@@ -1057,10 +1060,11 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1057
1060
|
this.base.send(wrapped);
|
|
1058
1061
|
}
|
|
1059
1062
|
wrap(message) {
|
|
1063
|
+
const keyring = this.keyringSource();
|
|
1060
1064
|
const serialised = serialiseMessage(message);
|
|
1061
1065
|
let payloadToSign;
|
|
1062
1066
|
if (this.encryptionEnabled) {
|
|
1063
|
-
const docKey =
|
|
1067
|
+
const docKey = keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
|
|
1064
1068
|
if (!docKey) {
|
|
1065
1069
|
throw new Error(`MeshNetworkAdapter: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}". Provision the key in the keyring before sending.`);
|
|
1066
1070
|
}
|
|
@@ -1069,7 +1073,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1069
1073
|
} else {
|
|
1070
1074
|
payloadToSign = serialised;
|
|
1071
1075
|
}
|
|
1072
|
-
const signed = signEnvelope(payloadToSign, message.senderId,
|
|
1076
|
+
const signed = signEnvelope(payloadToSign, message.senderId, keyring.identity.secretKey);
|
|
1073
1077
|
const signedBytes = encodeSignedEnvelope(signed);
|
|
1074
1078
|
return {
|
|
1075
1079
|
type: message.type,
|
|
@@ -1087,10 +1091,11 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1087
1091
|
} catch {
|
|
1088
1092
|
return;
|
|
1089
1093
|
}
|
|
1090
|
-
|
|
1094
|
+
const keyring = this.keyringSource();
|
|
1095
|
+
if (keyring.revokedPeers.has(signed.senderId)) {
|
|
1091
1096
|
return;
|
|
1092
1097
|
}
|
|
1093
|
-
const senderKey =
|
|
1098
|
+
const senderKey = keyring.knownPeers.get(signed.senderId);
|
|
1094
1099
|
if (!senderKey) {
|
|
1095
1100
|
return;
|
|
1096
1101
|
}
|
|
@@ -1109,7 +1114,7 @@ class MeshNetworkAdapter extends NetworkAdapter {
|
|
|
1109
1114
|
} catch {
|
|
1110
1115
|
return;
|
|
1111
1116
|
}
|
|
1112
|
-
const docKey =
|
|
1117
|
+
const docKey = keyring.documentKeys.get(encrypted.documentId);
|
|
1113
1118
|
if (!docKey) {
|
|
1114
1119
|
return;
|
|
1115
1120
|
}
|
|
@@ -1842,6 +1847,92 @@ function $meshList(key, initialValue, options = {}) {
|
|
|
1842
1847
|
import {
|
|
1843
1848
|
NetworkAdapter as NetworkAdapter2
|
|
1844
1849
|
} from "@automerge/automerge-repo/slim";
|
|
1850
|
+
|
|
1851
|
+
// src/shared/lib/sync-fragment.ts
|
|
1852
|
+
var SYNC_FRAGMENT_THRESHOLD = 64 * 1024;
|
|
1853
|
+
var SYNC_FRAGMENT_CHUNK_SIZE = 64 * 1024;
|
|
1854
|
+
function serialiseSyncFragment(header, data) {
|
|
1855
|
+
const headerBytes = new TextEncoder().encode(JSON.stringify(header));
|
|
1856
|
+
const size = 4 + headerBytes.length + data.length;
|
|
1857
|
+
const buffer = new ArrayBuffer(size);
|
|
1858
|
+
const out = new Uint8Array(buffer);
|
|
1859
|
+
const view = new DataView(buffer);
|
|
1860
|
+
view.setUint32(0, headerBytes.length, false);
|
|
1861
|
+
out.set(headerBytes, 4);
|
|
1862
|
+
out.set(data, 4 + headerBytes.length);
|
|
1863
|
+
return out;
|
|
1864
|
+
}
|
|
1865
|
+
function parseSyncFragment(bytes) {
|
|
1866
|
+
if (bytes.length < 4)
|
|
1867
|
+
return;
|
|
1868
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1869
|
+
const headerLen = view.getUint32(0, false);
|
|
1870
|
+
if (bytes.length < 4 + headerLen)
|
|
1871
|
+
return;
|
|
1872
|
+
try {
|
|
1873
|
+
const header = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + headerLen)));
|
|
1874
|
+
if (header.type !== "sync-fragment")
|
|
1875
|
+
return;
|
|
1876
|
+
const data = bytes.subarray(4 + headerLen);
|
|
1877
|
+
return { header, data };
|
|
1878
|
+
} catch {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function isSyncFragmentType(bytes) {
|
|
1883
|
+
if (bytes.length < 4)
|
|
1884
|
+
return false;
|
|
1885
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1886
|
+
const headerLen = view.getUint32(0, false);
|
|
1887
|
+
if (bytes.length < 4 + headerLen)
|
|
1888
|
+
return false;
|
|
1889
|
+
const headerSlice = bytes.subarray(4, 4 + headerLen);
|
|
1890
|
+
const needle = new TextEncoder().encode('"type":"sync-fragment"');
|
|
1891
|
+
return findSubarray2(headerSlice, needle) !== -1;
|
|
1892
|
+
}
|
|
1893
|
+
function chunkSyncMessage(bytes, id, chunkSize = SYNC_FRAGMENT_CHUNK_SIZE) {
|
|
1894
|
+
const total = Math.max(1, Math.ceil(bytes.length / chunkSize));
|
|
1895
|
+
const fragments = [];
|
|
1896
|
+
for (let index = 0;index < total; index++) {
|
|
1897
|
+
const start = index * chunkSize;
|
|
1898
|
+
const end = Math.min(start + chunkSize, bytes.length);
|
|
1899
|
+
fragments.push(serialiseSyncFragment({ type: "sync-fragment", id, index, total }, bytes.subarray(start, end)));
|
|
1900
|
+
}
|
|
1901
|
+
return fragments;
|
|
1902
|
+
}
|
|
1903
|
+
function reassembleSyncFragments(chunks, total) {
|
|
1904
|
+
let totalBytes = 0;
|
|
1905
|
+
for (let i = 0;i < total; i++) {
|
|
1906
|
+
const chunk = chunks.get(i);
|
|
1907
|
+
if (!chunk) {
|
|
1908
|
+
throw new Error(`reassembleSyncFragments: missing fragment ${i} of ${total}`);
|
|
1909
|
+
}
|
|
1910
|
+
totalBytes += chunk.length;
|
|
1911
|
+
}
|
|
1912
|
+
const out = new Uint8Array(totalBytes);
|
|
1913
|
+
let offset = 0;
|
|
1914
|
+
for (let i = 0;i < total; i++) {
|
|
1915
|
+
const chunk = chunks.get(i);
|
|
1916
|
+
out.set(chunk, offset);
|
|
1917
|
+
offset += chunk.length;
|
|
1918
|
+
}
|
|
1919
|
+
return out;
|
|
1920
|
+
}
|
|
1921
|
+
function findSubarray2(haystack, needle) {
|
|
1922
|
+
if (needle.length === 0)
|
|
1923
|
+
return 0;
|
|
1924
|
+
outer:
|
|
1925
|
+
for (let i = 0;i <= haystack.length - needle.length; i++) {
|
|
1926
|
+
for (let j = 0;j < needle.length; j++) {
|
|
1927
|
+
if (haystack[i + j] !== needle[j])
|
|
1928
|
+
continue outer;
|
|
1929
|
+
}
|
|
1930
|
+
return i;
|
|
1931
|
+
}
|
|
1932
|
+
return -1;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// src/shared/lib/mesh-webrtc-adapter.ts
|
|
1845
1936
|
var DEFAULT_ICE_SERVERS = [
|
|
1846
1937
|
{ urls: "stun:stun.l.google.com:19302" },
|
|
1847
1938
|
{ urls: "stun:stun1.l.google.com:19302" }
|
|
@@ -1960,11 +2051,22 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
1960
2051
|
slot = this.createInitiatingSlot(targetId);
|
|
1961
2052
|
}
|
|
1962
2053
|
if (slot.channel && slot.channel.readyState === "open") {
|
|
1963
|
-
slot.channel
|
|
2054
|
+
this.sendBytesMaybeFragmented(slot.channel, bytes);
|
|
1964
2055
|
} else {
|
|
1965
2056
|
slot.pendingSends.push(bytes);
|
|
1966
2057
|
}
|
|
1967
2058
|
}
|
|
2059
|
+
sendBytesMaybeFragmented(channel, bytes) {
|
|
2060
|
+
if (bytes.length <= SYNC_FRAGMENT_THRESHOLD) {
|
|
2061
|
+
channel.send(bytes);
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const id = crypto.randomUUID();
|
|
2065
|
+
const fragments = chunkSyncMessage(bytes, id);
|
|
2066
|
+
for (const fragment of fragments) {
|
|
2067
|
+
channel.send(fragment);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
1968
2070
|
handleSignal(fromPeerId, rawPayload) {
|
|
1969
2071
|
const payload = rawPayload;
|
|
1970
2072
|
if (!payload || typeof payload !== "object" || !("kind" in payload)) {
|
|
@@ -1985,7 +2087,12 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
1985
2087
|
createInitiatingSlot(targetId) {
|
|
1986
2088
|
const connection = new this.RTCPeerConnectionCtor({ iceServers: this.iceServers });
|
|
1987
2089
|
const channel = connection.createDataChannel(this.dataChannelLabel, { ordered: true });
|
|
1988
|
-
const slot = {
|
|
2090
|
+
const slot = {
|
|
2091
|
+
connection,
|
|
2092
|
+
channel,
|
|
2093
|
+
pendingSends: [],
|
|
2094
|
+
pendingFragments: new Map
|
|
2095
|
+
};
|
|
1989
2096
|
this.slots.set(targetId, slot);
|
|
1990
2097
|
this.wireConnection(targetId, connection);
|
|
1991
2098
|
this.wireDataChannel(targetId, channel);
|
|
@@ -2009,7 +2116,12 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2009
2116
|
this.slots.delete(fromPeerId);
|
|
2010
2117
|
}
|
|
2011
2118
|
const connection = new this.RTCPeerConnectionCtor({ iceServers: this.iceServers });
|
|
2012
|
-
const slot = {
|
|
2119
|
+
const slot = {
|
|
2120
|
+
connection,
|
|
2121
|
+
channel: undefined,
|
|
2122
|
+
pendingSends: [],
|
|
2123
|
+
pendingFragments: new Map
|
|
2124
|
+
};
|
|
2013
2125
|
this.slots.set(fromPeerId, slot);
|
|
2014
2126
|
this.wireConnection(fromPeerId, connection);
|
|
2015
2127
|
connection.ondatachannel = (event) => {
|
|
@@ -2068,7 +2180,7 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2068
2180
|
if (!slot)
|
|
2069
2181
|
return;
|
|
2070
2182
|
for (const bytes of slot.pendingSends) {
|
|
2071
|
-
|
|
2183
|
+
this.sendBytesMaybeFragmented(channel, bytes);
|
|
2072
2184
|
}
|
|
2073
2185
|
slot.pendingSends = [];
|
|
2074
2186
|
};
|
|
@@ -2089,6 +2201,10 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2089
2201
|
}
|
|
2090
2202
|
dispatchMessage(fromPeerId, bytes) {
|
|
2091
2203
|
try {
|
|
2204
|
+
if (isSyncFragmentType(bytes)) {
|
|
2205
|
+
this.handleSyncFragment(fromPeerId, bytes);
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2092
2208
|
if (this.onBlobMessage && isBlobMessageType(bytes)) {
|
|
2093
2209
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2094
2210
|
const headerLen = view.getUint32(0, false);
|
|
@@ -2101,6 +2217,26 @@ class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
|
2101
2217
|
this.emit("message", message);
|
|
2102
2218
|
} catch {}
|
|
2103
2219
|
}
|
|
2220
|
+
handleSyncFragment(fromPeerId, bytes) {
|
|
2221
|
+
const parsed = parseSyncFragment(bytes);
|
|
2222
|
+
if (!parsed)
|
|
2223
|
+
return;
|
|
2224
|
+
const slot = this.slots.get(fromPeerId);
|
|
2225
|
+
if (!slot)
|
|
2226
|
+
return;
|
|
2227
|
+
const { header, data } = parsed;
|
|
2228
|
+
let entry = slot.pendingFragments.get(header.id);
|
|
2229
|
+
if (!entry) {
|
|
2230
|
+
entry = { chunks: new Map, total: header.total };
|
|
2231
|
+
slot.pendingFragments.set(header.id, entry);
|
|
2232
|
+
}
|
|
2233
|
+
entry.chunks.set(header.index, data.slice());
|
|
2234
|
+
if (entry.chunks.size < entry.total)
|
|
2235
|
+
return;
|
|
2236
|
+
slot.pendingFragments.delete(header.id);
|
|
2237
|
+
const reassembled = reassembleSyncFragments(entry.chunks, entry.total);
|
|
2238
|
+
this.dispatchMessage(fromPeerId, reassembled);
|
|
2239
|
+
}
|
|
2104
2240
|
get connectedPeerIds() {
|
|
2105
2241
|
const ids = [];
|
|
2106
2242
|
for (const [peerId, slot] of this.slots) {
|
|
@@ -2165,7 +2301,8 @@ async function resolveIceServers(rtc) {
|
|
|
2165
2301
|
return rtc?.iceServers;
|
|
2166
2302
|
}
|
|
2167
2303
|
async function createMeshClient(options) {
|
|
2168
|
-
const
|
|
2304
|
+
const keyringSource = await resolveKeyringSource(options.keyring);
|
|
2305
|
+
const keyring = keyringSource();
|
|
2169
2306
|
const encryptionEnabled = options.encryptionEnabled ?? true;
|
|
2170
2307
|
if (encryptionEnabled && !keyring.documentKeys.has(DEFAULT_MESH_KEY_ID)) {
|
|
2171
2308
|
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.`);
|
|
@@ -2210,7 +2347,7 @@ async function createMeshClient(options) {
|
|
|
2210
2347
|
webrtcAdapter = new MeshWebRTCAdapter(webrtcAdapterOptions);
|
|
2211
2348
|
const networkAdapter = new MeshNetworkAdapter({
|
|
2212
2349
|
base: webrtcAdapter,
|
|
2213
|
-
|
|
2350
|
+
keyringSource,
|
|
2214
2351
|
encryptionEnabled
|
|
2215
2352
|
});
|
|
2216
2353
|
const repo = new Repo({
|
|
@@ -2222,7 +2359,9 @@ async function createMeshClient(options) {
|
|
|
2222
2359
|
await signaling.connect();
|
|
2223
2360
|
return {
|
|
2224
2361
|
repo,
|
|
2225
|
-
keyring
|
|
2362
|
+
get keyring() {
|
|
2363
|
+
return keyringSource();
|
|
2364
|
+
},
|
|
2226
2365
|
signaling,
|
|
2227
2366
|
networkAdapter,
|
|
2228
2367
|
webrtcAdapter,
|
|
@@ -2233,15 +2372,18 @@ async function createMeshClient(options) {
|
|
|
2233
2372
|
}
|
|
2234
2373
|
};
|
|
2235
2374
|
}
|
|
2236
|
-
async function
|
|
2375
|
+
async function resolveKeyringSource(source) {
|
|
2376
|
+
if (typeof source === "object" && source !== null && "source" in source) {
|
|
2377
|
+
return source.source;
|
|
2378
|
+
}
|
|
2237
2379
|
if ("storage" in source) {
|
|
2238
2380
|
const loaded = await source.storage.load();
|
|
2239
2381
|
if (loaded === null) {
|
|
2240
2382
|
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.");
|
|
2241
2383
|
}
|
|
2242
|
-
return loaded;
|
|
2384
|
+
return () => loaded;
|
|
2243
2385
|
}
|
|
2244
|
-
return source;
|
|
2386
|
+
return () => source;
|
|
2245
2387
|
}
|
|
2246
2388
|
// src/shared/lib/pairing.ts
|
|
2247
2389
|
init_encryption();
|
|
@@ -2637,4 +2779,4 @@ export {
|
|
|
2637
2779
|
$meshCounter
|
|
2638
2780
|
};
|
|
2639
2781
|
|
|
2640
|
-
//# debugId=
|
|
2782
|
+
//# debugId=FD1853161F9E945F64756E2164756E21
|