@fairfox/polly 0.21.0 → 0.22.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 +83 -3
- package/dist/cli/polly.js +21 -1
- package/dist/cli/polly.js.map +3 -3
- package/dist/src/index.d.ts +0 -32
- package/dist/src/index.js +2 -1642
- package/dist/src/index.js.map +4 -20
- package/dist/src/mesh.d.ts +29 -0
- package/dist/src/mesh.js +1502 -0
- package/dist/src/mesh.js.map +22 -0
- package/dist/src/peer.d.ts +29 -0
- package/dist/src/peer.js +928 -0
- package/dist/src/peer.js.map +20 -0
- package/dist/tools/quality/src/{index.js → cli.js} +22 -37
- package/dist/tools/quality/src/{index.js.map → cli.js.map} +5 -4
- package/package.json +8 -4
package/dist/src/index.js
CHANGED
|
@@ -2062,1579 +2062,6 @@ async function createBlobRef({
|
|
|
2062
2062
|
// src/index.ts
|
|
2063
2063
|
init_constraints();
|
|
2064
2064
|
|
|
2065
|
-
// src/shared/lib/crdt-specialised.ts
|
|
2066
|
-
import { Counter, updateText } from "@automerge/automerge-repo";
|
|
2067
|
-
import { effect as effect3, signal as signal4 } from "@preact/signals";
|
|
2068
|
-
|
|
2069
|
-
// src/shared/lib/migrate-primitive.ts
|
|
2070
|
-
class MigrationError extends Error {
|
|
2071
|
-
code;
|
|
2072
|
-
key;
|
|
2073
|
-
primitive;
|
|
2074
|
-
constructor(message, code, key, primitive) {
|
|
2075
|
-
super(message);
|
|
2076
|
-
this.name = "MigrationError";
|
|
2077
|
-
this.code = code;
|
|
2078
|
-
this.key = key;
|
|
2079
|
-
this.primitive = primitive;
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
class MigrationRegistry {
|
|
2084
|
-
marks = new Set;
|
|
2085
|
-
entryKey(key, primitive) {
|
|
2086
|
-
return `${primitive}:${key}`;
|
|
2087
|
-
}
|
|
2088
|
-
mark(key, primitive) {
|
|
2089
|
-
this.marks.add(this.entryKey(key, primitive));
|
|
2090
|
-
}
|
|
2091
|
-
isMarked(key, primitive) {
|
|
2092
|
-
return this.marks.has(this.entryKey(key, primitive));
|
|
2093
|
-
}
|
|
2094
|
-
clear() {
|
|
2095
|
-
this.marks.clear();
|
|
2096
|
-
}
|
|
2097
|
-
get size() {
|
|
2098
|
-
return this.marks.size;
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
var migrationRegistry = new MigrationRegistry;
|
|
2102
|
-
async function migratePrimitive(source, destination, transform) {
|
|
2103
|
-
if (source === destination) {
|
|
2104
|
-
throw new MigrationError(`Cannot migrate a primitive to itself: "${source.key}" under ${source.primitive}.`, "same-primitive-instance", source.key, source.primitive);
|
|
2105
|
-
}
|
|
2106
|
-
if (migrationRegistry.isMarked(source.key, source.primitive)) {
|
|
2107
|
-
throw new MigrationError(`Cannot migrate: source "${source.key}" under $${source.primitive} has already been migrated. Migrations are one-way and one-time.`, "already-migrated", source.key, source.primitive);
|
|
2108
|
-
}
|
|
2109
|
-
await source.loaded;
|
|
2110
|
-
await destination.loaded;
|
|
2111
|
-
const transformed = transform(source.value);
|
|
2112
|
-
destination.value = transformed;
|
|
2113
|
-
migrationRegistry.mark(source.key, source.primitive);
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
// src/shared/lib/primitive-registry.ts
|
|
2117
|
-
class PrimitiveCollisionError extends Error {
|
|
2118
|
-
key;
|
|
2119
|
-
firstPrimitive;
|
|
2120
|
-
firstCallSite;
|
|
2121
|
-
secondPrimitive;
|
|
2122
|
-
secondCallSite;
|
|
2123
|
-
constructor(key, firstPrimitive, firstCallSite, secondPrimitive, secondCallSite) {
|
|
2124
|
-
const firstLocation = firstCallSite ? ` (at ${firstCallSite})` : "";
|
|
2125
|
-
const secondLocation = secondCallSite ? ` (at ${secondCallSite})` : "";
|
|
2126
|
-
super(`Polly primitive key collision: "${key}" is already registered as ` + `$${firstPrimitive}${firstLocation} and cannot also be registered ` + `as $${secondPrimitive}${secondLocation}. Pick a different key or ` + `use the same primitive in both places.`);
|
|
2127
|
-
this.name = "PrimitiveCollisionError";
|
|
2128
|
-
this.key = key;
|
|
2129
|
-
this.firstPrimitive = firstPrimitive;
|
|
2130
|
-
this.firstCallSite = firstCallSite;
|
|
2131
|
-
this.secondPrimitive = secondPrimitive;
|
|
2132
|
-
this.secondCallSite = secondCallSite;
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
class PrimitiveRegistry {
|
|
2137
|
-
entries = new Map;
|
|
2138
|
-
register(key, primitive, callSite) {
|
|
2139
|
-
const existing = this.entries.get(key);
|
|
2140
|
-
if (existing && existing.primitive !== primitive) {
|
|
2141
|
-
throw new PrimitiveCollisionError(key, existing.primitive, existing.callSite, primitive, callSite);
|
|
2142
|
-
}
|
|
2143
|
-
if (!existing) {
|
|
2144
|
-
this.entries.set(key, { primitive, callSite });
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
has(key) {
|
|
2148
|
-
return this.entries.has(key);
|
|
2149
|
-
}
|
|
2150
|
-
kindOf(key) {
|
|
2151
|
-
return this.entries.get(key)?.primitive;
|
|
2152
|
-
}
|
|
2153
|
-
clear() {
|
|
2154
|
-
this.entries.clear();
|
|
2155
|
-
}
|
|
2156
|
-
get size() {
|
|
2157
|
-
return this.entries.size;
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
var primitiveRegistry = new PrimitiveRegistry;
|
|
2161
|
-
|
|
2162
|
-
// src/shared/lib/schema-version.ts
|
|
2163
|
-
var SCHEMA_VERSION_FIELD = "__schemaVersion";
|
|
2164
|
-
|
|
2165
|
-
class SchemaVersionError extends Error {
|
|
2166
|
-
code;
|
|
2167
|
-
docVersion;
|
|
2168
|
-
targetVersion;
|
|
2169
|
-
opVersion;
|
|
2170
|
-
missingVersion;
|
|
2171
|
-
constructor(message, code, details = {}) {
|
|
2172
|
-
super(message);
|
|
2173
|
-
this.name = "SchemaVersionError";
|
|
2174
|
-
this.code = code;
|
|
2175
|
-
if (details.docVersion !== undefined)
|
|
2176
|
-
this.docVersion = details.docVersion;
|
|
2177
|
-
if (details.targetVersion !== undefined)
|
|
2178
|
-
this.targetVersion = details.targetVersion;
|
|
2179
|
-
if (details.opVersion !== undefined)
|
|
2180
|
-
this.opVersion = details.opVersion;
|
|
2181
|
-
if (details.missingVersion !== undefined)
|
|
2182
|
-
this.missingVersion = details.missingVersion;
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
function getDocVersion(doc) {
|
|
2186
|
-
if (typeof doc !== "object" || doc === null)
|
|
2187
|
-
return 0;
|
|
2188
|
-
const record = doc;
|
|
2189
|
-
const value = record[SCHEMA_VERSION_FIELD];
|
|
2190
|
-
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
2191
|
-
}
|
|
2192
|
-
function setDocVersion(doc, version) {
|
|
2193
|
-
doc[SCHEMA_VERSION_FIELD] = version;
|
|
2194
|
-
}
|
|
2195
|
-
function runMigrations(doc, targetVersion, migrations) {
|
|
2196
|
-
const current = getDocVersion(doc);
|
|
2197
|
-
if (current > targetVersion) {
|
|
2198
|
-
throw new SchemaVersionError(`Document is at schema version ${current} but the application targets ${targetVersion}. Upgrade the application to continue.`, "doc-ahead-of-app", { docVersion: current, targetVersion });
|
|
2199
|
-
}
|
|
2200
|
-
for (let v = current + 1;v <= targetVersion; v++) {
|
|
2201
|
-
const migration = migrations[v];
|
|
2202
|
-
if (!migration) {
|
|
2203
|
-
throw new SchemaVersionError(`Missing migration for schema version ${v}. Migrations must be contiguous from ${current + 1} through ${targetVersion}.`, "missing-migration", { docVersion: current, targetVersion, missingVersion: v });
|
|
2204
|
-
}
|
|
2205
|
-
migration(doc);
|
|
2206
|
-
setDocVersion(doc, v);
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
function checkOpVersion(opVersion, docVersion) {
|
|
2210
|
-
if (opVersion < docVersion) {
|
|
2211
|
-
return { compatible: false, reason: "op-older-than-doc", opVersion, docVersion };
|
|
2212
|
-
}
|
|
2213
|
-
if (opVersion > docVersion) {
|
|
2214
|
-
return { compatible: false, reason: "op-newer-than-doc", opVersion, docVersion };
|
|
2215
|
-
}
|
|
2216
|
-
return { compatible: true };
|
|
2217
|
-
}
|
|
2218
|
-
function assertOpVersion(opVersion, docVersion) {
|
|
2219
|
-
const result = checkOpVersion(opVersion, docVersion);
|
|
2220
|
-
if (result.compatible)
|
|
2221
|
-
return;
|
|
2222
|
-
const message = result.reason === "op-older-than-doc" ? `Incoming op was produced at schema version ${opVersion} but the document is at version ${docVersion}. The producing peer is behind.` : `Incoming op was produced at schema version ${opVersion} but the document is at version ${docVersion}. The current peer is behind and should upgrade.`;
|
|
2223
|
-
throw new SchemaVersionError(message, result.reason, { opVersion, docVersion });
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
// src/shared/lib/crdt-specialised.ts
|
|
2227
|
-
function createSpecialisedPrimitive(config) {
|
|
2228
|
-
if (migrationRegistry.isMarked(config.key, config.primitive)) {
|
|
2229
|
-
throw new MigrationError(`Cannot construct $${config.primitive}("${config.key}"): this key has been marked as migrated. Migrations are one-way; use the destination primitive instead.`, "already-migrated", config.key, config.primitive);
|
|
2230
|
-
}
|
|
2231
|
-
primitiveRegistry.register(config.key, config.primitive, config.callSite);
|
|
2232
|
-
const inner = signal4(config.initialValue);
|
|
2233
|
-
let updating = false;
|
|
2234
|
-
let currentHandle;
|
|
2235
|
-
const loaded = (async () => {
|
|
2236
|
-
const handle = await config.getHandle();
|
|
2237
|
-
await handle.whenReady();
|
|
2238
|
-
currentHandle = handle;
|
|
2239
|
-
if (config.schemaVersion !== undefined) {
|
|
2240
|
-
const targetVersion = config.schemaVersion;
|
|
2241
|
-
const migrations = config.migrations ?? {};
|
|
2242
|
-
handle.change((doc) => {
|
|
2243
|
-
runMigrations(doc, targetVersion, migrations);
|
|
2244
|
-
setDocVersion(doc, targetVersion);
|
|
2245
|
-
});
|
|
2246
|
-
}
|
|
2247
|
-
updating = true;
|
|
2248
|
-
try {
|
|
2249
|
-
inner.value = config.extractValue(handle.doc());
|
|
2250
|
-
} finally {
|
|
2251
|
-
updating = false;
|
|
2252
|
-
}
|
|
2253
|
-
handle.on("change", (payload) => {
|
|
2254
|
-
if (updating)
|
|
2255
|
-
return;
|
|
2256
|
-
updating = true;
|
|
2257
|
-
try {
|
|
2258
|
-
inner.value = config.extractValue(payload.doc);
|
|
2259
|
-
} finally {
|
|
2260
|
-
updating = false;
|
|
2261
|
-
}
|
|
2262
|
-
});
|
|
2263
|
-
effect3(() => {
|
|
2264
|
-
const value = inner.value;
|
|
2265
|
-
if (updating)
|
|
2266
|
-
return;
|
|
2267
|
-
if (!currentHandle)
|
|
2268
|
-
return;
|
|
2269
|
-
updating = true;
|
|
2270
|
-
try {
|
|
2271
|
-
currentHandle.change((doc) => {
|
|
2272
|
-
config.applyWrite(doc, value);
|
|
2273
|
-
});
|
|
2274
|
-
} finally {
|
|
2275
|
-
updating = false;
|
|
2276
|
-
}
|
|
2277
|
-
});
|
|
2278
|
-
})();
|
|
2279
|
-
return {
|
|
2280
|
-
key: config.key,
|
|
2281
|
-
primitive: config.primitive,
|
|
2282
|
-
get value() {
|
|
2283
|
-
return inner.value;
|
|
2284
|
-
},
|
|
2285
|
-
set value(next) {
|
|
2286
|
-
inner.value = next;
|
|
2287
|
-
},
|
|
2288
|
-
loaded,
|
|
2289
|
-
get handle() {
|
|
2290
|
-
return currentHandle;
|
|
2291
|
-
}
|
|
2292
|
-
};
|
|
2293
|
-
}
|
|
2294
|
-
function $crdtText(key, initialValue, options) {
|
|
2295
|
-
return createSpecialisedPrimitive({
|
|
2296
|
-
key,
|
|
2297
|
-
primitive: options.primitive ?? "peerState",
|
|
2298
|
-
initialValue,
|
|
2299
|
-
getHandle: options.getHandle,
|
|
2300
|
-
extractValue: (doc) => doc.text ?? "",
|
|
2301
|
-
applyWrite: (doc, value) => {
|
|
2302
|
-
if (doc.text === undefined) {
|
|
2303
|
-
doc.text = value;
|
|
2304
|
-
} else {
|
|
2305
|
-
updateText(doc, ["text"], value);
|
|
2306
|
-
}
|
|
2307
|
-
},
|
|
2308
|
-
schemaVersion: options.schemaVersion,
|
|
2309
|
-
migrations: options.migrations,
|
|
2310
|
-
access: options.access,
|
|
2311
|
-
callSite: options.callSite
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
function $crdtCounter(key, initialValue, options) {
|
|
2315
|
-
return createSpecialisedPrimitive({
|
|
2316
|
-
key,
|
|
2317
|
-
primitive: options.primitive ?? "peerState",
|
|
2318
|
-
initialValue,
|
|
2319
|
-
getHandle: options.getHandle,
|
|
2320
|
-
extractValue: (doc) => {
|
|
2321
|
-
const c = doc.count;
|
|
2322
|
-
if (c === undefined)
|
|
2323
|
-
return 0;
|
|
2324
|
-
return c.value;
|
|
2325
|
-
},
|
|
2326
|
-
applyWrite: (doc, value) => {
|
|
2327
|
-
const existing = doc.count;
|
|
2328
|
-
if (existing === undefined) {
|
|
2329
|
-
doc.count = new Counter(value);
|
|
2330
|
-
} else {
|
|
2331
|
-
const delta = value - existing.value;
|
|
2332
|
-
if (delta !== 0) {
|
|
2333
|
-
existing.increment(delta);
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
},
|
|
2337
|
-
schemaVersion: options.schemaVersion,
|
|
2338
|
-
migrations: options.migrations,
|
|
2339
|
-
access: options.access,
|
|
2340
|
-
callSite: options.callSite
|
|
2341
|
-
});
|
|
2342
|
-
}
|
|
2343
|
-
function $crdtList(key, initialValue, options) {
|
|
2344
|
-
return createSpecialisedPrimitive({
|
|
2345
|
-
key,
|
|
2346
|
-
primitive: options.primitive ?? "peerState",
|
|
2347
|
-
initialValue,
|
|
2348
|
-
getHandle: options.getHandle,
|
|
2349
|
-
extractValue: (doc) => doc.items ? [...doc.items] : [],
|
|
2350
|
-
applyWrite: (doc, value) => {
|
|
2351
|
-
doc.items = [...value];
|
|
2352
|
-
},
|
|
2353
|
-
schemaVersion: options.schemaVersion,
|
|
2354
|
-
migrations: options.migrations,
|
|
2355
|
-
access: options.access,
|
|
2356
|
-
callSite: options.callSite
|
|
2357
|
-
});
|
|
2358
|
-
}
|
|
2359
|
-
// src/shared/lib/crdt-state.ts
|
|
2360
|
-
import { effect as effect4, signal as signal5 } from "@preact/signals";
|
|
2361
|
-
function $crdtState(options) {
|
|
2362
|
-
if (migrationRegistry.isMarked(options.key, options.primitive)) {
|
|
2363
|
-
throw new MigrationError(`Cannot construct $${options.primitive}("${options.key}"): this key has been marked as migrated. Migrations are one-way; use the destination primitive instead.`, "already-migrated", options.key, options.primitive);
|
|
2364
|
-
}
|
|
2365
|
-
primitiveRegistry.register(options.key, options.primitive, options.callSite);
|
|
2366
|
-
const inner = signal5(options.initialValue);
|
|
2367
|
-
let updating = false;
|
|
2368
|
-
let currentHandle;
|
|
2369
|
-
const loaded = (async () => {
|
|
2370
|
-
const handle = await options.getHandle();
|
|
2371
|
-
await handle.whenReady();
|
|
2372
|
-
currentHandle = handle;
|
|
2373
|
-
if (options.schemaVersion !== undefined) {
|
|
2374
|
-
const targetVersion = options.schemaVersion;
|
|
2375
|
-
const migrations = options.migrations ?? {};
|
|
2376
|
-
handle.change((doc) => {
|
|
2377
|
-
runMigrations(doc, targetVersion, migrations);
|
|
2378
|
-
setDocVersion(doc, targetVersion);
|
|
2379
|
-
});
|
|
2380
|
-
}
|
|
2381
|
-
updating = true;
|
|
2382
|
-
try {
|
|
2383
|
-
inner.value = cloneDoc(handle.doc());
|
|
2384
|
-
} finally {
|
|
2385
|
-
updating = false;
|
|
2386
|
-
}
|
|
2387
|
-
handle.on("change", (payload) => {
|
|
2388
|
-
if (updating)
|
|
2389
|
-
return;
|
|
2390
|
-
updating = true;
|
|
2391
|
-
try {
|
|
2392
|
-
inner.value = cloneDoc(payload.doc);
|
|
2393
|
-
} finally {
|
|
2394
|
-
updating = false;
|
|
2395
|
-
}
|
|
2396
|
-
});
|
|
2397
|
-
effect4(() => {
|
|
2398
|
-
const value = inner.value;
|
|
2399
|
-
if (updating)
|
|
2400
|
-
return;
|
|
2401
|
-
if (!currentHandle)
|
|
2402
|
-
return;
|
|
2403
|
-
updating = true;
|
|
2404
|
-
try {
|
|
2405
|
-
currentHandle.change((doc) => {
|
|
2406
|
-
applyTopLevel(doc, value);
|
|
2407
|
-
});
|
|
2408
|
-
} finally {
|
|
2409
|
-
updating = false;
|
|
2410
|
-
}
|
|
2411
|
-
});
|
|
2412
|
-
})();
|
|
2413
|
-
return {
|
|
2414
|
-
key: options.key,
|
|
2415
|
-
primitive: options.primitive,
|
|
2416
|
-
get value() {
|
|
2417
|
-
return inner.value;
|
|
2418
|
-
},
|
|
2419
|
-
set value(next) {
|
|
2420
|
-
inner.value = next;
|
|
2421
|
-
},
|
|
2422
|
-
loaded,
|
|
2423
|
-
get handle() {
|
|
2424
|
-
return currentHandle;
|
|
2425
|
-
}
|
|
2426
|
-
};
|
|
2427
|
-
}
|
|
2428
|
-
function cloneDoc(doc) {
|
|
2429
|
-
return JSON.parse(JSON.stringify(doc));
|
|
2430
|
-
}
|
|
2431
|
-
function applyTopLevel(doc, value) {
|
|
2432
|
-
for (const key of Object.keys(value)) {
|
|
2433
|
-
if (key === SCHEMA_VERSION_FIELD)
|
|
2434
|
-
continue;
|
|
2435
|
-
doc[key] = value[key];
|
|
2436
|
-
}
|
|
2437
|
-
}
|
|
2438
|
-
// src/shared/lib/encryption.ts
|
|
2439
|
-
import nacl from "tweetnacl";
|
|
2440
|
-
var KEY_BYTES = 32;
|
|
2441
|
-
var NONCE_BYTES = 24;
|
|
2442
|
-
var TAG_BYTES = 16;
|
|
2443
|
-
|
|
2444
|
-
class EncryptionError extends Error {
|
|
2445
|
-
code;
|
|
2446
|
-
constructor(message, code) {
|
|
2447
|
-
super(message);
|
|
2448
|
-
this.name = "EncryptionError";
|
|
2449
|
-
this.code = code;
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
function generateDocumentKey() {
|
|
2453
|
-
return nacl.randomBytes(KEY_BYTES);
|
|
2454
|
-
}
|
|
2455
|
-
function encrypt(payload, key) {
|
|
2456
|
-
if (key.length !== KEY_BYTES) {
|
|
2457
|
-
throw new EncryptionError(`secretbox key must be ${KEY_BYTES} bytes, got ${key.length}.`, "invalid-key-length");
|
|
2458
|
-
}
|
|
2459
|
-
const nonce = nacl.randomBytes(NONCE_BYTES);
|
|
2460
|
-
const ciphertext = nacl.secretbox(payload, nonce, key);
|
|
2461
|
-
const out = new Uint8Array(NONCE_BYTES + ciphertext.length);
|
|
2462
|
-
out.set(nonce, 0);
|
|
2463
|
-
out.set(ciphertext, NONCE_BYTES);
|
|
2464
|
-
return out;
|
|
2465
|
-
}
|
|
2466
|
-
function decrypt(sealed, key) {
|
|
2467
|
-
if (key.length !== KEY_BYTES) {
|
|
2468
|
-
throw new EncryptionError(`secretbox key must be ${KEY_BYTES} bytes, got ${key.length}.`, "invalid-key-length");
|
|
2469
|
-
}
|
|
2470
|
-
if (sealed.length < NONCE_BYTES + TAG_BYTES) {
|
|
2471
|
-
return;
|
|
2472
|
-
}
|
|
2473
|
-
const nonce = sealed.subarray(0, NONCE_BYTES);
|
|
2474
|
-
const ciphertext = sealed.subarray(NONCE_BYTES);
|
|
2475
|
-
const opened = nacl.secretbox.open(ciphertext, nonce, key);
|
|
2476
|
-
return opened ?? undefined;
|
|
2477
|
-
}
|
|
2478
|
-
function decryptOrThrow(sealed, key) {
|
|
2479
|
-
const opened = decrypt(sealed, key);
|
|
2480
|
-
if (!opened) {
|
|
2481
|
-
throw new EncryptionError(`Failed to decrypt sealed blob: wrong key, malformed input, or tampered ciphertext.`, "decrypt-failed");
|
|
2482
|
-
}
|
|
2483
|
-
return opened;
|
|
2484
|
-
}
|
|
2485
|
-
function sealEnvelope(payload, documentId, key) {
|
|
2486
|
-
return {
|
|
2487
|
-
documentId,
|
|
2488
|
-
sealed: encrypt(payload, key)
|
|
2489
|
-
};
|
|
2490
|
-
}
|
|
2491
|
-
function openEnvelope(envelope, key) {
|
|
2492
|
-
return decryptOrThrow(envelope.sealed, key);
|
|
2493
|
-
}
|
|
2494
|
-
function encodeEncryptedEnvelope(envelope) {
|
|
2495
|
-
const idBytes = new TextEncoder().encode(envelope.documentId);
|
|
2496
|
-
const out = new Uint8Array(4 + idBytes.length + envelope.sealed.length);
|
|
2497
|
-
const view = new DataView(out.buffer);
|
|
2498
|
-
view.setUint32(0, idBytes.length, false);
|
|
2499
|
-
out.set(idBytes, 4);
|
|
2500
|
-
out.set(envelope.sealed, 4 + idBytes.length);
|
|
2501
|
-
return out;
|
|
2502
|
-
}
|
|
2503
|
-
function decodeEncryptedEnvelope(bytes) {
|
|
2504
|
-
if (bytes.length < 4) {
|
|
2505
|
-
throw new EncryptionError(`Encrypted envelope too short: ${bytes.length} bytes.`, "envelope-malformed");
|
|
2506
|
-
}
|
|
2507
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2508
|
-
const idLen = view.getUint32(0, false);
|
|
2509
|
-
if (bytes.length < 4 + idLen) {
|
|
2510
|
-
throw new EncryptionError(`Encrypted envelope truncated: declared id length ${idLen}, total ${bytes.length}.`, "envelope-malformed");
|
|
2511
|
-
}
|
|
2512
|
-
const documentId = new TextDecoder().decode(bytes.subarray(4, 4 + idLen));
|
|
2513
|
-
const sealed = bytes.slice(4 + idLen);
|
|
2514
|
-
return { documentId, sealed };
|
|
2515
|
-
}
|
|
2516
|
-
// src/shared/lib/mesh-network-adapter.ts
|
|
2517
|
-
import {
|
|
2518
|
-
NetworkAdapter
|
|
2519
|
-
} from "@automerge/automerge-repo";
|
|
2520
|
-
|
|
2521
|
-
// src/shared/lib/signing.ts
|
|
2522
|
-
import nacl2 from "tweetnacl";
|
|
2523
|
-
var PUBLIC_KEY_BYTES = 32;
|
|
2524
|
-
var SECRET_KEY_BYTES = 64;
|
|
2525
|
-
var SIGNATURE_BYTES = 64;
|
|
2526
|
-
|
|
2527
|
-
class SigningError extends Error {
|
|
2528
|
-
code;
|
|
2529
|
-
constructor(message, code) {
|
|
2530
|
-
super(message);
|
|
2531
|
-
this.name = "SigningError";
|
|
2532
|
-
this.code = code;
|
|
2533
|
-
}
|
|
2534
|
-
}
|
|
2535
|
-
function generateSigningKeyPair() {
|
|
2536
|
-
const pair = nacl2.sign.keyPair();
|
|
2537
|
-
return {
|
|
2538
|
-
publicKey: pair.publicKey,
|
|
2539
|
-
secretKey: pair.secretKey
|
|
2540
|
-
};
|
|
2541
|
-
}
|
|
2542
|
-
function signingKeyPairFromSecret(secretKey) {
|
|
2543
|
-
if (secretKey.length !== SECRET_KEY_BYTES) {
|
|
2544
|
-
throw new SigningError(`Ed25519 secret key must be ${SECRET_KEY_BYTES} bytes, got ${secretKey.length}.`, "invalid-secret-key");
|
|
2545
|
-
}
|
|
2546
|
-
const pair = nacl2.sign.keyPair.fromSecretKey(secretKey);
|
|
2547
|
-
return {
|
|
2548
|
-
publicKey: pair.publicKey,
|
|
2549
|
-
secretKey: pair.secretKey
|
|
2550
|
-
};
|
|
2551
|
-
}
|
|
2552
|
-
function sign(payload, secretKey) {
|
|
2553
|
-
if (secretKey.length !== SECRET_KEY_BYTES) {
|
|
2554
|
-
throw new SigningError(`Ed25519 secret key must be ${SECRET_KEY_BYTES} bytes, got ${secretKey.length}.`, "invalid-secret-key");
|
|
2555
|
-
}
|
|
2556
|
-
return nacl2.sign.detached(payload, secretKey);
|
|
2557
|
-
}
|
|
2558
|
-
function verify(payload, signature, publicKey) {
|
|
2559
|
-
if (publicKey.length !== PUBLIC_KEY_BYTES) {
|
|
2560
|
-
throw new SigningError(`Ed25519 public key must be ${PUBLIC_KEY_BYTES} bytes, got ${publicKey.length}.`, "invalid-public-key");
|
|
2561
|
-
}
|
|
2562
|
-
if (signature.length !== SIGNATURE_BYTES) {
|
|
2563
|
-
throw new SigningError(`Ed25519 signature must be ${SIGNATURE_BYTES} bytes, got ${signature.length}.`, "invalid-signature-length");
|
|
2564
|
-
}
|
|
2565
|
-
return nacl2.sign.detached.verify(payload, signature, publicKey);
|
|
2566
|
-
}
|
|
2567
|
-
function signEnvelope(payload, senderId, secretKey) {
|
|
2568
|
-
const signature = sign(payload, secretKey);
|
|
2569
|
-
return { senderId, payload, signature };
|
|
2570
|
-
}
|
|
2571
|
-
function openEnvelope2(envelope, publicKey) {
|
|
2572
|
-
const ok = verify(envelope.payload, envelope.signature, publicKey);
|
|
2573
|
-
if (!ok) {
|
|
2574
|
-
throw new SigningError(`Signature verification failed for envelope from ${envelope.senderId}.`, "envelope-malformed");
|
|
2575
|
-
}
|
|
2576
|
-
return envelope.payload;
|
|
2577
|
-
}
|
|
2578
|
-
function encodeSignedEnvelope(envelope) {
|
|
2579
|
-
const senderBytes = new TextEncoder().encode(envelope.senderId);
|
|
2580
|
-
const total = 4 + senderBytes.length + SIGNATURE_BYTES + envelope.payload.length;
|
|
2581
|
-
const out = new Uint8Array(total);
|
|
2582
|
-
const view = new DataView(out.buffer);
|
|
2583
|
-
view.setUint32(0, senderBytes.length, false);
|
|
2584
|
-
out.set(senderBytes, 4);
|
|
2585
|
-
out.set(envelope.signature, 4 + senderBytes.length);
|
|
2586
|
-
out.set(envelope.payload, 4 + senderBytes.length + SIGNATURE_BYTES);
|
|
2587
|
-
return out;
|
|
2588
|
-
}
|
|
2589
|
-
function decodeSignedEnvelope(bytes) {
|
|
2590
|
-
if (bytes.length < 4 + SIGNATURE_BYTES) {
|
|
2591
|
-
throw new SigningError(`Envelope too short: ${bytes.length} bytes, need at least ${4 + SIGNATURE_BYTES}.`, "envelope-malformed");
|
|
2592
|
-
}
|
|
2593
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2594
|
-
const senderLen = view.getUint32(0, false);
|
|
2595
|
-
if (bytes.length < 4 + senderLen + SIGNATURE_BYTES) {
|
|
2596
|
-
throw new SigningError(`Envelope truncated: declared sender length ${senderLen}, total ${bytes.length}.`, "envelope-malformed");
|
|
2597
|
-
}
|
|
2598
|
-
const senderId = new TextDecoder().decode(bytes.subarray(4, 4 + senderLen));
|
|
2599
|
-
const signature = bytes.slice(4 + senderLen, 4 + senderLen + SIGNATURE_BYTES);
|
|
2600
|
-
const payload = bytes.slice(4 + senderLen + SIGNATURE_BYTES);
|
|
2601
|
-
return { senderId, payload, signature };
|
|
2602
|
-
}
|
|
2603
|
-
|
|
2604
|
-
// src/shared/lib/mesh-network-adapter.ts
|
|
2605
|
-
var DEFAULT_MESH_KEY_ID = "polly-mesh-default";
|
|
2606
|
-
|
|
2607
|
-
class MeshNetworkAdapter extends NetworkAdapter {
|
|
2608
|
-
base;
|
|
2609
|
-
keyring;
|
|
2610
|
-
encryptionEnabled;
|
|
2611
|
-
constructor(options) {
|
|
2612
|
-
super();
|
|
2613
|
-
this.base = options.base;
|
|
2614
|
-
this.keyring = options.keyring;
|
|
2615
|
-
this.encryptionEnabled = options.encryptionEnabled ?? true;
|
|
2616
|
-
this.base.on("close", () => this.emit("close"));
|
|
2617
|
-
this.base.on("peer-candidate", (payload) => this.emit("peer-candidate", payload));
|
|
2618
|
-
this.base.on("peer-disconnected", (payload) => this.emit("peer-disconnected", payload));
|
|
2619
|
-
this.base.on("message", (rawMessage) => {
|
|
2620
|
-
const unwrapped = this.tryUnwrap(rawMessage);
|
|
2621
|
-
if (unwrapped) {
|
|
2622
|
-
this.emit("message", unwrapped);
|
|
2623
|
-
}
|
|
2624
|
-
});
|
|
2625
|
-
}
|
|
2626
|
-
isReady() {
|
|
2627
|
-
return this.base.isReady();
|
|
2628
|
-
}
|
|
2629
|
-
whenReady() {
|
|
2630
|
-
return this.base.whenReady();
|
|
2631
|
-
}
|
|
2632
|
-
connect(peerId, peerMetadata) {
|
|
2633
|
-
this.peerId = peerId;
|
|
2634
|
-
if (peerMetadata !== undefined) {
|
|
2635
|
-
this.peerMetadata = peerMetadata;
|
|
2636
|
-
}
|
|
2637
|
-
this.base.connect(peerId, peerMetadata);
|
|
2638
|
-
}
|
|
2639
|
-
disconnect() {
|
|
2640
|
-
this.base.disconnect();
|
|
2641
|
-
}
|
|
2642
|
-
send(message) {
|
|
2643
|
-
const wrapped = this.wrap(message);
|
|
2644
|
-
this.base.send(wrapped);
|
|
2645
|
-
}
|
|
2646
|
-
wrap(message) {
|
|
2647
|
-
const serialised = serialiseMessage(message);
|
|
2648
|
-
let payloadToSign;
|
|
2649
|
-
if (this.encryptionEnabled) {
|
|
2650
|
-
const docKey = this.keyring.documentKeys.get(DEFAULT_MESH_KEY_ID);
|
|
2651
|
-
if (!docKey) {
|
|
2652
|
-
throw new Error(`MeshNetworkAdapter: missing document encryption key under id "${DEFAULT_MESH_KEY_ID}". Provision the key in the keyring before sending.`);
|
|
2653
|
-
}
|
|
2654
|
-
const encrypted = sealEnvelope(serialised, DEFAULT_MESH_KEY_ID, docKey);
|
|
2655
|
-
payloadToSign = encodeEncryptedEnvelope(encrypted);
|
|
2656
|
-
} else {
|
|
2657
|
-
payloadToSign = serialised;
|
|
2658
|
-
}
|
|
2659
|
-
const signed = signEnvelope(payloadToSign, message.senderId, this.keyring.identity.secretKey);
|
|
2660
|
-
const signedBytes = encodeSignedEnvelope(signed);
|
|
2661
|
-
return {
|
|
2662
|
-
type: message.type,
|
|
2663
|
-
senderId: message.senderId,
|
|
2664
|
-
targetId: message.targetId,
|
|
2665
|
-
data: signedBytes
|
|
2666
|
-
};
|
|
2667
|
-
}
|
|
2668
|
-
tryUnwrap(message) {
|
|
2669
|
-
if (!message.data)
|
|
2670
|
-
return;
|
|
2671
|
-
let signed;
|
|
2672
|
-
try {
|
|
2673
|
-
signed = decodeSignedEnvelope(message.data);
|
|
2674
|
-
} catch {
|
|
2675
|
-
return;
|
|
2676
|
-
}
|
|
2677
|
-
if (this.keyring.revokedPeers.has(signed.senderId)) {
|
|
2678
|
-
return;
|
|
2679
|
-
}
|
|
2680
|
-
const senderKey = this.keyring.knownPeers.get(signed.senderId);
|
|
2681
|
-
if (!senderKey) {
|
|
2682
|
-
return;
|
|
2683
|
-
}
|
|
2684
|
-
let verifiedPayload;
|
|
2685
|
-
try {
|
|
2686
|
-
verifiedPayload = openEnvelope2(signed, senderKey);
|
|
2687
|
-
} catch {
|
|
2688
|
-
return;
|
|
2689
|
-
}
|
|
2690
|
-
if (!this.encryptionEnabled) {
|
|
2691
|
-
return deserialiseMessage(verifiedPayload);
|
|
2692
|
-
}
|
|
2693
|
-
let encrypted;
|
|
2694
|
-
try {
|
|
2695
|
-
encrypted = decodeEncryptedEnvelope(verifiedPayload);
|
|
2696
|
-
} catch {
|
|
2697
|
-
return;
|
|
2698
|
-
}
|
|
2699
|
-
const docKey = this.keyring.documentKeys.get(encrypted.documentId);
|
|
2700
|
-
if (!docKey) {
|
|
2701
|
-
return;
|
|
2702
|
-
}
|
|
2703
|
-
let plaintext;
|
|
2704
|
-
try {
|
|
2705
|
-
plaintext = openEnvelope(encrypted, docKey);
|
|
2706
|
-
} catch {
|
|
2707
|
-
return;
|
|
2708
|
-
}
|
|
2709
|
-
return deserialiseMessage(plaintext);
|
|
2710
|
-
}
|
|
2711
|
-
}
|
|
2712
|
-
function serialiseMessage(message) {
|
|
2713
|
-
const headerObj = {
|
|
2714
|
-
type: message.type,
|
|
2715
|
-
senderId: message.senderId,
|
|
2716
|
-
targetId: message.targetId
|
|
2717
|
-
};
|
|
2718
|
-
if ("documentId" in message && message.documentId !== undefined) {
|
|
2719
|
-
headerObj["documentId"] = message.documentId;
|
|
2720
|
-
}
|
|
2721
|
-
if ("count" in message && message.count !== undefined) {
|
|
2722
|
-
headerObj["count"] = message.count;
|
|
2723
|
-
}
|
|
2724
|
-
if ("sessionId" in message && message.sessionId !== undefined) {
|
|
2725
|
-
headerObj["sessionId"] = message.sessionId;
|
|
2726
|
-
}
|
|
2727
|
-
const headerBytes = new TextEncoder().encode(JSON.stringify(headerObj));
|
|
2728
|
-
const dataBytes = "data" in message && message.data instanceof Uint8Array ? message.data : new Uint8Array(0);
|
|
2729
|
-
const out = new Uint8Array(4 + headerBytes.length + dataBytes.length);
|
|
2730
|
-
const view = new DataView(out.buffer);
|
|
2731
|
-
view.setUint32(0, headerBytes.length, false);
|
|
2732
|
-
out.set(headerBytes, 4);
|
|
2733
|
-
out.set(dataBytes, 4 + headerBytes.length);
|
|
2734
|
-
return out;
|
|
2735
|
-
}
|
|
2736
|
-
function deserialiseMessage(bytes) {
|
|
2737
|
-
if (bytes.length < 4) {
|
|
2738
|
-
throw new Error("MeshNetworkAdapter: message too short to deserialise.");
|
|
2739
|
-
}
|
|
2740
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2741
|
-
const headerLen = view.getUint32(0, false);
|
|
2742
|
-
if (bytes.length < 4 + headerLen) {
|
|
2743
|
-
throw new Error("MeshNetworkAdapter: message header truncated.");
|
|
2744
|
-
}
|
|
2745
|
-
const header = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + headerLen)));
|
|
2746
|
-
const data = bytes.slice(4 + headerLen);
|
|
2747
|
-
return { ...header, data };
|
|
2748
|
-
}
|
|
2749
|
-
// src/shared/lib/mesh-signaling-client.ts
|
|
2750
|
-
class MeshSignalingClient {
|
|
2751
|
-
url;
|
|
2752
|
-
peerId;
|
|
2753
|
-
onSignal;
|
|
2754
|
-
onError;
|
|
2755
|
-
onOpen;
|
|
2756
|
-
onClose;
|
|
2757
|
-
socket;
|
|
2758
|
-
joined = false;
|
|
2759
|
-
constructor(options) {
|
|
2760
|
-
this.url = options.url;
|
|
2761
|
-
this.peerId = options.peerId;
|
|
2762
|
-
this.onSignal = options.onSignal;
|
|
2763
|
-
if (options.onError !== undefined)
|
|
2764
|
-
this.onError = options.onError;
|
|
2765
|
-
if (options.onOpen !== undefined)
|
|
2766
|
-
this.onOpen = options.onOpen;
|
|
2767
|
-
if (options.onClose !== undefined)
|
|
2768
|
-
this.onClose = options.onClose;
|
|
2769
|
-
}
|
|
2770
|
-
async connect() {
|
|
2771
|
-
return new Promise((resolve, reject) => {
|
|
2772
|
-
const ws = new WebSocket(this.url);
|
|
2773
|
-
this.socket = ws;
|
|
2774
|
-
ws.addEventListener("open", () => {
|
|
2775
|
-
ws.send(JSON.stringify({ type: "join", peerId: this.peerId }));
|
|
2776
|
-
this.joined = true;
|
|
2777
|
-
this.onOpen?.();
|
|
2778
|
-
resolve();
|
|
2779
|
-
});
|
|
2780
|
-
ws.addEventListener("message", (event) => {
|
|
2781
|
-
let msg;
|
|
2782
|
-
try {
|
|
2783
|
-
msg = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
|
|
2784
|
-
} catch {
|
|
2785
|
-
return;
|
|
2786
|
-
}
|
|
2787
|
-
if (msg.type === "signal" && typeof msg.peerId === "string") {
|
|
2788
|
-
this.onSignal(msg.peerId, msg.payload);
|
|
2789
|
-
return;
|
|
2790
|
-
}
|
|
2791
|
-
if (msg.type === "error" && msg.reason) {
|
|
2792
|
-
this.onError?.(msg.reason, msg.targetPeerId);
|
|
2793
|
-
}
|
|
2794
|
-
});
|
|
2795
|
-
ws.addEventListener("error", (err) => {
|
|
2796
|
-
reject(err);
|
|
2797
|
-
});
|
|
2798
|
-
ws.addEventListener("close", () => {
|
|
2799
|
-
this.joined = false;
|
|
2800
|
-
this.onClose?.();
|
|
2801
|
-
});
|
|
2802
|
-
});
|
|
2803
|
-
}
|
|
2804
|
-
sendSignal(targetPeerId, payload) {
|
|
2805
|
-
if (!this.socket || this.socket.readyState !== WebSocket.OPEN || !this.joined) {
|
|
2806
|
-
return false;
|
|
2807
|
-
}
|
|
2808
|
-
const msg = {
|
|
2809
|
-
type: "signal",
|
|
2810
|
-
peerId: this.peerId,
|
|
2811
|
-
targetPeerId,
|
|
2812
|
-
payload
|
|
2813
|
-
};
|
|
2814
|
-
this.socket.send(JSON.stringify(msg));
|
|
2815
|
-
return true;
|
|
2816
|
-
}
|
|
2817
|
-
close() {
|
|
2818
|
-
this.socket?.close();
|
|
2819
|
-
this.socket = undefined;
|
|
2820
|
-
this.joined = false;
|
|
2821
|
-
}
|
|
2822
|
-
get isConnected() {
|
|
2823
|
-
return this.joined && this.socket?.readyState === WebSocket.OPEN;
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
// src/shared/lib/mesh-state.ts
|
|
2827
|
-
var keyMapsByRepo = new WeakMap;
|
|
2828
|
-
var defaultRepo;
|
|
2829
|
-
function configureMeshState(repo) {
|
|
2830
|
-
defaultRepo = repo;
|
|
2831
|
-
keyMapsByRepo.set(repo, new Map);
|
|
2832
|
-
}
|
|
2833
|
-
function resetMeshState() {
|
|
2834
|
-
defaultRepo = undefined;
|
|
2835
|
-
}
|
|
2836
|
-
function resolveRepo(option) {
|
|
2837
|
-
const repo = option ?? defaultRepo;
|
|
2838
|
-
if (!repo) {
|
|
2839
|
-
throw new Error("Polly $meshState: no Repo configured. Call configureMeshState(repo) at startup or pass { repo } in the primitive options.");
|
|
2840
|
-
}
|
|
2841
|
-
return repo;
|
|
2842
|
-
}
|
|
2843
|
-
function getKeyMap(repo) {
|
|
2844
|
-
let map = keyMapsByRepo.get(repo);
|
|
2845
|
-
if (!map) {
|
|
2846
|
-
map = new Map;
|
|
2847
|
-
keyMapsByRepo.set(repo, map);
|
|
2848
|
-
}
|
|
2849
|
-
return map;
|
|
2850
|
-
}
|
|
2851
|
-
function buildHandleFactory(repo, key, initialDoc) {
|
|
2852
|
-
return async () => {
|
|
2853
|
-
const map = getKeyMap(repo);
|
|
2854
|
-
const existingId = map.get(key);
|
|
2855
|
-
if (existingId !== undefined) {
|
|
2856
|
-
return repo.find(existingId);
|
|
2857
|
-
}
|
|
2858
|
-
const handle = repo.create(initialDoc);
|
|
2859
|
-
map.set(key, handle.documentId);
|
|
2860
|
-
return handle;
|
|
2861
|
-
};
|
|
2862
|
-
}
|
|
2863
|
-
function $meshState(key, initialValue, options = {}) {
|
|
2864
|
-
const repo = resolveRepo(options.repo);
|
|
2865
|
-
return $crdtState({
|
|
2866
|
-
key,
|
|
2867
|
-
primitive: "meshState",
|
|
2868
|
-
initialValue,
|
|
2869
|
-
getHandle: buildHandleFactory(repo, key, initialValue),
|
|
2870
|
-
schemaVersion: options.schemaVersion,
|
|
2871
|
-
migrations: options.migrations,
|
|
2872
|
-
access: options.access
|
|
2873
|
-
});
|
|
2874
|
-
}
|
|
2875
|
-
function $meshText(key, initialValue, options = {}) {
|
|
2876
|
-
const repo = resolveRepo(options.repo);
|
|
2877
|
-
return $crdtText(key, initialValue, {
|
|
2878
|
-
primitive: "meshState",
|
|
2879
|
-
getHandle: buildHandleFactory(repo, key, { text: initialValue }),
|
|
2880
|
-
schemaVersion: options.schemaVersion,
|
|
2881
|
-
migrations: options.migrations,
|
|
2882
|
-
access: options.access
|
|
2883
|
-
});
|
|
2884
|
-
}
|
|
2885
|
-
function $meshCounter(key, initialValue, options = {}) {
|
|
2886
|
-
const repo = resolveRepo(options.repo);
|
|
2887
|
-
return $crdtCounter(key, initialValue, {
|
|
2888
|
-
primitive: "meshState",
|
|
2889
|
-
getHandle: buildHandleFactory(repo, key, {}),
|
|
2890
|
-
schemaVersion: options.schemaVersion,
|
|
2891
|
-
migrations: options.migrations,
|
|
2892
|
-
access: options.access
|
|
2893
|
-
});
|
|
2894
|
-
}
|
|
2895
|
-
function $meshList(key, initialValue, options = {}) {
|
|
2896
|
-
const repo = resolveRepo(options.repo);
|
|
2897
|
-
return $crdtList(key, initialValue, {
|
|
2898
|
-
primitive: "meshState",
|
|
2899
|
-
getHandle: buildHandleFactory(repo, key, { items: initialValue }),
|
|
2900
|
-
schemaVersion: options.schemaVersion,
|
|
2901
|
-
migrations: options.migrations,
|
|
2902
|
-
access: options.access
|
|
2903
|
-
});
|
|
2904
|
-
}
|
|
2905
|
-
// src/shared/lib/mesh-webrtc-adapter.ts
|
|
2906
|
-
import {
|
|
2907
|
-
NetworkAdapter as NetworkAdapter2
|
|
2908
|
-
} from "@automerge/automerge-repo";
|
|
2909
|
-
var DEFAULT_ICE_SERVERS = [
|
|
2910
|
-
{ urls: "stun:stun.l.google.com:19302" },
|
|
2911
|
-
{ urls: "stun:stun1.l.google.com:19302" }
|
|
2912
|
-
];
|
|
2913
|
-
|
|
2914
|
-
class MeshWebRTCAdapter extends NetworkAdapter2 {
|
|
2915
|
-
signaling;
|
|
2916
|
-
iceServers;
|
|
2917
|
-
dataChannelLabel;
|
|
2918
|
-
knownPeerIds;
|
|
2919
|
-
slots = new Map;
|
|
2920
|
-
ready = false;
|
|
2921
|
-
readyResolver;
|
|
2922
|
-
constructor(options) {
|
|
2923
|
-
super();
|
|
2924
|
-
this.signaling = options.signaling;
|
|
2925
|
-
this.iceServers = options.iceServers ?? DEFAULT_ICE_SERVERS;
|
|
2926
|
-
this.dataChannelLabel = options.dataChannelLabel ?? "polly-mesh";
|
|
2927
|
-
this.knownPeerIds = options.knownPeerIds ?? [];
|
|
2928
|
-
}
|
|
2929
|
-
isReady() {
|
|
2930
|
-
return this.ready;
|
|
2931
|
-
}
|
|
2932
|
-
whenReady() {
|
|
2933
|
-
if (this.ready)
|
|
2934
|
-
return Promise.resolve();
|
|
2935
|
-
return new Promise((resolve) => {
|
|
2936
|
-
this.readyResolver = resolve;
|
|
2937
|
-
});
|
|
2938
|
-
}
|
|
2939
|
-
connect(peerId, peerMetadata) {
|
|
2940
|
-
this.peerId = peerId;
|
|
2941
|
-
if (peerMetadata !== undefined) {
|
|
2942
|
-
this.peerMetadata = peerMetadata;
|
|
2943
|
-
}
|
|
2944
|
-
this.ready = true;
|
|
2945
|
-
this.readyResolver?.();
|
|
2946
|
-
for (const remotePeerId of this.knownPeerIds) {
|
|
2947
|
-
if (remotePeerId !== peerId && !this.slots.has(remotePeerId)) {
|
|
2948
|
-
this.createInitiatingSlot(remotePeerId);
|
|
2949
|
-
}
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
disconnect() {
|
|
2953
|
-
for (const slot of this.slots.values()) {
|
|
2954
|
-
slot.channel?.close();
|
|
2955
|
-
slot.connection.close();
|
|
2956
|
-
}
|
|
2957
|
-
this.slots.clear();
|
|
2958
|
-
this.signaling.close();
|
|
2959
|
-
this.ready = false;
|
|
2960
|
-
this.emit("close");
|
|
2961
|
-
}
|
|
2962
|
-
send(message) {
|
|
2963
|
-
const targetId = message.targetId;
|
|
2964
|
-
const bytes = this.serialiseMessage(message);
|
|
2965
|
-
let slot = this.slots.get(targetId);
|
|
2966
|
-
if (!slot) {
|
|
2967
|
-
slot = this.createInitiatingSlot(targetId);
|
|
2968
|
-
}
|
|
2969
|
-
if (slot.channel && slot.channel.readyState === "open") {
|
|
2970
|
-
slot.channel.send(bytes);
|
|
2971
|
-
} else {
|
|
2972
|
-
slot.pendingSends.push(bytes);
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
handleSignal(fromPeerId, rawPayload) {
|
|
2976
|
-
const payload = rawPayload;
|
|
2977
|
-
if (!payload || typeof payload !== "object" || !("kind" in payload)) {
|
|
2978
|
-
return;
|
|
2979
|
-
}
|
|
2980
|
-
switch (payload.kind) {
|
|
2981
|
-
case "offer":
|
|
2982
|
-
this.handleOffer(fromPeerId, payload.sdp);
|
|
2983
|
-
return;
|
|
2984
|
-
case "answer":
|
|
2985
|
-
this.handleAnswer(fromPeerId, payload.sdp);
|
|
2986
|
-
return;
|
|
2987
|
-
case "ice":
|
|
2988
|
-
this.handleIceCandidate(fromPeerId, payload.candidate);
|
|
2989
|
-
return;
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
createInitiatingSlot(targetId) {
|
|
2993
|
-
const connection = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
2994
|
-
const channel = connection.createDataChannel(this.dataChannelLabel, { ordered: true });
|
|
2995
|
-
const slot = { connection, channel, pendingSends: [] };
|
|
2996
|
-
this.slots.set(targetId, slot);
|
|
2997
|
-
this.wireConnection(targetId, connection);
|
|
2998
|
-
this.wireDataChannel(targetId, channel);
|
|
2999
|
-
this.initiateOffer(targetId, connection);
|
|
3000
|
-
return slot;
|
|
3001
|
-
}
|
|
3002
|
-
async initiateOffer(targetId, connection) {
|
|
3003
|
-
const offer = await connection.createOffer();
|
|
3004
|
-
await connection.setLocalDescription(offer);
|
|
3005
|
-
this.signaling.sendSignal(targetId, { kind: "offer", sdp: offer });
|
|
3006
|
-
}
|
|
3007
|
-
async handleOffer(fromPeerId, sdp) {
|
|
3008
|
-
const existing = this.slots.get(fromPeerId);
|
|
3009
|
-
if (existing) {
|
|
3010
|
-
const localId = this.peerId;
|
|
3011
|
-
if (localId > fromPeerId) {
|
|
3012
|
-
return;
|
|
3013
|
-
}
|
|
3014
|
-
existing.channel?.close();
|
|
3015
|
-
existing.connection.close();
|
|
3016
|
-
this.slots.delete(fromPeerId);
|
|
3017
|
-
}
|
|
3018
|
-
const connection = new RTCPeerConnection({ iceServers: this.iceServers });
|
|
3019
|
-
const slot = { connection, channel: undefined, pendingSends: [] };
|
|
3020
|
-
this.slots.set(fromPeerId, slot);
|
|
3021
|
-
this.wireConnection(fromPeerId, connection);
|
|
3022
|
-
connection.ondatachannel = (event) => {
|
|
3023
|
-
slot.channel = event.channel;
|
|
3024
|
-
this.wireDataChannel(fromPeerId, event.channel);
|
|
3025
|
-
};
|
|
3026
|
-
await connection.setRemoteDescription(sdp);
|
|
3027
|
-
const answer = await connection.createAnswer();
|
|
3028
|
-
await connection.setLocalDescription(answer);
|
|
3029
|
-
this.signaling.sendSignal(fromPeerId, {
|
|
3030
|
-
kind: "answer",
|
|
3031
|
-
sdp: answer
|
|
3032
|
-
});
|
|
3033
|
-
}
|
|
3034
|
-
async handleAnswer(fromPeerId, sdp) {
|
|
3035
|
-
const slot = this.slots.get(fromPeerId);
|
|
3036
|
-
if (!slot)
|
|
3037
|
-
return;
|
|
3038
|
-
await slot.connection.setRemoteDescription(sdp);
|
|
3039
|
-
}
|
|
3040
|
-
async handleIceCandidate(fromPeerId, candidate) {
|
|
3041
|
-
const slot = this.slots.get(fromPeerId);
|
|
3042
|
-
if (!slot)
|
|
3043
|
-
return;
|
|
3044
|
-
try {
|
|
3045
|
-
await slot.connection.addIceCandidate(candidate);
|
|
3046
|
-
} catch {}
|
|
3047
|
-
}
|
|
3048
|
-
wireConnection(peerId, connection) {
|
|
3049
|
-
connection.onicecandidate = (event) => {
|
|
3050
|
-
if (event.candidate) {
|
|
3051
|
-
this.signaling.sendSignal(peerId, {
|
|
3052
|
-
kind: "ice",
|
|
3053
|
-
candidate: event.candidate.toJSON()
|
|
3054
|
-
});
|
|
3055
|
-
}
|
|
3056
|
-
};
|
|
3057
|
-
connection.onconnectionstatechange = () => {
|
|
3058
|
-
const state = connection.connectionState;
|
|
3059
|
-
if (state === "connected") {
|
|
3060
|
-
this.emit("peer-candidate", {
|
|
3061
|
-
peerId,
|
|
3062
|
-
peerMetadata: {}
|
|
3063
|
-
});
|
|
3064
|
-
} else if (state === "disconnected" || state === "failed" || state === "closed") {
|
|
3065
|
-
this.slots.delete(peerId);
|
|
3066
|
-
this.emit("peer-disconnected", { peerId });
|
|
3067
|
-
}
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
|
-
wireDataChannel(peerId, channel) {
|
|
3071
|
-
channel.onopen = () => {
|
|
3072
|
-
const slot = this.slots.get(peerId);
|
|
3073
|
-
if (!slot)
|
|
3074
|
-
return;
|
|
3075
|
-
for (const bytes of slot.pendingSends) {
|
|
3076
|
-
channel.send(bytes);
|
|
3077
|
-
}
|
|
3078
|
-
slot.pendingSends = [];
|
|
3079
|
-
};
|
|
3080
|
-
channel.onmessage = (event) => {
|
|
3081
|
-
const data = event.data;
|
|
3082
|
-
if (data instanceof ArrayBuffer) {
|
|
3083
|
-
this.dispatchMessage(new Uint8Array(data));
|
|
3084
|
-
} else if (data instanceof Uint8Array) {
|
|
3085
|
-
this.dispatchMessage(data);
|
|
3086
|
-
}
|
|
3087
|
-
};
|
|
3088
|
-
channel.onclose = () => {
|
|
3089
|
-
const slot = this.slots.get(peerId);
|
|
3090
|
-
if (slot?.channel === channel) {
|
|
3091
|
-
slot.channel = undefined;
|
|
3092
|
-
}
|
|
3093
|
-
};
|
|
3094
|
-
}
|
|
3095
|
-
dispatchMessage(bytes) {
|
|
3096
|
-
try {
|
|
3097
|
-
const message = this.deserialiseMessage(bytes);
|
|
3098
|
-
this.emit("message", message);
|
|
3099
|
-
} catch {}
|
|
3100
|
-
}
|
|
3101
|
-
serialiseMessage(message) {
|
|
3102
|
-
const headerObj = {
|
|
3103
|
-
type: message.type,
|
|
3104
|
-
senderId: message.senderId,
|
|
3105
|
-
targetId: message.targetId
|
|
3106
|
-
};
|
|
3107
|
-
if ("documentId" in message && message.documentId !== undefined) {
|
|
3108
|
-
headerObj["documentId"] = message.documentId;
|
|
3109
|
-
}
|
|
3110
|
-
const headerBytes = new TextEncoder().encode(JSON.stringify(headerObj));
|
|
3111
|
-
const dataBytes = "data" in message && message.data instanceof Uint8Array ? message.data : new Uint8Array(0);
|
|
3112
|
-
const size = 4 + headerBytes.length + dataBytes.length;
|
|
3113
|
-
const buffer = new ArrayBuffer(size);
|
|
3114
|
-
const out = new Uint8Array(buffer);
|
|
3115
|
-
const view = new DataView(buffer);
|
|
3116
|
-
view.setUint32(0, headerBytes.length, false);
|
|
3117
|
-
out.set(headerBytes, 4);
|
|
3118
|
-
out.set(dataBytes, 4 + headerBytes.length);
|
|
3119
|
-
return out;
|
|
3120
|
-
}
|
|
3121
|
-
deserialiseMessage(bytes) {
|
|
3122
|
-
if (bytes.length < 4) {
|
|
3123
|
-
throw new Error("MeshWebRTCAdapter: message too short to deserialise.");
|
|
3124
|
-
}
|
|
3125
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3126
|
-
const headerLen = view.getUint32(0, false);
|
|
3127
|
-
if (bytes.length < 4 + headerLen) {
|
|
3128
|
-
throw new Error("MeshWebRTCAdapter: message header truncated.");
|
|
3129
|
-
}
|
|
3130
|
-
const header = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + headerLen)));
|
|
3131
|
-
const data = bytes.slice(4 + headerLen);
|
|
3132
|
-
return { ...header, data };
|
|
3133
|
-
}
|
|
3134
|
-
}
|
|
3135
|
-
// src/shared/lib/pairing.ts
|
|
3136
|
-
var PAIRING_TOKEN_VERSION = 1;
|
|
3137
|
-
var PAIRING_TOKEN_MAGIC = new Uint8Array([80, 80, 84, 49]);
|
|
3138
|
-
var PAIRING_NONCE_BYTES = 16;
|
|
3139
|
-
var DEFAULT_PAIRING_TTL_MS = 10 * 60 * 1000;
|
|
3140
|
-
|
|
3141
|
-
class PairingError extends Error {
|
|
3142
|
-
code;
|
|
3143
|
-
constructor(message, code) {
|
|
3144
|
-
super(message);
|
|
3145
|
-
this.name = "PairingError";
|
|
3146
|
-
this.code = code;
|
|
3147
|
-
}
|
|
3148
|
-
}
|
|
3149
|
-
function createPairingToken(options) {
|
|
3150
|
-
const now = options.now ? options.now() : Date.now();
|
|
3151
|
-
const ttlMs = options.ttlMs ?? DEFAULT_PAIRING_TTL_MS;
|
|
3152
|
-
const documentKey = options.documentKey ?? generateDocumentKey();
|
|
3153
|
-
const nonce = randomBytes(PAIRING_NONCE_BYTES);
|
|
3154
|
-
return {
|
|
3155
|
-
version: PAIRING_TOKEN_VERSION,
|
|
3156
|
-
issuerPeerId: options.issuerPeerId,
|
|
3157
|
-
issuerPublicKey: options.identity.publicKey,
|
|
3158
|
-
documentKey,
|
|
3159
|
-
documentKeyId: options.documentKeyId,
|
|
3160
|
-
expiresAt: now + ttlMs,
|
|
3161
|
-
nonce
|
|
3162
|
-
};
|
|
3163
|
-
}
|
|
3164
|
-
function createPairingTokenWithFreshIdentity(args) {
|
|
3165
|
-
const identity = generateSigningKeyPair();
|
|
3166
|
-
const token = createPairingToken({
|
|
3167
|
-
identity,
|
|
3168
|
-
issuerPeerId: args.issuerPeerId,
|
|
3169
|
-
documentKeyId: args.documentKeyId,
|
|
3170
|
-
ttlMs: args.ttlMs,
|
|
3171
|
-
now: args.now
|
|
3172
|
-
});
|
|
3173
|
-
return { identity, token };
|
|
3174
|
-
}
|
|
3175
|
-
function isPairingTokenExpired(token, now) {
|
|
3176
|
-
const t = now ? now() : Date.now();
|
|
3177
|
-
return t >= token.expiresAt;
|
|
3178
|
-
}
|
|
3179
|
-
function applyPairingToken(token, keyring, options = {}) {
|
|
3180
|
-
if (isPairingTokenExpired(token, options.now)) {
|
|
3181
|
-
throw new PairingError(`Pairing token from ${token.issuerPeerId} expired at ${new Date(token.expiresAt).toISOString()}.`, "expired");
|
|
3182
|
-
}
|
|
3183
|
-
keyring.knownPeers.set(token.issuerPeerId, token.issuerPublicKey);
|
|
3184
|
-
keyring.documentKeys.set(token.documentKeyId, token.documentKey);
|
|
3185
|
-
}
|
|
3186
|
-
function serialisePairingToken(token) {
|
|
3187
|
-
validateForSerialisation(token);
|
|
3188
|
-
const issuerBytes = new TextEncoder().encode(token.issuerPeerId);
|
|
3189
|
-
const keyIdBytes = new TextEncoder().encode(token.documentKeyId);
|
|
3190
|
-
const total = PAIRING_TOKEN_MAGIC.length + 1 + 4 + issuerBytes.length + PUBLIC_KEY_BYTES + KEY_BYTES + 4 + keyIdBytes.length + 8 + PAIRING_NONCE_BYTES;
|
|
3191
|
-
const out = new Uint8Array(total);
|
|
3192
|
-
let offset = 0;
|
|
3193
|
-
out.set(PAIRING_TOKEN_MAGIC, offset);
|
|
3194
|
-
offset += PAIRING_TOKEN_MAGIC.length;
|
|
3195
|
-
out[offset] = token.version;
|
|
3196
|
-
offset += 1;
|
|
3197
|
-
const view = new DataView(out.buffer);
|
|
3198
|
-
view.setUint32(offset, issuerBytes.length, false);
|
|
3199
|
-
offset += 4;
|
|
3200
|
-
out.set(issuerBytes, offset);
|
|
3201
|
-
offset += issuerBytes.length;
|
|
3202
|
-
out.set(token.issuerPublicKey, offset);
|
|
3203
|
-
offset += PUBLIC_KEY_BYTES;
|
|
3204
|
-
out.set(token.documentKey, offset);
|
|
3205
|
-
offset += KEY_BYTES;
|
|
3206
|
-
view.setUint32(offset, keyIdBytes.length, false);
|
|
3207
|
-
offset += 4;
|
|
3208
|
-
out.set(keyIdBytes, offset);
|
|
3209
|
-
offset += keyIdBytes.length;
|
|
3210
|
-
view.setBigUint64(offset, BigInt(token.expiresAt), false);
|
|
3211
|
-
offset += 8;
|
|
3212
|
-
out.set(token.nonce, offset);
|
|
3213
|
-
offset += PAIRING_NONCE_BYTES;
|
|
3214
|
-
return out;
|
|
3215
|
-
}
|
|
3216
|
-
function parsePairingToken(bytes) {
|
|
3217
|
-
let offset = 0;
|
|
3218
|
-
if (bytes.length < PAIRING_TOKEN_MAGIC.length) {
|
|
3219
|
-
throw new PairingError(`Pairing token too short: ${bytes.length} bytes.`, "truncated");
|
|
3220
|
-
}
|
|
3221
|
-
for (let i = 0;i < PAIRING_TOKEN_MAGIC.length; i++) {
|
|
3222
|
-
if (bytes[offset + i] !== PAIRING_TOKEN_MAGIC[i]) {
|
|
3223
|
-
throw new PairingError(`Pairing token magic mismatch: not a Polly pairing token.`, "wrong-magic");
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
offset += PAIRING_TOKEN_MAGIC.length;
|
|
3227
|
-
if (bytes.length < offset + 1) {
|
|
3228
|
-
throw new PairingError("Pairing token truncated at version.", "truncated");
|
|
3229
|
-
}
|
|
3230
|
-
const version = bytes[offset];
|
|
3231
|
-
offset += 1;
|
|
3232
|
-
if (version !== PAIRING_TOKEN_VERSION) {
|
|
3233
|
-
throw new PairingError(`Unknown pairing token version: ${version}. This Polly build supports version ${PAIRING_TOKEN_VERSION}.`, "unknown-version");
|
|
3234
|
-
}
|
|
3235
|
-
if (bytes.length < offset + 4) {
|
|
3236
|
-
throw new PairingError("Pairing token truncated at issuer id length.", "truncated");
|
|
3237
|
-
}
|
|
3238
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3239
|
-
const issuerLen = view.getUint32(offset, false);
|
|
3240
|
-
offset += 4;
|
|
3241
|
-
if (bytes.length < offset + issuerLen) {
|
|
3242
|
-
throw new PairingError("Pairing token truncated at issuer id.", "truncated");
|
|
3243
|
-
}
|
|
3244
|
-
const issuerPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + issuerLen));
|
|
3245
|
-
offset += issuerLen;
|
|
3246
|
-
if (bytes.length < offset + PUBLIC_KEY_BYTES) {
|
|
3247
|
-
throw new PairingError("Pairing token truncated at public key.", "truncated");
|
|
3248
|
-
}
|
|
3249
|
-
const issuerPublicKey = bytes.slice(offset, offset + PUBLIC_KEY_BYTES);
|
|
3250
|
-
offset += PUBLIC_KEY_BYTES;
|
|
3251
|
-
if (bytes.length < offset + KEY_BYTES) {
|
|
3252
|
-
throw new PairingError("Pairing token truncated at document key.", "truncated");
|
|
3253
|
-
}
|
|
3254
|
-
const documentKey = bytes.slice(offset, offset + KEY_BYTES);
|
|
3255
|
-
offset += KEY_BYTES;
|
|
3256
|
-
if (bytes.length < offset + 4) {
|
|
3257
|
-
throw new PairingError("Pairing token truncated at document key id length.", "truncated");
|
|
3258
|
-
}
|
|
3259
|
-
const keyIdLen = view.getUint32(offset, false);
|
|
3260
|
-
offset += 4;
|
|
3261
|
-
if (bytes.length < offset + keyIdLen) {
|
|
3262
|
-
throw new PairingError("Pairing token truncated at document key id.", "truncated");
|
|
3263
|
-
}
|
|
3264
|
-
const documentKeyId = new TextDecoder().decode(bytes.subarray(offset, offset + keyIdLen));
|
|
3265
|
-
offset += keyIdLen;
|
|
3266
|
-
if (bytes.length < offset + 8) {
|
|
3267
|
-
throw new PairingError("Pairing token truncated at expiry.", "truncated");
|
|
3268
|
-
}
|
|
3269
|
-
const expiresAtBig = view.getBigUint64(offset, false);
|
|
3270
|
-
offset += 8;
|
|
3271
|
-
const expiresAt = Number(expiresAtBig);
|
|
3272
|
-
if (bytes.length < offset + PAIRING_NONCE_BYTES) {
|
|
3273
|
-
throw new PairingError("Pairing token truncated at nonce.", "truncated");
|
|
3274
|
-
}
|
|
3275
|
-
const nonce = bytes.slice(offset, offset + PAIRING_NONCE_BYTES);
|
|
3276
|
-
offset += PAIRING_NONCE_BYTES;
|
|
3277
|
-
return {
|
|
3278
|
-
version,
|
|
3279
|
-
issuerPeerId,
|
|
3280
|
-
issuerPublicKey,
|
|
3281
|
-
documentKey,
|
|
3282
|
-
documentKeyId,
|
|
3283
|
-
expiresAt,
|
|
3284
|
-
nonce
|
|
3285
|
-
};
|
|
3286
|
-
}
|
|
3287
|
-
function encodePairingToken(token) {
|
|
3288
|
-
const bytes = serialisePairingToken(token);
|
|
3289
|
-
let binary = "";
|
|
3290
|
-
for (const byte of bytes) {
|
|
3291
|
-
binary += String.fromCharCode(byte);
|
|
3292
|
-
}
|
|
3293
|
-
return btoa(binary);
|
|
3294
|
-
}
|
|
3295
|
-
function decodePairingToken(encoded) {
|
|
3296
|
-
let binary;
|
|
3297
|
-
try {
|
|
3298
|
-
binary = atob(encoded);
|
|
3299
|
-
} catch {
|
|
3300
|
-
throw new PairingError("Pairing token is not valid base64.", "wrong-magic");
|
|
3301
|
-
}
|
|
3302
|
-
const bytes = new Uint8Array(binary.length);
|
|
3303
|
-
for (let i = 0;i < binary.length; i++) {
|
|
3304
|
-
bytes[i] = binary.charCodeAt(i);
|
|
3305
|
-
}
|
|
3306
|
-
return parsePairingToken(bytes);
|
|
3307
|
-
}
|
|
3308
|
-
function validateForSerialisation(token) {
|
|
3309
|
-
if (token.issuerPublicKey.length !== PUBLIC_KEY_BYTES) {
|
|
3310
|
-
throw new PairingError(`Issuer public key must be ${PUBLIC_KEY_BYTES} bytes, got ${token.issuerPublicKey.length}.`, "invalid-public-key");
|
|
3311
|
-
}
|
|
3312
|
-
if (token.documentKey.length !== KEY_BYTES) {
|
|
3313
|
-
throw new PairingError(`Document key must be ${KEY_BYTES} bytes, got ${token.documentKey.length}.`, "invalid-document-key");
|
|
3314
|
-
}
|
|
3315
|
-
if (token.nonce.length !== PAIRING_NONCE_BYTES) {
|
|
3316
|
-
throw new PairingError(`Nonce must be ${PAIRING_NONCE_BYTES} bytes, got ${token.nonce.length}.`, "invalid-nonce");
|
|
3317
|
-
}
|
|
3318
|
-
}
|
|
3319
|
-
function randomBytes(n) {
|
|
3320
|
-
const out = new Uint8Array(n);
|
|
3321
|
-
crypto.getRandomValues(out);
|
|
3322
|
-
return out;
|
|
3323
|
-
}
|
|
3324
|
-
// src/shared/lib/peer-relay-adapter.ts
|
|
3325
|
-
import { Repo } from "@automerge/automerge-repo";
|
|
3326
|
-
import { WebSocketClientAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
3327
|
-
import { signal as signal6 } from "@preact/signals";
|
|
3328
|
-
function createPeerStateClient(options) {
|
|
3329
|
-
if (options.sign && !options.keyring) {
|
|
3330
|
-
throw new Error("Polly createPeerStateClient: { sign: true } requires a keyring. Pass { keyring: { identity, knownPeers, documentKeys: new Map(), revokedPeers: new Set() } } to enable signing.");
|
|
3331
|
-
}
|
|
3332
|
-
const adapter = new WebSocketClientAdapter(options.url, options.retryInterval);
|
|
3333
|
-
const connectionState = signal6("connecting");
|
|
3334
|
-
adapter.on("peer-candidate", () => {
|
|
3335
|
-
connectionState.value = "connected";
|
|
3336
|
-
});
|
|
3337
|
-
adapter.on("peer-disconnected", () => {
|
|
3338
|
-
connectionState.value = "disconnected";
|
|
3339
|
-
});
|
|
3340
|
-
adapter.on("close", () => {
|
|
3341
|
-
connectionState.value = "disconnected";
|
|
3342
|
-
});
|
|
3343
|
-
const networkAdapter = options.sign && options.keyring ? new MeshNetworkAdapter({
|
|
3344
|
-
base: adapter,
|
|
3345
|
-
keyring: options.keyring,
|
|
3346
|
-
encryptionEnabled: false
|
|
3347
|
-
}) : adapter;
|
|
3348
|
-
const repo = new Repo({
|
|
3349
|
-
network: [networkAdapter],
|
|
3350
|
-
...options.storage !== undefined && { storage: options.storage }
|
|
3351
|
-
});
|
|
3352
|
-
return {
|
|
3353
|
-
repo,
|
|
3354
|
-
connectionState,
|
|
3355
|
-
adapter,
|
|
3356
|
-
signEnabled: options.sign === true,
|
|
3357
|
-
close: async () => {
|
|
3358
|
-
await repo.shutdown();
|
|
3359
|
-
}
|
|
3360
|
-
};
|
|
3361
|
-
}
|
|
3362
|
-
// src/shared/lib/peer-repo-server.ts
|
|
3363
|
-
import { Repo as Repo2 } from "@automerge/automerge-repo";
|
|
3364
|
-
import { WebSocketServerAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
3365
|
-
import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs";
|
|
3366
|
-
import * as ws from "ws";
|
|
3367
|
-
async function createPeerRepoServer(options) {
|
|
3368
|
-
const wss = await (options.webSocketServer ? Promise.resolve(options.webSocketServer) : new Promise((resolve, reject) => {
|
|
3369
|
-
const created = new ws.WebSocketServer({
|
|
3370
|
-
port: options.port,
|
|
3371
|
-
...options.host !== undefined && { host: options.host }
|
|
3372
|
-
}, () => resolve(created));
|
|
3373
|
-
created.once("error", reject);
|
|
3374
|
-
}));
|
|
3375
|
-
const adapter = new WebSocketServerAdapter(wss);
|
|
3376
|
-
const storage2 = new NodeFSStorageAdapter(options.storagePath);
|
|
3377
|
-
const repo = new Repo2({
|
|
3378
|
-
network: [adapter],
|
|
3379
|
-
storage: storage2
|
|
3380
|
-
});
|
|
3381
|
-
await repo.storageId();
|
|
3382
|
-
return {
|
|
3383
|
-
repo,
|
|
3384
|
-
webSocketServer: wss,
|
|
3385
|
-
adapter,
|
|
3386
|
-
storage: storage2,
|
|
3387
|
-
close: async () => {
|
|
3388
|
-
for (const client of wss.clients) {
|
|
3389
|
-
try {
|
|
3390
|
-
client.terminate();
|
|
3391
|
-
} catch {}
|
|
3392
|
-
}
|
|
3393
|
-
repo.shutdown();
|
|
3394
|
-
try {
|
|
3395
|
-
wss.close();
|
|
3396
|
-
} catch {}
|
|
3397
|
-
}
|
|
3398
|
-
};
|
|
3399
|
-
}
|
|
3400
|
-
// src/shared/lib/peer-state.ts
|
|
3401
|
-
var keyMapsByRepo2 = new WeakMap;
|
|
3402
|
-
var signingEnabledRepos = new WeakSet;
|
|
3403
|
-
var defaultRepo2;
|
|
3404
|
-
function configurePeerState(repo, options) {
|
|
3405
|
-
defaultRepo2 = repo;
|
|
3406
|
-
keyMapsByRepo2.set(repo, new Map);
|
|
3407
|
-
if (options?.signEnabled) {
|
|
3408
|
-
signingEnabledRepos.add(repo);
|
|
3409
|
-
}
|
|
3410
|
-
}
|
|
3411
|
-
function resetPeerState() {
|
|
3412
|
-
defaultRepo2 = undefined;
|
|
3413
|
-
}
|
|
3414
|
-
function resolveRepo2(option) {
|
|
3415
|
-
const repo = option ?? defaultRepo2;
|
|
3416
|
-
if (!repo) {
|
|
3417
|
-
throw new Error("Polly $peerState: no Repo configured. Call configurePeerState(repo) at startup or pass { repo } in the primitive options.");
|
|
3418
|
-
}
|
|
3419
|
-
return repo;
|
|
3420
|
-
}
|
|
3421
|
-
function getKeyMap2(repo) {
|
|
3422
|
-
let map = keyMapsByRepo2.get(repo);
|
|
3423
|
-
if (!map) {
|
|
3424
|
-
map = new Map;
|
|
3425
|
-
keyMapsByRepo2.set(repo, map);
|
|
3426
|
-
}
|
|
3427
|
-
return map;
|
|
3428
|
-
}
|
|
3429
|
-
function validateSignOption(options, repo) {
|
|
3430
|
-
if (!options.sign)
|
|
3431
|
-
return;
|
|
3432
|
-
if (!signingEnabledRepos.has(repo)) {
|
|
3433
|
-
throw new Error("Polly $peerState: { sign: true } was passed to the primitive but the configured Repo does not have signing enabled. " + "Pass { sign: true, keyring: ... } to createPeerStateClient to enable signing at the transport level, " + "then call configurePeerState(client.repo, { signEnabled: true }).");
|
|
3434
|
-
}
|
|
3435
|
-
}
|
|
3436
|
-
function buildHandleFactory2(repo, key, initialDoc) {
|
|
3437
|
-
return async () => {
|
|
3438
|
-
const map = getKeyMap2(repo);
|
|
3439
|
-
const existingId = map.get(key);
|
|
3440
|
-
if (existingId !== undefined) {
|
|
3441
|
-
return repo.find(existingId);
|
|
3442
|
-
}
|
|
3443
|
-
const handle = repo.create(initialDoc);
|
|
3444
|
-
map.set(key, handle.documentId);
|
|
3445
|
-
return handle;
|
|
3446
|
-
};
|
|
3447
|
-
}
|
|
3448
|
-
function $peerState(key, initialValue, options = {}) {
|
|
3449
|
-
const repo = resolveRepo2(options.repo);
|
|
3450
|
-
validateSignOption(options, repo);
|
|
3451
|
-
return $crdtState({
|
|
3452
|
-
key,
|
|
3453
|
-
primitive: "peerState",
|
|
3454
|
-
initialValue,
|
|
3455
|
-
getHandle: buildHandleFactory2(repo, key, initialValue),
|
|
3456
|
-
schemaVersion: options.schemaVersion,
|
|
3457
|
-
migrations: options.migrations,
|
|
3458
|
-
access: options.access
|
|
3459
|
-
});
|
|
3460
|
-
}
|
|
3461
|
-
function $peerText(key, initialValue, options = {}) {
|
|
3462
|
-
const repo = resolveRepo2(options.repo);
|
|
3463
|
-
validateSignOption(options, repo);
|
|
3464
|
-
return $crdtText(key, initialValue, {
|
|
3465
|
-
primitive: "peerState",
|
|
3466
|
-
getHandle: buildHandleFactory2(repo, key, { text: initialValue }),
|
|
3467
|
-
schemaVersion: options.schemaVersion,
|
|
3468
|
-
migrations: options.migrations,
|
|
3469
|
-
access: options.access
|
|
3470
|
-
});
|
|
3471
|
-
}
|
|
3472
|
-
function $peerCounter(key, initialValue, options = {}) {
|
|
3473
|
-
const repo = resolveRepo2(options.repo);
|
|
3474
|
-
validateSignOption(options, repo);
|
|
3475
|
-
return $crdtCounter(key, initialValue, {
|
|
3476
|
-
primitive: "peerState",
|
|
3477
|
-
getHandle: buildHandleFactory2(repo, key, {}),
|
|
3478
|
-
schemaVersion: options.schemaVersion,
|
|
3479
|
-
migrations: options.migrations,
|
|
3480
|
-
access: options.access
|
|
3481
|
-
});
|
|
3482
|
-
}
|
|
3483
|
-
function $peerList(key, initialValue, options = {}) {
|
|
3484
|
-
const repo = resolveRepo2(options.repo);
|
|
3485
|
-
validateSignOption(options, repo);
|
|
3486
|
-
return $crdtList(key, initialValue, {
|
|
3487
|
-
primitive: "peerState",
|
|
3488
|
-
getHandle: buildHandleFactory2(repo, key, { items: initialValue }),
|
|
3489
|
-
schemaVersion: options.schemaVersion,
|
|
3490
|
-
migrations: options.migrations,
|
|
3491
|
-
access: options.access
|
|
3492
|
-
});
|
|
3493
|
-
}
|
|
3494
|
-
// src/shared/lib/revocation.ts
|
|
3495
|
-
var REVOCATION_RECORD_VERSION = 1;
|
|
3496
|
-
var REVOCATION_MAGIC = new Uint8Array([80, 82, 86, 49]);
|
|
3497
|
-
|
|
3498
|
-
class RevocationError extends Error {
|
|
3499
|
-
code;
|
|
3500
|
-
constructor(message, code) {
|
|
3501
|
-
super(message);
|
|
3502
|
-
this.name = "RevocationError";
|
|
3503
|
-
this.code = code;
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
function createRevocation(options) {
|
|
3507
|
-
const now = options.now ? options.now() : Date.now();
|
|
3508
|
-
return {
|
|
3509
|
-
version: REVOCATION_RECORD_VERSION,
|
|
3510
|
-
issuerPeerId: options.issuerPeerId,
|
|
3511
|
-
revokedPeerId: options.revokedPeerId,
|
|
3512
|
-
issuedAt: now,
|
|
3513
|
-
...options.reason === undefined ? {} : { reason: options.reason }
|
|
3514
|
-
};
|
|
3515
|
-
}
|
|
3516
|
-
function applyRevocation(record, keyring) {
|
|
3517
|
-
keyring.revokedPeers.add(record.revokedPeerId);
|
|
3518
|
-
}
|
|
3519
|
-
function revokePeerLocally(peerId, keyring) {
|
|
3520
|
-
keyring.revokedPeers.add(peerId);
|
|
3521
|
-
}
|
|
3522
|
-
function serialiseRevocationPayload(record) {
|
|
3523
|
-
const issuerBytes = new TextEncoder().encode(record.issuerPeerId);
|
|
3524
|
-
const revokedBytes = new TextEncoder().encode(record.revokedPeerId);
|
|
3525
|
-
const reasonBytes = new TextEncoder().encode(record.reason ?? "");
|
|
3526
|
-
const total = REVOCATION_MAGIC.length + 1 + 4 + issuerBytes.length + 4 + revokedBytes.length + 8 + 4 + reasonBytes.length;
|
|
3527
|
-
const out = new Uint8Array(total);
|
|
3528
|
-
let offset = 0;
|
|
3529
|
-
out.set(REVOCATION_MAGIC, offset);
|
|
3530
|
-
offset += REVOCATION_MAGIC.length;
|
|
3531
|
-
out[offset] = record.version;
|
|
3532
|
-
offset += 1;
|
|
3533
|
-
const view = new DataView(out.buffer);
|
|
3534
|
-
view.setUint32(offset, issuerBytes.length, false);
|
|
3535
|
-
offset += 4;
|
|
3536
|
-
out.set(issuerBytes, offset);
|
|
3537
|
-
offset += issuerBytes.length;
|
|
3538
|
-
view.setUint32(offset, revokedBytes.length, false);
|
|
3539
|
-
offset += 4;
|
|
3540
|
-
out.set(revokedBytes, offset);
|
|
3541
|
-
offset += revokedBytes.length;
|
|
3542
|
-
view.setBigUint64(offset, BigInt(record.issuedAt), false);
|
|
3543
|
-
offset += 8;
|
|
3544
|
-
view.setUint32(offset, reasonBytes.length, false);
|
|
3545
|
-
offset += 4;
|
|
3546
|
-
out.set(reasonBytes, offset);
|
|
3547
|
-
return out;
|
|
3548
|
-
}
|
|
3549
|
-
function parseRevocationPayload(bytes) {
|
|
3550
|
-
let offset = 0;
|
|
3551
|
-
if (bytes.length < REVOCATION_MAGIC.length) {
|
|
3552
|
-
throw new RevocationError("Revocation record too short for magic.", "truncated");
|
|
3553
|
-
}
|
|
3554
|
-
for (let i = 0;i < REVOCATION_MAGIC.length; i++) {
|
|
3555
|
-
if (bytes[offset + i] !== REVOCATION_MAGIC[i]) {
|
|
3556
|
-
throw new RevocationError("Revocation record magic mismatch.", "wrong-magic");
|
|
3557
|
-
}
|
|
3558
|
-
}
|
|
3559
|
-
offset += REVOCATION_MAGIC.length;
|
|
3560
|
-
if (bytes.length < offset + 1) {
|
|
3561
|
-
throw new RevocationError("Revocation record truncated at version.", "truncated");
|
|
3562
|
-
}
|
|
3563
|
-
const version = bytes[offset];
|
|
3564
|
-
offset += 1;
|
|
3565
|
-
if (version !== REVOCATION_RECORD_VERSION) {
|
|
3566
|
-
throw new RevocationError(`Unknown revocation record version: ${version}.`, "unknown-version");
|
|
3567
|
-
}
|
|
3568
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3569
|
-
if (bytes.length < offset + 4) {
|
|
3570
|
-
throw new RevocationError("Revocation record truncated at issuer length.", "truncated");
|
|
3571
|
-
}
|
|
3572
|
-
const issuerLen = view.getUint32(offset, false);
|
|
3573
|
-
offset += 4;
|
|
3574
|
-
if (bytes.length < offset + issuerLen) {
|
|
3575
|
-
throw new RevocationError("Revocation record truncated at issuer id.", "truncated");
|
|
3576
|
-
}
|
|
3577
|
-
const issuerPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + issuerLen));
|
|
3578
|
-
offset += issuerLen;
|
|
3579
|
-
if (bytes.length < offset + 4) {
|
|
3580
|
-
throw new RevocationError("Revocation record truncated at revoked id length.", "truncated");
|
|
3581
|
-
}
|
|
3582
|
-
const revokedLen = view.getUint32(offset, false);
|
|
3583
|
-
offset += 4;
|
|
3584
|
-
if (bytes.length < offset + revokedLen) {
|
|
3585
|
-
throw new RevocationError("Revocation record truncated at revoked id.", "truncated");
|
|
3586
|
-
}
|
|
3587
|
-
const revokedPeerId = new TextDecoder().decode(bytes.subarray(offset, offset + revokedLen));
|
|
3588
|
-
offset += revokedLen;
|
|
3589
|
-
if (bytes.length < offset + 8) {
|
|
3590
|
-
throw new RevocationError("Revocation record truncated at issuedAt.", "truncated");
|
|
3591
|
-
}
|
|
3592
|
-
const issuedAt = Number(view.getBigUint64(offset, false));
|
|
3593
|
-
offset += 8;
|
|
3594
|
-
if (bytes.length < offset + 4) {
|
|
3595
|
-
throw new RevocationError("Revocation record truncated at reason length.", "truncated");
|
|
3596
|
-
}
|
|
3597
|
-
const reasonLen = view.getUint32(offset, false);
|
|
3598
|
-
offset += 4;
|
|
3599
|
-
if (bytes.length < offset + reasonLen) {
|
|
3600
|
-
throw new RevocationError("Revocation record truncated at reason.", "truncated");
|
|
3601
|
-
}
|
|
3602
|
-
const reason = new TextDecoder().decode(bytes.subarray(offset, offset + reasonLen));
|
|
3603
|
-
offset += reasonLen;
|
|
3604
|
-
return {
|
|
3605
|
-
version,
|
|
3606
|
-
issuerPeerId,
|
|
3607
|
-
revokedPeerId,
|
|
3608
|
-
issuedAt,
|
|
3609
|
-
...reason ? { reason } : {}
|
|
3610
|
-
};
|
|
3611
|
-
}
|
|
3612
|
-
function encodeRevocation(record, issuer) {
|
|
3613
|
-
const payload = serialiseRevocationPayload(record);
|
|
3614
|
-
const envelope = signEnvelope(payload, record.issuerPeerId, issuer.secretKey);
|
|
3615
|
-
return encodeSignedEnvelope(envelope);
|
|
3616
|
-
}
|
|
3617
|
-
function decodeRevocation(bytes, keyring) {
|
|
3618
|
-
const envelope = decodeSignedEnvelope(bytes);
|
|
3619
|
-
const issuerKey = keyring.knownPeers.get(envelope.senderId);
|
|
3620
|
-
if (!issuerKey) {
|
|
3621
|
-
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the local keyring.`, "unknown-issuer");
|
|
3622
|
-
}
|
|
3623
|
-
if (keyring.revocationAuthority !== undefined && keyring.revocationAuthority.size > 0 && !keyring.revocationAuthority.has(envelope.senderId)) {
|
|
3624
|
-
throw new RevocationError(`Revocation issuer ${envelope.senderId} is not in the keyring's revocation authority set.`, "unauthorised-issuer");
|
|
3625
|
-
}
|
|
3626
|
-
let payloadBytes;
|
|
3627
|
-
try {
|
|
3628
|
-
payloadBytes = openEnvelope2(envelope, issuerKey);
|
|
3629
|
-
} catch {
|
|
3630
|
-
throw new RevocationError(`Revocation signature failed verification for issuer ${envelope.senderId}.`, "signature-invalid");
|
|
3631
|
-
}
|
|
3632
|
-
const record = parseRevocationPayload(payloadBytes);
|
|
3633
|
-
if (record.issuerPeerId !== envelope.senderId) {
|
|
3634
|
-
throw new RevocationError(`Revocation payload claims issuer ${record.issuerPeerId} but the envelope was signed by ${envelope.senderId}.`, "not-signed-by-issuer");
|
|
3635
|
-
}
|
|
3636
|
-
return record;
|
|
3637
|
-
}
|
|
3638
2065
|
// src/shared/lib/validation.ts
|
|
3639
2066
|
function checkPrimitiveType(val, type) {
|
|
3640
2067
|
if (type === "array")
|
|
@@ -3685,116 +2112,49 @@ function validatePartial(_validator) {
|
|
|
3685
2112
|
};
|
|
3686
2113
|
}
|
|
3687
2114
|
export {
|
|
3688
|
-
verify,
|
|
3689
2115
|
validateShape,
|
|
3690
2116
|
validatePartial,
|
|
3691
2117
|
validateEnum,
|
|
3692
2118
|
validateArray,
|
|
3693
|
-
signingKeyPairFromSecret,
|
|
3694
|
-
sign,
|
|
3695
2119
|
settings,
|
|
3696
|
-
serialisePairingToken,
|
|
3697
2120
|
runInContext,
|
|
3698
|
-
revokePeerLocally,
|
|
3699
|
-
resetPeerState,
|
|
3700
|
-
resetMeshState,
|
|
3701
2121
|
registerConstraints,
|
|
3702
2122
|
registerConstraint,
|
|
3703
2123
|
readOnlyExcept,
|
|
3704
2124
|
quickTest,
|
|
3705
2125
|
publicAccess,
|
|
3706
|
-
parsePairingToken,
|
|
3707
2126
|
ownerAccess,
|
|
3708
2127
|
or,
|
|
3709
2128
|
onlyPeer,
|
|
3710
2129
|
not,
|
|
3711
2130
|
nobody,
|
|
3712
|
-
migratePrimitive,
|
|
3713
2131
|
isRuntimeConstraintsEnabled,
|
|
3714
|
-
isPairingTokenExpired,
|
|
3715
2132
|
isBlobRef,
|
|
3716
2133
|
groupAccess,
|
|
3717
2134
|
getMessageBus,
|
|
3718
|
-
getDocVersion,
|
|
3719
|
-
generateSigningKeyPair,
|
|
3720
|
-
generateDocumentKey,
|
|
3721
|
-
encrypt,
|
|
3722
|
-
encodeRevocation,
|
|
3723
|
-
encodePairingToken,
|
|
3724
|
-
decryptOrThrow,
|
|
3725
|
-
decrypt,
|
|
3726
|
-
decodeRevocation,
|
|
3727
|
-
decodePairingToken,
|
|
3728
2135
|
createTestSuite,
|
|
3729
|
-
createRevocation,
|
|
3730
|
-
createPeerStateClient,
|
|
3731
|
-
createPeerRepoServer,
|
|
3732
|
-
createPairingTokenWithFreshIdentity,
|
|
3733
|
-
createPairingToken,
|
|
3734
2136
|
createContext,
|
|
3735
2137
|
createChromeAdapters2 as createChromeAdapters,
|
|
3736
2138
|
createBlobRef,
|
|
3737
|
-
configurePeerState,
|
|
3738
|
-
configureMeshState,
|
|
3739
2139
|
computeBlobHash,
|
|
3740
2140
|
clearConstraints,
|
|
3741
2141
|
checkPreconditions,
|
|
3742
2142
|
checkPostconditions,
|
|
3743
|
-
checkOpVersion,
|
|
3744
|
-
assertOpVersion,
|
|
3745
|
-
applyRevocation,
|
|
3746
|
-
applyPairingToken,
|
|
3747
2143
|
anyone,
|
|
3748
2144
|
anyOfPeers,
|
|
3749
2145
|
and,
|
|
3750
2146
|
TimeoutError,
|
|
3751
2147
|
TestRunner,
|
|
3752
|
-
SigningError,
|
|
3753
|
-
SchemaVersionError,
|
|
3754
|
-
SIGNATURE_BYTES as SIGNING_SIGNATURE_BYTES,
|
|
3755
|
-
SECRET_KEY_BYTES as SIGNING_SECRET_KEY_BYTES,
|
|
3756
|
-
PUBLIC_KEY_BYTES as SIGNING_PUBLIC_KEY_BYTES,
|
|
3757
|
-
SCHEMA_VERSION_FIELD,
|
|
3758
|
-
RevocationError,
|
|
3759
|
-
REVOCATION_RECORD_VERSION,
|
|
3760
|
-
REVOCATION_MAGIC,
|
|
3761
|
-
PrimitiveCollisionError,
|
|
3762
|
-
PairingError,
|
|
3763
|
-
PAIRING_TOKEN_VERSION,
|
|
3764
|
-
PAIRING_NONCE_BYTES,
|
|
3765
|
-
MigrationError,
|
|
3766
2148
|
MessageBus,
|
|
3767
|
-
MeshWebRTCAdapter,
|
|
3768
|
-
MeshSignalingClient,
|
|
3769
|
-
MeshNetworkAdapter,
|
|
3770
2149
|
HandlerError,
|
|
3771
2150
|
ExtensionError,
|
|
3772
2151
|
ErrorHandler,
|
|
3773
|
-
EncryptionError,
|
|
3774
|
-
TAG_BYTES as ENCRYPTION_TAG_BYTES,
|
|
3775
|
-
NONCE_BYTES as ENCRYPTION_NONCE_BYTES,
|
|
3776
|
-
KEY_BYTES as ENCRYPTION_KEY_BYTES,
|
|
3777
|
-
DEFAULT_PAIRING_TTL_MS,
|
|
3778
|
-
DEFAULT_MESH_KEY_ID,
|
|
3779
|
-
DEFAULT_ICE_SERVERS,
|
|
3780
2152
|
ConnectionError,
|
|
3781
2153
|
$syncedState,
|
|
3782
2154
|
$state,
|
|
3783
2155
|
$sharedState,
|
|
3784
2156
|
$resource,
|
|
3785
|
-
$persistedState
|
|
3786
|
-
$peerText,
|
|
3787
|
-
$peerState,
|
|
3788
|
-
$peerList,
|
|
3789
|
-
$peerCounter,
|
|
3790
|
-
$meshText,
|
|
3791
|
-
$meshState,
|
|
3792
|
-
$meshList,
|
|
3793
|
-
$meshCounter,
|
|
3794
|
-
$crdtText,
|
|
3795
|
-
$crdtState,
|
|
3796
|
-
$crdtList,
|
|
3797
|
-
$crdtCounter
|
|
2157
|
+
$persistedState
|
|
3798
2158
|
};
|
|
3799
2159
|
|
|
3800
|
-
//# debugId=
|
|
2160
|
+
//# debugId=0888B09580CA471964756E2164756E21
|