@fairfox/polly 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +11 -0
  2. package/dist/src/background/index.js +22 -12
  3. package/dist/src/background/index.js.map +8 -8
  4. package/dist/src/background/message-router.js +22 -12
  5. package/dist/src/background/message-router.js.map +8 -8
  6. package/dist/src/client/index.js +187 -154
  7. package/dist/src/client/index.js.map +4 -4
  8. package/dist/src/elysia/index.d.ts +2 -0
  9. package/dist/src/elysia/index.js +195 -25
  10. package/dist/src/elysia/index.js.map +8 -5
  11. package/dist/src/elysia/peer-repo-plugin.d.ts +79 -0
  12. package/dist/src/elysia/plugin.d.ts +3 -3
  13. package/dist/src/elysia/signaling-server-plugin.d.ts +121 -0
  14. package/dist/src/index.d.ts +36 -0
  15. package/dist/src/index.js +1752 -13
  16. package/dist/src/index.js.map +31 -13
  17. package/dist/src/shared/adapters/index.js +22 -12
  18. package/dist/src/shared/adapters/index.js.map +7 -7
  19. package/dist/src/shared/lib/_client-only.d.ts +38 -0
  20. package/dist/src/shared/lib/access.d.ts +124 -0
  21. package/dist/src/shared/lib/blob-ref.d.ts +72 -0
  22. package/dist/src/shared/lib/context-helpers.js +22 -12
  23. package/dist/src/shared/lib/context-helpers.js.map +8 -8
  24. package/dist/src/shared/lib/crdt-specialised.d.ts +129 -0
  25. package/dist/src/shared/lib/crdt-state.d.ts +86 -0
  26. package/dist/src/shared/lib/encryption.d.ts +117 -0
  27. package/dist/src/shared/lib/errors.js +19 -9
  28. package/dist/src/shared/lib/errors.js.map +2 -2
  29. package/dist/src/shared/lib/mesh-network-adapter.d.ts +130 -0
  30. package/dist/src/shared/lib/mesh-signaling-client.d.ts +85 -0
  31. package/dist/src/shared/lib/mesh-state.d.ts +102 -0
  32. package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +132 -0
  33. package/dist/src/shared/lib/message-bus.js +22 -12
  34. package/dist/src/shared/lib/message-bus.js.map +8 -8
  35. package/dist/src/shared/lib/migrate-primitive.d.ts +100 -0
  36. package/dist/src/shared/lib/pairing.d.ts +170 -0
  37. package/dist/src/shared/lib/peer-relay-adapter.d.ts +80 -0
  38. package/dist/src/shared/lib/peer-repo-server.d.ts +83 -0
  39. package/dist/src/shared/lib/peer-state.d.ts +117 -0
  40. package/dist/src/shared/lib/primitive-registry.d.ts +88 -0
  41. package/dist/src/shared/lib/resource.js +22 -12
  42. package/dist/src/shared/lib/resource.js.map +5 -5
  43. package/dist/src/shared/lib/revocation.d.ts +126 -0
  44. package/dist/src/shared/lib/schema-version.d.ts +129 -0
  45. package/dist/src/shared/lib/signing.d.ts +118 -0
  46. package/dist/src/shared/lib/state.js +22 -12
  47. package/dist/src/shared/lib/state.js.map +5 -5
  48. package/dist/src/shared/lib/test-helpers.js +19 -9
  49. package/dist/src/shared/lib/test-helpers.js.map +2 -2
  50. package/dist/src/shared/state/app-state.js +22 -12
  51. package/dist/src/shared/state/app-state.js.map +6 -6
  52. package/dist/src/shared/types/messages.js +19 -9
  53. package/dist/src/shared/types/messages.js.map +2 -2
  54. package/dist/tools/init/src/cli.js +6 -2
  55. package/dist/tools/init/src/cli.js.map +3 -3
  56. package/dist/tools/quality/src/index.js +177 -0
  57. package/dist/tools/quality/src/index.js.map +10 -0
  58. package/dist/tools/test/src/adapters/index.d.ts +2 -2
  59. package/dist/tools/test/src/adapters/index.js +19 -9
  60. package/dist/tools/test/src/adapters/index.js.map +5 -5
  61. package/dist/tools/test/src/browser/harness.d.ts +80 -0
  62. package/dist/tools/test/src/browser/index.d.ts +32 -0
  63. package/dist/tools/test/src/browser/index.js +243 -0
  64. package/dist/tools/test/src/browser/index.js.map +10 -0
  65. package/dist/tools/test/src/browser/run.d.ts +26 -0
  66. package/dist/tools/test/src/index.js +19 -9
  67. package/dist/tools/test/src/index.js.map +5 -5
  68. package/dist/tools/test/src/test-utils.js +19 -9
  69. package/dist/tools/test/src/test-utils.js.map +2 -2
  70. package/dist/tools/verify/specs/tla/MeshState.cfg +21 -0
  71. package/dist/tools/verify/specs/tla/MeshState.tla +247 -0
  72. package/dist/tools/verify/specs/tla/PeerState.cfg +27 -0
  73. package/dist/tools/verify/specs/tla/PeerState.tla +238 -0
  74. package/dist/tools/verify/specs/tla/README.md +27 -3
  75. package/dist/tools/verify/src/cli.js +10 -6
  76. package/dist/tools/verify/src/cli.js.map +10 -10
  77. package/dist/tools/verify/src/config.js +19 -9
  78. package/dist/tools/verify/src/config.js.map +2 -2
  79. package/dist/tools/visualize/src/cli.js +6 -2
  80. package/dist/tools/visualize/src/cli.js.map +8 -8
  81. package/package.json +52 -12
