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.
@@ -0,0 +1,64 @@
1
+ const PEER_GROUP_SCOPE_PREFIX = 'gossip:';
2
+
3
+ const DEFAULT_PEER_GROUP_OPTIONS = {
4
+ fanout: 3,
5
+ maxActivePeers: 8,
6
+ maxHops: 6,
7
+ relayEnabled: true
8
+ };
9
+
10
+ function peerGroupScope(groupId) {
11
+ if (!groupId) {
12
+ throw new Error('peerGroupScope requires groupId');
13
+ }
14
+ return `${PEER_GROUP_SCOPE_PREFIX}${groupId}`;
15
+ }
16
+
17
+ function parsePeerGroupScope(scope) {
18
+ if (!scope || !scope.startsWith(PEER_GROUP_SCOPE_PREFIX)) {
19
+ return null;
20
+ }
21
+ return scope.slice(PEER_GROUP_SCOPE_PREFIX.length);
22
+ }
23
+
24
+ function shufflePeerIds(peerIds, randomFn = Math.random) {
25
+ const list = [...peerIds];
26
+ for (let i = list.length - 1; i > 0; i -= 1) {
27
+ const j = Math.floor(randomFn() * (i + 1));
28
+ [list[i], list[j]] = [list[j], list[i]];
29
+ }
30
+ return list;
31
+ }
32
+
33
+ function selectFanoutPeers({
34
+ peers,
35
+ count,
36
+ excludePeerIds = [],
37
+ connectedPeerIds = [],
38
+ randomFn = Math.random
39
+ }) {
40
+ const excluded = new Set(excludePeerIds.filter(Boolean));
41
+ const candidates = peers
42
+ .map((entry) => entry.peerId || entry)
43
+ .filter((peerId) => peerId && !excluded.has(peerId));
44
+
45
+ const connected = new Set(connectedPeerIds.filter(Boolean));
46
+ const preferred = candidates.filter((peerId) => connected.has(peerId));
47
+ const others = candidates.filter((peerId) => !connected.has(peerId));
48
+
49
+ const ordered = [
50
+ ...shufflePeerIds(preferred, randomFn),
51
+ ...shufflePeerIds(others, randomFn)
52
+ ];
53
+
54
+ return ordered.slice(0, Math.max(0, count));
55
+ }
56
+
57
+ module.exports = {
58
+ PEER_GROUP_SCOPE_PREFIX,
59
+ DEFAULT_PEER_GROUP_OPTIONS,
60
+ peerGroupScope,
61
+ parsePeerGroupScope,
62
+ shufflePeerIds,
63
+ selectFanoutPeers
64
+ };
package/src/index.js CHANGED
@@ -30,6 +30,13 @@ const {
30
30
  MessageSecurityService,
31
31
  DEFAULT_SECURITY_OPTIONS
32
32
  } = require('./security/message-security-service');
33
+ const {
34
+ PEER_GROUP_SCOPE_PREFIX,
35
+ DEFAULT_PEER_GROUP_OPTIONS,
36
+ peerGroupScope,
37
+ parsePeerGroupScope,
38
+ selectFanoutPeers
39
+ } = require('./gossip/peer-group');
33
40
 
34
41
  module.exports = {
35
42
  DignityP2P,
@@ -47,5 +54,10 @@ module.exports = {
47
54
  VDF,
48
55
  SlothPermutation,
49
56
  MessageSecurityService,
50
- DEFAULT_SECURITY_OPTIONS
57
+ DEFAULT_SECURITY_OPTIONS,
58
+ PEER_GROUP_SCOPE_PREFIX,
59
+ DEFAULT_PEER_GROUP_OPTIONS,
60
+ peerGroupScope,
61
+ parsePeerGroupScope,
62
+ selectFanoutPeers
51
63
  };
@@ -20,6 +20,19 @@ class InMemoryNetworkHub {
20
20
  }
21
21
  await Promise.all(deliveries);
22
22
  }
23
+
24
+ async sendToPeers(senderId, message, peerIds = []) {
25
+ const targets = new Set((peerIds || []).filter((peerId) => peerId && peerId !== senderId));
26
+ const deliveries = [];
27
+
28
+ for (const [nodeId, adapter] of this.adapters.entries()) {
29
+ if (nodeId !== senderId && targets.has(nodeId)) {
30
+ deliveries.push(adapter.receive(message));
31
+ }
32
+ }
33
+
34
+ await Promise.all(deliveries);
35
+ }
23
36
  }
