dignity.js 0.5.4 → 0.6.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.
@@ -16,43 +16,6 @@ var __commonJS = (cb, mod) => function __require2() {
16
16
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
17
17
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
18
18
 
19
- // src/utils/event-emitter.js
20
- var require_event_emitter = __commonJS({
21
- "src/utils/event-emitter.js"(exports, module) {
22
- var EventEmitter = class {
23
- constructor() {
24
- this.handlers = /* @__PURE__ */ new Map();
25
- }
26
- on(eventName, handler) {
27
- if (!this.handlers.has(eventName)) {
28
- this.handlers.set(eventName, /* @__PURE__ */ new Set());
29
- }
30
- this.handlers.get(eventName).add(handler);
31
- }
32
- off(eventName, handler) {
33
- const eventHandlers = this.handlers.get(eventName);
34
- if (!eventHandlers) {
35
- return;
36
- }
37
- eventHandlers.delete(handler);
38
- if (eventHandlers.size === 0) {
39
- this.handlers.delete(eventName);
40
- }
41
- }
42
- emit(eventName, payload) {
43
- const eventHandlers = this.handlers.get(eventName);
44
- if (!eventHandlers) {
45
- return;
46
- }
47
- for (const handler of eventHandlers) {
48
- handler(payload);
49
- }
50
- }
51
- };
52
- module.exports = EventEmitter;
53
- }
54
- });
55
-
56
19
  // (disabled):crypto
57
20
  var require_crypto = __commonJS({
58
21
  "(disabled):crypto"() {
@@ -2349,6 +2312,43 @@ var require_nacl_util = __commonJS({
2349
2312
  }
2350
2313
  });
2351
2314
 
2315
+ // src/utils/event-emitter.js
2316
+ var require_event_emitter = __commonJS({
2317
+ "src/utils/event-emitter.js"(exports, module) {
2318
+ var EventEmitter = class {
2319
+ constructor() {
2320
+ this.handlers = /* @__PURE__ */ new Map();
2321
+ }
2322
+ on(eventName, handler) {
2323
+ if (!this.handlers.has(eventName)) {
2324
+ this.handlers.set(eventName, /* @__PURE__ */ new Set());
2325
+ }
2326
+ this.handlers.get(eventName).add(handler);
2327
+ }
2328
+ off(eventName, handler) {
2329
+ const eventHandlers = this.handlers.get(eventName);
2330
+ if (!eventHandlers) {
2331
+ return;
2332
+ }
2333
+ eventHandlers.delete(handler);
2334
+ if (eventHandlers.size === 0) {
2335
+ this.handlers.delete(eventName);
2336
+ }
2337
+ }
2338
+ emit(eventName, payload) {
2339
+ const eventHandlers = this.handlers.get(eventName);
2340
+ if (!eventHandlers) {
2341
+ return;
2342
+ }
2343
+ for (const handler of eventHandlers) {
2344
+ handler(payload);
2345
+ }
2346
+ }
2347
+ };
2348
+ module.exports = EventEmitter;
2349
+ }
2350
+ });
2351
+
2352
2352
  // src/security/sloth-vdf.js
2353
2353
  var require_sloth_vdf = __commonJS({
2354
2354
  "src/security/sloth-vdf.js"(exports, module) {
@@ -2361,25 +2361,25 @@ var require_sloth_vdf = __commonJS({
2361
2361
  let powBase = base % modulus;
2362
2362
  let powExponent = exponent;
2363
2363
  while (powExponent > 0) {
2364
- if (powExponent % BigInt(2) === BigInt(1)) {
2364
+ if ((powExponent & BigInt(1)) === BigInt(1)) {
2365
2365
  result = result * powBase % modulus;
2366
2366
  }
2367
- powExponent = powExponent / BigInt(2);
2367
+ powExponent = powExponent >> BigInt(1);
2368
2368
  powBase = powBase * powBase % modulus;
2369
2369
  }
2370
2370
  return result;
2371
2371
  }
2372
2372
  quadRes(x) {
2373
- return this.fastPow(x, (_SlothPermutation.p - BigInt(1)) / BigInt(2), _SlothPermutation.p) === BigInt(1);
2373
+ return this.fastPow(x, _SlothPermutation.pHalf, _SlothPermutation.p) === BigInt(1);
2374
2374
  }
2375
2375
  modSqrtOp(x) {
2376
2376
  let y;
2377
2377
  let value = x;
2378
2378
  if (this.quadRes(value)) {
2379
- y = this.fastPow(value, (_SlothPermutation.p + BigInt(1)) / BigInt(4), _SlothPermutation.p);
2379
+ y = this.fastPow(value, _SlothPermutation.pQuarter, _SlothPermutation.p);
2380
2380
  } else {
2381
2381
  value = (-value + _SlothPermutation.p) % _SlothPermutation.p;
2382
- y = this.fastPow(value, (_SlothPermutation.p + BigInt(1)) / BigInt(4), _SlothPermutation.p);
2382
+ y = this.fastPow(value, _SlothPermutation.pQuarter, _SlothPermutation.p);
2383
2383
  }
2384
2384
  return y;
2385
2385
  }
@@ -2411,6 +2411,12 @@ var require_sloth_vdf = __commonJS({
2411
2411
  __publicField(_SlothPermutation, "p", BigInt(
2412
2412
  "170082004324204494273811327264862981553264701145937538369570764779791492622392118654022654452947093285873855529044371650895045691292912712699015605832276411308653107069798639938826015099738961427172366594187783204437869906954750443653318078358839409699824714551430573905637228307966826784684174483831608534979"
2413
2413
  ));
2414
+ // precompute values for optimization:
2415
+ // (p - 1) / 2
2416
+ __publicField(_SlothPermutation, "pHalf", _SlothPermutation.p - BigInt(1) >> BigInt(1));
2417
+ // (p + 1) / 4
2418
+ // p ≡ 3 (mod 4) ⇒ (p+1) divisible by 4
2419
+ __publicField(_SlothPermutation, "pQuarter", _SlothPermutation.p + BigInt(1) >> BigInt(2));
2414
2420
  var SlothPermutation = _SlothPermutation;
2415
2421
  module.exports = SlothPermutation;
2416
2422
  }
@@ -2868,11 +2874,84 @@ var require_message_security_service = __commonJS({
2868
2874
  }
2869
2875
  });
2870
2876
 
2877
+ // src/gossip/peer-group.js
2878
+ var require_peer_group = __commonJS({
2879
+ "src/gossip/peer-group.js"(exports, module) {
2880
+ var PEER_GROUP_SCOPE_PREFIX = "gossip:";
2881
+ var DEFAULT_PEER_GROUP_OPTIONS = {
2882
+ fanout: 3,
2883
+ maxActivePeers: 8,
2884
+ maxHops: 6,
2885
+ relayEnabled: true
2886
+ };
2887
+ function peerGroupScope(groupId) {
2888
+ if (!groupId) {
2889
+ throw new Error("peerGroupScope requires groupId");
2890
+ }
2891
+ return `${PEER_GROUP_SCOPE_PREFIX}${groupId}`;
2892
+ }
2893
+ function parsePeerGroupScope(scope) {
2894
+ if (!scope || !scope.startsWith(PEER_GROUP_SCOPE_PREFIX)) {
2895
+ return null;
2896
+ }
2897
+ return scope.slice(PEER_GROUP_SCOPE_PREFIX.length);
2898
+ }
2899
+ function shufflePeerIds(peerIds, randomFn = Math.random) {
2900
+ const list = [...peerIds];
2901
+ for (let i = list.length - 1; i > 0; i -= 1) {
2902
+ const j = Math.floor(randomFn() * (i + 1));
2903
+ [list[i], list[j]] = [list[j], list[i]];
2904
+ }
2905
+ return list;
2906
+ }
2907
+ function selectFanoutPeers({
2908
+ peers,
2909
+ count,
2910
+ excludePeerIds = [],
2911
+ connectedPeerIds = [],
2912
+ randomFn = Math.random
2913
+ }) {
2914
+ const excluded = new Set(excludePeerIds.filter(Boolean));
2915
+ const candidates = peers.map((entry) => entry.peerId || entry).filter((peerId) => peerId && !excluded.has(peerId));
2916
+ const connected = new Set(connectedPeerIds.filter(Boolean));
2917
+ const preferred = candidates.filter((peerId) => connected.has(peerId));
2918
+ const others = candidates.filter((peerId) => !connected.has(peerId));
2919
+ const ordered = [
2920
+ ...shufflePeerIds(preferred, randomFn),
2921
+ ...shufflePeerIds(others, randomFn)
2922
+ ];
2923
+ return ordered.slice(0, Math.max(0, count));
2924
+ }
2925
+ module.exports = {
2926
+ PEER_GROUP_SCOPE_PREFIX,
2927
+ DEFAULT_PEER_GROUP_OPTIONS,
2928
+ peerGroupScope,
2929
+ parsePeerGroupScope,
2930
+ shufflePeerIds,
2931
+ selectFanoutPeers
2932
+ };
2933
+ }
2934
+ });
2935
+
2871
2936
  // src/core/dignity-p2p.js
2872
2937
  var require_dignity_p2p = __commonJS({
2873
2938
  "src/core/dignity-p2p.js"(exports, module) {
2939
+ var nacl = require_nacl_fast();
2940
+ var naclUtil = require_nacl_util();
2874
2941
  var EventEmitter = require_event_emitter();
2875
- var { MessageSecurityService } = require_message_security_service();
2942
+ var { MessageSecurityService, stableStringify } = require_message_security_service();
2943
+ var {
2944
+ DEFAULT_PEER_GROUP_OPTIONS,
2945
+ peerGroupScope,
2946
+ selectFanoutPeers
2947
+ } = require_peer_group();
2948
+ function computeContentHash(data) {
2949
+ const canonical = stableStringify(data || {});
2950
+ const bytes = naclUtil.decodeUTF8(canonical);
2951
+ const hash = nacl.hash(bytes);
2952
+ const hex = Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("");
2953
+ return `sha512:${hex}`;
2954
+ }
2876
2955
  var DignityP2P = class extends EventEmitter {
2877
2956
  constructor({ nodeId, networkAdapter, idGenerator, now, security } = {}) {
2878
2957
  super();
@@ -2898,6 +2977,13 @@ var require_dignity_p2p = __commonJS({
2898
2977
  this.defaultPresenceTtlMs = security && typeof security.presenceTtlMs === "number" ? security.presenceTtlMs : 45e3;
2899
2978
  this.discoveryRooms = /* @__PURE__ */ new Map();
2900
2979
  this.presenceByScope = /* @__PURE__ */ new Map();
2980
+ this.peerGroups = /* @__PURE__ */ new Map();
2981
+ this.seenGossipIds = /* @__PURE__ */ new Map();
2982
+ this.defaultPeerGroupFanout = security && typeof security.peerGroupFanout === "number" ? security.peerGroupFanout : DEFAULT_PEER_GROUP_OPTIONS.fanout;
2983
+ this.defaultPeerGroupMaxActivePeers = security && typeof security.peerGroupMaxActivePeers === "number" ? security.peerGroupMaxActivePeers : DEFAULT_PEER_GROUP_OPTIONS.maxActivePeers;
2984
+ this.defaultGossipMaxHops = security && typeof security.gossipMaxHops === "number" ? security.gossipMaxHops : DEFAULT_PEER_GROUP_OPTIONS.maxHops;
2985
+ this.globalMaxOpenConnections = security && typeof security.globalMaxOpenConnections === "number" ? security.globalMaxOpenConnections : 32;
2986
+ this.gossipIdTtlMs = security && typeof security.gossipIdTtlMs === "number" ? security.gossipIdTtlMs : 5 * 60 * 1e3;
2901
2987
  this.state = /* @__PURE__ */ new Map();
2902
2988
  this.appliedOperations = /* @__PURE__ */ new Set();
2903
2989
  this.boundMessageHandler = this.handleIncomingMessage.bind(this);
@@ -2907,6 +2993,14 @@ var require_dignity_p2p = __commonJS({
2907
2993
  await this.networkAdapter.start(this.nodeId);
2908
2994
  }
2909
2995
  async stop() {
2996
+ const joinedGroups = Array.from(this.peerGroups.keys());
2997
+ for (const groupId of joinedGroups) {
2998
+ try {
2999
+ await this.leavePeerGroup(groupId);
3000
+ } catch (error) {
3001
+ this.emit("warning", { type: "peer-group-leave-failed", groupId, error });
3002
+ }
3003
+ }
2910
3004
  const joinedScopes = Array.from(this.discoveryRooms.keys());
2911
3005
  for (const scope of joinedScopes) {
2912
3006
  try {
@@ -2931,6 +3025,7 @@ var require_dignity_p2p = __commonJS({
2931
3025
  if (!record || record.deletedAt) {
2932
3026
  return null;
2933
3027
  }
3028
+ const normalizedData = { ...record.data || {} };
2934
3029
  return {
2935
3030
  id: record.id,
2936
3031
  ownerId: record.ownerId,
@@ -2938,7 +3033,8 @@ var require_dignity_p2p = __commonJS({
2938
3033
  createdAt: record.createdAt,
2939
3034
  updatedAt: record.updatedAt,
2940
3035
  version: record.version,
2941
- data: { ...record.data }
3036
+ hash: record.hash || computeContentHash(normalizedData),
3037
+ data: normalizedData
2942
3038
  };
2943
3039
  }
2944
3040
  canUpdateRecord(record, actorId) {
@@ -3016,7 +3112,7 @@ var require_dignity_p2p = __commonJS({
3016
3112
  ownerId: this.nodeId,
3017
3113
  collaboratorIds,
3018
3114
  timestamp,
3019
- payload: { ...data }
3115
+ payload: { ...data || {} }
3020
3116
  };
3021
3117
  this.applyOperation(operation);
3022
3118
  await this.broadcastMessage("operation", operation, {
@@ -3253,6 +3349,7 @@ var require_dignity_p2p = __commonJS({
3253
3349
  const connectToPeers = securityContext.connectToPeers;
3254
3350
  if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3255
3351
  await this.ensureConnectedToPeers(connectToPeers);
3352
+ await this.enforceConnectionBudget();
3256
3353
  }
3257
3354
  const envelope = await this.securityService.secureOutgoingMessage({
3258
3355
  messageType,
@@ -3260,6 +3357,11 @@ var require_dignity_p2p = __commonJS({
3260
3357
  targetId: null,
3261
3358
  securityContext
3262
3359
  });
3360
+ const fanoutPeerIds = securityContext.fanoutPeerIds;
3361
+ if (Array.isArray(fanoutPeerIds) && fanoutPeerIds.length > 0 && typeof this.networkAdapter.sendToPeers === "function") {
3362
+ await this.networkAdapter.sendToPeers(envelope, fanoutPeerIds);
3363
+ return;
3364
+ }
3263
3365
  await this.networkAdapter.broadcast(envelope);
3264
3366
  }
3265
3367
  async sendDirectMessage(targetId, messageType, payload) {
@@ -3275,8 +3377,237 @@ var require_dignity_p2p = __commonJS({
3275
3377
  payload,
3276
3378
  targetId
3277
3379
  });
3380
+ if (targetId && typeof this.networkAdapter.sendToPeers === "function") {
3381
+ await this.networkAdapter.sendToPeers(envelope, [targetId]);
3382
+ return;
3383
+ }
3278
3384
  await this.networkAdapter.broadcast(envelope);
3279
3385
  }
3386
+ peerGroupScopeFor(groupId) {
3387
+ return peerGroupScope(groupId);
3388
+ }
3389
+ getPeerGroupConfig(groupId) {
3390
+ return this.peerGroups.get(groupId) || null;
3391
+ }
3392
+ listPeerGroupMembers(groupId, options = {}) {
3393
+ return this.listPeers(this.peerGroupScopeFor(groupId), options);
3394
+ }
3395
+ getPeerGroupStats() {
3396
+ const adapter = this.networkAdapter;
3397
+ const openPeerIds = typeof adapter.listOpenPeerIds === "function" ? adapter.listOpenPeerIds() : [];
3398
+ return {
3399
+ joinedGroups: Array.from(this.peerGroups.keys()),
3400
+ seenGossipCount: this.seenGossipIds.size,
3401
+ openConnectionCount: openPeerIds.length,
3402
+ globalMaxOpenConnections: this.globalMaxOpenConnections
3403
+ };
3404
+ }
3405
+ pruneSeenGossip() {
3406
+ const now = this.now();
3407
+ for (const [gossipId, expiresAt] of this.seenGossipIds.entries()) {
3408
+ if (expiresAt <= now) {
3409
+ this.seenGossipIds.delete(gossipId);
3410
+ }
3411
+ }
3412
+ }
3413
+ hasSeenGossip(gossipId) {
3414
+ if (!gossipId) {
3415
+ return false;
3416
+ }
3417
+ this.pruneSeenGossip();
3418
+ return this.seenGossipIds.has(gossipId);
3419
+ }
3420
+ markSeenGossip(gossipId) {
3421
+ if (!gossipId) {
3422
+ return;
3423
+ }
3424
+ this.seenGossipIds.set(gossipId, this.now() + this.gossipIdTtlMs);
3425
+ }
3426
+ listConnectedPeerIds() {
3427
+ if (typeof this.networkAdapter.listOpenPeerIds === "function") {
3428
+ return this.networkAdapter.listOpenPeerIds();
3429
+ }
3430
+ return [];
3431
+ }
3432
+ selectPeerGroupFanout(groupId, count, excludePeerIds = []) {
3433
+ const scope = this.peerGroupScopeFor(groupId);
3434
+ const peers = this.listPeers(scope, { includeSelf: false });
3435
+ return selectFanoutPeers({
3436
+ peers,
3437
+ count,
3438
+ excludePeerIds: [...excludePeerIds, this.nodeId],
3439
+ connectedPeerIds: this.listConnectedPeerIds()
3440
+ });
3441
+ }
3442
+ async enforceConnectionBudget() {
3443
+ const adapter = this.networkAdapter;
3444
+ if (typeof adapter.listOpenPeerIds !== "function" || typeof adapter.disconnectPeer !== "function") {
3445
+ return;
3446
+ }
3447
+ const openPeerIds = adapter.listOpenPeerIds();
3448
+ if (openPeerIds.length <= this.globalMaxOpenConnections) {
3449
+ return;
3450
+ }
3451
+ const excess = openPeerIds.length - this.globalMaxOpenConnections;
3452
+ const toClose = openPeerIds.slice(0, excess);
3453
+ for (const peerId of toClose) {
3454
+ try {
3455
+ await adapter.disconnectPeer(peerId);
3456
+ } catch (error) {
3457
+ this.emit("warning", { type: "peer-disconnect-failed", peerId, error });
3458
+ }
3459
+ }
3460
+ }
3461
+ async joinPeerGroup(groupId, options = {}) {
3462
+ if (!groupId) {
3463
+ throw new Error("joinPeerGroup requires groupId");
3464
+ }
3465
+ const scope = this.peerGroupScopeFor(groupId);
3466
+ const config = {
3467
+ fanout: typeof options.fanout === "number" ? options.fanout : this.defaultPeerGroupFanout,
3468
+ maxActivePeers: typeof options.maxActivePeers === "number" ? options.maxActivePeers : this.defaultPeerGroupMaxActivePeers,
3469
+ maxHops: typeof options.maxHops === "number" ? options.maxHops : this.defaultGossipMaxHops,
3470
+ relayEnabled: options.relayEnabled !== false
3471
+ };
3472
+ await this.joinDiscovery(scope, {
3473
+ metadata: {
3474
+ peerGroup: groupId,
3475
+ ...options.metadata || {}
3476
+ },
3477
+ bootstrapPeerIds: options.bootstrapPeerIds,
3478
+ heartbeatIntervalMs: options.heartbeatIntervalMs,
3479
+ ttlMs: options.ttlMs
3480
+ });
3481
+ this.peerGroups.set(groupId, config);
3482
+ this.emit("peergroupjoined", { groupId, config });
3483
+ return config;
3484
+ }
3485
+ async leavePeerGroup(groupId) {
3486
+ if (!groupId) {
3487
+ return;
3488
+ }
3489
+ const scope = this.peerGroupScopeFor(groupId);
3490
+ await this.leaveDiscovery(scope);
3491
+ this.peerGroups.delete(groupId);
3492
+ this.emit("peergroupleft", { groupId });
3493
+ }
3494
+ async publishToPeerGroup(groupId, innerMessageType, innerPayload, options = {}) {
3495
+ if (!groupId) {
3496
+ throw new Error("publishToPeerGroup requires groupId");
3497
+ }
3498
+ const group = this.peerGroups.get(groupId);
3499
+ if (!group && options.allowUnjoined !== true) {
3500
+ throw new Error(`PeerGroup ${groupId} has not been joined`);
3501
+ }
3502
+ const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
3503
+ const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
3504
+ const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
3505
+ const fanoutPeerIds = this.selectPeerGroupFanout(groupId, fanout, [this.nodeId]);
3506
+ if (fanoutPeerIds.length > 0) {
3507
+ await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
3508
+ await this.enforceConnectionBudget();
3509
+ }
3510
+ const gossipId = options.gossipId || this.idGenerator();
3511
+ this.markSeenGossip(gossipId);
3512
+ await this.broadcastMessage("peer-group:gossip", {
3513
+ groupId,
3514
+ gossipId,
3515
+ hop: 0,
3516
+ maxHop,
3517
+ innerMessageType,
3518
+ innerPayload
3519
+ }, {
3520
+ broadcastScope: this.peerGroupScopeFor(groupId),
3521
+ fanoutPeerIds
3522
+ });
3523
+ return { gossipId, fanoutPeerIds };
3524
+ }
3525
+ async publishRecordToPeerGroup(groupId, collectionName, id, options = {}) {
3526
+ const collection = this.getCollection(collectionName);
3527
+ const raw = collection.get(id);
3528
+ if (!raw || raw.deletedAt) {
3529
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3530
+ }
3531
+ const record = this.normalizeRecord(raw);
3532
+ return this.publishToPeerGroup(groupId, "record:snapshot", {
3533
+ collectionName,
3534
+ record
3535
+ }, options);
3536
+ }
3537
+ async handlePeerGroupGossip(decrypted) {
3538
+ const payload = decrypted.payload || {};
3539
+ const {
3540
+ groupId,
3541
+ gossipId,
3542
+ hop = 0,
3543
+ maxHop = this.defaultGossipMaxHops,
3544
+ innerMessageType,
3545
+ innerPayload
3546
+ } = payload;
3547
+ if (!groupId || !innerMessageType) {
3548
+ return;
3549
+ }
3550
+ if (this.hasSeenGossip(gossipId)) {
3551
+ return;
3552
+ }
3553
+ this.markSeenGossip(gossipId);
3554
+ await this.dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, {
3555
+ groupId,
3556
+ senderId: decrypted.senderId
3557
+ });
3558
+ const group = this.peerGroups.get(groupId);
3559
+ if (!group || group.relayEnabled === false || hop >= maxHop) {
3560
+ return;
3561
+ }
3562
+ const relayPeers = this.selectPeerGroupFanout(groupId, group.fanout, [
3563
+ decrypted.senderId,
3564
+ this.nodeId
3565
+ ]);
3566
+ if (relayPeers.length === 0) {
3567
+ return;
3568
+ }
3569
+ await this.ensureConnectedToPeers(relayPeers.slice(0, group.maxActivePeers));
3570
+ await this.enforceConnectionBudget();
3571
+ await this.broadcastMessage("peer-group:gossip", {
3572
+ groupId,
3573
+ gossipId,
3574
+ hop: hop + 1,
3575
+ maxHop,
3576
+ innerMessageType,
3577
+ innerPayload
3578
+ }, {
3579
+ broadcastScope: this.peerGroupScopeFor(groupId),
3580
+ fanoutPeerIds: relayPeers
3581
+ });
3582
+ }
3583
+ async dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, context = {}) {
3584
+ if (innerMessageType === "operation") {
3585
+ this.applyOperation(innerPayload);
3586
+ return;
3587
+ }
3588
+ if (innerMessageType === "record:snapshot") {
3589
+ const { collectionName, record } = innerPayload || {};
3590
+ if (collectionName && record) {
3591
+ const applied = this.restoreRecord(collectionName, record);
3592
+ if (applied) {
3593
+ this.emit("change", {
3594
+ kind: "snapshot",
3595
+ collection: collectionName,
3596
+ id: record.id,
3597
+ via: "peer-group",
3598
+ groupId: context.groupId
3599
+ });
3600
+ }
3601
+ }
3602
+ return;
3603
+ }
3604
+ this.emit("peergroupmessage", {
3605
+ groupId: context.groupId,
3606
+ senderId: context.senderId,
3607
+ type: innerMessageType,
3608
+ payload: innerPayload
3609
+ });
3610
+ }
3280
3611
  getPresenceMap(scope) {
3281
3612
  if (!this.presenceByScope.has(scope)) {
3282
3613
  this.presenceByScope.set(scope, /* @__PURE__ */ new Map());
@@ -3495,6 +3826,10 @@ var require_dignity_p2p = __commonJS({
3495
3826
  }
3496
3827
  return;
3497
3828
  }
3829
+ if (decrypted.messageType === "peer-group:gossip") {
3830
+ await this.handlePeerGroupGossip(decrypted);
3831
+ return;
3832
+ }
3498
3833
  this.emit("message", {
3499
3834
  senderId: decrypted.senderId,
3500
3835
  targetId: decrypted.targetId,
@@ -3549,11 +3884,23 @@ var require_dignity_p2p = __commonJS({
3549
3884
  if (current && current.version >= record.version) {
3550
3885
  return false;
3551
3886
  }
3887
+ const restoredData = { ...record.data || {} };
3888
+ const computedHash = computeContentHash(restoredData);
3889
+ if (record.hash && record.hash !== computedHash) {
3890
+ this.emit("warning", {
3891
+ type: "content-hash-mismatch",
3892
+ collection: collectionName,
3893
+ id: record.id,
3894
+ advertisedHash: record.hash,
3895
+ computedHash
3896
+ });
3897
+ }
3552
3898
  collection.set(record.id, {
3553
3899
  id: record.id,
3554
3900
  ownerId: record.ownerId,
3555
3901
  collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
3556
- data: { ...record.data || {} },
3902
+ data: restoredData,
3903
+ hash: computedHash,
3557
3904
  createdAt: record.createdAt,
3558
3905
  updatedAt: record.updatedAt,
3559
3906
  deletedAt: record.deletedAt || null,
@@ -3571,7 +3918,8 @@ var require_dignity_p2p = __commonJS({
3571
3918
  id: raw.id,
3572
3919
  ownerId: raw.ownerId,
3573
3920
  collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
3574
- data: { ...raw.data },
3921
+ data: { ...raw.data || {} },
3922
+ hash: raw.hash || computeContentHash(raw.data || {}),
3575
3923
  createdAt: raw.createdAt,
3576
3924
  updatedAt: raw.updatedAt,
3577
3925
  deletedAt: raw.deletedAt || null,
@@ -3601,7 +3949,8 @@ var require_dignity_p2p = __commonJS({
3601
3949
  id: operation.id,
3602
3950
  ownerId: operation.ownerId,
3603
3951
  collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
3604
- data: { ...operation.payload },
3952
+ data: { ...operation.payload || {} },
3953
+ hash: computeContentHash(operation.payload || {}),
3605
3954
  createdAt: operation.timestamp,
3606
3955
  updatedAt: operation.timestamp,
3607
3956
  deletedAt: null,
@@ -3704,6 +4053,7 @@ var require_dignity_p2p = __commonJS({
3704
4053
  ...current.data,
3705
4054
  ...operation.payload
3706
4055
  };
4056
+ current.hash = computeContentHash(current.data);
3707
4057
  if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
3708
4058
  current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
3709
4059
  }
@@ -3789,6 +4139,14 @@ var require_signaling_pool = __commonJS({
3789
4139
  // src/signaling/websocket-signaling-provider.js
3790
4140
  var require_websocket_signaling_provider = __commonJS({
3791
4141
  "src/signaling/websocket-signaling-provider.js"(exports, module) {
4142
+ function randomBase36(length) {
4143
+ let value = "";
4144
+ while (value.length < length) {
4145
+ const chunk = Math.random().toString(36).slice(2);
4146
+ value += chunk.length > 0 ? chunk : "0";
4147
+ }
4148
+ return value.slice(0, length);
4149
+ }
3792
4150
  var WebSocketSignalingProvider = class {
3793
4151
  constructor({ id, url, WebSocketImpl, priority = 0 }) {
3794
4152
  if (!url) {
@@ -3832,8 +4190,8 @@ var require_websocket_signaling_provider = __commonJS({
3832
4190
  if (!peerJsHostPattern.test(this.url)) {
3833
4191
  return this.url;
3834
4192
  }
3835
- const connectionId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
3836
- const token = Math.random().toString(36).slice(2, 12);
4193
+ const connectionId = `dignityjs_${randomBase36(10)}`;
4194
+ const token = randomBase36(10);
3837
4195
  const hasQuery = this.url.includes("?");
3838
4196
  const hasId = /[?&]id=/.test(this.url);
3839
4197
  const hasToken = /[?&]token=/.test(this.url);
@@ -10749,6 +11107,16 @@ var require_in_memory_network = __commonJS({
10749
11107
  }
10750
11108
  await Promise.all(deliveries);
10751
11109
  }
11110
+ async sendToPeers(senderId, message, peerIds = []) {
11111
+ const targets = new Set((peerIds || []).filter((peerId) => peerId && peerId !== senderId));
11112
+ const deliveries = [];
11113
+ for (const [nodeId, adapter] of this.adapters.entries()) {
11114
+ if (nodeId !== senderId && targets.has(nodeId)) {
11115
+ deliveries.push(adapter.receive(message));
11116
+ }
11117
+ }
11118
+ await Promise.all(deliveries);
11119
+ }
10752
11120
  };
10753
11121
  var InMemoryNetworkAdapter = class {
10754
11122
  constructor(hub) {
@@ -10758,6 +11126,7 @@ var require_in_memory_network = __commonJS({
10758
11126
  this.hub = hub;
10759
11127
  this.nodeId = null;
10760
11128
  this.messageHandlers = /* @__PURE__ */ new Set();
11129
+ this.connectedPeers = /* @__PURE__ */ new Set();
10761
11130
  }
10762
11131
  async start(nodeId) {
10763
11132
  this.nodeId = nodeId;
@@ -10768,6 +11137,13 @@ var require_in_memory_network = __commonJS({
10768
11137
  this.hub.unregister(this.nodeId);
10769
11138
  }
10770
11139
  this.nodeId = null;
11140
+ this.connectedPeers.clear();
11141
+ }
11142
+ async connectToPeer(remotePeerId) {
11143
+ if (!remotePeerId || remotePeerId === this.nodeId) {
11144
+ return;
11145
+ }
11146
+ this.connectedPeers.add(remotePeerId);
10771
11147
  }
10772
11148
  async broadcast(message) {
10773
11149
  if (!this.nodeId) {
@@ -10775,6 +11151,21 @@ var require_in_memory_network = __commonJS({
10775
11151
  }
10776
11152
  await this.hub.broadcast(this.nodeId, message);
10777
11153
  }
11154
+ async sendToPeers(message, peerIds = []) {
11155
+ if (!this.nodeId) {
11156
+ throw new Error("Network adapter has not been started");
11157
+ }
11158
+ await this.hub.sendToPeers(this.nodeId, message, peerIds);
11159
+ }
11160
+ listOpenPeerIds() {
11161
+ return [...this.connectedPeers];
11162
+ }
11163
+ getOpenConnectionCount() {
11164
+ return this.connectedPeers.size;
11165
+ }
11166
+ isConnectedTo(remotePeerId) {
11167
+ return this.connectedPeers.has(remotePeerId);
11168
+ }
10778
11169
  onMessage(handler) {
10779
11170
  this.messageHandlers.add(handler);
10780
11171
  }
@@ -10953,6 +11344,29 @@ var require_peerjs_network = __commonJS({
10953
11344
  }
10954
11345
  await Promise.all(deliveries);
10955
11346
  }
11347
+ async sendToPeers(message, peerIds = []) {
11348
+ if (!this.peer) {
11349
+ throw new Error("PeerJS network adapter has not been started");
11350
+ }
11351
+ const targets = new Set((peerIds || []).filter(Boolean));
11352
+ if (targets.size === 0) {
11353
+ return;
11354
+ }
11355
+ const deliveries = [];
11356
+ for (const [peerId, connection] of this.connections.entries()) {
11357
+ if (targets.has(peerId) && connection.open) {
11358
+ deliveries.push(connection.send(message));
11359
+ }
11360
+ }
11361
+ await Promise.all(deliveries);
11362
+ }
11363
+ async disconnectPeer(remotePeerId) {
11364
+ const connection = this.connections.get(remotePeerId);
11365
+ if (connection && typeof connection.close === "function") {
11366
+ connection.close();
11367
+ }
11368
+ this.connections.delete(remotePeerId);
11369
+ }
10956
11370
  getOpenConnectionCount() {
10957
11371
  return this.listOpenPeerIds().length;
10958
11372
  }
@@ -11059,6 +11473,7 @@ var require_indexeddb_persistence = __commonJS({
11059
11473
  ownerId: record.ownerId,
11060
11474
  collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
11061
11475
  data: { ...record.data },
11476
+ hash: record.hash || null,
11062
11477
  createdAt: record.createdAt,
11063
11478
  updatedAt: record.updatedAt,
11064
11479
  deletedAt: record.deletedAt,
@@ -11119,6 +11534,7 @@ var require_indexeddb_persistence = __commonJS({
11119
11534
  ownerId: stored.ownerId,
11120
11535
  collaboratorIds: stored.collaboratorIds,
11121
11536
  data: stored.data,
11537
+ hash: stored.hash || null,
11122
11538
  createdAt: stored.createdAt,
11123
11539
  updatedAt: stored.updatedAt,
11124
11540
  deletedAt: stored.deletedAt,
@@ -11181,6 +11597,12 @@ var require_index = __commonJS({
11181
11597
  MessageSecurityService,
11182
11598
  DEFAULT_SECURITY_OPTIONS
11183
11599
  } = require_message_security_service();
11600
+ var {
11601
+ PEER_GROUP_SCOPE_PREFIX,
11602
+ DEFAULT_PEER_GROUP_OPTIONS,
11603
+ peerGroupScope,
11604
+ selectFanoutPeers
11605
+ } = require_peer_group();
11184
11606
  module.exports = {
11185
11607
  DignityP2P,
11186
11608
  createDefaultSignalingPool,
@@ -11197,7 +11619,11 @@ var require_index = __commonJS({
11197
11619
  VDF,
11198
11620
  SlothPermutation,
11199
11621
  MessageSecurityService,
11200
- DEFAULT_SECURITY_OPTIONS
11622
+ DEFAULT_SECURITY_OPTIONS,
11623
+ PEER_GROUP_SCOPE_PREFIX,
11624
+ DEFAULT_PEER_GROUP_OPTIONS,
11625
+ peerGroupScope,
11626
+ selectFanoutPeers
11201
11627
  };
11202
11628
  }
11203
11629
  });