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.
- package/README.md +61 -3
- package/dist/dignity.cjs.js +603 -65
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +603 -65
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +27 -27
- package/docs/assets/dignity.esm.js +477 -51
- package/docs/index.html +346 -6
- package/docs/openapi-like.json +40 -5
- package/examples/decentralized-chess-lite.js +9 -0
- package/package.json +2 -1
- package/src/core/dignity-p2p.js +506 -20
- package/src/gossip/peer-group.js +64 -0
- package/src/index.js +13 -1
- package/src/network/in-memory-network.js +42 -0
- package/src/network/peerjs-network.js +28 -0
- package/src/persistence/indexeddb-persistence.js +2 -0
- package/src/security/message-security-service.js +10 -2
- package/src/security/sloth-vdf.js +11 -5
- package/src/signaling/websocket-signaling-provider.js +11 -2
|
@@ -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
|
|
391
|
-
|
|
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
|
|
26
|
+
if ((powExponent & BigInt(1)) === BigInt(1)) {
|
|
21
27
|
result = (result * powBase) % modulus;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
powExponent = powExponent
|
|
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,
|
|
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,
|
|
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,
|
|
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_${
|
|
54
|
-
const token =
|
|
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);
|