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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.1.2",
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 challenge = bytesToHex(hash32(utf8ToBytes(this.canonicalPowInput(envelope))));
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 expectedChallenge = bytesToHex(hash32(utf8ToBytes(this.canonicalPowInput(envelope))));
445
+ const expectedMessageHash = this.computePowMessageHash(envelope);
439
446
  const pow = envelope.security.pow;
440
447
 
441
- if (!pow || pow.challenge !== expectedChallenge) {
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.challenge, BigInt(pow.steps), pow.proof);
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 WebSocketSignalingProvider({
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://signaling.cloudflare.com',
3
- 'wss://cloudflare-webrtc-signaling.example',
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.url);
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
  }