dignity.js 0.5.3 → 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.
@@ -3,43 +3,6 @@ var __commonJS = (cb, mod) => function __require() {
3
3
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4
4
  };
5
5
 
6
- // src/utils/event-emitter.js
7
- var require_event_emitter = __commonJS({
8
- "src/utils/event-emitter.js"(exports2, module2) {
9
- var EventEmitter = class {
10
- constructor() {
11
- this.handlers = /* @__PURE__ */ new Map();
12
- }
13
- on(eventName, handler) {
14
- if (!this.handlers.has(eventName)) {
15
- this.handlers.set(eventName, /* @__PURE__ */ new Set());
16
- }
17
- this.handlers.get(eventName).add(handler);
18
- }
19
- off(eventName, handler) {
20
- const eventHandlers = this.handlers.get(eventName);
21
- if (!eventHandlers) {
22
- return;
23
- }
24
- eventHandlers.delete(handler);
25
- if (eventHandlers.size === 0) {
26
- this.handlers.delete(eventName);
27
- }
28
- }
29
- emit(eventName, payload) {
30
- const eventHandlers = this.handlers.get(eventName);
31
- if (!eventHandlers) {
32
- return;
33
- }
34
- for (const handler of eventHandlers) {
35
- handler(payload);
36
- }
37
- }
38
- };
39
- module2.exports = EventEmitter;
40
- }
41
- });
42
-
43
6
  // node_modules/tweetnacl/nacl-fast.js