24
37
 
25
38
  class InMemoryNetworkAdapter {
@@ -31,6 +44,7 @@ class InMemoryNetworkAdapter {
31
44
  this.hub = hub;
32
45
  this.nodeId = null;
33
46
  this.messageHandlers = new Set();
47
+ this.connectedPeers = new Set();
34
48
  }
35
49
 
36
50
  async start(nodeId) {
@@ -44,6 +58,14 @@ class InMemoryNetworkAdapter {
44
58
  }
45
59
 
46
60
  this.nodeId = null;
61
+ this.connectedPeers.clear();
62
+ }
63
+
64
+ async connectToPeer(remotePeerId) {
65
+ if (!remotePeerId || remotePeerId === this.nodeId) {
66
+ return;
67
+ }
68
+ this.connectedPeers.add(remotePeerId);
47
69
  }
48
70
 
49
71
  async broadcast(message) {
@@ -54,6 +76,26 @@ class InMemoryNetworkAdapter {
54
76
  await this.hub.broadcast(this.nodeId, message);
55
77
  }
56
78
 
79
+ async sendToPeers(message, peerIds = []) {
80
+ if (!this.nodeId) {
81
+ throw new Error('Network adapter has not been started');
82
+ }
83
+
84
+ await this.hub.sendToPeers(this.nodeId, message, peerIds);
85
+ }
86
+
87
+ listOpenPeerIds() {
88
+ return [...this.connectedPeers];
89
+ }
90
+
91
+ getOpenConnectionCount() {
92
+ return this.connectedPeers.size;
93
+ }
94
+
95
+ isConnectedTo(remotePeerId) {
96
+ return this.connectedPeers.has(remotePeerId);
97
+ }
98
+
57
99
  onMessage(handler) {
58
100
  this.messageHandlers.add(handler);
59
101
  }
@@ -185,6 +185,34 @@ class PeerJSNetworkAdapter {
185
185
  await Promise.all(deliveries);
186
186
  }
187
187
 
188
+ async sendToPeers(message, peerIds = []) {
189
+ if (!this.peer) {
190
+ throw new Error('PeerJS network adapter has not been started');
191
+ }
192
+
193
+ const targets = new Set((peerIds || []).filter(Boolean));
194
+ if (targets.size === 0) {
195
+ return;
196
+ }
197
+
198
+ const deliveries = [];
199
+ for (const [peerId, connection] of this.connections.entries()) {
200
+ if (targets.has(peerId) && connection.open) {
201
+ deliveries.push(connection.send(message));
202
+ }
203
+ }
204
+
205
+ await Promise.all(deliveries);
206
+ }
207
+
208
+ async disconnectPeer(remotePeerId) {
209
+ const connection = this.connections.get(remotePeerId);
210
+ if (connection && typeof connection.close === 'function') {
211
+ connection.close();
212
+ }
213
+ this.connections.delete(remotePeerId);
214
+ }
215
+
188
216
  getOpenConnectionCount() {
189
217
  return this.listOpenPeerIds().length;
190
218
  }
@@ -73,6 +73,7 @@ class IndexedDBPersistence {
73
73
  ownerId: record.ownerId,
74
74
  collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
75
75
  data: { ...record.data },
76
+ hash: record.hash || null,
76
77
  createdAt: record.createdAt,
77
78
  updatedAt: record.updatedAt,
78
79
  deletedAt: record.deletedAt,
@@ -143,6 +144,7 @@ class IndexedDBPersistence {
143
144
  ownerId: stored.ownerId,
144
145
  collaboratorIds: stored.collaboratorIds,
145
146
  data: stored.data,
147
+ hash: stored.hash || null,
146
148
  createdAt: stored.createdAt,
147
149
  updatedAt: stored.updatedAt,
148
150
  deletedAt: stored.deletedAt,
@@ -387,8 +387,16 @@ class MessageSecurityService {
387
387
 
388
388
  let key;
389
389
  if (encryption.kdf === 'pbkdf2') {
390
- const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
391
- key = await deriveBroadcastKey(password, salt, iterations);
390
+ const configuredIterations = this.options.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
391
+ const requestedIterations = encryption.kdfIterations || configuredIterations;
392
+ const minIterations = Math.max(1000, Math.floor(configuredIterations * 0.1));
393
+ const maxIterations = configuredIterations * 2;
394
+
395
+ if (requestedIterations < minIterations || requestedIterations > maxIterations) {
396
+ throw new Error(`Invalid kdfIterations: ${requestedIterations}`);
397
+ }
398
+
399
+ key = await deriveBroadcastKey(password, salt, requestedIterations);
392
400
  } else {
393
401
  key = legacyBroadcastKey(password, salt);
394
402
  }
@@ -6,6 +6,12 @@ class SlothPermutation {
6
6
  static p = BigInt(
7
7
  '170082004324204494273811327264862981553264701145937538369570764779791492622392118654022654452947093285873855529044371650895045691292912712699015605832276411308653107069798639938826015099738961427172366594187783204437869906954750443653318078358839409699824714551430573905637228307966826784684174483831608534979'
8
8
  );
9
+ // precompute values for optimization:
10
+ // (p - 1) / 2
11
+ static pHalf = (SlothPermutation.p - BigInt(1)) >> BigInt(1);
12
+ // (p + 1) / 4
13
+ // p ≡ 3 (mod 4) ⇒ (p+1) divisible by 4
14
+ static pQuarter = (SlothPermutation.p + BigInt(1)) >> BigInt(2);
9
15
 
10
16
  fastPow(base, exponent, modulus) {
11
17
  if (modulus === BigInt(1)) {
@@ -17,11 +23,11 @@ class SlothPermutation {
17
23
  let powExponent = exponent;
18
24
 
19
25
  while (powExponent > 0) {
20
- if (powExponent % BigInt(2) === BigInt(1)) {
26
+ if ((powExponent & BigInt(1)) === BigInt(1)) {
21
27
  result = (result * powBase) % modulus;
22
28
  }
23
29
 
24
- powExponent = powExponent / BigInt(2);
30
+ powExponent = powExponent >> BigInt(1);
25
31
  powBase = (powBase * powBase) % modulus;
26
32
  }
27
33
 
@@ -29,7 +35,7 @@ class SlothPermutation {
29
35
  }
30
36
 
31
37
  quadRes(x) {
32
- return this.fastPow(x, (SlothPermutation.p - BigInt(1)) / BigInt(2), SlothPermutation.p) === BigInt(1);
38
+ return this.fastPow(x, SlothPermutation.pHalf, SlothPermutation.p) === BigInt(1);
33
39
  }
34
40
 
35
41
  modSqrtOp(x) {
@@ -37,10 +43,10 @@ class SlothPermutation {
37
43
  let value = x;
38
44
 
39
45
  if (this.quadRes(value)) {
40
- y = this.fastPow(value, (SlothPermutation.p + BigInt(1)) / BigInt(4), SlothPermutation.p);
46
+ y = this.fastPow(value, SlothPermutation.pQuarter, SlothPermutation.p);
41
47
  } else {
42
48
  value = (-value + SlothPermutation.p) % SlothPermutation.p;
43
- y = this.fastPow(value, (SlothPermutation.p + BigInt(1)) / BigInt(4), SlothPermutation.p);
49
+ y = this.fastPow(value, SlothPermutation.pQuarter, SlothPermutation.p);
44
50
  }
45
51
 
46
52
  return y;
@@ -1,3 +1,12 @@
1
+ function randomBase36(length) {
2
+ let value = '';
3
+ while (value.length < length) {
4
+ const chunk = Math.random().toString(36).slice(2);
5
+ value += chunk.length > 0 ? chunk : '0';
6
+ }
7
+ return value.slice(0, length);
8
+ }
9
+
1
10
  class WebSocketSignalingProvider {
2
11
  constructor({ id, url, WebSocketImpl, priority = 0 }) {
3
12
  if (!url) {
@@ -50,8 +59,8 @@ class WebSocketSignalingProvider {
50
59
  return this.url;
51
60
  }
52
61
 
53
- const connectionId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
54
- const token = Math.random().toString(36).slice(2, 12);
62
+ const connectionId = `dignityjs_${randomBase36(10)}`;
63
+ const token = randomBase36(10);
55
64
  const hasQuery = this.url.includes('?');
56
65
  const hasId = /[?&]id=/.test(this.url);
57
66
  const hasToken = /[?&]token=/.test(this.url);