dignity.js 0.1.2 → 0.2.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 +13 -0
- package/dist/dignity.cjs.js +6835 -37
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +6849 -37
- package/dist/dignity.esm.js.map +4 -4
- package/dist/dignity.min.js +42 -1
- package/package.json +3 -1
- package/src/index.js +2 -0
- package/src/security/message-security-service.js +16 -4
- package/src/signaling/create-default-signaling-pool.js +5 -1
- package/src/signaling/default-signaling-config.js +2 -3
- package/src/signaling/peerjs-signaling-provider.js +210 -0
- package/src/signaling/websocket-signaling-provider.js +24 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dignity.js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "P2P object API for decentralized JavaScript applications",
|
|
5
5
|
"main": "dist/dignity.cjs.js",
|
|
6
6
|
"module": "dist/dignity.esm.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "jest --coverage",
|
|
25
25
|
"test:unit": "jest tests/unit --runInBand",
|
|
26
|
+
"test:cloudflare-live": "RUN_CLOUDFLARE_LIVE_TESTS=1 jest tests/integration/cloudflare-signaling-live.test.js --runInBand",
|
|
26
27
|
"test:pow-calibrate": "jest tests/unit/sloth-vdf-timing.test.js --runInBand",
|
|
27
28
|
"build": "node scripts/build.js",
|
|
28
29
|
"docs:serve": "npx http-server docs -p 4173 -o",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"jest": "^29.7.0"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
48
|
+
"peerjs": "^1.5.5",
|
|
47
49
|
"tweetnacl": "^1.0.3",
|
|
48
50
|
"tweetnacl-util": "^0.15.1"
|
|
49
51
|
},
|
package/src/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const DignityP2P = require('./core/dignity-p2p');
|
|
|
10
10
|
const createDefaultSignalingPool = require('./signaling/create-default-signaling-pool');
|
|
11
11
|
const SignalingPool = require('./signaling/signaling-pool');
|
|
12
12
|
const WebSocketSignalingProvider = require('./signaling/websocket-signaling-provider');
|
|
13
|
+
const PeerJSSignalingProvider = require('./signaling/peerjs-signaling-provider');
|
|
13
14
|
const {
|
|
14
15
|
InMemoryNetworkHub,
|
|
15
16
|
InMemoryNetworkAdapter
|
|
@@ -30,6 +31,7 @@ module.exports = {
|
|
|
30
31
|
createDefaultSignalingPool,
|
|
31
32
|
SignalingPool,
|
|
32
33
|
WebSocketSignalingProvider,
|
|
34
|
+
PeerJSSignalingProvider,
|
|
33
35
|
InMemoryNetworkHub,
|
|
34
36
|
InMemoryNetworkAdapter,
|
|
35
37
|
DEFAULT_CLOUDFLARE_SIGNALING_URLS,
|
|
@@ -153,6 +153,7 @@ class MessageSecurityService {
|
|
|
153
153
|
const pow = await this.generatePow(envelope);
|
|
154
154
|
envelope.security.pow = {
|
|
155
155
|
enabled: true,
|
|
156
|
+
messageHash: pow.messageHash,
|
|
156
157
|
challenge: pow.challenge,
|
|
157
158
|
proof: pow.proof,
|
|
158
159
|
steps: pow.steps,
|
|
@@ -208,6 +209,10 @@ class MessageSecurityService {
|
|
|
208
209
|
});
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
computePowMessageHash(envelope) {
|
|
213
|
+
return bytesToHex(hash32(utf8ToBytes(this.canonicalPowInput(envelope))));
|
|
214
|
+
}
|
|
215
|
+
|
|
211
216
|
async decryptIncomingMessage(envelope) {
|
|
212
217
|
if (!this.options.enabled) {
|
|
213
218
|
return {
|
|
@@ -420,13 +425,15 @@ class MessageSecurityService {
|
|
|
420
425
|
}
|
|
421
426
|
|
|
422
427
|
async generatePow(envelope) {
|
|
423
|
-
const
|
|
428
|
+
const messageHash = this.computePowMessageHash(envelope);
|
|
429
|
+
const challenge = messageHash;
|
|
424
430
|
const steps = await this.determinePowSteps();
|
|
425
431
|
const start = this.now();
|
|
426
432
|
const proof = await VDF.compute(challenge, steps);
|
|
427
433
|
const durationMs = this.now() - start;
|
|
428
434
|
|
|
429
435
|
return {
|
|
436
|
+
messageHash,
|
|
430
437
|
challenge,
|
|
431
438
|
proof,
|
|
432
439
|
steps: steps.toString(),
|
|
@@ -435,16 +442,21 @@ class MessageSecurityService {
|
|
|
435
442
|
}
|
|
436
443
|
|
|
437
444
|
async verifyPow(envelope) {
|
|
438
|
-
const
|
|
445
|
+
const expectedMessageHash = this.computePowMessageHash(envelope);
|
|
439
446
|
const pow = envelope.security.pow;
|
|
440
447
|
|
|
441
|
-
if (
|
|
448
|
+
if (
|
|
449
|
+
!pow ||
|
|
450
|
+
!pow.messageHash ||
|
|
451
|
+
pow.messageHash !== expectedMessageHash ||
|
|
452
|
+
pow.challenge !== pow.messageHash
|
|
453
|
+
) {
|
|
442
454
|
const error = new Error('PoW challenge mismatch');
|
|
443
455
|
error.code = 'INVALID_POW';
|
|
444
456
|
throw error;
|
|
445
457
|
}
|
|
446
458
|
|
|
447
|
-
const verified = await VDF.verify(pow.
|
|
459
|
+
const verified = await VDF.verify(pow.messageHash, BigInt(pow.steps), pow.proof);
|
|
448
460
|
if (!verified) {
|
|
449
461
|
const error = new Error('PoW verification failed');
|
|
450
462
|
error.code = 'INVALID_POW';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const SignalingPool = require('./signaling-pool');
|
|
2
2
|
const WebSocketSignalingProvider = require('./websocket-signaling-provider');
|
|
3
|
+
const PeerJSSignalingProvider = require('./peerjs-signaling-provider');
|
|
3
4
|
const {
|
|
4
5
|
DEFAULT_CLOUDFLARE_SIGNALING_URLS,
|
|
5
6
|
DEFAULT_SIGNALING_FALLBACK_URLS
|
|
@@ -13,8 +14,11 @@ function createDefaultSignalingPool(options = {}) {
|
|
|
13
14
|
const providers = [];
|
|
14
15
|
|
|
15
16
|
cloudflareUrls.forEach((url, index) => {
|
|
17
|
+
const usePeerJsProvider = /^wss:\/\/(peerjs\.92k\.de|0\.peerjs\.com)(\/|$)/.test(url);
|
|
18
|
+
const ProviderClass = usePeerJsProvider ? PeerJSSignalingProvider : WebSocketSignalingProvider;
|
|
19
|
+
|
|
16
20
|
providers.push(
|
|
17
|
-
new
|
|
21
|
+
new ProviderClass({
|
|
18
22
|
id: `cloudflare-${index + 1}`,
|
|
19
23
|
url,
|
|
20
24
|
WebSocketImpl,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const DEFAULT_CLOUDFLARE_SIGNALING_URLS = [
|
|
2
|
-
'wss://
|
|
3
|
-
'wss://
|
|
4
|
-
'wss://trycloudflare-signaling.example'
|
|
2
|
+
'wss://peerjs.92k.de/peerjs?key=peerjs',
|
|
3
|
+
'wss://0.peerjs.com/peerjs?key=peerjs'
|
|
5
4
|
];
|
|
6
5
|
|
|
7
6
|
const DEFAULT_SIGNALING_FALLBACK_URLS = [
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const WebSocketSignalingProvider = require('./websocket-signaling-provider');
|
|
2
|
+
|
|
3
|
+
class PeerJSSignalingProvider {
|
|
4
|
+
constructor({ id, url, PeerImpl, WebSocketImpl, priority = 0, connectTimeoutMs = 10000 }) {
|
|
5
|
+
if (!url) {
|
|
6
|
+
throw new Error('PeerJS signaling provider requires a url');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
this.id = id || url;
|
|
10
|
+
this.url = url;
|
|
11
|
+
this.priority = priority;
|
|
12
|
+
this.isCustomPeerImpl = Boolean(PeerImpl);
|
|
13
|
+
this.PeerImpl = PeerImpl || this.resolvePeerImplementation();
|
|
14
|
+
this.WebSocketImpl = WebSocketImpl;
|
|
15
|
+
this.connectTimeoutMs = connectTimeoutMs;
|
|
16
|
+
this.peer = null;
|
|
17
|
+
this.peerId = null;
|
|
18
|
+
this.connections = new Map();
|
|
19
|
+
this.messageHandlers = new Set();
|
|
20
|
+
this.fallbackProvider = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resolvePeerImplementation() {
|
|
24
|
+
try {
|
|
25
|
+
const peerjs = require('peerjs');
|
|
26
|
+
return peerjs.Peer || peerjs;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
parsePeerJsServerUrl() {
|
|
33
|
+
const parsed = new URL(this.url);
|
|
34
|
+
const secure = parsed.protocol === 'wss:';
|
|
35
|
+
const host = parsed.hostname;
|
|
36
|
+
const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
|
|
37
|
+
const path = parsed.pathname || '/';
|
|
38
|
+
const key = parsed.searchParams.get('key') || 'peerjs';
|
|
39
|
+
|
|
40
|
+
return { secure, host, port, path, key };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
shouldUseWebSocketFallback() {
|
|
44
|
+
return !this.isCustomPeerImpl && typeof globalThis.RTCPeerConnection !== 'function';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
useWebSocketFallback() {
|
|
48
|
+
if (!this.fallbackProvider) {
|
|
49
|
+
this.fallbackProvider = new WebSocketSignalingProvider({
|
|
50
|
+
id: `${this.id}-ws-fallback`,
|
|
51
|
+
url: this.url,
|
|
52
|
+
WebSocketImpl: this.WebSocketImpl,
|
|
53
|
+
priority: this.priority
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return this.fallbackProvider;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async connect() {
|
|
60
|
+
if (this.shouldUseWebSocketFallback()) {
|
|
61
|
+
await this.useWebSocketFallback().connect();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!this.PeerImpl) {
|
|
66
|
+
throw new Error('PeerJS implementation is not available');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const server = this.parsePeerJsServerUrl();
|
|
70
|
+
const peerId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
|
|
71
|
+
|
|
72
|
+
await new Promise((resolve, reject) => {
|
|
73
|
+
const peer = new this.PeerImpl(peerId, {
|
|
74
|
+
host: server.host,
|
|
75
|
+
port: server.port,
|
|
76
|
+
path: server.path,
|
|
77
|
+
secure: server.secure,
|
|
78
|
+
key: server.key
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const timeout = setTimeout(() => {
|
|
82
|
+
reject(new Error(`Unable to connect to signaling url ${this.url}`));
|
|
83
|
+
}, this.connectTimeoutMs);
|
|
84
|
+
|
|
85
|
+
peer.on('open', () => {
|
|
86
|
+
clearTimeout(timeout);
|
|
87
|
+
this.peer = peer;
|
|
88
|
+
this.peerId = peerId;
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
peer.on('connection', (connection) => {
|
|
93
|
+
this.attachConnectionHandlers(connection);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
peer.on('error', async (error) => {
|
|
97
|
+
clearTimeout(timeout);
|
|
98
|
+
if (error && error.type === 'browser-incompatible') {
|
|
99
|
+
try {
|
|
100
|
+
await this.useWebSocketFallback().connect();
|
|
101
|
+
resolve();
|
|
102
|
+
return;
|
|
103
|
+
} catch (fallbackError) {
|
|
104
|
+
reject(new Error(`Unable to connect to signaling url ${this.url}`));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
reject(new Error(`Unable to connect to signaling url ${this.url}`));
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
attachConnectionHandlers(connection) {
|
|
114
|
+
const remoteId = connection.peer;
|
|
115
|
+
this.connections.set(remoteId, connection);
|
|
116
|
+
|
|
117
|
+
connection.on('data', (payload) => {
|
|
118
|
+
for (const handler of this.messageHandlers) {
|
|
119
|
+
handler(payload);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
connection.on('close', () => {
|
|
124
|
+
this.connections.delete(remoteId);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async openConnection(remotePeerId) {
|
|
129
|
+
if (!this.peer) {
|
|
130
|
+
throw new Error('PeerJS is not connected');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const existing = this.connections.get(remotePeerId);
|
|
134
|
+
if (existing && existing.open) {
|
|
135
|
+
return existing;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return await new Promise((resolve, reject) => {
|
|
139
|
+
const connection = this.peer.connect(remotePeerId, { reliable: true, serialization: 'json' });
|
|
140
|
+
const timeout = setTimeout(() => {
|
|
141
|
+
reject(new Error(`Unable to connect peer ${remotePeerId} via ${this.url}`));
|
|
142
|
+
}, this.connectTimeoutMs);
|
|
143
|
+
|
|
144
|
+
connection.on('open', () => {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
this.attachConnectionHandlers(connection);
|
|
147
|
+
resolve(connection);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
connection.on('error', () => {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
reject(new Error(`Unable to connect peer ${remotePeerId} via ${this.url}`));
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
onMessage(handler) {
|
|
158
|
+
if (this.fallbackProvider) {
|
|
159
|
+
this.fallbackProvider.onMessage(handler);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.messageHandlers.add(handler);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async send(message) {
|
|
166
|
+
if (this.fallbackProvider) {
|
|
167
|
+
await this.fallbackProvider.send(message);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!this.peer) {
|
|
172
|
+
throw new Error(`Signaling socket is not open for ${this.url}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (message && message.to) {
|
|
176
|
+
const connection = await this.openConnection(message.to);
|
|
177
|
+
connection.send(message);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const connection of this.connections.values()) {
|
|
182
|
+
if (connection.open) {
|
|
183
|
+
connection.send(message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async disconnect() {
|
|
189
|
+
if (this.fallbackProvider) {
|
|
190
|
+
await this.fallbackProvider.disconnect();
|
|
191
|
+
this.fallbackProvider = null;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const connection of this.connections.values()) {
|
|
196
|
+
if (typeof connection.close === 'function') {
|
|
197
|
+
connection.close();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
this.connections.clear();
|
|
201
|
+
|
|
202
|
+
if (this.peer && typeof this.peer.destroy === 'function') {
|
|
203
|
+
this.peer.destroy();
|
|
204
|
+
}
|
|
205
|
+
this.peer = null;
|
|
206
|
+
this.peerId = null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = PeerJSSignalingProvider;
|
|
@@ -18,7 +18,7 @@ class WebSocketSignalingProvider {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
await new Promise((resolve, reject) => {
|
|
21
|
-
const socket = new this.WebSocketImpl(this.
|
|
21
|
+
const socket = new this.WebSocketImpl(this.buildConnectionUrl());
|
|
22
22
|
|
|
23
23
|
socket.onopen = () => {
|
|
24
24
|
this.socket = socket;
|
|
@@ -44,6 +44,29 @@ class WebSocketSignalingProvider {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
buildConnectionUrl() {
|
|
48
|
+
const peerJsHostPattern = /^wss:\/\/(peerjs\.92k\.de|0\.peerjs\.com)(\/|$)/;
|
|
49
|
+
if (!peerJsHostPattern.test(this.url)) {
|
|
50
|
+
return this.url;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const connectionId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
|
|
54
|
+
const token = Math.random().toString(36).slice(2, 12);
|
|
55
|
+
const hasQuery = this.url.includes('?');
|
|
56
|
+
const hasId = /[?&]id=/.test(this.url);
|
|
57
|
+
const hasToken = /[?&]token=/.test(this.url);
|
|
58
|
+
|
|
59
|
+
let url = this.url;
|
|
60
|
+
if (!hasId) {
|
|
61
|
+
url += `${hasQuery ? '&' : '?'}id=${connectionId}`;
|
|
62
|
+
}
|
|
63
|
+
if (!hasToken) {
|
|
64
|
+
url += `${url.includes('?') ? '&' : '?'}token=${token}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
onMessage(handler) {
|
|
48
71
|
this.messageHandlers.add(handler);
|
|
49
72
|
}
|