44
7
  var require_nacl_fast = __commonJS({
45
8
  "node_modules/tweetnacl/nacl-fast.js"(exports2, module2) {
@@ -2330,6 +2293,43 @@ var require_nacl_util = __commonJS({
2330
2293
  }
2331
2294
  });
2332
2295
 
2296
+ // src/utils/event-emitter.js
2297
+ var require_event_emitter = __commonJS({
2298
+ "src/utils/event-emitter.js"(exports2, module2) {
2299
+ var EventEmitter = class {
2300
+ constructor() {
2301
+ this.handlers = /* @__PURE__ */ new Map();
2302
+ }
2303
+ on(eventName, handler) {
2304
+ if (!this.handlers.has(eventName)) {
2305
+ this.handlers.set(eventName, /* @__PURE__ */ new Set());
2306
+ }
2307
+ this.handlers.get(eventName).add(handler);
2308
+ }
2309
+ off(eventName, handler) {
2310
+ const eventHandlers = this.handlers.get(eventName);
2311
+ if (!eventHandlers) {
2312
+ return;
2313
+ }
2314
+ eventHandlers.delete(handler);
2315
+ if (eventHandlers.size === 0) {
2316
+ this.handlers.delete(eventName);
2317
+ }
2318
+ }
2319
+ emit(eventName, payload) {
2320
+ const eventHandlers = this.handlers.get(eventName);
2321
+ if (!eventHandlers) {
2322
+ return;
2323
+ }
2324
+ for (const handler of eventHandlers) {
2325
+ handler(payload);
2326
+ }
2327
+ }
2328
+ };
2329
+ module2.exports = EventEmitter;
2330
+ }
2331
+ });
2332
+
2333
2333
  // src/security/sloth-vdf.js
2334
2334
  var require_sloth_vdf = __commonJS({
2335
2335
  "src/security/sloth-vdf.js"(exports2, module2) {
@@ -2337,6 +2337,12 @@ var require_sloth_vdf = __commonJS({
2337
2337
  static p = BigInt(
2338
2338
  "170082004324204494273811327264862981553264701145937538369570764779791492622392118654022654452947093285873855529044371650895045691292912712699015605832276411308653107069798639938826015099738961427172366594187783204437869906954750443653318078358839409699824714551430573905637228307966826784684174483831608534979"
2339
2339
  );
2340
+ // precompute values for optimization:
2341
+ // (p - 1) / 2
2342
+ static pHalf = _SlothPermutation.p - BigInt(1) >> BigInt(1);
2343
+ // (p + 1) / 4
2344
+ // p ≡ 3 (mod 4) ⇒ (p+1) divisible by 4
2345
+ static pQuarter = _SlothPermutation.p + BigInt(1) >> BigInt(2);
2340
2346
  fastPow(base, exponent, modulus) {
2341
2347
  if (modulus === BigInt(1)) {
2342
2348
  return BigInt(0);
@@ -2345,25 +2351,25 @@ var require_sloth_vdf = __commonJS({
2345
2351
  let powBase = base % modulus;
2346
2352
  let powExponent = exponent;
2347
2353
  while (powExponent > 0) {
2348
- if (powExponent % BigInt(2) === BigInt(1)) {
2354
+ if ((powExponent & BigInt(1)) === BigInt(1)) {
2349
2355
  result = result * powBase % modulus;
2350
2356
  }
2351
- powExponent = powExponent / BigInt(2);
2357
+ powExponent = powExponent >> BigInt(1);
2352
2358
  powBase = powBase * powBase % modulus;
2353
2359
  }
2354
2360
  return result;
2355
2361
  }
2356
2362
  quadRes(x) {
2357
- return this.fastPow(x, (_SlothPermutation.p - BigInt(1)) / BigInt(2), _SlothPermutation.p) === BigInt(1);
2363
+ return this.fastPow(x, _SlothPermutation.pHalf, _SlothPermutation.p) === BigInt(1);
2358
2364
  }
2359
2365
  modSqrtOp(x) {
2360
2366
  let y;
2361
2367
  let value = x;
2362
2368
  if (this.quadRes(value)) {
2363
- y = this.fastPow(value, (_SlothPermutation.p + BigInt(1)) / BigInt(4), _SlothPermutation.p);
2369
+ y = this.fastPow(value, _SlothPermutation.pQuarter, _SlothPermutation.p);
2364
2370
  } else {
2365
2371
  value = (-value + _SlothPermutation.p) % _SlothPermutation.p;
2366
- y = this.fastPow(value, (_SlothPermutation.p + BigInt(1)) / BigInt(4), _SlothPermutation.p);
2372
+ y = this.fastPow(value, _SlothPermutation.pQuarter, _SlothPermutation.p);
2367
2373
  }
2368
2374
  return y;
2369
2375
  }
@@ -2743,8 +2749,14 @@ var require_message_security_service = __commonJS({
2743
2749
  const nonce = naclUtil.decodeBase64(encryption.nonce);
2744
2750
  let key;
2745
2751
  if (encryption.kdf === "pbkdf2") {
2746
- const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS2.kdfIterations;
2747
- 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);
2748
2760
  } else {
2749
2761
  key = legacyBroadcastKey(password, salt);
2750
2762
  }
@@ -2848,11 +2860,84 @@ var require_message_security_service = __commonJS({
2848
2860
  }
2849
2861
  });
2850
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
+
2851
2922
  // src/core/dignity-p2p.js
2852
2923
  var require_dignity_p2p = __commonJS({
2853
2924
  "src/core/dignity-p2p.js"(exports2, module2) {
2925
+ var nacl = require_nacl_fast();
2926
+ var naclUtil = require_nacl_util();
2854
2927
  var EventEmitter = require_event_emitter();
2855
- var { MessageSecurityService: MessageSecurityService2 } = require_message_security_service();
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();
2934
+ function computeContentHash(data) {
2935
+ const canonical = stableStringify(data || {});
2936
+ const bytes = naclUtil.decodeUTF8(canonical);
2937
+ const hash = nacl.hash(bytes);
2938
+ const hex = Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("");
2939
+ return `sha512:${hex}`;
2940
+ }
2856
2941
  var DignityP2P2 = class extends EventEmitter {
2857
2942
  constructor({ nodeId, networkAdapter, idGenerator, now, security } = {}) {
2858
2943
  super();
@@ -2878,8 +2963,16 @@ var require_dignity_p2p = __commonJS({
2878
2963
  this.defaultPresenceTtlMs = security && typeof security.presenceTtlMs === "number" ? security.presenceTtlMs : 45e3;
2879
2964
  this.discoveryRooms = /* @__PURE__ */ new Map();
2880
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;
2881
2974
  this.state = /* @__PURE__ */ new Map();
2882
- this.appliedOperations = /* @__PURE__ */ new Set();
2975
+ this.appliedOperations = /* @__PURE__ */ new Map();
2883
2976
  this.boundMessageHandler = this.handleIncomingMessage.bind(this);
2884
2977
  }
2885
2978
  async start() {
@@ -2887,6 +2980,14 @@ var require_dignity_p2p = __commonJS({
2887
2980
  await this.networkAdapter.start(this.nodeId);
2888
2981
  }
2889
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
+ }
2890
2991
  const joinedScopes = Array.from(this.discoveryRooms.keys());
2891
2992
  for (const scope of joinedScopes) {
2892
2993
  try {
@@ -2911,6 +3012,7 @@ var require_dignity_p2p = __commonJS({
2911
3012
  if (!record || record.deletedAt) {
2912
3013
  return null;
2913
3014
  }
3015
+ const normalizedData = { ...record.data || {} };
2914
3016
  return {
2915
3017
  id: record.id,
2916
3018
  ownerId: record.ownerId,
@@ -2918,7 +3020,8 @@ var require_dignity_p2p = __commonJS({
2918
3020
  createdAt: record.createdAt,
2919
3021
  updatedAt: record.updatedAt,
2920
3022
  version: record.version,
2921
- data: { ...record.data }
3023
+ hash: record.hash || computeContentHash(normalizedData),
3024
+ data: normalizedData
2922
3025
  };
2923
3026
  }
2924
3027
  canUpdateRecord(record, actorId) {
@@ -2996,7 +3099,7 @@ var require_dignity_p2p = __commonJS({
2996
3099
  ownerId: this.nodeId,
2997
3100
  collaboratorIds,
2998
3101
  timestamp,
2999
- payload: { ...data }
3102
+ payload: { ...data || {} }
3000
3103
  };
3001
3104
  this.applyOperation(operation);
3002
3105
  await this.broadcastMessage("operation", operation, {
@@ -3233,6 +3336,7 @@ var require_dignity_p2p = __commonJS({
3233
3336
  const connectToPeers = securityContext.connectToPeers;
3234
3337
  if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3235
3338
  await this.ensureConnectedToPeers(connectToPeers);
3339
+ await this.enforceConnectionBudget();
3236
3340
  }
3237
3341
  const envelope = await this.securityService.secureOutgoingMessage({
3238
3342
  messageType,
@@ -3240,6 +3344,11 @@ var require_dignity_p2p = __commonJS({
3240
3344
  targetId: null,
3241
3345
  securityContext
3242
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
+ }
3243
3352
  await this.networkAdapter.broadcast(envelope);
3244
3353
  }
3245
3354
  async sendDirectMessage(targetId, messageType, payload) {
@@ -3255,8 +3364,280 @@ var require_dignity_p2p = __commonJS({
3255
3364
  payload,
3256
3365
  targetId
3257
3366
  });
3367
+ if (targetId && typeof this.networkAdapter.sendToPeers === "function") {
3368
+ await this.networkAdapter.sendToPeers(envelope, [targetId]);
3369
+ return;
3370
+ }
3258
3371
  await this.networkAdapter.broadcast(envelope);
3259
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
+ }
3260
3641
  getPresenceMap(scope) {
3261
3642
  if (!this.presenceByScope.has(scope)) {
3262
3643
  this.presenceByScope.set(scope, /* @__PURE__ */ new Map());
@@ -3387,6 +3768,13 @@ var require_dignity_p2p = __commonJS({
3387
3768
  }
3388
3769
  async handleIncomingMessage(message) {
3389
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
+ }
3390
3778
  this.applyOperation(message);
3391
3779
  return;
3392
3780
  }
@@ -3397,9 +3785,6 @@ var require_dignity_p2p = __commonJS({
3397
3785
  });
3398
3786
  return;
3399
3787
  }
3400
- if (message && message.senderId && message.senderPublicKey) {
3401
- this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3402
- }
3403
3788
  let decrypted;
3404
3789
  try {
3405
3790
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3417,6 +3802,9 @@ var require_dignity_p2p = __commonJS({
3417
3802
  if (!decrypted || decrypted.ignored) {
3418
3803
  return;
3419
3804
  }
3805
+ if (message && message.senderId && message.senderPublicKey) {
3806
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3807
+ }
3420
3808
  if (decrypted.messageType === "operation") {
3421
3809
  this.applyOperation(decrypted.payload);
3422
3810
  return;
@@ -3440,18 +3828,27 @@ var require_dignity_p2p = __commonJS({
3440
3828
  const payload = decrypted.payload || {};
3441
3829
  const scope = payload.scope || "main";
3442
3830
  const peerId = payload.peerId || decrypted.senderId;
3443
- if (!peerId) {
3831
+ if (!peerId || peerId !== decrypted.senderId) {
3444
3832
  return;
3445
3833
  }
3834
+ if (!this.discoveryRooms.has(scope)) {
3835
+ return;
3836
+ }
3837
+ const room = this.discoveryRooms.get(scope);
3446
3838
  const presenceMap = this.getPresenceMap(scope);
3447
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);
3448
3842
  this.upsertPresence(
3449
3843
  scope,
3450
3844
  peerId,
3451
3845
  payload.metadata || {},
3452
- payload.ttlMs || this.defaultPresenceTtlMs,
3453
- payload.announcedAt || this.now()
3846
+ ttlMs,
3847
+ this.now()
3454
3848
  );
3849
+ if (payload.metadata && payload.metadata.publicKey) {
3850
+ this.trustPeerPublicKey(peerId, payload.metadata.publicKey);
3851
+ }
3455
3852
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3456
3853
  if (typeof this.networkAdapter.connectToPeer === "function") {
3457
3854
  Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
@@ -3468,6 +3865,9 @@ var require_dignity_p2p = __commonJS({
3468
3865
  const payload = decrypted.payload || {};
3469
3866
  const scope = payload.scope || "main";
3470
3867
  const peerId = payload.peerId || decrypted.senderId;
3868
+ if (!peerId || peerId !== decrypted.senderId) {
3869
+ return;
3870
+ }
3471
3871
  const map = this.presenceByScope.get(scope);
3472
3872
  if (map && peerId && map.has(peerId)) {
3473
3873
  map.delete(peerId);
@@ -3475,6 +3875,10 @@ var require_dignity_p2p = __commonJS({
3475
3875
  }
3476
3876
  return;
3477
3877
  }
3878
+ if (decrypted.messageType === "peer-group:gossip") {
3879
+ await this.handlePeerGroupGossip(decrypted);
3880
+ return;
3881
+ }
3478
3882
  this.emit("message", {
3479
3883
  senderId: decrypted.senderId,
3480
3884
  targetId: decrypted.targetId,
@@ -3520,7 +3924,7 @@ var require_dignity_p2p = __commonJS({
3520
3924
  emitConflict(details) {
3521
3925
  this.emit("conflict", details);
3522
3926
  }
3523
- restoreRecord(collectionName, record) {
3927
+ restoreRecord(collectionName, record, options = {}) {
3524
3928
  if (!record || !record.id) {
3525
3929
  return false;
3526
3930
  }
@@ -3529,11 +3933,51 @@ var require_dignity_p2p = __commonJS({
3529
3933
  if (current && current.version >= record.version) {
3530
3934
  return false;
3531
3935
  }
3936
+ const restoredData = { ...record.data || {} };
3937
+ const computedHash = computeContentHash(restoredData);
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) {
3963
+ this.emit("warning", {
3964
+ type: "content-hash-mismatch",
3965
+ collection: collectionName,
3966
+ id: record.id,
3967
+ advertisedHash: record.hash,
3968
+ computedHash,
3969
+ via: options.via || null
3970
+ });
3971
+ if (rejectOnHashMismatch) {
3972
+ return false;
3973
+ }
3974
+ }
3532
3975
  collection.set(record.id, {
3533
3976
  id: record.id,
3534
3977
  ownerId: record.ownerId,
3535
3978
  collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
3536
- data: { ...record.data || {} },
3979
+ data: restoredData,
3980
+ hash: computedHash,
3537
3981
  createdAt: record.createdAt,
3538
3982
  updatedAt: record.updatedAt,
3539
3983
  deletedAt: record.deletedAt || null,
@@ -3551,7 +3995,8 @@ var require_dignity_p2p = __commonJS({
3551
3995
  id: raw.id,
3552
3996
  ownerId: raw.ownerId,
3553
3997
  collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
3554
- data: { ...raw.data },
3998
+ data: { ...raw.data || {} },
3999
+ hash: raw.hash || computeContentHash(raw.data || {}),
3555
4000
  createdAt: raw.createdAt,
3556
4001
  updatedAt: raw.updatedAt,
3557
4002
  deletedAt: raw.deletedAt || null,
@@ -3567,6 +4012,15 @@ var require_dignity_p2p = __commonJS({
3567
4012
  });
3568
4013
  return record;
3569
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
+ }
3570
4024
  applyOperation(operation) {
3571
4025
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3572
4026
  return false;
@@ -3581,13 +4035,15 @@ var require_dignity_p2p = __commonJS({
3581
4035
  id: operation.id,
3582
4036
  ownerId: operation.ownerId,
3583
4037
  collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
3584
- data: { ...operation.payload },
4038
+ data: { ...operation.payload || {} },
4039
+ hash: computeContentHash(operation.payload || {}),
3585
4040
  createdAt: operation.timestamp,
3586
4041
  updatedAt: operation.timestamp,
3587
4042
  deletedAt: null,
3588
4043
  version: 1
3589
4044
  });
3590
- this.appliedOperations.add(operation.opId);
4045
+ this.appliedOperations.set(operation.opId, this.now());
4046
+ this.pruneAppliedOperations();
3591
4047
  this.emit("change", { kind: "create", collection: operation.collectionName, id: operation.id });
3592
4048
  return true;
3593
4049
  }
@@ -3631,7 +4087,8 @@ var require_dignity_p2p = __commonJS({
3631
4087
  }
3632
4088
  current.updatedAt = operation.timestamp;
3633
4089
  current.version += 1;
3634
- this.appliedOperations.add(operation.opId);
4090
+ this.appliedOperations.set(operation.opId, this.now());
4091
+ this.pruneAppliedOperations();
3635
4092
  this.emit("change", {
3636
4093
  kind: "transfer-ownership",
3637
4094
  collection: operation.collectionName,
@@ -3660,7 +4117,8 @@ var require_dignity_p2p = __commonJS({
3660
4117
  current.deletedAt = operation.timestamp;
3661
4118
  current.updatedAt = operation.timestamp;
3662
4119
  current.version += 1;
3663
- this.appliedOperations.add(operation.opId);
4120
+ this.appliedOperations.set(operation.opId, this.now());
4121
+ this.pruneAppliedOperations();
3664
4122
  this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3665
4123
  return true;
3666
4124
  }
@@ -3684,12 +4142,14 @@ var require_dignity_p2p = __commonJS({
3684
4142
  ...current.data,
3685
4143
  ...operation.payload
3686
4144
  };
4145
+ current.hash = computeContentHash(current.data);
3687
4146
  if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
3688
4147
  current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
3689
4148
  }
3690
4149
  current.updatedAt = operation.timestamp;
3691
4150
  current.version += 1;
3692
- this.appliedOperations.add(operation.opId);
4151
+ this.appliedOperations.set(operation.opId, this.now());
4152
+ this.pruneAppliedOperations();
3693
4153
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
3694
4154
  return true;
3695
4155
  }
@@ -3769,6 +4229,14 @@ var require_signaling_pool = __commonJS({
3769
4229
  // src/signaling/websocket-signaling-provider.js
3770
4230
  var require_websocket_signaling_provider = __commonJS({
3771
4231
  "src/signaling/websocket-signaling-provider.js"(exports2, module2) {
4232
+ function randomBase36(length) {
4233
+ let value = "";
4234
+ while (value.length < length) {
4235
+ const chunk = Math.random().toString(36).slice(2);
4236
+ value += chunk.length > 0 ? chunk : "0";
4237
+ }
4238
+ return value.slice(0, length);
4239
+ }
3772
4240
  var WebSocketSignalingProvider2 = class {
3773
4241
  constructor({ id, url, WebSocketImpl, priority = 0 }) {
3774
4242
  if (!url) {
@@ -3812,8 +4280,8 @@ var require_websocket_signaling_provider = __commonJS({
3812
4280
  if (!peerJsHostPattern.test(this.url)) {
3813
4281
  return this.url;
3814
4282
  }
3815
- const connectionId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
3816
- const token = Math.random().toString(36).slice(2, 12);
4283
+ const connectionId = `dignityjs_${randomBase36(10)}`;
4284
+ const token = randomBase36(10);
3817
4285
  const hasQuery = this.url.includes("?");
3818
4286
  const hasId = /[?&]id=/.test(this.url);
3819
4287
  const hasToken = /[?&]token=/.test(this.url);
@@ -10719,6 +11187,16 @@ var require_in_memory_network = __commonJS({
10719
11187
  }
10720
11188
  await Promise.all(deliveries);
10721
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
+ }
10722
11200
  };
10723
11201
  var InMemoryNetworkAdapter2 = class {
10724
11202
  constructor(hub) {
@@ -10728,6 +11206,7 @@ var require_in_memory_network = __commonJS({
10728
11206
  this.hub = hub;
10729
11207
  this.nodeId = null;
10730
11208
  this.messageHandlers = /* @__PURE__ */ new Set();
11209
+ this.connectedPeers = /* @__PURE__ */ new Set();
10731
11210
  }
10732
11211
  async start(nodeId) {
10733
11212
  this.nodeId = nodeId;
@@ -10738,6 +11217,13 @@ var require_in_memory_network = __commonJS({
10738
11217
  this.hub.unregister(this.nodeId);
10739
11218
  }
10740
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);
10741
11227
  }
10742
11228
  async broadcast(message) {
10743
11229
  if (!this.nodeId) {
@@ -10745,6 +11231,21 @@ var require_in_memory_network = __commonJS({
10745
11231
  }
10746
11232
  await this.hub.broadcast(this.nodeId, message);
10747
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
+ }
10748
11249
  onMessage(handler) {
10749
11250
  this.messageHandlers.add(handler);
10750
11251
  }
@@ -10923,6 +11424,29 @@ var require_peerjs_network = __commonJS({
10923
11424
  }
10924
11425
  await Promise.all(deliveries);
10925
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
+ }
10926
11450
  getOpenConnectionCount() {
10927
11451
  return this.listOpenPeerIds().length;
10928
11452
  }
@@ -11029,6 +11553,7 @@ var require_indexeddb_persistence = __commonJS({
11029
11553
  ownerId: record.ownerId,
11030
11554
  collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
11031
11555
  data: { ...record.data },
11556
+ hash: record.hash || null,
11032
11557
  createdAt: record.createdAt,
11033
11558
  updatedAt: record.updatedAt,
11034
11559
  deletedAt: record.deletedAt,
@@ -11089,6 +11614,7 @@ var require_indexeddb_persistence = __commonJS({
11089
11614
  ownerId: stored.ownerId,
11090
11615
  collaboratorIds: stored.collaboratorIds,
11091
11616
  data: stored.data,
11617
+ hash: stored.hash || null,
11092
11618
  createdAt: stored.createdAt,
11093
11619
  updatedAt: stored.updatedAt,
11094
11620
  deletedAt: stored.deletedAt,
@@ -11149,6 +11675,13 @@ var {
11149
11675
  MessageSecurityService,
11150
11676
  DEFAULT_SECURITY_OPTIONS
11151
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();
11152
11685
  module.exports = {
11153
11686
  DignityP2P,
11154
11687
  createDefaultSignalingPool,
@@ -11165,6 +11698,11 @@ module.exports = {
11165
11698
  VDF,
11166
11699
  SlothPermutation,
11167
11700
  MessageSecurityService,
11168
- DEFAULT_SECURITY_OPTIONS
11701
+ DEFAULT_SECURITY_OPTIONS,
11702
+ PEER_GROUP_SCOPE_PREFIX,
11703
+ DEFAULT_PEER_GROUP_OPTIONS,
11704
+ peerGroupScope,
11705
+ parsePeerGroupScope,
11706
+ selectFanoutPeers
11169
11707
  };
11170
11708
  //# sourceMappingURL=dignity.cjs.js.map