diodejs 0.4.0 → 0.4.2

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,82 @@
1
+ [
2
+ {
3
+ "last_seen": "0x69aac815",
4
+ "retries": "0x00",
5
+ "last_error": "0x00",
6
+ "node_id": "0x7e4cd38d266902444dc9c8f7c0aa716a32497d0b",
7
+ "node": [
8
+ "server",
9
+ "144.126.157.138",
10
+ "0xa056",
11
+ "0xc76f",
12
+ "1.9.3",
13
+ [["name", "pause_chalk@diode-us2b"]]
14
+ ],
15
+ "connected": true
16
+ },
17
+ {
18
+ "last_seen": "0x69aac815",
19
+ "retries": "0x00",
20
+ "last_error": null,
21
+ "node_id": "0x3edd3d61a0f9b85a3adac02dfb97c7d48eaf5462",
22
+ "node": [
23
+ "server",
24
+ "192.168.100.4",
25
+ "0xa056",
26
+ "0xc76f",
27
+ "1.4.2",
28
+ [["name", "private_node"]]
29
+ ],
30
+ "connected": true
31
+ },
32
+ {
33
+ "last_seen": "0x69aac815",
34
+ "retries": "0x01",
35
+ "last_error": "0x69a69d52",
36
+ "node_id": "0x1350d3b501d6842ed881b59de4b95b27372bfae8",
37
+ "node": [
38
+ "server",
39
+ "194.233.80.251",
40
+ "0xa056",
41
+ "0xc76f",
42
+ "1.9.3",
43
+ [["name", "fringe_quiz@diode-as2b"]]
44
+ ],
45
+ "connected": true
46
+ },
47
+ {
48
+ "last_seen": "0x69aac815",
49
+ "retries": "0x00",
50
+ "last_error": "0x00",
51
+ "node_id": "0x1111111111111111111111111111111111111111",
52
+ "node": [
53
+ "client",
54
+ "1.2.3.4",
55
+ "0xa056",
56
+ "0xc76f",
57
+ "1.9.3",
58
+ [["name", "wrong_type"]]
59
+ ],
60
+ "connected": true
61
+ },
62
+ {
63
+ "last_seen": "0x69aac815",
64
+ "retries": "0x00",
65
+ "last_error": "0x00",
66
+ "node_id": "0x2222222222222222222222222222222222222222",
67
+ "node": [
68
+ "server",
69
+ "34.129.36.236",
70
+ "0xa056",
71
+ "0xc76f",
72
+ "1.9.3",
73
+ [["name", "public-alt"]]
74
+ ],
75
+ "connected": false
76
+ },
77
+ {
78
+ "node_id": "0x3333333333333333333333333333333333333333",
79
+ "node": ["server", "", "0xa056", "0xc76f", "1.9.3", []],
80
+ "connected": true
81
+ }
82
+ ]
@@ -0,0 +1,71 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const DiodeConnection = require('../connection');
8
+ const { DEFAULT_FLEET_CONTRACT } = require('../utils');
9
+
10
+ function makeTempDir() {
11
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'diode-fleet-test-'));
12
+ }
13
+
14
+ function makeConnection() {
15
+ const tempDir = makeTempDir();
16
+ const keyLocation = path.join(tempDir, 'keys.json');
17
+ const connection = new DiodeConnection('relay.example', 41046, keyLocation);
18
+ connection.RPC = {
19
+ getEpoch: async () => 77,
20
+ };
21
+ connection._waitForServerEthereumAddress = async () => Buffer.from('aa'.repeat(20), 'hex');
22
+ return connection;
23
+ }
24
+
25
+ test('createTicketCommand uses the configured fleet contract in ticketv2', async () => {
26
+ const connection = makeConnection();
27
+
28
+ connection.setFleetContract('0x1111111111111111111111111111111111111111');
29
+ const command = await connection.createTicketCommand();
30
+
31
+ assert.equal(command[0], 'ticketv2');
32
+ assert.equal(command[2], 77);
33
+ assert.ok(Buffer.isBuffer(command[3]));
34
+ assert.equal(command[3].toString('hex'), '1111111111111111111111111111111111111111');
35
+ });
36
+
37
+ test('createTicketSignature changes when the fleet contract changes', async () => {
38
+ const connection = makeConnection();
39
+ const serverIdBuffer = Buffer.from('bb'.repeat(20), 'hex');
40
+ const totalConnections = 5;
41
+ const totalBytes = 123456;
42
+ const localAddress = 'client-a';
43
+ const epoch = 88;
44
+
45
+ const defaultSignature = await connection.createTicketSignature(
46
+ serverIdBuffer,
47
+ totalConnections,
48
+ totalBytes,
49
+ localAddress,
50
+ epoch,
51
+ );
52
+
53
+ connection.setFleetContract('0x2222222222222222222222222222222222222222');
54
+ const updatedSignature = await connection.createTicketSignature(
55
+ serverIdBuffer,
56
+ totalConnections,
57
+ totalBytes,
58
+ localAddress,
59
+ epoch,
60
+ );
61
+
62
+ assert.notDeepEqual(updatedSignature, defaultSignature);
63
+ });
64
+
65
+ test('DiodeConnection defaults to the existing fleet contract', async () => {
66
+ const connection = makeConnection();
67
+
68
+ const command = await connection.createTicketCommand();
69
+
70
+ assert.equal(command[3].toString('hex'), DEFAULT_FLEET_CONTRACT.slice(2));
71
+ });
@@ -0,0 +1,399 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const EventEmitter = require('events');
4
+ const net = require('net');
5
+ const tls = require('tls');
6
+ const dgram = require('dgram');
7
+
8
+ const PublishPort = require('../publishPort');
9
+
10
+ class FakeStreamSocket extends EventEmitter {
11
+ constructor() {
12
+ super();
13
+ this.destroyed = false;
14
+ this.remoteAddress = undefined;
15
+ this.remotePort = undefined;
16
+ }
17
+
18
+ setNoDelay() {}
19
+ pause() {}
20
+ write() {}
21
+ end() {}
22
+ destroy() {
23
+ this.destroyed = true;
24
+ }
25
+ pipe(destination) {
26
+ return destination;
27
+ }
28
+ }
29
+
30
+ class FakeDatagramSocket extends EventEmitter {
31
+ constructor() {
32
+ super();
33
+ this.connectCalls = [];
34
+ this.sendCalls = [];
35
+ this.closed = false;
36
+ this.remoteAddress = undefined;
37
+ this.remotePort = undefined;
38
+ }
39
+
40
+ connect(port, host, callback) {
41
+ this.connectCalls.push({ port, host });
42
+ if (typeof callback === 'function') {
43
+ callback();
44
+ }
45
+ }
46
+
47
+ send(...args) {
48
+ if (args.length >= 3 && typeof args[1] === 'number' && typeof args[2] === 'string') {
49
+ this.sendCalls.push({ data: args[0], port: args[1], address: args[2] });
50
+ } else {
51
+ this.sendCalls.push({ data: args[0] });
52
+ }
53
+ const callback = args.find((arg) => typeof arg === 'function');
54
+ if (callback) {
55
+ callback(null);
56
+ }
57
+ }
58
+
59
+ close() {
60
+ this.closed = true;
61
+ }
62
+
63
+ setRecvBufferSize() {}
64
+ setSendBufferSize() {}
65
+ }
66
+
67
+ class FakeTlsSocket extends EventEmitter {
68
+ setNoDelay() {}
69
+ pipe(destination) {
70
+ return destination;
71
+ }
72
+ }
73
+
74
+ class FakeConnection extends EventEmitter {
75
+ constructor() {
76
+ super();
77
+ this.connections = new Map();
78
+ this.sentResponses = [];
79
+ this.sentErrors = [];
80
+ this.portCloseCalls = [];
81
+ this.portSendCalls = [];
82
+ this.RPC = {
83
+ sendResponse: async (...args) => {
84
+ this.sentResponses.push(args);
85
+ },
86
+ sendError: async (...args) => {
87
+ this.sentErrors.push(args);
88
+ },
89
+ portClose: async (...args) => {
90
+ this.portCloseCalls.push(args);
91
+ },
92
+ portSend: async (...args) => {
93
+ this.portSendCalls.push(args);
94
+ },
95
+ };
96
+ }
97
+
98
+ addConnection(ref, connectionInfo) {
99
+ this.connections.set(ref.toString('hex'), connectionInfo);
100
+ }
101
+
102
+ getConnection(ref) {
103
+ return this.connections.get(ref.toString('hex'));
104
+ }
105
+
106
+ deleteConnection(ref) {
107
+ return this.connections.delete(ref.toString('hex'));
108
+ }
109
+
110
+ getClientSocket() {
111
+ return null;
112
+ }
113
+
114
+ getServerRelayHost() {
115
+ return 'relay.example';
116
+ }
117
+
118
+ getDeviceCertificate() {
119
+ return 'fake-cert';
120
+ }
121
+
122
+ getEthereumAddress() {
123
+ return '0x' + 'aa'.repeat(20);
124
+ }
125
+
126
+ getPrivateKey() {
127
+ return Buffer.alloc(32, 1);
128
+ }
129
+ }
130
+
131
+ function makeRef(value = '01') {
132
+ return Buffer.from(value.padStart(2, '0'), 'hex');
133
+ }
134
+
135
+ function makeSessionId(value = '02') {
136
+ return Buffer.from(value.padStart(2, '0'), 'hex');
137
+ }
138
+
139
+ function makeDeviceId(hexByte) {
140
+ const byte = hexByte.length === 1 ? hexByte.repeat(2) : hexByte;
141
+ return Buffer.from(byte.repeat(20), 'hex');
142
+ }
143
+
144
+ test('PublishPort array input defaults host to 127.0.0.1', () => {
145
+ const connection = new FakeConnection();
146
+ const publishPort = new PublishPort(connection, [8080]);
147
+
148
+ assert.deepEqual(publishPort.getPublishedPorts(), {
149
+ 8080: { mode: 'public', whitelist: [], host: '127.0.0.1' },
150
+ });
151
+
152
+ publishPort.stopListening();
153
+ });
154
+
155
+ test('PublishPort preserves explicit host in object config', () => {
156
+ const connection = new FakeConnection();
157
+ const publishPort = new PublishPort(connection, {
158
+ 8080: { mode: 'private', whitelist: ['0xabc'], host: ' backend.internal ' },
159
+ });
160
+
161
+ assert.deepEqual(publishPort.getPublishedPorts(), {
162
+ 8080: { mode: 'private', whitelist: ['0xabc'], host: 'backend.internal' },
163
+ });
164
+
165
+ publishPort.stopListening();
166
+ });
167
+
168
+ test('PublishPort rejects invalid host values', () => {
169
+ const connection = new FakeConnection();
170
+
171
+ assert.throws(() => {
172
+ new PublishPort(connection, { 8080: { mode: 'public', host: ' ' } });
173
+ }, TypeError);
174
+
175
+ assert.throws(() => {
176
+ new PublishPort(connection, { 8080: { mode: 'public', host: 42 } });
177
+ }, TypeError);
178
+ });
179
+
180
+ test('TCP publish connects to configured host', () => {
181
+ const connection = new FakeConnection();
182
+ const publishPort = new PublishPort(connection, {
183
+ 8080: { mode: 'public', host: '192.168.1.10' },
184
+ });
185
+ const originalConnect = net.connect;
186
+ const connectCalls = [];
187
+
188
+ net.connect = (options, callback) => {
189
+ const socket = new FakeStreamSocket();
190
+ connectCalls.push(options);
191
+ process.nextTick(() => {
192
+ if (typeof callback === 'function') {
193
+ callback();
194
+ }
195
+ });
196
+ return socket;
197
+ };
198
+
199
+ try {
200
+ publishPort.handleTCPConnection(makeSessionId(), makeRef(), 8080, '0x' + '11'.repeat(20), publishPort.getPublishedPorts()[8080], connection);
201
+ } finally {
202
+ net.connect = originalConnect;
203
+ publishPort.stopListening();
204
+ }
205
+
206
+ assert.equal(connectCalls.length, 1);
207
+ assert.deepEqual(connectCalls[0], { port: 8080, host: '192.168.1.10' });
208
+ });
209
+
210
+ test('TLS publish backend socket connects to configured host', () => {
211
+ const connection = new FakeConnection();
212
+ const publishPort = new PublishPort(connection, {
213
+ 8443: { mode: 'public', host: 'backend.internal' },
214
+ });
215
+ const originalConnect = net.connect;
216
+ const originalTlsSocket = tls.TLSSocket;
217
+ const connectCalls = [];
218
+
219
+ net.connect = (options, callback) => {
220
+ const socket = new FakeStreamSocket();
221
+ connectCalls.push(options);
222
+ process.nextTick(() => {
223
+ if (typeof callback === 'function') {
224
+ callback();
225
+ }
226
+ });
227
+ return socket;
228
+ };
229
+ tls.TLSSocket = FakeTlsSocket;
230
+
231
+ try {
232
+ publishPort.handleTLSConnection(makeSessionId(), makeRef('03'), 8443, '0x' + '11'.repeat(20), publishPort.getPublishedPorts()[8443], connection);
233
+ } finally {
234
+ net.connect = originalConnect;
235
+ tls.TLSSocket = originalTlsSocket;
236
+ publishPort.stopListening();
237
+ }
238
+
239
+ assert.equal(connectCalls.length, 1);
240
+ assert.deepEqual(connectCalls[0], { port: 8443, host: 'backend.internal' });
241
+ });
242
+
243
+ test('UDP publish stores and sends to configured host', () => {
244
+ const connection = new FakeConnection();
245
+ const publishPort = new PublishPort(connection, {
246
+ 5353: { mode: 'public', host: '192.168.1.20' },
247
+ });
248
+ const originalCreateSocket = dgram.createSocket;
249
+ const sockets = [];
250
+
251
+ dgram.createSocket = () => {
252
+ const socket = new FakeDatagramSocket();
253
+ sockets.push(socket);
254
+ return socket;
255
+ };
256
+
257
+ try {
258
+ const ref = makeRef('04');
259
+ publishPort.handleUDPConnection(makeSessionId('05'), ref, 5353, '0x' + '11'.repeat(20), publishPort.getPublishedPorts()[5353], connection);
260
+ publishPort.handlePortSend(makeSessionId('06'), ['portsend', ref, Buffer.from('hello')], connection);
261
+
262
+ const connectionInfo = connection.getConnection(ref);
263
+ assert.deepEqual(connectionInfo.remoteInfo, { port: 5353, address: '192.168.1.20' });
264
+ assert.equal(sockets[0].sendCalls.length, 1);
265
+ assert.equal(sockets[0].sendCalls[0].address, '192.168.1.20');
266
+ assert.equal(sockets[0].sendCalls[0].port, 5353);
267
+ } finally {
268
+ dgram.createSocket = originalCreateSocket;
269
+ publishPort.stopListening();
270
+ }
271
+ });
272
+
273
+ test('native TCP publish connects local socket to configured host', () => {
274
+ const connection = new FakeConnection();
275
+ const publishPort = new PublishPort(connection, {
276
+ 8089: { mode: 'public', host: '10.0.0.8' },
277
+ });
278
+ const originalConnect = net.connect;
279
+ const connectCalls = [];
280
+
281
+ net.connect = (options, callback) => {
282
+ const socket = new FakeStreamSocket();
283
+ connectCalls.push(options);
284
+ process.nextTick(() => {
285
+ if (typeof callback === 'function') {
286
+ callback();
287
+ }
288
+ socket.emit('connect');
289
+ });
290
+ return socket;
291
+ };
292
+
293
+ try {
294
+ publishPort.handleNativeTCPRelay(
295
+ makeSessionId('07'),
296
+ 41000,
297
+ { physicalPort: 41000, port: 8089, host: '10.0.0.8', deviceId: '0x' + '11'.repeat(20), ready: false, session: null },
298
+ connection
299
+ );
300
+ } finally {
301
+ net.connect = originalConnect;
302
+ publishPort.stopListening();
303
+ }
304
+
305
+ assert.equal(connectCalls.length, 2);
306
+ assert.deepEqual(connectCalls[0], { host: 'relay.example', port: 41000 });
307
+ assert.deepEqual(connectCalls[1], { port: 8089, host: '10.0.0.8' });
308
+ });
309
+
310
+ test('native UDP publish connects local socket to configured host', () => {
311
+ const connection = new FakeConnection();
312
+ const publishPort = new PublishPort(connection, {
313
+ 8090: { mode: 'public', host: 'backend.internal' },
314
+ });
315
+ const originalCreateSocket = dgram.createSocket;
316
+ const sockets = [];
317
+
318
+ dgram.createSocket = () => {
319
+ const socket = new FakeDatagramSocket();
320
+ sockets.push(socket);
321
+ return socket;
322
+ };
323
+
324
+ try {
325
+ publishPort.handleNativeUDPRelay(
326
+ makeSessionId('08'),
327
+ 41001,
328
+ { physicalPort: 41001, port: 8090, host: 'backend.internal', deviceId: '0x' + '11'.repeat(20), ready: false, session: null },
329
+ connection
330
+ );
331
+ } finally {
332
+ dgram.createSocket = originalCreateSocket;
333
+ publishPort.stopListening();
334
+ }
335
+
336
+ assert.equal(sockets.length, 2);
337
+ assert.deepEqual(sockets[0].connectCalls[0], { port: 41001, host: 'relay.example' });
338
+ assert.deepEqual(sockets[1].connectCalls[0], { port: 8090, host: 'backend.internal' });
339
+ });
340
+
341
+ test('handlePortOpen preserves localhost default when host is omitted', () => {
342
+ const connection = new FakeConnection();
343
+ const publishPort = new PublishPort(connection, [8081]);
344
+ const originalConnect = net.connect;
345
+ const connectCalls = [];
346
+
347
+ net.connect = (options, callback) => {
348
+ const socket = new FakeStreamSocket();
349
+ connectCalls.push(options);
350
+ process.nextTick(() => {
351
+ if (typeof callback === 'function') {
352
+ callback();
353
+ }
354
+ });
355
+ return socket;
356
+ };
357
+
358
+ try {
359
+ publishPort.handlePortOpen(
360
+ makeSessionId('09'),
361
+ ['portopen', '8081', makeRef('0a'), makeDeviceId('1')],
362
+ connection
363
+ );
364
+ } finally {
365
+ net.connect = originalConnect;
366
+ publishPort.stopListening();
367
+ }
368
+
369
+ assert.deepEqual(connectCalls[0], { port: 8081, host: '127.0.0.1' });
370
+ });
371
+
372
+ test('handlePortOpen rejects non-whitelisted devices before connecting', () => {
373
+ const connection = new FakeConnection();
374
+ const publishPort = new PublishPort(connection, {
375
+ 3000: { mode: 'private', whitelist: ['0x' + '22'.repeat(20)], host: '192.168.1.30' },
376
+ });
377
+ const originalConnect = net.connect;
378
+ let connectCalled = false;
379
+
380
+ net.connect = () => {
381
+ connectCalled = true;
382
+ return new FakeStreamSocket();
383
+ };
384
+
385
+ try {
386
+ publishPort.handlePortOpen(
387
+ makeSessionId('0b'),
388
+ ['portopen', '3000', makeRef('0c'), makeDeviceId('1')],
389
+ connection
390
+ );
391
+ } finally {
392
+ net.connect = originalConnect;
393
+ publishPort.stopListening();
394
+ }
395
+
396
+ assert.equal(connectCalled, false);
397
+ assert.equal(connection.sentErrors.length, 1);
398
+ assert.equal(connection.sentErrors[0][2], 'Device not whitelisted');
399
+ });
package/utils.js CHANGED
@@ -199,6 +199,30 @@ function ensureDirectoryExistence(filePath) {
199
199
  fs.mkdirSync(dirname);
200
200
  }
