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.
@@ -2749,8 +2749,14 @@ var require_message_security_service = __commonJS({
2749
2749
  const nonce = naclUtil.decodeBase64(encryption.nonce);
2750
2750
  let key;
2751
2751
  if (encryption.kdf === "pbkdf2") {
2752
- const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS2.kdfIterations;
2753
- key = await deriveBroadcastKey(password, salt, iterations);
2752
+ const configuredIterations = this.options.kdfIterations || DEFAULT_SECURITY_OPTIONS2.kdfIterations;
2753
+ const requestedIterations = encryption.kdfIterations || configuredIterations;
2754
+ const minIterations = Math.max(1e3, Math.floor(configuredIterations * 0.1));
2755
+ const maxIterations = configuredIterations * 2;
2756
+ if (requestedIterations < minIterations || requestedIterations > maxIterations) {
2757
+ throw new Error(`Invalid kdfIterations: ${requestedIterations}`);
2758
+ }
2759
+ key = await deriveBroadcastKey(password, salt, requestedIterations);
2754
2760
  } else {
2755
2761
  key = legacyBroadcastKey(password, salt);
2756
2762
  }
@@ -2854,6 +2860,65 @@ var require_message_security_service = __commonJS({
2854
2860
  }
2855
2861
  });
2856
2862
 
2863
+ // src/gossip/peer-group.js
2864
+ var require_peer_group = __commonJS({
2865
+ "src/gossip/peer-group.js"(exports2, module2) {
2866
+ var PEER_GROUP_SCOPE_PREFIX2 = "gossip:";
2867
+ var DEFAULT_PEER_GROUP_OPTIONS2 = {
2868
+ fanout: 3,
2869
+ maxActivePeers: 8,
2870
+ maxHops: 6,
2871
+ relayEnabled: true
2872
+ };
2873
+ function peerGroupScope2(groupId) {
2874
+ if (!groupId) {
2875
+ throw new Error("peerGroupScope requires groupId");
2876
+ }
2877
+ return `${PEER_GROUP_SCOPE_PREFIX2}${groupId}`;
2878
+ }
2879
+ function parsePeerGroupScope2(scope) {
2880
+ if (!scope || !scope.startsWith(PEER_GROUP_SCOPE_PREFIX2)) {
2881
+ return null;
2882
+ }
2883
+ return scope.slice(PEER_GROUP_SCOPE_PREFIX2.length);
2884
+ }
2885
+ function shufflePeerIds(peerIds, randomFn = Math.random) {
2886
+ const list = [...peerIds];
2887
+ for (let i = list.length - 1; i > 0; i -= 1) {
2888
+ const j = Math.floor(randomFn() * (i + 1));
2889
+ [list[i], list[j]] = [list[j], list[i]];
2890
+ }
2891
+ return list;
2892
+ }
2893
+ function selectFanoutPeers2({
2894
+ peers,
2895
+ count,
2896
+ excludePeerIds = [],
2897
+ connectedPeerIds = [],
2898
+ randomFn = Math.random
2899
+ }) {
2900
+ const excluded = new Set(excludePeerIds.filter(Boolean));
2901
+ const candidates = peers.map((entry) => entry.peerId || entry).filter((peerId) => peerId && !excluded.has(peerId));
2902
+ const connected = new Set(connectedPeerIds.filter(Boolean));
2903
+ const preferred = candidates.filter((peerId) => connected.has(peerId));
2904
+ const others = candidates.filter((peerId) => !connected.has(peerId));
2905
+ const ordered = [
2906
+ ...shufflePeerIds(preferred, randomFn),
2907
+ ...shufflePeerIds(others, randomFn)
2908
+ ];
2909
+ return ordered.slice(0, Math.max(0, count));
2910
+ }
2911
+ module2.exports = {
2912
+ PEER_GROUP_SCOPE_PREFIX: PEER_GROUP_SCOPE_PREFIX2,
2913
+ DEFAULT_PEER_GROUP_OPTIONS: DEFAULT_PEER_GROUP_OPTIONS2,
2914
+ peerGroupScope: peerGroupScope2,
2915
+ parsePeerGroupScope: parsePeerGroupScope2,
2916
+ shufflePeerIds,
2917
+ selectFanoutPeers: selectFanoutPeers2
2918
+ };
2919
+ }
2920
+ });
2921
+
2857
2922
  // src/core/dignity-p2p.js
2858
2923
  var require_dignity_p2p = __commonJS({
2859
2924
  "src/core/dignity-p2p.js"(exports2, module2) {
@@ -2861,6 +2926,11 @@ var require_dignity_p2p = __commonJS({
2861
2926
  var naclUtil = require_nacl_util();
2862
2927
  var EventEmitter = require_event_emitter();
2863
2928
  var { MessageSecurityService: MessageSecurityService2, stableStringify } = require_message_security_service();
2929
+ var {
2930
+ DEFAULT_PEER_GROUP_OPTIONS: DEFAULT_PEER_GROUP_OPTIONS2,
2931
+ peerGroupScope: peerGroupScope2,
2932
+ selectFanoutPeers: selectFanoutPeers2
2933
+ } = require_peer_group();
2864
2934
  function computeContentHash(data) {
2865
2935
  const canonical = stableStringify(data || {});
2866
2936
  const bytes = naclUtil.decodeUTF8(canonical);
@@ -2893,8 +2963,16 @@ var require_dignity_p2p = __commonJS({
2893
2963
  this.defaultPresenceTtlMs = security && typeof security.presenceTtlMs === "number" ? security.presenceTtlMs : 45e3;
2894
2964
  this.discoveryRooms = /* @__PURE__ */ new Map();
2895
2965
  this.presenceByScope = /* @__PURE__ */ new Map();
2966
+ this.peerGroups = /* @__PURE__ */ new Map();
2967
+ this.seenGossipIds = /* @__PURE__ */ new Map();
2968
+ this.defaultPeerGroupFanout = security && typeof security.peerGroupFanout === "number" ? security.peerGroupFanout : DEFAULT_PEER_GROUP_OPTIONS2.fanout;
2969
+ this.defaultPeerGroupMaxActivePeers = security && typeof security.peerGroupMaxActivePeers === "number" ? security.peerGroupMaxActivePeers : DEFAULT_PEER_GROUP_OPTIONS2.maxActivePeers;
2970
+ this.defaultGossipMaxHops = security && typeof security.gossipMaxHops === "number" ? security.gossipMaxHops : DEFAULT_PEER_GROUP_OPTIONS2.maxHops;
2971
+ this.globalMaxOpenConnections = security && typeof security.globalMaxOpenConnections === "number" ? security.globalMaxOpenConnections : 32;
2972
+ this.gossipIdTtlMs = security && typeof security.gossipIdTtlMs === "number" ? security.gossipIdTtlMs : 5 * 60 * 1e3;
2973
+ this.maxAppliedOperations = security && typeof security.maxAppliedOperations === "number" ? security.maxAppliedOperations : 5e4;
2896
2974
  this.state = /* @__PURE__ */ new Map();
2897
- this.appliedOperations = /* @__PURE__ */ new Set();
2975
+ this.appliedOperations = /* @__PURE__ */ new Map();
2898
2976
  this.boundMessageHandler = this.handleIncomingMessage.bind(this);
2899
2977
  }
2900
2978
  async start() {
@@ -2902,6 +2980,14 @@ var require_dignity_p2p = __commonJS({
2902
2980
  await this.networkAdapter.start(this.nodeId);
2903
2981
  }
2904
2982
  async stop() {
2983
+ const joinedGroups = Array.from(this.peerGroups.keys());
2984
+ for (const groupId of joinedGroups) {
2985
+ try {
2986
+ await this.leavePeerGroup(groupId);
2987
+ } catch (error) {
2988
+ this.emit("warning", { type: "peer-group-leave-failed", groupId, error });
2989
+ }
2990
+ }
2905
2991
  const joinedScopes = Array.from(this.discoveryRooms.keys());
2906
2992
  for (const scope of joinedScopes) {
2907
2993
  try {
@@ -3250,6 +3336,7 @@ var require_dignity_p2p = __commonJS({
3250
3336
  const connectToPeers = securityContext.connectToPeers;
3251
3337
  if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3252
3338
  await this.ensureConnectedToPeers(connectToPeers);
3339
+ await this.enforceConnectionBudget();
3253
3340
  }
3254
3341
  const envelope = await this.securityService.secureOutgoingMessage({
3255
3342
  messageType,
@@ -3257,6 +3344,11 @@ var require_dignity_p2p = __commonJS({
3257
3344
  targetId: null,
3258
3345
  securityContext
3259
3346
  });
3347
+ const fanoutPeerIds = securityContext.fanoutPeerIds;
3348
+ if (Array.isArray(fanoutPeerIds) && fanoutPeerIds.length > 0 && typeof this.networkAdapter.sendToPeers === "function") {
3349
+ await this.networkAdapter.sendToPeers(envelope, fanoutPeerIds);
3350
+ return;
3351
+ }
3260
3352
  await this.networkAdapter.broadcast(envelope);
3261
3353
  }
3262
3354
  async sendDirectMessage(targetId, messageType, payload) {
@@ -3272,8 +3364,280 @@ var require_dignity_p2p = __commonJS({
3272
3364
  payload,
3273
3365
  targetId
3274
3366
  });
3367
+ if (targetId && typeof this.networkAdapter.sendToPeers === "function") {
3368
+ await this.networkAdapter.sendToPeers(envelope, [targetId]);
3369
+ return;
3370
+ }
3275
3371
  await this.networkAdapter.broadcast(envelope);
3276
3372
  }
3373
+ peerGroupScopeFor(groupId) {
3374
+ return peerGroupScope2(groupId);
3375
+ }
3376
+ getPeerGroupConfig(groupId) {
3377
+ return this.peerGroups.get(groupId) || null;
3378
+ }
3379
+ listPeerGroupMembers(groupId, options = {}) {
3380
+ return this.listPeers(this.peerGroupScopeFor(groupId), options);
3381
+ }
3382
+ getPeerGroupStats() {
3383
+ const adapter = this.networkAdapter;
3384
+ const openPeerIds = typeof adapter.listOpenPeerIds === "function" ? adapter.listOpenPeerIds() : [];
3385
+ return {
3386
+ joinedGroups: Array.from(this.peerGroups.keys()),
3387
+ seenGossipCount: this.seenGossipIds.size,
3388
+ openConnectionCount: openPeerIds.length,
3389
+ globalMaxOpenConnections: this.globalMaxOpenConnections
3390
+ };
3391
+ }
3392
+ pruneSeenGossip() {
3393
+ const now = this.now();
3394
+ for (const [gossipId, expiresAt] of this.seenGossipIds.entries()) {
3395
+ if (expiresAt <= now) {
3396
+ this.seenGossipIds.delete(gossipId);
3397
+ }
3398
+ }
3399
+ }
3400
+ hasSeenGossip(gossipId) {
3401
+ if (!gossipId) {
3402
+ return false;
3403
+ }
3404
+ this.pruneSeenGossip();
3405
+ return this.seenGossipIds.has(gossipId);
3406
+ }
3407
+ markSeenGossip(gossipId) {
3408
+ if (!gossipId) {
3409
+ return;
3410
+ }
3411
+ this.seenGossipIds.set(gossipId, this.now() + this.gossipIdTtlMs);
3412
+ }
3413
+ listConnectedPeerIds() {
3414
+ if (typeof this.networkAdapter.listOpenPeerIds === "function") {
3415
+ return this.networkAdapter.listOpenPeerIds();
3416
+ }
3417
+ return [];
3418
+ }
3419
+ selectPeerGroupFanout(groupId, count, excludePeerIds = []) {
3420
+ const scope = this.peerGroupScopeFor(groupId);
3421
+ const peers = this.listPeers(scope, { includeSelf: false });
3422
+ return selectFanoutPeers2({
3423
+ peers,
3424
+ count,
3425
+ excludePeerIds: [...excludePeerIds, this.nodeId],
3426
+ connectedPeerIds: this.listConnectedPeerIds()
3427
+ });
3428
+ }
3429
+ async enforceConnectionBudget() {
3430
+ const adapter = this.networkAdapter;
3431
+ if (typeof adapter.listOpenPeerIds !== "function" || typeof adapter.disconnectPeer !== "function") {
3432
+ return;
3433
+ }
3434
+ const openPeerIds = adapter.listOpenPeerIds();
3435
+ if (openPeerIds.length <= this.globalMaxOpenConnections) {
3436
+ return;
3437
+ }
3438
+ const excess = openPeerIds.length - this.globalMaxOpenConnections;
3439
+ const toClose = openPeerIds.slice(0, excess);
3440
+ for (const peerId of toClose) {
3441
+ try {
3442
+ await adapter.disconnectPeer(peerId);
3443
+ } catch (error) {
3444
+ this.emit("warning", { type: "peer-disconnect-failed", peerId, error });
3445
+ }
3446
+ }
3447
+ }
3448
+ async joinPeerGroup(groupId, options = {}) {
3449
+ if (!groupId) {
3450
+ throw new Error("joinPeerGroup requires groupId");
3451
+ }
3452
+ const scope = this.peerGroupScopeFor(groupId);
3453
+ const config = {
3454
+ fanout: typeof options.fanout === "number" ? options.fanout : this.defaultPeerGroupFanout,
3455
+ maxActivePeers: typeof options.maxActivePeers === "number" ? options.maxActivePeers : this.defaultPeerGroupMaxActivePeers,
3456
+ maxHops: typeof options.maxHops === "number" ? options.maxHops : this.defaultGossipMaxHops,
3457
+ relayEnabled: options.relayEnabled !== false
3458
+ };
3459
+ await this.joinDiscovery(scope, {
3460
+ metadata: {
3461
+ peerGroup: groupId,
3462
+ ...options.metadata || {}
3463
+ },
3464
+ bootstrapPeerIds: options.bootstrapPeerIds,
3465
+ heartbeatIntervalMs: options.heartbeatIntervalMs,
3466
+ ttlMs: options.ttlMs
3467
+ });
3468
+ this.peerGroups.set(groupId, config);
3469
+ this.emit("peergroupjoined", { groupId, config });
3470
+ return config;
3471
+ }
3472
+ async leavePeerGroup(groupId) {
3473
+ if (!groupId) {
3474
+ return;
3475
+ }
3476
+ const scope = this.peerGroupScopeFor(groupId);
3477
+ await this.leaveDiscovery(scope);
3478
+ this.peerGroups.delete(groupId);
3479
+ this.emit("peergroupleft", { groupId });
3480
+ }
3481
+ async publishToPeerGroup(groupId, innerMessageType, innerPayload, options = {}) {
3482
+ if (!groupId) {
3483
+ throw new Error("publishToPeerGroup requires groupId");
3484
+ }
3485
+ const group = this.peerGroups.get(groupId);
3486
+ if (!group && options.allowUnjoined !== true) {
3487
+ throw new Error(`PeerGroup ${groupId} has not been joined`);
3488
+ }
3489
+ const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
3490
+ const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
3491
+ const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
3492
+ const fanoutPeerIds = this.selectPeerGroupFanout(groupId, fanout, [this.nodeId]);
3493
+ if (fanoutPeerIds.length > 0) {
3494
+ await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
3495
+ await this.enforceConnectionBudget();
3496
+ }
3497
+ const gossipId = options.gossipId || this.idGenerator();
3498
+ this.markSeenGossip(gossipId);
3499
+ await this.broadcastMessage("peer-group:gossip", {
3500
+ groupId,
3501
+ gossipId,
3502
+ publisherId: this.nodeId,
3503
+ hop: 0,
3504
+ maxHop,
3505
+ innerMessageType,
3506
+ innerPayload
3507
+ }, {
3508
+ broadcastScope: this.peerGroupScopeFor(groupId),
3509
+ fanoutPeerIds
3510
+ });
3511
+ return { gossipId, fanoutPeerIds };
3512
+ }
3513
+ async publishRecordToPeerGroup(groupId, collectionName, id, options = {}) {
3514
+ const collection = this.getCollection(collectionName);
3515
+ const raw = collection.get(id);
3516
+ if (!raw || raw.deletedAt) {
3517
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3518
+ }
3519
+ const record = this.normalizeRecord(raw);
3520
+ return this.publishToPeerGroup(groupId, "record:snapshot", {
3521
+ collectionName,
3522
+ record
3523
+ }, options);
3524
+ }
3525
+ async handlePeerGroupGossip(decrypted) {
3526
+ const payload = decrypted.payload || {};
3527
+ const {
3528
+ groupId,
3529
+ gossipId,
3530
+ publisherId = decrypted.senderId,
3531
+ hop = 0,
3532
+ maxHop: payloadMaxHop,
3533
+ innerMessageType,
3534
+ innerPayload
3535
+ } = payload;
3536
+ if (!groupId || !innerMessageType || !gossipId) {
3537
+ return;
3538
+ }
3539
+ if (!this.peerGroups.has(groupId)) {
3540
+ return;
3541
+ }
3542
+ if (this.hasSeenGossip(gossipId)) {
3543
+ return;
3544
+ }
3545
+ this.markSeenGossip(gossipId);
3546
+ await this.dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, {
3547
+ groupId,
3548
+ senderId: decrypted.senderId,
3549
+ publisherId
3550
+ });
3551
+ const group = this.peerGroups.get(groupId);
3552
+ const configuredMaxHop = group ? group.maxHops : this.defaultGossipMaxHops;
3553
+ const maxHop = typeof payloadMaxHop === "number" ? Math.min(payloadMaxHop, configuredMaxHop) : configuredMaxHop;
3554
+ if (!group || group.relayEnabled === false || hop >= maxHop) {
3555
+ return;
3556
+ }
3557
+ const relayPeers = this.selectPeerGroupFanout(groupId, group.fanout, [
3558
+ decrypted.senderId,
3559
+ this.nodeId
3560
+ ]);
3561
+ if (relayPeers.length === 0) {
3562
+ return;
3563
+ }
3564
+ await this.ensureConnectedToPeers(relayPeers.slice(0, group.maxActivePeers));
3565
+ await this.enforceConnectionBudget();
3566
+ await this.broadcastMessage("peer-group:gossip", {
3567
+ groupId,
3568
+ gossipId,
3569
+ publisherId,
3570
+ hop: hop + 1,
3571
+ maxHop,
3572
+ innerMessageType,
3573
+ innerPayload
3574
+ }, {
3575
+ broadcastScope: this.peerGroupScopeFor(groupId),
3576
+ fanoutPeerIds: relayPeers
3577
+ });
3578
+ }
3579
+ normalizeGossipOperation(operation, publisherId) {
3580
+ if (!operation || !publisherId) {
3581
+ return null;
3582
+ }
3583
+ if (operation.actorId && operation.actorId !== publisherId) {
3584
+ this.emit("warning", {
3585
+ type: "gossip-operation-actor-mismatch",
3586
+ publisherId,
3587
+ actorId: operation.actorId,
3588
+ kind: operation.kind,
3589
+ collection: operation.collectionName,
3590
+ id: operation.id
3591
+ });
3592
+ return null;
3593
+ }
3594
+ const normalized = {
3595
+ ...operation,
3596
+ actorId: publisherId
3597
+ };
3598
+ if (normalized.kind === "create") {
3599
+ normalized.ownerId = publisherId;
3600
+ }
3601
+ return normalized;
3602
+ }
3603
+ async dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, context = {}) {
3604
+ if (innerMessageType === "operation") {
3605
+ const operation = this.normalizeGossipOperation(
3606
+ innerPayload,
3607
+ context.publisherId || context.senderId
3608
+ );
3609
+ if (operation) {
3610
+ this.applyOperation(operation);
3611
+ }
3612
+ return;
3613
+ }
3614
+ if (innerMessageType === "record:snapshot") {
3615
+ const { collectionName, record } = innerPayload || {};
3616
+ if (collectionName && record) {
3617
+ const applied = this.restoreRecord(collectionName, record, {
3618
+ rejectOnHashMismatch: true,
3619
+ rejectOnOwnershipMismatch: true,
3620
+ via: "peer-group"
3621
+ });
3622
+ if (applied) {
3623
+ this.emit("change", {
3624
+ kind: "snapshot",
3625
+ collection: collectionName,
3626
+ id: record.id,
3627
+ via: "peer-group",
3628
+ groupId: context.groupId
3629
+ });
3630
+ }
3631
+ }
3632
+ return;
3633
+ }
3634
+ this.emit("peergroupmessage", {
3635
+ groupId: context.groupId,
3636
+ senderId: context.senderId,
3637
+ type: innerMessageType,
3638
+ payload: innerPayload
3639
+ });
3640
+ }
3277
3641
  getPresenceMap(scope) {
3278
3642
  if (!this.presenceByScope.has(scope)) {
3279
3643
  this.presenceByScope.set(scope, /* @__PURE__ */ new Map());
@@ -3404,6 +3768,13 @@ var require_dignity_p2p = __commonJS({
3404
3768
  }
3405
3769
  async handleIncomingMessage(message) {
3406
3770
  if (message && message.opId && message.kind) {
3771
+ if (this.securityService.options.enabled) {
3772
+ this.emit("messageignored", {
3773
+ reason: "raw-operation-rejected",
3774
+ hint: "Unsigned raw operations are disabled when security is enabled"
3775
+ });
3776
+ return;
3777
+ }
3407
3778
  this.applyOperation(message);
3408
3779
  return;
3409
3780
  }
@@ -3414,9 +3785,6 @@ var require_dignity_p2p = __commonJS({
3414
3785
  });
3415
3786
  return;
3416
3787
  }
3417
- if (message && message.senderId && message.senderPublicKey) {
3418
- this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3419
- }
3420
3788
  let decrypted;
3421
3789
  try {
3422
3790
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3434,6 +3802,9 @@ var require_dignity_p2p = __commonJS({
3434
3802
  if (!decrypted || decrypted.ignored) {
3435
3803
  return;
3436
3804
  }
3805
+ if (message && message.senderId && message.senderPublicKey) {
3806
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3807
+ }
3437
3808
  if (decrypted.messageType === "operation") {
3438
3809
  this.applyOperation(decrypted.payload);
3439
3810
  return;
@@ -3457,18 +3828,27 @@ var require_dignity_p2p = __commonJS({
3457
3828
  const payload = decrypted.payload || {};
3458
3829
  const scope = payload.scope || "main";
3459
3830
  const peerId = payload.peerId || decrypted.senderId;
3460
- if (!peerId) {
3831
+ if (!peerId || peerId !== decrypted.senderId) {
3832
+ return;
3833
+ }
3834
+ if (!this.discoveryRooms.has(scope)) {
3461
3835
  return;
3462
3836
  }
3837
+ const room = this.discoveryRooms.get(scope);
3463
3838
  const presenceMap = this.getPresenceMap(scope);
3464
3839
  const isNewPeerInScope = !presenceMap.has(peerId);
3840
+ const requestedTtl = typeof payload.ttlMs === "number" ? payload.ttlMs : room.ttlMs;
3841
+ const ttlMs = Math.min(requestedTtl, room.ttlMs);
3465
3842
  this.upsertPresence(
3466
3843
  scope,
3467
3844
  peerId,
3468
3845
  payload.metadata || {},
3469
- payload.ttlMs || this.defaultPresenceTtlMs,
3470
- payload.announcedAt || this.now()
3846
+ ttlMs,
3847
+ this.now()
3471
3848
  );
3849
+ if (payload.metadata && payload.metadata.publicKey) {
3850
+ this.trustPeerPublicKey(peerId, payload.metadata.publicKey);
3851
+ }
3472
3852
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3473
3853
  if (typeof this.networkAdapter.connectToPeer === "function") {
3474
3854
  Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
@@ -3485,6 +3865,9 @@ var require_dignity_p2p = __commonJS({
3485
3865
  const payload = decrypted.payload || {};
3486
3866
  const scope = payload.scope || "main";
3487
3867
  const peerId = payload.peerId || decrypted.senderId;
3868
+ if (!peerId || peerId !== decrypted.senderId) {
3869
+ return;
3870
+ }
3488
3871
  const map = this.presenceByScope.get(scope);
3489
3872
  if (map && peerId && map.has(peerId)) {
3490
3873
  map.delete(peerId);
@@ -3492,6 +3875,10 @@ var require_dignity_p2p = __commonJS({
3492
3875
  }
3493
3876
  return;
3494
3877
  }
3878
+ if (decrypted.messageType === "peer-group:gossip") {
3879
+ await this.handlePeerGroupGossip(decrypted);
3880
+ return;
3881
+ }
3495
3882
  this.emit("message", {
3496
3883
  senderId: decrypted.senderId,
3497
3884
  targetId: decrypted.targetId,
@@ -3537,7 +3924,7 @@ var require_dignity_p2p = __commonJS({
3537
3924
  emitConflict(details) {
3538
3925
  this.emit("conflict", details);
3539
3926
  }
3540
- restoreRecord(collectionName, record) {
3927
+ restoreRecord(collectionName, record, options = {}) {
3541
3928
  if (!record || !record.id) {
3542
3929
  return false;
3543
3930
  }
@@ -3548,14 +3935,42 @@ var require_dignity_p2p = __commonJS({
3548
3935
  }
3549
3936
  const restoredData = { ...record.data || {} };
3550
3937
  const computedHash = computeContentHash(restoredData);
3551
- if (record.hash && record.hash !== computedHash) {
3938
+ const rejectOnHashMismatch = options.rejectOnHashMismatch === true;
3939
+ const rejectOnOwnershipMismatch = options.rejectOnOwnershipMismatch === true;
3940
+ if (rejectOnOwnershipMismatch && current && record.ownerId && current.ownerId !== record.ownerId) {
3941
+ this.emit("warning", {
3942
+ type: "ownership-mismatch",
3943
+ collection: collectionName,
3944
+ id: record.id,
3945
+ currentOwnerId: current.ownerId,
3946
+ advertisedOwnerId: record.ownerId,
3947
+ via: options.via || null
3948
+ });
3949
+ return false;
3950
+ }
3951
+ if (!record.hash) {
3952
+ const warning = {
3953
+ type: "content-hash-missing",
3954
+ collection: collectionName,
3955
+ id: record.id,
3956
+ via: options.via || null
3957
+ };
3958
+ this.emit("warning", warning);
3959
+ if (rejectOnHashMismatch) {
3960
+ return false;
3961
+ }
3962
+ } else if (record.hash !== computedHash) {
3552
3963
  this.emit("warning", {
3553
3964
  type: "content-hash-mismatch",
3554
3965
  collection: collectionName,
3555
3966
  id: record.id,
3556
3967
  advertisedHash: record.hash,
3557
- computedHash
3968
+ computedHash,
3969
+ via: options.via || null
3558
3970
  });
3971
+ if (rejectOnHashMismatch) {
3972
+ return false;
3973
+ }
3559
3974
  }
3560
3975
  collection.set(record.id, {
3561
3976
  id: record.id,
@@ -3597,6 +4012,15 @@ var require_dignity_p2p = __commonJS({
3597
4012
  });
3598
4013
  return record;
3599
4014
  }
4015
+ pruneAppliedOperations() {
4016
+ while (this.appliedOperations.size > this.maxAppliedOperations) {
4017
+ const oldestOpId = this.appliedOperations.keys().next().value;
4018
+ if (!oldestOpId) {
4019
+ break;
4020
+ }
4021
+ this.appliedOperations.delete(oldestOpId);
4022
+ }
4023
+ }
3600
4024
  applyOperation(operation) {
3601
4025
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3602
4026
  return false;
@@ -3618,7 +4042,8 @@ var require_dignity_p2p = __commonJS({
3618
4042
  deletedAt: null,
3619
4043
  version: 1
3620
4044
  });
3621
- this.appliedOperations.add(operation.opId);
4045
+ this.appliedOperations.set(operation.opId, this.now());
4046
+ this.pruneAppliedOperations();
3622
4047
  this.emit("change", { kind: "create", collection: operation.collectionName, id: operation.id });
3623
4048
  return true;
3624
4049
  }
@@ -3662,7 +4087,8 @@ var require_dignity_p2p = __commonJS({
3662
4087
  }
3663
4088
  current.updatedAt = operation.timestamp;
3664
4089
  current.version += 1;
3665
- this.appliedOperations.add(operation.opId);
4090
+ this.appliedOperations.set(operation.opId, this.now());
4091
+ this.pruneAppliedOperations();
3666
4092
  this.emit("change", {
3667
4093
  kind: "transfer-ownership",
3668
4094
  collection: operation.collectionName,
@@ -3691,7 +4117,8 @@ var require_dignity_p2p = __commonJS({
3691
4117
  current.deletedAt = operation.timestamp;
3692
4118
  current.updatedAt = operation.timestamp;
3693
4119
  current.version += 1;
3694
- this.appliedOperations.add(operation.opId);
4120
+ this.appliedOperations.set(operation.opId, this.now());
4121
+ this.pruneAppliedOperations();
3695
4122
  this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3696
4123
  return true;
3697
4124
  }
@@ -3721,7 +4148,8 @@ var require_dignity_p2p = __commonJS({
3721
4148
  }
3722
4149
  current.updatedAt = operation.timestamp;
3723
4150
  current.version += 1;
3724
- this.appliedOperations.add(operation.opId);
4151
+ this.appliedOperations.set(operation.opId, this.now());
4152
+ this.pruneAppliedOperations();
3725
4153
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
3726
4154
  return true;
3727
4155
  }
@@ -10759,6 +11187,16 @@ var require_in_memory_network = __commonJS({
10759
11187
  }
10760
11188
  await Promise.all(deliveries);
10761
11189
  }
11190
+ async sendToPeers(senderId, message, peerIds = []) {
11191
+ const targets = new Set((peerIds || []).filter((peerId) => peerId && peerId !== senderId));
11192
+ const deliveries = [];
11193
+ for (const [nodeId, adapter] of this.adapters.entries()) {
11194
+ if (nodeId !== senderId && targets.has(nodeId)) {
11195
+ deliveries.push(adapter.receive(message));
11196
+ }
11197
+ }
11198
+ await Promise.all(deliveries);
11199
+ }
10762
11200
  };
10763
11201
  var InMemoryNetworkAdapter2 = class {
10764
11202
  constructor(hub) {
@@ -10768,6 +11206,7 @@ var require_in_memory_network = __commonJS({
10768
11206
  this.hub = hub;
10769
11207
  this.nodeId = null;
10770
11208
  this.messageHandlers = /* @__PURE__ */ new Set();
11209
+ this.connectedPeers = /* @__PURE__ */ new Set();
10771
11210
  }
10772
11211
  async start(nodeId) {
10773
11212
  this.nodeId = nodeId;
@@ -10778,6 +11217,13 @@ var require_in_memory_network = __commonJS({
10778
11217
  this.hub.unregister(this.nodeId);
10779
11218
  }
10780
11219
  this.nodeId = null;
11220
+ this.connectedPeers.clear();
11221
+ }
11222
+ async connectToPeer(remotePeerId) {
11223
+ if (!remotePeerId || remotePeerId === this.nodeId) {
11224
+ return;
11225
+ }
11226
+ this.connectedPeers.add(remotePeerId);
10781
11227
  }
10782
11228
  async broadcast(message) {
10783
11229
  if (!this.nodeId) {
@@ -10785,6 +11231,21 @@ var require_in_memory_network = __commonJS({
10785
11231
  }
10786
11232
  await this.hub.broadcast(this.nodeId, message);
10787
11233
  }
11234
+ async sendToPeers(message, peerIds = []) {
11235
+ if (!this.nodeId) {
11236
+ throw new Error("Network adapter has not been started");
11237
+ }
11238
+ await this.hub.sendToPeers(this.nodeId, message, peerIds);
11239
+ }
11240
+ listOpenPeerIds() {
11241
+ return [...this.connectedPeers];
11242
+ }
11243
+ getOpenConnectionCount() {
11244
+ return this.connectedPeers.size;
11245
+ }
11246
+ isConnectedTo(remotePeerId) {
11247
+ return this.connectedPeers.has(remotePeerId);
11248
+ }
10788
11249
  onMessage(handler) {
10789
11250
  this.messageHandlers.add(handler);
10790
11251
  }
@@ -10963,6 +11424,29 @@ var require_peerjs_network = __commonJS({
10963
11424
  }
10964
11425
  await Promise.all(deliveries);
10965
11426
  }
11427
+ async sendToPeers(message, peerIds = []) {
11428
+ if (!this.peer) {
11429
+ throw new Error("PeerJS network adapter has not been started");
11430
+ }
11431
+ const targets = new Set((peerIds || []).filter(Boolean));
11432
+ if (targets.size === 0) {
11433
+ return;
11434
+ }
11435
+ const deliveries = [];
11436
+ for (const [peerId, connection] of this.connections.entries()) {
11437
+ if (targets.has(peerId) && connection.open) {
11438
+ deliveries.push(connection.send(message));
11439
+ }
11440
+ }
11441
+ await Promise.all(deliveries);
11442
+ }
11443
+ async disconnectPeer(remotePeerId) {
11444
+ const connection = this.connections.get(remotePeerId);
11445
+ if (connection && typeof connection.close === "function") {
11446
+ connection.close();
11447
+ }
11448
+ this.connections.delete(remotePeerId);
11449
+ }
10966
11450
  getOpenConnectionCount() {
10967
11451
  return this.listOpenPeerIds().length;
10968
11452
  }
@@ -11191,6 +11675,13 @@ var {
11191
11675
  MessageSecurityService,
11192
11676
  DEFAULT_SECURITY_OPTIONS
11193
11677
  } = require_message_security_service();
11678
+ var {
11679
+ PEER_GROUP_SCOPE_PREFIX,
11680
+ DEFAULT_PEER_GROUP_OPTIONS,
11681
+ peerGroupScope,
11682
+ parsePeerGroupScope,
11683
+ selectFanoutPeers
11684
+ } = require_peer_group();
11194
11685
  module.exports = {
11195
11686
  DignityP2P,
11196
11687
  createDefaultSignalingPool,
@@ -11207,6 +11698,11 @@ module.exports = {
11207
11698
  VDF,
11208
11699
  SlothPermutation,
11209
11700
  MessageSecurityService,
11210
- DEFAULT_SECURITY_OPTIONS
11701
+ DEFAULT_SECURITY_OPTIONS,
11702
+ PEER_GROUP_SCOPE_PREFIX,
11703
+ DEFAULT_PEER_GROUP_OPTIONS,
11704
+ peerGroupScope,
11705
+ parsePeerGroupScope,
11706
+ selectFanoutPeers
11211
11707
  };
11212
11708
  //# sourceMappingURL=dignity.cjs.js.map