package/dist/src/index.js CHANGED
@@ -2,27 +2,37 @@ var __defProp = Object.defineProperty;
2
2
  var __getOwnPropNames = Object.getOwnPropertyNames;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __hasOwnProp = Object.prototype.hasOwnProperty;
5
- var __moduleCache = /* @__PURE__ */ new WeakMap;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
6
8
  var __toCommonJS = (from) => {
7
- var entry = __moduleCache.get(from), desc;
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
8
10
  if (entry)
9
11
  return entry;
10
12
  entry = __defProp({}, "__esModule", { value: true });
11
- if (from && typeof from === "object" || typeof from === "function")
12
- __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
- get: () => from[key],
14
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
- }));
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
16
21
  __moduleCache.set(from, entry);
17
22
  return entry;
18
23
  };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
19
29
  var __export = (target, all) => {
20
30
  for (var name in all)
21
31
  __defProp(target, name, {
22
32
  get: all[name],
23
33
  enumerable: true,
24
34
  configurable: true,
25
- set: (newValue) => all[name] = () => newValue
35
+ set: __exportSetter.bind(all, name)
26
36
  });
27
37
  };
28
38
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -259,7 +269,9 @@ class BroadcastChannelSyncAdapter {
259
269
  channel = null;
260
270
  listeners = [];
261
271
  constructor(channelName = "polly-sync") {
262
- if (typeof BroadcastChannel !== "undefined") {
272
+ if (typeof BroadcastChannel === "undefined") {
273
+ console.warn("[SyncAdapter] BroadcastChannel not available");
274
+ } else {
263
275
  this.channel = new BroadcastChannel(channelName);
264
276
  this.channel.onmessage = (event) => {
265
277
  if (event.data.type === "STATE_SYNC") {
@@ -268,8 +280,6 @@ class BroadcastChannelSyncAdapter {
268
280
  });
269
281
  }
270
282
  };
271
- } else {
272
- console.warn("[SyncAdapter] BroadcastChannel not available");
273
283
  }
274
284
  }
275
285
  broadcast(message) {
@@ -1973,10 +1983,1658 @@ var uiState = signal3({
1973
1983
  sidebarOpen: false,
1974
1984
  selectedPanel: "main"
1975
1985
  });
1986
+ // src/shared/lib/access.ts
1987
+ function anyone() {
1988
+ return () => true;
1989
+ }
1990
+ function nobody() {
1991
+ return () => false;
1992
+ }
1993
+ function onlyPeer(peerId) {
1994
+ return (identity) => identity.peerId === peerId;
1995
+ }
1996
+ function anyOfPeers(peerIds) {
1997
+ const set = new Set(peerIds);
1998
+ return (identity) => set.has(identity.peerId);
1999
+ }
2000
+ function and(a, b) {
2001
+ return (identity) => a(identity) && b(identity);
2002
+ }
2003
+ function or(a, b) {
2004
+ return (identity) => a(identity) || b(identity);
2005
+ }
2006
+ function not(a) {
2007
+ return (identity) => !a(identity);
2008
+ }
2009
+ function publicAccess() {
2010
+ return {
2011
+ read: anyone(),
2012
+ write: anyone()
2013
+ };
2014
+ }
2015
+ function ownerAccess(peerId) {
2016
+ const pred = onlyPeer(peerId);
2017
+ return { read: pred, write: pred };
2018
+ }
2019
+ function groupAccess(peerIds) {
2020
+ const pred = anyOfPeers(peerIds);
2021
+ return { read: pred, write: pred };
2022
+ }
2023
+ function readOnlyExcept(writer) {
2024
+ return {
2025
+ read: anyone(),
2026
+ write: writer
2027
+ };
2028
+ }
2029
+ // src/shared/lib/blob-ref.ts
2030
+ function isBlobRef(value) {
2031
+ if (typeof value !== "object" || value === null)
2032
+ return false;
2033
+ const v = value;
2034
+ 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";
2035
+ }
2036
+ async function computeBlobHash(bytes) {
2037
+ const buffer = new ArrayBuffer(bytes.byteLength);
2038
+ const copy = new Uint8Array(buffer);
2039
+ copy.set(bytes);
2040
+ const digest = await crypto.subtle.digest("SHA-256", buffer);
2041
+ const view = new Uint8Array(digest);
2042
+ let hex = "";
2043
+ for (const byte of view) {
2044
+ hex += byte.toString(16).padStart(2, "0");
2045
+ }
2046
+ return hex;
2047
+ }
2048
+ async function createBlobRef({
2049
+ bytes,
2050
+ filename,
2051
+ mimeType
2052
+ }) {
2053
+ const hash = await computeBlobHash(bytes);
2054
+ return {
2055
+ hash,
2056
+ size: bytes.byteLength,
2057
+ filename,
2058
+ mimeType
2059
+ };
2060
+ }
1976
2061
 
1977
2062
  // src/index.ts
1978
2063
  init_constraints();
1979
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
+ }
1980
3638
  // src/shared/lib/validation.ts
1981
3639
  function checkPrimitiveType(val, type) {
1982
3640
  if (type === "array")
@@ -2027,35 +3685,116 @@ function validatePartial(_validator) {
2027
3685
  };
2028
3686
  }
2029
3687
  export {
3688
+ verify,
2030
3689
  validateShape,
2031
3690
  validatePartial,
2032
3691
  validateEnum,
2033
3692
  validateArray,
3693
+ signingKeyPairFromSecret,
3694
+ sign,
2034
3695
  settings,
3696
+ serialisePairingToken,
2035
3697
  runInContext,
3698
+ revokePeerLocally,
3699
+ resetPeerState,
3700
+ resetMeshState,
2036
3701
  registerConstraints,
2037
3702
  registerConstraint,
3703
+ readOnlyExcept,
2038
3704
  quickTest,
3705
+ publicAccess,
3706
+ parsePairingToken,
3707
+ ownerAccess,
3708
+ or,
3709
+ onlyPeer,
3710
+ not,
3711
+ nobody,
3712
+ migratePrimitive,
2039
3713
  isRuntimeConstraintsEnabled,
3714
+ isPairingTokenExpired,
3715
+ isBlobRef,
3716
+ groupAccess,
2040
3717
  getMessageBus,
3718
+ getDocVersion,
3719
+ generateSigningKeyPair,
3720
+ generateDocumentKey,
3721
+ encrypt,
3722
+ encodeRevocation,
3723
+ encodePairingToken,
3724
+ decryptOrThrow,
3725
+ decrypt,
3726
+ decodeRevocation,
3727
+ decodePairingToken,
2041
3728
  createTestSuite,
3729
+ createRevocation,
3730
+ createPeerStateClient,
3731
+ createPeerRepoServer,
3732
+ createPairingTokenWithFreshIdentity,
3733
+ createPairingToken,
2042
3734
  createContext,
2043
3735
  createChromeAdapters2 as createChromeAdapters,
3736
+ createBlobRef,
3737
+ configurePeerState,
3738
+ configureMeshState,
3739
+ computeBlobHash,
2044
3740
  clearConstraints,
2045
3741
  checkPreconditions,
2046
3742
  checkPostconditions,
3743
+ checkOpVersion,
3744
+ assertOpVersion,
3745
+ applyRevocation,
3746
+ applyPairingToken,
3747
+ anyone,
3748
+ anyOfPeers,
3749
+ and,
2047
3750
  TimeoutError,
2048
3751
  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,
2049
3766
  MessageBus,
3767
+ MeshWebRTCAdapter,
3768
+ MeshSignalingClient,
3769
+ MeshNetworkAdapter,
2050
3770
  HandlerError,
2051
3771
  ExtensionError,
2052
3772
  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,
2053
3780
  ConnectionError,
2054
3781
  $syncedState,
2055
3782
  $state,
2056
3783
  $sharedState,
2057
3784
  $resource,
2058
- $persistedState
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
2059
3798
  };
2060
3799
 
2061
- //# debugId=742FE904639ADD9064756E2164756E21
3800
+ //# debugId=DC7C621AC65ABFA764756E2164756E21