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.
@@ -16,43 +16,6 @@ var __commonJS = (cb, mod) => function __require2() {
16
16
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
17
17
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
18
18
 
19
- // src/utils/event-emitter.js
20
- var require_event_emitter = __commonJS({
21
- "src/utils/event-emitter.js"(exports, module) {
22
- var EventEmitter = class {
23
- constructor() {
24
- this.handlers = /* @__PURE__ */ new Map();
25
- }
26
- on(eventName, handler) {
27
- if (!this.handlers.has(eventName)) {
28
- this.handlers.set(eventName, /* @__PURE__ */ new Set());
29
- }
30
- this.handlers.get(eventName).add(handler);
31
- }
32
- off(eventName, handler) {
33
- const eventHandlers = this.handlers.get(eventName);
34
- if (!eventHandlers) {
35
- return;
36
- }
37
- eventHandlers.delete(handler);
38
- if (eventHandlers.size === 0) {
39
- this.handlers.delete(eventName);
40
- }
41
- }
42
- emit(eventName, payload) {
43
- const eventHandlers = this.handlers.get(eventName);
44
- if (!eventHandlers) {
45
- return;
46
- }
47
- for (const handler of eventHandlers) {
48
- handler(payload);
49
- }
50
- }
51
- };
52
- module.exports = EventEmitter;
53
- }
54
- });
55
-
56
19
  // (disabled):crypto
57
20
  var require_crypto = __commonJS({
58
21
  "(disabled):crypto"() {
@@ -2349,6 +2312,43 @@ var require_nacl_util = __commonJS({
2349
2312
  }
2350
2313
  });
2351
2314
 
2315
+ // src/utils/event-emitter.js
2316
+ var require_event_emitter = __commonJS({
2317
+ "src/utils/event-emitter.js"(exports, module) {
2318
+ var EventEmitter = class {
2319
+ constructor() {
2320
+ this.handlers = /* @__PURE__ */ new Map();
2321
+ }
2322
+ on(eventName, handler) {
2323
+ if (!this.handlers.has(eventName)) {
2324
+ this.handlers.set(eventName, /* @__PURE__ */ new Set());
2325
+ }
2326
+ this.handlers.get(eventName).add(handler);
2327
+ }
2328
+ off(eventName, handler) {
2329
+ const eventHandlers = this.handlers.get(eventName);
2330
+ if (!eventHandlers) {
2331
+ return;
2332
+ }
2333
+ eventHandlers.delete(handler);
2334
+ if (eventHandlers.size === 0) {
2335
+ this.handlers.delete(eventName);
2336
+ }
2337
+ }
2338
+ emit(eventName, payload) {
2339
+ const eventHandlers = this.handlers.get(eventName);
2340
+ if (!eventHandlers) {
2341
+ return;
2342
+ }
2343
+ for (const handler of eventHandlers) {
2344
+ handler(payload);
2345
+ }
2346
+ }
2347
+ };
2348
+ module.exports = EventEmitter;
2349
+ }
2350
+ });
2351
+
2352
2352
  // src/security/sloth-vdf.js
2353
2353
  var require_sloth_vdf = __commonJS({
2354
2354
  "src/security/sloth-vdf.js"(exports, module) {
@@ -2361,25 +2361,25 @@ var require_sloth_vdf = __commonJS({
2361
2361
  let powBase = base % modulus;
2362
2362
  let powExponent = exponent;
2363
2363
  while (powExponent > 0) {
2364
- if (powExponent % BigInt(2) === BigInt(1)) {
2364
+ if ((powExponent & BigInt(1)) === BigInt(1)) {
2365
2365
  result = result * powBase % modulus;
2366
2366
  }
2367
- powExponent = powExponent / BigInt(2);
2367
+ powExponent = powExponent >> BigInt(1);
2368
2368
  powBase = powBase * powBase % modulus;
2369
2369
  }
2370
2370
  return result;
2371
2371
  }
2372
2372
  quadRes(x) {
2373
- return this.fastPow(x, (_SlothPermutation.p - BigInt(1)) / BigInt(2), _SlothPermutation.p) === BigInt(1);
2373
+ return this.fastPow(x, _SlothPermutation.pHalf, _SlothPermutation.p) === BigInt(1);
2374
2374
  }
2375
2375
  modSqrtOp(x) {
2376
2376
  let y;
2377
2377
  let value = x;
2378
2378
  if (this.quadRes(value)) {
2379
- y = this.fastPow(value, (_SlothPermutation.p + BigInt(1)) / BigInt(4), _SlothPermutation.p);
2379
+ y = this.fastPow(value, _SlothPermutation.pQuarter, _SlothPermutation.p);
2380
2380
  } else {
2381
2381
  value = (-value + _SlothPermutation.p) % _SlothPermutation.p;
2382
- y = this.fastPow(value, (_SlothPermutation.p + BigInt(1)) / BigInt(4), _SlothPermutation.p);
2382
+ y = this.fastPow(value, _SlothPermutation.pQuarter, _SlothPermutation.p);
2383
2383
  }
2384
2384
  return y;
2385
2385
  }
@@ -2411,6 +2411,12 @@ var require_sloth_vdf = __commonJS({
2411
2411
  __publicField(_SlothPermutation, "p", BigInt(
2412
2412
  "170082004324204494273811327264862981553264701145937538369570764779791492622392118654022654452947093285873855529044371650895045691292912712699015605832276411308653107069798639938826015099738961427172366594187783204437869906954750443653318078358839409699824714551430573905637228307966826784684174483831608534979"
2413
2413
  ));
2414
+ // precompute values for optimization:
2415
+ // (p - 1) / 2
2416
+ __publicField(_SlothPermutation, "pHalf", _SlothPermutation.p - BigInt(1) >> BigInt(1));
2417
+ // (p + 1) / 4
2418
+ // p ≡ 3 (mod 4) ⇒ (p+1) divisible by 4
2419
+ __publicField(_SlothPermutation, "pQuarter", _SlothPermutation.p + BigInt(1) >> BigInt(2));
2414
2420
  var SlothPermutation = _SlothPermutation;
2415
2421
  module.exports = SlothPermutation;
2416
2422
  }
@@ -2763,8 +2769,14 @@ var require_message_security_service = __commonJS({
2763
2769
  const nonce = naclUtil.decodeBase64(encryption.nonce);
2764
2770
  let key;
2765
2771
  if (encryption.kdf === "pbkdf2") {
2766
- const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
2767
- 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);
2768
2780
  } else {
2769
2781
  key = legacyBroadcastKey(password, salt);
2770
2782
  }
@@ -2868,11 +2880,84 @@ var require_message_security_service = __commonJS({
2868
2880
  }
2869
2881
  });
2870
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
+
2871
2942
  // src/core/dignity-p2p.js
2872
2943
  var require_dignity_p2p = __commonJS({
2873
2944
  "src/core/dignity-p2p.js"(exports, module) {
2945
+ var nacl = require_nacl_fast();
2946
+ var naclUtil = require_nacl_util();
2874
2947
  var EventEmitter = require_event_emitter();
2875
- var { MessageSecurityService } = require_message_security_service();
2948
+ var { MessageSecurityService, stableStringify } = require_message_security_service();
2949
+ var {
2950
+ DEFAULT_PEER_GROUP_OPTIONS,
2951
+ peerGroupScope,
2952
+ selectFanoutPeers
2953
+ } = require_peer_group();
2954
+ function computeContentHash(data) {
2955
+ const canonical = stableStringify(data || {});
2956
+ const bytes = naclUtil.decodeUTF8(canonical);
2957
+ const hash = nacl.hash(bytes);
2958
+ const hex = Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("");
2959
+ return `sha512:${hex}`;
2960
+ }
2876
2961
  var DignityP2P = class extends EventEmitter {
2877
2962
  constructor({ nodeId, networkAdapter, idGenerator, now, security } = {}) {
2878
2963
  super();
@@ -2898,8 +2983,16 @@ var require_dignity_p2p = __commonJS({
2898
2983
  this.defaultPresenceTtlMs = security && typeof security.presenceTtlMs === "number" ? security.presenceTtlMs : 45e3;
2899
2984
  this.discoveryRooms = /* @__PURE__ */ new Map();
2900
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;
2901
2994
  this.state = /* @__PURE__ */ new Map();
2902
- this.appliedOperations = /* @__PURE__ */ new Set();
2995
+ this.appliedOperations = /* @__PURE__ */ new Map();
2903
2996
  this.boundMessageHandler = this.handleIncomingMessage.bind(this);
2904
2997
  }
2905
2998
  async start() {
@@ -2907,6 +3000,14 @@ var require_dignity_p2p = __commonJS({
2907
3000
  await this.networkAdapter.start(this.nodeId);
2908
3001
  }
2909
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
+ }
2910
3011
  const joinedScopes = Array.from(this.discoveryRooms.keys());
2911
3012
  for (const scope of joinedScopes) {
2912
3013
  try {
@@ -2931,6 +3032,7 @@ var require_dignity_p2p = __commonJS({
2931
3032
  if (!record || record.deletedAt) {
2932
3033
  return null;
2933
3034
  }
3035
+ const normalizedData = { ...record.data || {} };
2934
3036
  return {
2935
3037
  id: record.id,
2936
3038
  ownerId: record.ownerId,
@@ -2938,7 +3040,8 @@ var require_dignity_p2p = __commonJS({
2938
3040
  createdAt: record.createdAt,
2939
3041
  updatedAt: record.updatedAt,
2940
3042
  version: record.version,
2941
- data: { ...record.data }
3043
+ hash: record.hash || computeContentHash(normalizedData),
3044
+ data: normalizedData
2942
3045
  };
2943
3046
  }
2944
3047
  canUpdateRecord(record, actorId) {
@@ -3016,7 +3119,7 @@ var require_dignity_p2p = __commonJS({
3016
3119
  ownerId: this.nodeId,
3017
3120
  collaboratorIds,
3018
3121
  timestamp,
3019
- payload: { ...data }
3122
+ payload: { ...data || {} }
3020
3123
  };
3021
3124
  this.applyOperation(operation);
3022
3125
  await this.broadcastMessage("operation", operation, {
@@ -3253,6 +3356,7 @@ var require_dignity_p2p = __commonJS({
3253
3356
  const connectToPeers = securityContext.connectToPeers;
3254
3357
  if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3255
3358
  await this.ensureConnectedToPeers(connectToPeers);
3359
+ await this.enforceConnectionBudget();
3256
3360
  }
3257
3361
  const envelope = await this.securityService.secureOutgoingMessage({
3258
3362
  messageType,
@@ -3260,6 +3364,11 @@ var require_dignity_p2p = __commonJS({
3260
3364
  targetId: null,
3261
3365
  securityContext
3262
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
+ }
3263
3372
  await this.networkAdapter.broadcast(envelope);
3264
3373
  }
3265
3374
  async sendDirectMessage(targetId, messageType, payload) {
@@ -3275,8 +3384,280 @@ var require_dignity_p2p = __commonJS({
3275
3384
  payload,
3276
3385
  targetId
3277
3386
  });
3387
+ if (targetId && typeof this.networkAdapter.sendToPeers === "function") {
3388
+ await this.networkAdapter.sendToPeers(envelope, [targetId]);
3389
+ return;
3390
+ }
3278
3391
  await this.networkAdapter.broadcast(envelope);
3279
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
+ }
3280
3661
  getPresenceMap(scope) {
3281
3662
  if (!this.presenceByScope.has(scope)) {
3282
3663
  this.presenceByScope.set(scope, /* @__PURE__ */ new Map());
@@ -3407,6 +3788,13 @@ var require_dignity_p2p = __commonJS({
3407
3788
  }
3408
3789
  async handleIncomingMessage(message) {
3409
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
+ }
3410
3798
  this.applyOperation(message);
3411
3799
  return;
3412
3800
  }
@@ -3417,9 +3805,6 @@ var require_dignity_p2p = __commonJS({
3417
3805
  });
3418
3806
  return;
3419
3807
  }
3420
- if (message && message.senderId && message.senderPublicKey) {
3421
- this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3422
- }
3423
3808
  let decrypted;
3424
3809
  try {
3425
3810
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3437,6 +3822,9 @@ var require_dignity_p2p = __commonJS({
3437
3822
  if (!decrypted || decrypted.ignored) {
3438
3823
  return;
3439
3824
  }
3825
+ if (message && message.senderId && message.senderPublicKey) {
3826
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3827
+ }
3440
3828
  if (decrypted.messageType === "operation") {
3441
3829
  this.applyOperation(decrypted.payload);
3442
3830
  return;
@@ -3460,18 +3848,27 @@ var require_dignity_p2p = __commonJS({
3460
3848
  const payload = decrypted.payload || {};
3461
3849
  const scope = payload.scope || "main";
3462
3850
  const peerId = payload.peerId || decrypted.senderId;
3463
- if (!peerId) {
3851
+ if (!peerId || peerId !== decrypted.senderId) {
3464
3852
  return;
3465
3853
  }
3854
+ if (!this.discoveryRooms.has(scope)) {
3855
+ return;
3856
+ }
3857
+ const room = this.discoveryRooms.get(scope);
3466
3858
  const presenceMap = this.getPresenceMap(scope);
3467
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);
3468
3862
  this.upsertPresence(
3469
3863
  scope,
3470
3864
  peerId,
3471
3865
  payload.metadata || {},
3472
- payload.ttlMs || this.defaultPresenceTtlMs,
3473
- payload.announcedAt || this.now()
3866
+ ttlMs,
3867
+ this.now()
3474
3868
  );
3869
+ if (payload.metadata && payload.metadata.publicKey) {
3870
+ this.trustPeerPublicKey(peerId, payload.metadata.publicKey);
3871
+ }
3475
3872
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3476
3873
  if (typeof this.networkAdapter.connectToPeer === "function") {
3477
3874
  Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
@@ -3488,6 +3885,9 @@ var require_dignity_p2p = __commonJS({
3488
3885
  const payload = decrypted.payload || {};
3489
3886
  const scope = payload.scope || "main";
3490
3887
  const peerId = payload.peerId || decrypted.senderId;
3888
+ if (!peerId || peerId !== decrypted.senderId) {
3889
+ return;
3890
+ }
3491
3891
  const map = this.presenceByScope.get(scope);
3492
3892
  if (map && peerId && map.has(peerId)) {
3493
3893
  map.delete(peerId);
@@ -3495,6 +3895,10 @@ var require_dignity_p2p = __commonJS({
3495
3895
  }
3496
3896
  return;
3497
3897
  }
3898
+ if (decrypted.messageType === "peer-group:gossip") {
3899
+ await this.handlePeerGroupGossip(decrypted);
3900
+ return;
3901
+ }
3498
3902
  this.emit("message", {
3499
3903
  senderId: decrypted.senderId,
3500
3904
  targetId: decrypted.targetId,
@@ -3540,7 +3944,7 @@ var require_dignity_p2p = __commonJS({
3540
3944
  emitConflict(details) {
3541
3945
  this.emit("conflict", details);
3542
3946
  }
3543
- restoreRecord(collectionName, record) {
3947
+ restoreRecord(collectionName, record, options = {}) {
3544
3948
  if (!record || !record.id) {
3545
3949
  return false;
3546
3950
  }
@@ -3549,11 +3953,51 @@ var require_dignity_p2p = __commonJS({
3549
3953
  if (current && current.version >= record.version) {
3550
3954
  return false;
3551
3955
  }
3956
+ const restoredData = { ...record.data || {} };
3957
+ const computedHash = computeContentHash(restoredData);
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) {
3983
+ this.emit("warning", {
3984
+ type: "content-hash-mismatch",
3985
+ collection: collectionName,
3986
+ id: record.id,
3987
+ advertisedHash: record.hash,
3988
+ computedHash,
3989
+ via: options.via || null
3990
+ });
3991
+ if (rejectOnHashMismatch) {
3992
+ return false;
3993
+ }
3994
+ }
3552
3995
  collection.set(record.id, {
3553
3996
  id: record.id,
3554
3997
  ownerId: record.ownerId,
3555
3998
  collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
3556
- data: { ...record.data || {} },
3999
+ data: restoredData,
4000
+ hash: computedHash,
3557
4001
  createdAt: record.createdAt,
3558
4002
  updatedAt: record.updatedAt,
3559
4003
  deletedAt: record.deletedAt || null,
@@ -3571,7 +4015,8 @@ var require_dignity_p2p = __commonJS({
3571
4015
  id: raw.id,
3572
4016
  ownerId: raw.ownerId,
3573
4017
  collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
3574
- data: { ...raw.data },
4018
+ data: { ...raw.data || {} },
4019
+ hash: raw.hash || computeContentHash(raw.data || {}),
3575
4020
  createdAt: raw.createdAt,
3576
4021
  updatedAt: raw.updatedAt,
3577
4022
  deletedAt: raw.deletedAt || null,
@@ -3587,6 +4032,15 @@ var require_dignity_p2p = __commonJS({
3587
4032
  });
3588
4033
  return record;
3589
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
+ }
3590
4044
  applyOperation(operation) {
3591
4045
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3592
4046
  return false;
@@ -3601,13 +4055,15 @@ var require_dignity_p2p = __commonJS({
3601
4055
  id: operation.id,
3602
4056
  ownerId: operation.ownerId,
3603
4057
  collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
3604
- data: { ...operation.payload },
4058
+ data: { ...operation.payload || {} },
4059
+ hash: computeContentHash(operation.payload || {}),
3605
4060
  createdAt: operation.timestamp,
3606
4061
  updatedAt: operation.timestamp,
3607
4062
  deletedAt: null,
3608
4063
  version: 1
3609
4064
  });
3610
- this.appliedOperations.add(operation.opId);
4065
+ this.appliedOperations.set(operation.opId, this.now());
4066
+ this.pruneAppliedOperations();
3611
4067
  this.emit("change", { kind: "create", collection: operation.collectionName, id: operation.id });
3612
4068
  return true;
3613
4069
  }
@@ -3651,7 +4107,8 @@ var require_dignity_p2p = __commonJS({
3651
4107
  }
3652
4108
  current.updatedAt = operation.timestamp;
3653
4109
  current.version += 1;
3654
- this.appliedOperations.add(operation.opId);
4110
+ this.appliedOperations.set(operation.opId, this.now());
4111
+ this.pruneAppliedOperations();
3655
4112
  this.emit("change", {
3656
4113
  kind: "transfer-ownership",
3657
4114
  collection: operation.collectionName,
@@ -3680,7 +4137,8 @@ var require_dignity_p2p = __commonJS({
3680
4137
  current.deletedAt = operation.timestamp;
3681
4138
  current.updatedAt = operation.timestamp;
3682
4139
  current.version += 1;
3683
- this.appliedOperations.add(operation.opId);
4140
+ this.appliedOperations.set(operation.opId, this.now());
4141
+ this.pruneAppliedOperations();
3684
4142
  this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3685
4143
  return true;
3686
4144
  }
@@ -3704,12 +4162,14 @@ var require_dignity_p2p = __commonJS({
3704
4162
  ...current.data,
3705
4163
  ...operation.payload
3706
4164
  };
4165
+ current.hash = computeContentHash(current.data);
3707
4166
  if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
3708
4167
  current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
3709
4168
  }
3710
4169
  current.updatedAt = operation.timestamp;
3711
4170
  current.version += 1;
3712
- this.appliedOperations.add(operation.opId);
4171
+ this.appliedOperations.set(operation.opId, this.now());
4172
+ this.pruneAppliedOperations();
3713
4173
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
3714
4174
  return true;
3715
4175
  }
@@ -3789,6 +4249,14 @@ var require_signaling_pool = __commonJS({
3789
4249
  // src/signaling/websocket-signaling-provider.js
3790
4250
  var require_websocket_signaling_provider = __commonJS({
3791
4251
  "src/signaling/websocket-signaling-provider.js"(exports, module) {
4252
+ function randomBase36(length) {
4253
+ let value = "";
4254
+ while (value.length < length) {
4255
+ const chunk = Math.random().toString(36).slice(2);
4256
+ value += chunk.length > 0 ? chunk : "0";
4257
+ }
4258
+ return value.slice(0, length);
4259
+ }
3792
4260
  var WebSocketSignalingProvider = class {
3793
4261
  constructor({ id, url, WebSocketImpl, priority = 0 }) {
3794
4262
  if (!url) {
@@ -3832,8 +4300,8 @@ var require_websocket_signaling_provider = __commonJS({
3832
4300
  if (!peerJsHostPattern.test(this.url)) {
3833
4301
  return this.url;
3834
4302
  }
3835
- const connectionId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
3836
- const token = Math.random().toString(36).slice(2, 12);
4303
+ const connectionId = `dignityjs_${randomBase36(10)}`;
4304
+ const token = randomBase36(10);
3837
4305
  const hasQuery = this.url.includes("?");
3838
4306
  const hasId = /[?&]id=/.test(this.url);
3839
4307
  const hasToken = /[?&]token=/.test(this.url);
@@ -10749,6 +11217,16 @@ var require_in_memory_network = __commonJS({
10749
11217
  }
10750
11218
  await Promise.all(deliveries);
10751
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
+ }
10752
11230
  };
10753
11231
  var InMemoryNetworkAdapter = class {
10754
11232
  constructor(hub) {
@@ -10758,6 +11236,7 @@ var require_in_memory_network = __commonJS({
10758
11236
  this.hub = hub;
10759
11237
  this.nodeId = null;
10760
11238
  this.messageHandlers = /* @__PURE__ */ new Set();
11239
+ this.connectedPeers = /* @__PURE__ */ new Set();
10761
11240
  }
10762
11241
  async start(nodeId) {
10763
11242
  this.nodeId = nodeId;
@@ -10768,6 +11247,13 @@ var require_in_memory_network = __commonJS({
10768
11247
  this.hub.unregister(this.nodeId);
10769
11248
  }
10770
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);
10771
11257
  }
10772
11258
  async broadcast(message) {
10773
11259
  if (!this.nodeId) {
@@ -10775,6 +11261,21 @@ var require_in_memory_network = __commonJS({
10775
11261
  }
10776
11262
  await this.hub.broadcast(this.nodeId, message);
10777
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
+ }
10778
11279
  onMessage(handler) {
10779
11280
  this.messageHandlers.add(handler);
10780
11281
  }
@@ -10953,6 +11454,29 @@ var require_peerjs_network = __commonJS({
10953
11454
  }
10954
11455
  await Promise.all(deliveries);
10955
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
+ }
10956
11480
  getOpenConnectionCount() {
10957
11481
  return this.listOpenPeerIds().length;
10958
11482
  }
@@ -11059,6 +11583,7 @@ var require_indexeddb_persistence = __commonJS({
11059
11583
  ownerId: record.ownerId,
11060
11584
  collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
11061
11585
  data: { ...record.data },
11586
+ hash: record.hash || null,
11062
11587
  createdAt: record.createdAt,
11063
11588
  updatedAt: record.updatedAt,
11064
11589
  deletedAt: record.deletedAt,
@@ -11119,6 +11644,7 @@ var require_indexeddb_persistence = __commonJS({
11119
11644
  ownerId: stored.ownerId,
11120
11645
  collaboratorIds: stored.collaboratorIds,
11121
11646
  data: stored.data,
11647
+ hash: stored.hash || null,
11122
11648
  createdAt: stored.createdAt,
11123
11649
  updatedAt: stored.updatedAt,
11124
11650
  deletedAt: stored.deletedAt,
@@ -11181,6 +11707,13 @@ var require_index = __commonJS({
11181
11707
  MessageSecurityService,
11182
11708
  DEFAULT_SECURITY_OPTIONS
11183
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();
11184
11717
  module.exports = {
11185
11718
  DignityP2P,
11186
11719
  createDefaultSignalingPool,
@@ -11197,7 +11730,12 @@ var require_index = __commonJS({
11197
11730
  VDF,
11198
11731
  SlothPermutation,
11199
11732
  MessageSecurityService,
11200
- DEFAULT_SECURITY_OPTIONS
11733
+ DEFAULT_SECURITY_OPTIONS,
11734
+ PEER_GROUP_SCOPE_PREFIX,
11735
+ DEFAULT_PEER_GROUP_OPTIONS,
11736
+ peerGroupScope,
11737
+ parsePeerGroupScope,
11738
+ selectFanoutPeers
11201
11739
  };
11202
11740
  }
11203
11741
  });