201
201
 
202
+ const DEFAULT_FLEET_CONTRACT = '0x6000000000000000000000000000000000000000';
203
+
204
+ function normalizeFleetContractAddress(value) {
205
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
206
+ const buffer = toBufferView(value);
207
+ if (buffer.length !== 20) {
208
+ throw new Error('fleetContract must be a 20-byte EVM address');
209
+ }
210
+ return `0x${buffer.toString('hex')}`.toLowerCase();
211
+ }
212
+
213
+ if (typeof value !== 'string') {
214
+ throw new Error('fleetContract must be a 20-byte EVM address hex string');
215
+ }
216
+
217
+ const trimmed = value.trim();
218
+ const hex = trimmed.toLowerCase().startsWith('0x') ? trimmed.slice(2) : trimmed;
219
+ if (!/^[0-9a-fA-F]{40}$/.test(hex)) {
220
+ throw new Error('fleetContract must be a 20-byte EVM address hex string');
221
+ }
222
+
223
+ return `0x${hex.toLowerCase()}`;
224
+ }
225
+
202
226
  module.exports = {
203
227
  makeReadable,
204
228
  parseRequestId,
@@ -209,4 +233,6 @@ module.exports = {
209
233
  loadOrGenerateKeyPair,
210
234
  ensureDirectoryExistence,
211
235
  toBufferView,
236
+ DEFAULT_FLEET_CONTRACT,
237
+ normalizeFleetContractAddress,
212
238
  };