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.
@@ -2769,8 +2769,14 @@ var require_message_security_service = __commonJS({
2769
2769
  const nonce = naclUtil.decodeBase64(encryption.nonce);
2770
2770
  let key;
2771
2771
  if (encryption.kdf === "pbkdf2") {
2772
- const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
2773
- key = await deriveBroadcastKey(password, salt, iterations);
2772
+ const configuredIterations = this.options.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
2773
+ const requestedIterations = encryption.kdfIterations || configuredIterations;
2774
+ const minIterations = Math.max(1e3, Math.floor(configuredIterations * 0.1));
2775
+ const maxIterations = configuredIterations * 2;
2776
+ if (requestedIterations < minIterations || requestedIterations > maxIterations) {
2777
+ throw new Error(`Invalid kdfIterations: ${requestedIterations}`);
2778
+ }
2779
+ key = await deriveBroadcastKey(password, salt, requestedIterations);
2774
2780
  } else {
2775
2781
  key = legacyBroadcastKey(password, salt);
2776
2782
  }
@@ -2874,6 +2880,65 @@ var require_message_security_service = __commonJS({
2874
2880
  }
2875
2881
  });
2876
2882
 
2883
+ // src/gossip/peer-group.js
2884
+ var require_peer_group = __commonJS({
2885
+ "src/gossip/peer-group.js"(exports, module) {
2886
+ var PEER_GROUP_SCOPE_PREFIX = "gossip:";
2887
+ var DEFAULT_PEER_GROUP_OPTIONS = {
2888
+ fanout: 3,
2889
+ maxActivePeers: 8,
2890
+ maxHops: 6,
2891
+ relayEnabled: true
2892
+ };
2893
+ function peerGroupScope(groupId) {
2894
+ if (!groupId) {
2895
+ throw new Error("peerGroupScope requires groupId");
2896
+ }
2897
+ return `${PEER_GROUP_SCOPE_PREFIX}${groupId}`;
2898
+ }
2899
+ function parsePeerGroupScope(scope) {
2900
+ if (!scope || !scope.startsWith(PEER_GROUP_SCOPE_PREFIX)) {
2901
+ return null;
2902
+ }
2903
+ return scope.slice(PEER_GROUP_SCOPE_PREFIX.length);
2904
+ }
2905
+ function shufflePeerIds(peerIds, randomFn = Math.random) {
2906
+ const list = [...peerIds];
2907
+ for (let i = list.length - 1; i > 0; i -= 1) {
2908
+ const j = Math.floor(randomFn() * (i + 1));
2909
+ [list[i], list[j]] = [list[j], list[i]];
2910
+ }
2911
+ return list;
2912
+ }
2913
+ function selectFanoutPeers({
2914
+ peers,
2915
+ count,
2916
+ excludePeerIds = [],
2917
+ connectedPeerIds = [],
2918
+ randomFn = Math.random
2919
+ }) {
2920
+ const excluded = new Set(excludePeerIds.filter(Boolean));
2921
+ const candidates = peers.map((entry) => entry.peerId || entry).filter((peerId) => peerId && !excluded.has(peerId));
2922
+ const connected = new Set(connectedPeerIds.filter(Boolean));
2923
+ const preferred = candidates.filter((peerId) => connected.has(peerId));
2924
+ const others = candidates.filter((peerId) => !connected.has(peerId));
2925
+ const ordered = [
2926
+ ...shufflePeerIds(preferred, randomFn),
2927
+ ...shufflePeerIds(others, randomFn)
2928
+ ];
2929
+ return ordered.slice(0, Math.max(0, count));
2930
+ }
2931
+ module.exports = {
2932
+ PEER_GROUP_SCOPE_PREFIX,
2933
+ DEFAULT_PEER_GROUP_OPTIONS,
2934
+ peerGroupScope,
2935
+ parsePeerGroupScope,
2936
+ shufflePeerIds,
2937
+ selectFanoutPeers
2938
+ };
2939
+ }
2940
+ });
2941
+
2877
2942
  // src/core/dignity-p2p.js
2878
2943
  var require_dignity_p2p = __commonJS({
2879
2944
  "src/core/dignity-p2p.js"(exports, module) {
@@ -2881,6 +2946,11 @@ var require_dignity_p2p = __commonJS({
2881
2946
  var naclUtil = require_nacl_util();
2882
2947
  var EventEmitter = require_event_emitter();
2883
2948
  var { MessageSecurityService, stableStringify } = require_message_security_service();
2949
+ var {
2950
+ DEFAULT_PEER_GROUP_OPTIONS,
2951
+ peerGroupScope,
2952
+ selectFanoutPeers
2953
+ } = require_peer_group();
2884
2954
  function computeContentHash(data) {
2885
2955
  const canonical = stableStringify(data || {});
2886
2956
  const bytes = naclUtil.decodeUTF8(canonical);
@@ -2913,8 +2983,16 @@ var require_dignity_p2p = __commonJS({
2913
2983
  this.defaultPresenceTtlMs = security && typeof security.presenceTtlMs === "number" ? security.presenceTtlMs : 45e3;
2914
2984
  this.discoveryRooms = /* @__PURE__ */ new Map();
2915
2985
  this.presenceByScope = /* @__PURE__ */ new Map();
2986
+ this.peerGroups = /* @__PURE__ */ new Map();
2987
+ this.seenGossipIds = /* @__PURE__ */ new Map();
2988
+ this.defaultPeerGroupFanout = security && typeof security.peerGroupFanout === "number" ? security.peerGroupFanout : DEFAULT_PEER_GROUP_OPTIONS.fanout;
2989
+ this.defaultPeerGroupMaxActivePeers = security && typeof security.peerGroupMaxActivePeers === "number" ? security.peerGroupMaxActivePeers : DEFAULT_PEER_GROUP_OPTIONS.maxActivePeers;
2990
+ this.defaultGossipMaxHops = security && typeof security.gossipMaxHops === "number" ? security.gossipMaxHops : DEFAULT_PEER_GROUP_OPTIONS.maxHops;
2991
+ this.globalMaxOpenConnections = security && typeof security.globalMaxOpenConnections === "number" ? security.globalMaxOpenConnections : 32;
2992
+ this.gossipIdTtlMs = security && typeof security.gossipIdTtlMs === "number" ? security.gossipIdTtlMs : 5 * 60 * 1e3;
2993
+ this.maxAppliedOperations = security && typeof security.maxAppliedOperations === "number" ? security.maxAppliedOperations : 5e4;
2916
2994
  this.state = /* @__PURE__ */ new Map();
2917
- this.appliedOperations = /* @__PURE__ */ new Set();
2995
+ this.appliedOperations = /* @__PURE__ */ new Map();
2918
2996
  this.boundMessageHandler = this.handleIncomingMessage.bind(this);
2919
2997
  }
2920
2998
  async start() {
@@ -2922,6 +3000,14 @@ var require_dignity_p2p = __commonJS({
2922
3000
  await this.networkAdapter.start(this.nodeId);
2923
3001
  }
2924
3002
  async stop() {
3003
+ const joinedGroups = Array.from(this.peerGroups.keys());
3004
+ for (const groupId of joinedGroups) {
3005
+ try {
3006
+ await this.leavePeerGroup(groupId);
3007
+ } catch (error) {
3008
+ this.emit("warning", { type: "peer-group-leave-failed", groupId, error });
3009
+ }
3010
+ }
2925
3011
  const joinedScopes = Array.from(this.discoveryRooms.keys());
2926
3012
  for (const scope of joinedScopes) {
2927
3013
  try {
@@ -3270,6 +3356,7 @@ var require_dignity_p2p = __commonJS({
3270
3356
  const connectToPeers = securityContext.connectToPeers;
3271
3357
  if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3272
3358
  await this.ensureConnectedToPeers(connectToPeers);
3359
+ await this.enforceConnectionBudget();
3273
3360
  }
3274
3361
  const envelope = await this.securityService.secureOutgoingMessage({
3275
3362
  messageType,
@@ -3277,6 +3364,11 @@ var require_dignity_p2p = __commonJS({
3277
3364
  targetId: null,
3278
3365
  securityContext
3279
3366
  });
3367
+ const fanoutPeerIds = securityContext.fanoutPeerIds;
3368
+ if (Array.isArray(fanoutPeerIds) && fanoutPeerIds.length > 0 && typeof this.networkAdapter.sendToPeers === "function") {
3369
+ await this.networkAdapter.sendToPeers(envelope, fanoutPeerIds);
3370
+ return;
3371
+ }
3280
3372
  await this.networkAdapter.broadcast(envelope);
3281
3373
  }
3282
3374
  async sendDirectMessage(targetId, messageType, payload) {
@@ -3292,8 +3384,280 @@ var require_dignity_p2p = __commonJS({
3292
3384
  payload,
3293
3385
  targetId
3294
3386
  });
3387
+ if (targetId && typeof this.networkAdapter.sendToPeers === "function") {
3388
+ await this.networkAdapter.sendToPeers(envelope, [targetId]);
3389
+ return;
3390
+ }
3295
3391
  await this.networkAdapter.broadcast(envelope);
3296
3392
  }
3393
+ peerGroupScopeFor(groupId) {
3394
+ return peerGroupScope(groupId);
3395
+ }
3396
+ getPeerGroupConfig(groupId) {
3397
+ return this.peerGroups.get(groupId) || null;
3398
+ }
3399
+ listPeerGroupMembers(groupId, options = {}) {
3400
+ return this.listPeers(this.peerGroupScopeFor(groupId), options);
3401
+ }
3402
+ getPeerGroupStats() {
3403
+ const adapter = this.networkAdapter;
3404
+ const openPeerIds = typeof adapter.listOpenPeerIds === "function" ? adapter.listOpenPeerIds() : [];
3405
+ return {
3406
+ joinedGroups: Array.from(this.peerGroups.keys()),
3407
+ seenGossipCount: this.seenGossipIds.size,
3408
+ openConnectionCount: openPeerIds.length,
3409
+ globalMaxOpenConnections: this.globalMaxOpenConnections
3410
+ };
3411
+ }
3412
+ pruneSeenGossip() {
3413
+ const now = this.now();
3414
+ for (const [gossipId, expiresAt] of this.seenGossipIds.entries()) {
3415
+ if (expiresAt <= now) {
3416
+ this.seenGossipIds.delete(gossipId);
3417
+ }
3418
+ }
3419
+ }
3420
+ hasSeenGossip(gossipId) {
3421
+ if (!gossipId) {
3422
+ return false;
3423
+ }
3424
+ this.pruneSeenGossip();
3425
+ return this.seenGossipIds.has(gossipId);
3426
+ }
3427
+ markSeenGossip(gossipId) {
3428
+ if (!gossipId) {
3429
+ return;
3430
+ }
3431
+ this.seenGossipIds.set(gossipId, this.now() + this.gossipIdTtlMs);
3432
+ }
3433
+ listConnectedPeerIds() {
3434
+ if (typeof this.networkAdapter.listOpenPeerIds === "function") {
3435
+ return this.networkAdapter.listOpenPeerIds();
3436
+ }
3437
+ return [];
3438
+ }
3439
+ selectPeerGroupFanout(groupId, count, excludePeerIds = []) {
3440
+ const scope = this.peerGroupScopeFor(groupId);
3441
+ const peers = this.listPeers(scope, { includeSelf: false });
3442
+ return selectFanoutPeers({
3443
+ peers,
3444
+ count,
3445
+ excludePeerIds: [...excludePeerIds, this.nodeId],
3446
+ connectedPeerIds: this.listConnectedPeerIds()
3447
+ });
3448
+ }
3449
+ async enforceConnectionBudget() {
3450
+ const adapter = this.networkAdapter;
3451
+ if (typeof adapter.listOpenPeerIds !== "function" || typeof adapter.disconnectPeer !== "function") {
3452
+ return;
3453
+ }
3454
+ const openPeerIds = adapter.listOpenPeerIds();
3455
+ if (openPeerIds.length <= this.globalMaxOpenConnections) {
3456
+ return;
3457
+ }
3458
+ const excess = openPeerIds.length - this.globalMaxOpenConnections;
3459
+ const toClose = openPeerIds.slice(0, excess);
3460
+ for (const peerId of toClose) {
3461
+ try {
3462
+ await adapter.disconnectPeer(peerId);
3463
+ } catch (error) {
3464
+ this.emit("warning", { type: "peer-disconnect-failed", peerId, error });
3465
+ }
3466
+ }
3467
+ }
3468
+ async joinPeerGroup(groupId, options = {}) {
3469
+ if (!groupId) {
3470
+ throw new Error("joinPeerGroup requires groupId");
3471
+ }
3472
+ const scope = this.peerGroupScopeFor(groupId);
3473
+ const config = {
3474
+ fanout: typeof options.fanout === "number" ? options.fanout : this.defaultPeerGroupFanout,
3475
+ maxActivePeers: typeof options.maxActivePeers === "number" ? options.maxActivePeers : this.defaultPeerGroupMaxActivePeers,
3476
+ maxHops: typeof options.maxHops === "number" ? options.maxHops : this.defaultGossipMaxHops,
3477
+ relayEnabled: options.relayEnabled !== false
3478
+ };
3479
+ await this.joinDiscovery(scope, {
3480
+ metadata: {
3481
+ peerGroup: groupId,
3482
+ ...options.metadata || {}
3483
+ },
3484
+ bootstrapPeerIds: options.bootstrapPeerIds,
3485
+ heartbeatIntervalMs: options.heartbeatIntervalMs,
3486
+ ttlMs: options.ttlMs
3487
+ });
3488
+ this.peerGroups.set(groupId, config);
3489
+ this.emit("peergroupjoined", { groupId, config });
3490
+ return config;
3491
+ }
3492
+ async leavePeerGroup(groupId) {
3493
+ if (!groupId) {
3494
+ return;
3495
+ }
3496
+ const scope = this.peerGroupScopeFor(groupId);
3497
+ await this.leaveDiscovery(scope);
3498
+ this.peerGroups.delete(groupId);
3499
+ this.emit("peergroupleft", { groupId });
3500
+ }
3501
+ async publishToPeerGroup(groupId, innerMessageType, innerPayload, options = {}) {
3502
+ if (!groupId) {
3503
+ throw new Error("publishToPeerGroup requires groupId");
3504
+ }
3505
+ const group = this.peerGroups.get(groupId);
3506
+ if (!group && options.allowUnjoined !== true) {
3507
+ throw new Error(`PeerGroup ${groupId} has not been joined`);
3508
+ }
3509
+ const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
3510
+ const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
3511
+ const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
3512
+ const fanoutPeerIds = this.selectPeerGroupFanout(groupId, fanout, [this.nodeId]);
3513
+ if (fanoutPeerIds.length > 0) {
3514
+ await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
3515
+ await this.enforceConnectionBudget();
3516
+ }
3517
+ const gossipId = options.gossipId || this.idGenerator();
3518
+ this.markSeenGossip(gossipId);
3519
+ await this.broadcastMessage("peer-group:gossip", {
3520
+ groupId,
3521
+ gossipId,
3522
+ publisherId: this.nodeId,
3523
+ hop: 0,
3524
+ maxHop,
3525
+ innerMessageType,
3526
+ innerPayload
3527
+ }, {
3528
+ broadcastScope: this.peerGroupScopeFor(groupId),
3529
+ fanoutPeerIds
3530
+ });
3531
+ return { gossipId, fanoutPeerIds };
3532
+ }
3533
+ async publishRecordToPeerGroup(groupId, collectionName, id, options = {}) {
3534
+ const collection = this.getCollection(collectionName);
3535
+ const raw = collection.get(id);
3536
+ if (!raw || raw.deletedAt) {
3537
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3538
+ }
3539
+ const record = this.normalizeRecord(raw);
3540
+ return this.publishToPeerGroup(groupId, "record:snapshot", {
3541
+ collectionName,
3542
+ record
3543
+ }, options);
3544
+ }
3545
+ async handlePeerGroupGossip(decrypted) {
3546
+ const payload = decrypted.payload || {};
3547
+ const {
3548
+ groupId,
3549
+ gossipId,
3550
+ publisherId = decrypted.senderId,
3551
+ hop = 0,
3552
+ maxHop: payloadMaxHop,
3553
+ innerMessageType,
3554
+ innerPayload
3555
+ } = payload;
3556
+ if (!groupId || !innerMessageType || !gossipId) {
3557
+ return;
3558
+ }
3559
+ if (!this.peerGroups.has(groupId)) {
3560
+ return;
3561
+ }
3562
+ if (this.hasSeenGossip(gossipId)) {
3563
+ return;
3564
+ }
3565
+ this.markSeenGossip(gossipId);
3566
+ await this.dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, {
3567
+ groupId,
3568
+ senderId: decrypted.senderId,
3569
+ publisherId
3570
+ });
3571
+ const group = this.peerGroups.get(groupId);
3572
+ const configuredMaxHop = group ? group.maxHops : this.defaultGossipMaxHops;
3573
+ const maxHop = typeof payloadMaxHop === "number" ? Math.min(payloadMaxHop, configuredMaxHop) : configuredMaxHop;
3574
+ if (!group || group.relayEnabled === false || hop >= maxHop) {
3575
+ return;
3576
+ }
3577
+ const relayPeers = this.selectPeerGroupFanout(groupId, group.fanout, [
3578
+ decrypted.senderId,
3579
+ this.nodeId
3580
+ ]);
3581
+ if (relayPeers.length === 0) {
3582
+ return;
3583
+ }
3584
+ await this.ensureConnectedToPeers(relayPeers.slice(0, group.maxActivePeers));
3585
+ await this.enforceConnectionBudget();
3586
+ await this.broadcastMessage("peer-group:gossip", {
3587
+ groupId,
3588
+ gossipId,
3589
+ publisherId,
3590
+ hop: hop + 1,
3591
+ maxHop,
3592
+ innerMessageType,
3593
+ innerPayload
3594
+ }, {
3595
+ broadcastScope: this.peerGroupScopeFor(groupId),
3596
+ fanoutPeerIds: relayPeers
3597
+ });
3598
+ }
3599
+ normalizeGossipOperation(operation, publisherId) {
3600
+ if (!operation || !publisherId) {
3601
+ return null;
3602
+ }
3603
+ if (operation.actorId && operation.actorId !== publisherId) {
3604
+ this.emit("warning", {
3605
+ type: "gossip-operation-actor-mismatch",
3606
+ publisherId,
3607
+ actorId: operation.actorId,
3608
+ kind: operation.kind,
3609
+ collection: operation.collectionName,
3610
+ id: operation.id
3611
+ });
3612
+ return null;
3613
+ }
3614
+ const normalized = {
3615
+ ...operation,
3616
+ actorId: publisherId
3617
+ };
3618
+ if (normalized.kind === "create") {
3619
+ normalized.ownerId = publisherId;
3620
+ }
3621
+ return normalized;
3622
+ }
3623
+ async dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, context = {}) {
3624
+ if (innerMessageType === "operation") {
3625
+ const operation = this.normalizeGossipOperation(
3626
+ innerPayload,
3627
+ context.publisherId || context.senderId
3628
+ );
3629
+ if (operation) {
3630
+ this.applyOperation(operation);
3631
+ }
3632
+ return;
3633
+ }
3634
+ if (innerMessageType === "record:snapshot") {
3635
+ const { collectionName, record } = innerPayload || {};
3636
+ if (collectionName && record) {
3637
+ const applied = this.restoreRecord(collectionName, record, {
3638
+ rejectOnHashMismatch: true,
3639
+ rejectOnOwnershipMismatch: true,
3640
+ via: "peer-group"
3641
+ });
3642
+ if (applied) {
3643
+ this.emit("change", {
3644
+ kind: "snapshot",
3645
+ collection: collectionName,
3646
+ id: record.id,
3647
+ via: "peer-group",
3648
+ groupId: context.groupId
3649
+ });
3650
+ }
3651
+ }
3652
+ return;
3653
+ }
3654
+ this.emit("peergroupmessage", {
3655
+ groupId: context.groupId,
3656
+ senderId: context.senderId,
3657
+ type: innerMessageType,
3658
+ payload: innerPayload
3659
+ });
3660
+ }
3297
3661
  getPresenceMap(scope) {
3298
3662
  if (!this.presenceByScope.has(scope)) {
3299
3663
  this.presenceByScope.set(scope, /* @__PURE__ */ new Map());
@@ -3424,6 +3788,13 @@ var require_dignity_p2p = __commonJS({
3424
3788
  }
3425
3789
  async handleIncomingMessage(message) {
3426
3790
  if (message && message.opId && message.kind) {
3791
+ if (this.securityService.options.enabled) {
3792
+ this.emit("messageignored", {
3793
+ reason: "raw-operation-rejected",
3794
+ hint: "Unsigned raw operations are disabled when security is enabled"
3795
+ });
3796
+ return;
3797
+ }
3427
3798
  this.applyOperation(message);
3428
3799
  return;
3429
3800
  }
@@ -3434,9 +3805,6 @@ var require_dignity_p2p = __commonJS({
3434
3805
  });
3435
3806
  return;
3436
3807
  }
3437
- if (message && message.senderId && message.senderPublicKey) {
3438
- this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3439
- }
3440
3808
  let decrypted;
3441
3809
  try {
3442
3810
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3454,6 +3822,9 @@ var require_dignity_p2p = __commonJS({
3454
3822
  if (!decrypted || decrypted.ignored) {
3455
3823
  return;
3456
3824
  }
3825
+ if (message && message.senderId && message.senderPublicKey) {
3826
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3827
+ }
3457
3828
  if (decrypted.messageType === "operation") {
3458
3829
  this.applyOperation(decrypted.payload);
3459
3830
  return;
@@ -3477,18 +3848,27 @@ var require_dignity_p2p = __commonJS({
3477
3848
  const payload = decrypted.payload || {};
3478
3849
  const scope = payload.scope || "main";
3479
3850
  const peerId = payload.peerId || decrypted.senderId;
3480
- if (!peerId) {
3851
+ if (!peerId || peerId !== decrypted.senderId) {
3852
+ return;
3853
+ }
3854
+ if (!this.discoveryRooms.has(scope)) {
3481
3855
  return;
3482
3856
  }
3857
+ const room = this.discoveryRooms.get(scope);
3483
3858
  const presenceMap = this.getPresenceMap(scope);
3484
3859
  const isNewPeerInScope = !presenceMap.has(peerId);
3860
+ const requestedTtl = typeof payload.ttlMs === "number" ? payload.ttlMs : room.ttlMs;
3861
+ const ttlMs = Math.min(requestedTtl, room.ttlMs);
3485
3862
  this.upsertPresence(
3486
3863
  scope,
3487
3864
  peerId,
3488
3865
  payload.metadata || {},
3489
- payload.ttlMs || this.defaultPresenceTtlMs,
3490
- payload.announcedAt || this.now()
3866
+ ttlMs,
3867
+ this.now()
3491
3868
  );
3869
+ if (payload.metadata && payload.metadata.publicKey) {
3870
+ this.trustPeerPublicKey(peerId, payload.metadata.publicKey);
3871
+ }
3492
3872
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3493
3873
  if (typeof this.networkAdapter.connectToPeer === "function") {
3494
3874
  Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
@@ -3505,6 +3885,9 @@ var require_dignity_p2p = __commonJS({
3505
3885
  const payload = decrypted.payload || {};
3506
3886
  const scope = payload.scope || "main";
3507
3887
  const peerId = payload.peerId || decrypted.senderId;
3888
+ if (!peerId || peerId !== decrypted.senderId) {
3889
+ return;
3890
+ }
3508
3891
  const map = this.presenceByScope.get(scope);
3509
3892
  if (map && peerId && map.has(peerId)) {
3510
3893
  map.delete(peerId);
@@ -3512,6 +3895,10 @@ var require_dignity_p2p = __commonJS({
3512
3895
  }
3513
3896
  return;
3514
3897
  }
3898
+ if (decrypted.messageType === "peer-group:gossip") {
3899
+ await this.handlePeerGroupGossip(decrypted);
3900
+ return;
3901
+ }
3515
3902
  this.emit("message", {
3516
3903
  senderId: decrypted.senderId,
3517
3904
  targetId: decrypted.targetId,
@@ -3557,7 +3944,7 @@ var require_dignity_p2p = __commonJS({
3557
3944
  emitConflict(details) {
3558
3945
  this.emit("conflict", details);
3559
3946
  }
3560
- restoreRecord(collectionName, record) {
3947
+ restoreRecord(collectionName, record, options = {}) {
3561
3948
  if (!record || !record.id) {
3562
3949
  return false;
3563
3950
  }
@@ -3568,14 +3955,42 @@ var require_dignity_p2p = __commonJS({
3568
3955
  }
3569
3956
  const restoredData = { ...record.data || {} };
3570
3957
  const computedHash = computeContentHash(restoredData);
3571
- if (record.hash && record.hash !== computedHash) {
3958
+ const rejectOnHashMismatch = options.rejectOnHashMismatch === true;
3959
+ const rejectOnOwnershipMismatch = options.rejectOnOwnershipMismatch === true;
3960
+ if (rejectOnOwnershipMismatch && current && record.ownerId && current.ownerId !== record.ownerId) {
3961
+ this.emit("warning", {
3962
+ type: "ownership-mismatch",
3963
+ collection: collectionName,
3964
+ id: record.id,
3965
+ currentOwnerId: current.ownerId,
3966
+ advertisedOwnerId: record.ownerId,
3967
+ via: options.via || null
3968
+ });
3969
+ return false;
3970
+ }
3971
+ if (!record.hash) {
3972
+ const warning = {
3973
+ type: "content-hash-missing",
3974
+ collection: collectionName,
3975
+ id: record.id,
3976
+ via: options.via || null
3977
+ };
3978
+ this.emit("warning", warning);
3979
+ if (rejectOnHashMismatch) {
3980
+ return false;
3981
+ }
3982
+ } else if (record.hash !== computedHash) {
3572
3983
  this.emit("warning", {
3573
3984
  type: "content-hash-mismatch",
3574
3985
  collection: collectionName,
3575
3986
  id: record.id,
3576
3987
  advertisedHash: record.hash,
3577
- computedHash
3988
+ computedHash,
3989
+ via: options.via || null
3578
3990
  });
3991
+ if (rejectOnHashMismatch) {
3992
+ return false;
3993
+ }
3579
3994
  }
3580
3995
  collection.set(record.id, {
3581
3996
  id: record.id,
@@ -3617,6 +4032,15 @@ var require_dignity_p2p = __commonJS({
3617
4032
  });
3618
4033
  return record;
3619
4034
  }
4035
+ pruneAppliedOperations() {
4036
+ while (this.appliedOperations.size > this.maxAppliedOperations) {
4037
+ const oldestOpId = this.appliedOperations.keys().next().value;
4038
+ if (!oldestOpId) {
4039
+ break;
4040
+ }
4041
+ this.appliedOperations.delete(oldestOpId);
4042
+ }
4043
+ }
3620
4044
  applyOperation(operation) {
3621
4045
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3622
4046
  return false;
@@ -3638,7 +4062,8 @@ var require_dignity_p2p = __commonJS({
3638
4062
  deletedAt: null,
3639
4063
  version: 1
3640
4064
  });
3641
- this.appliedOperations.add(operation.opId);
4065
+ this.appliedOperations.set(operation.opId, this.now());
4066
+ this.pruneAppliedOperations();
3642
4067
  this.emit("change", { kind: "create", collection: operation.collectionName, id: operation.id });
3643
4068
  return true;
3644
4069
  }
@@ -3682,7 +4107,8 @@ var require_dignity_p2p = __commonJS({
3682
4107
  }
3683
4108
  current.updatedAt = operation.timestamp;
3684
4109
  current.version += 1;
3685
- this.appliedOperations.add(operation.opId);
4110
+ this.appliedOperations.set(operation.opId, this.now());
4111
+ this.pruneAppliedOperations();
3686
4112
  this.emit("change", {
3687
4113
  kind: "transfer-ownership",
3688
4114
  collection: operation.collectionName,
@@ -3711,7 +4137,8 @@ var require_dignity_p2p = __commonJS({
3711
4137
  current.deletedAt = operation.timestamp;
3712
4138
  current.updatedAt = operation.timestamp;
3713
4139
  current.version += 1;
3714
- this.appliedOperations.add(operation.opId);
4140
+ this.appliedOperations.set(operation.opId, this.now());
4141
+ this.pruneAppliedOperations();
3715
4142
  this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3716
4143
  return true;
3717
4144
  }
@@ -3741,7 +4168,8 @@ var require_dignity_p2p = __commonJS({
3741
4168
  }
3742
4169
  current.updatedAt = operation.timestamp;
3743
4170
  current.version += 1;
3744
- this.appliedOperations.add(operation.opId);
4171
+ this.appliedOperations.set(operation.opId, this.now());
4172
+ this.pruneAppliedOperations();
3745
4173
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
3746
4174
  return true;
3747
4175
  }
@@ -10789,6 +11217,16 @@ var require_in_memory_network = __commonJS({
10789
11217
  }
10790
11218
  await Promise.all(deliveries);
10791
11219
  }
11220
+ async sendToPeers(senderId, message, peerIds = []) {
11221
+ const targets = new Set((peerIds || []).filter((peerId) => peerId && peerId !== senderId));
11222
+ const deliveries = [];
11223
+ for (const [nodeId, adapter] of this.adapters.entries()) {
11224
+ if (nodeId !== senderId && targets.has(nodeId)) {
11225
+ deliveries.push(adapter.receive(message));
11226
+ }
11227
+ }
11228
+ await Promise.all(deliveries);
11229
+ }
10792
11230
  };
10793
11231
  var InMemoryNetworkAdapter = class {
10794
11232
  constructor(hub) {
@@ -10798,6 +11236,7 @@ var require_in_memory_network = __commonJS({
10798
11236
  this.hub = hub;
10799
11237
  this.nodeId = null;
10800
11238
  this.messageHandlers = /* @__PURE__ */ new Set();
11239
+ this.connectedPeers = /* @__PURE__ */ new Set();
10801
11240
  }
10802
11241
  async start(nodeId) {
10803
11242
  this.nodeId = nodeId;
@@ -10808,6 +11247,13 @@ var require_in_memory_network = __commonJS({
10808
11247
  this.hub.unregister(this.nodeId);
10809
11248
  }
10810
11249
  this.nodeId = null;
11250
+ this.connectedPeers.clear();
11251
+ }
11252
+ async connectToPeer(remotePeerId) {
11253
+ if (!remotePeerId || remotePeerId === this.nodeId) {
11254
+ return;
11255
+ }
11256
+ this.connectedPeers.add(remotePeerId);
10811
11257
  }
10812
11258
  async broadcast(message) {
10813
11259
  if (!this.nodeId) {
@@ -10815,6 +11261,21 @@ var require_in_memory_network = __commonJS({
10815
11261
  }
10816
11262
  await this.hub.broadcast(this.nodeId, message);
10817
11263
  }
11264
+ async sendToPeers(message, peerIds = []) {
11265
+ if (!this.nodeId) {
11266
+ throw new Error("Network adapter has not been started");
11267
+ }
11268
+ await this.hub.sendToPeers(this.nodeId, message, peerIds);
11269
+ }
11270
+ listOpenPeerIds() {
11271
+ return [...this.connectedPeers];
11272
+ }
11273
+ getOpenConnectionCount() {
11274
+ return this.connectedPeers.size;
11275
+ }
11276
+ isConnectedTo(remotePeerId) {
11277
+ return this.connectedPeers.has(remotePeerId);
11278
+ }
10818
11279
  onMessage(handler) {
10819
11280
  this.messageHandlers.add(handler);
10820
11281
  }
@@ -10993,6 +11454,29 @@ var require_peerjs_network = __commonJS({
10993
11454
  }
10994
11455
  await Promise.all(deliveries);
10995
11456
  }
11457
+ async sendToPeers(message, peerIds = []) {
11458
+ if (!this.peer) {
11459
+ throw new Error("PeerJS network adapter has not been started");
11460
+ }
11461
+ const targets = new Set((peerIds || []).filter(Boolean));
11462
+ if (targets.size === 0) {
11463
+ return;
11464
+ }
11465
+ const deliveries = [];
11466
+ for (const [peerId, connection] of this.connections.entries()) {
11467
+ if (targets.has(peerId) && connection.open) {
11468
+ deliveries.push(connection.send(message));
11469
+ }
11470
+ }
11471
+ await Promise.all(deliveries);
11472
+ }
11473
+ async disconnectPeer(remotePeerId) {
11474
+ const connection = this.connections.get(remotePeerId);
11475
+ if (connection && typeof connection.close === "function") {
11476
+ connection.close();
11477
+ }
11478
+ this.connections.delete(remotePeerId);
11479
+ }
10996
11480
  getOpenConnectionCount() {
10997
11481
  return this.listOpenPeerIds().length;
10998
11482
  }
@@ -11223,6 +11707,13 @@ var require_index = __commonJS({
11223
11707
  MessageSecurityService,
11224
11708
  DEFAULT_SECURITY_OPTIONS
11225
11709
  } = require_message_security_service();
11710
+ var {
11711
+ PEER_GROUP_SCOPE_PREFIX,
11712
+ DEFAULT_PEER_GROUP_OPTIONS,
11713
+ peerGroupScope,
11714
+ parsePeerGroupScope,
11715
+ selectFanoutPeers
11716
+ } = require_peer_group();
11226
11717
  module.exports = {
11227
11718
  DignityP2P,
11228
11719
  createDefaultSignalingPool,
@@ -11239,7 +11730,12 @@ var require_index = __commonJS({
11239
11730
  VDF,
11240
11731
  SlothPermutation,
11241
11732
  MessageSecurityService,
11242
- DEFAULT_SECURITY_OPTIONS
11733
+ DEFAULT_SECURITY_OPTIONS,
11734
+ PEER_GROUP_SCOPE_PREFIX,
11735
+ DEFAULT_PEER_GROUP_OPTIONS,
11736
+ peerGroupScope,
11737
+ parsePeerGroupScope,
11738
+ selectFanoutPeers
11243
11739
  };
11244
11740
  }
11245
11741
  });