devicelink 0.1.1

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.
Files changed (4) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +141 -0
  3. package/index.js +343 -0
  4. package/package.json +20 -0
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2026, Yassen
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # devicelink (beta)
2
+
3
+ ### _Zero-configuration local network discovery and bidirectional messaging for Node.js._
4
+
5
+ devicelink abstracts away the friction of IP hunting, port configuration, and manual endpoint routing. Whether you are building a distributed task queue across multiple laptops, orchestrating IoT microcontrollers, or just trying to get two local machines to talk to each other on the same subnet, devicelink handles the handshake and messaging instantly.
6
+
7
+ ## Features:
8
+
9
+ - **Auto-Discovery:** Concurrently scan your local subnet to find active nodes in under 2 seconds.
10
+ - **Zero-Config Handshakes:** Connect instances securely without manually tracking IP addresses.
11
+ - **Instant JSON Messaging:** Fire bidirectional JSON payloads between connected devices immediately.
12
+ - **Secure Routing:** Built-in IP validation prevents rogue network requests from spoofing messages.
13
+
14
+ ## Installation:
15
+
16
+ ```bash
17
+ npm install devicelink
18
+ ```
19
+
20
+ ## Quick Start:
21
+
22
+ Here is a complete example of two machines finding each other and communicating.
23
+
24
+ 1. **The Worker Node:** Run this on the first machine (or a separate terminal). It waits for a connection and listens for instructions.
25
+
26
+ ```js
27
+ const Devicelink = require("devicelink");
28
+
29
+ // Initialize on port 3000
30
+ const dl = new Devicelink(3000, "Worker-Node-1");
31
+
32
+ dl.onMessage((payload) => {
33
+ console.log(`[Message Received]:`, payload);
34
+
35
+ // Process task and respond...
36
+ if (payload.task === "ping") {
37
+ dl.message({ status: "success", response: "pong" });
38
+ }
39
+ });
40
+
41
+ dl.start();
42
+ console.log("Worker listening for connections...");
43
+ ```
44
+
45
+ 2. **The Master Node:** Run this on the second machine. It searches the network, connects to the worker, and sends a task.
46
+
47
+ ```js
48
+ const Devicelink = require("devicelink");
49
+
50
+ const dl = new Devicelink(4000, "Master-Server");
51
+ dl.start();
52
+
53
+ (async () {
54
+ console.log("Scanning local network...");
55
+
56
+ // Search for devicelink instances running on port 3000
57
+ const foundDevices = await dl.search(3000);
58
+
59
+ if (foundDevices.length === 0) {
60
+ return console.log("No devices found on the network.");
61
+ }
62
+
63
+ const target = foundDevices[0];
64
+ console.log(`Found device: ${target.deviceName} at ${target.host}`);
65
+
66
+ // Lock the connection
67
+ await dl.connect(target);
68
+
69
+ // Define what happens when the worker replies
70
+ dl.onMessage((reply) => {
71
+ console.log(`[Reply from Worker]:`, reply);
72
+ });
73
+
74
+ // Send a JSON payload
75
+ await dl.message({ task: "ping", timestamp: Date.now() });
76
+ })()
77
+ ```
78
+
79
+ ## API Reference:
80
+
81
+ ```js
82
+ new Devicelink(port?, deviceName?)
83
+ ```
84
+
85
+ Creates a new Devicelink instance.
86
+
87
+ - **port:** `(Number)` The port your local instance will listen on. Defaults to 3000.
88
+ - **deviceName:** `(String)` A human-readable identifier for this node. Defaults to "Unnamed".
89
+
90
+ ```js
91
+ Devicelink.start();
92
+ ```
93
+
94
+ Mounts the internal server and begins listening for incoming discovery pings and connections. Must be called before searching or receiving messages.\
95
+
96
+ ```js
97
+ Devicelink.search(port?)
98
+ ```
99
+
100
+ _Returns:_ `Promise<DevicelinkDevice[]>`
101
+ Scans the entire local subnet concurrently for other active devicelink instances.
102
+
103
+ - **port:** `(Number)` The target port to scan for. Defaults to the instance's own port.
104
+
105
+ ```js
106
+ Devicelink.connect(device);
107
+ ```
108
+
109
+ _Returns:_ `Promise<void>`
110
+
111
+ Initiates a secure handshake with a DevicelinkDevice (usually obtained via .search()). Locks the instance into a 1-to-1 communication channel with the target.
112
+
113
+ ```js
114
+ Devicelink.message(payload);
115
+ ```
116
+
117
+ _Returns:_ `Promise<void>`
118
+
119
+ Sends a JSON-serializable object to the currently connected device.
120
+
121
+ ```js
122
+ Devicelink.onConnect(callback);
123
+ ```
124
+
125
+ Registers a callback function that is triggered whenever a remote device successfully connects to this instance.
126
+
127
+ - **callback:** `(device: DevicelinkDevice) => void`
128
+
129
+ ```js
130
+ Devicelink.onMessage(callback);
131
+ ```
132
+
133
+ Registers a callback function that is triggered whenever a valid JSON payload is received from the connected device.
134
+
135
+ - **callback:** `(payload: Object) => void`
136
+
137
+ ## Architecture & Roadmap:
138
+
139
+ devicelink is currently in active v0.x development. The current architecture relies on concurrent HTTP subnet scanning and standard POST requests for high reliability across diverse environments.
140
+
141
+ ## License: ISC
package/index.js ADDED
@@ -0,0 +1,343 @@
1
+ const { log } = require("console");
2
+ const express = require("express");
3
+ const os = require("os");
4
+
5
+ class Devicelink {
6
+ #app;
7
+ #onMessageCallback;
8
+ #onConnectCallback;
9
+
10
+ /**
11
+ * Creates a new Devicelink instance.
12
+ *
13
+ * @param {number} [port=3000] - The port the HTTP server will use.
14
+ * @param {string} [deviceName="Unnamed"] - The local device name.
15
+ */
16
+ constructor(port = 3000, deviceName = "Unnamed") {
17
+ this.port = port;
18
+ this.deviceName = deviceName;
19
+ this.lanAddress = getLanAddress();
20
+ this.isConnected = false;
21
+ this.isActive = false;
22
+ this.connectedDevice = new DevicelinkDevice();
23
+ this.#app = express();
24
+
25
+ /**
26
+ *
27
+ * @param {JSON} payload
28
+ * @returns
29
+ */
30
+ this.#onMessageCallback = (payload) => {
31
+ return;
32
+ };
33
+
34
+ /**
35
+ *
36
+ * @param {DevicelinkDevice} device
37
+ * @returns
38
+ */
39
+ this.#onConnectCallback = (device) => {
40
+ return;
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Starts the Devicelink HTTP server and starts listening.
46
+ * @returns {void}
47
+ */
48
+ start() {
49
+ this.#app.set("trust proxy", true);
50
+ this.#app.use(express.json());
51
+
52
+ this.#app.post("/devicelink/connect", (req, res) => {
53
+ const deviceName = req.body["device_name"];
54
+ const isConnected = req.body["is_connected"];
55
+ const isActive = req.body["is_active"];
56
+
57
+ const host = getRequestHost(req);
58
+ this.connectedDevice = new DevicelinkDevice(
59
+ host,
60
+ deviceName,
61
+ true,
62
+ isActive,
63
+ );
64
+ res.sendStatus(200);
65
+ this.#onConnectCallback(this.connectedDevice);
66
+ });
67
+
68
+ this.#app.get("/devicelink/status", (req, res) => {
69
+ res.json({
70
+ device_name: this.deviceName,
71
+ is_connected: this.isConnected,
72
+ is_active: this.isActive,
73
+ });
74
+ });
75
+
76
+ this.#app.post(
77
+ "/devicelink/message",
78
+ (req, res, next) => this.#validateConnection(req, res, next),
79
+ (req, res) => {
80
+ const payload = req.body;
81
+ this.#onMessageCallback(payload);
82
+ res.sendStatus(200);
83
+ },
84
+ );
85
+
86
+ this.#app.listen(this.port);
87
+ this.isActive = true;
88
+ }
89
+
90
+ #validateConnection(req, res, next) {
91
+ const host = getRequestHost(req);
92
+ if (host != this.connectedDevice.host)
93
+ res.status(401).send("Unauthorized access");
94
+ else {
95
+ next();
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Register a callback for when a devicelink connection is established.
101
+ * @param {(device: DevicelinkDevice) => void} callback Callback invoked with the connected device.
102
+ * @returns {void}
103
+ */
104
+ onConnect(callback) {
105
+ this.#onConnectCallback = callback;
106
+ }
107
+
108
+ /**
109
+ * Register a callback for incoming devicelink messages.
110
+ * @param {(payload: JSON) => void} callback Callback invoked with the message payload.
111
+ * @returns {void}
112
+ */
113
+ onMessage(callback) {
114
+ this.#onMessageCallback = callback;
115
+ }
116
+
117
+ /**
118
+ * Connect to other devicelink instance.
119
+ * @param {DevicelinkDevice} device A devicelink device to connect to.
120
+ * @returns
121
+ */
122
+ async connect(device) {
123
+ try {
124
+ const response = await fetch(`http://${device.host}/devicelink/connect`, {
125
+ method: "POST",
126
+ headers: {
127
+ "Content-Type": "application/json",
128
+ "X-Devicelink-Port": this.port,
129
+ },
130
+ body: JSON.stringify({
131
+ device_name: this.deviceName,
132
+ is_connected: this.isConnected,
133
+ is_active: this.isActive,
134
+ }),
135
+ });
136
+ if (!response.ok)
137
+ return console.error(`Failed to connect to ${device.deviceName}`);
138
+ this.connectedDevice = device;
139
+ this.isConnected = true;
140
+ this.#onConnectCallback(device);
141
+ } catch (error) {
142
+ console.error(
143
+ `Network error: Could not reach ${this.connectedDevice.deviceName}`,
144
+ );
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Send a devicelink message to the currently connected device.
150
+ * @param {Object} payload JSON-serializable payload to send in the request body.
151
+ * @returns {Promise<void>} Resolves when the request completes; logs an error on failure.
152
+ */
153
+ async message(payload) {
154
+ try {
155
+ const response = await fetch(
156
+ `http://${this.connectedDevice.host}/devicelink/message`,
157
+ {
158
+ method: "POST",
159
+ headers: {
160
+ "Content-Type": "application/json",
161
+ "X-Devicelink-Port": this.port,
162
+ },
163
+ body: JSON.stringify(payload),
164
+ },
165
+ );
166
+ if (!response.ok)
167
+ return console.error(
168
+ `Failed to send message to ${this.connectedDevice.deviceName}`,
169
+ );
170
+ } catch (error) {
171
+ console.error(
172
+ `Network error: Could not reach ${this.connectedDevice.deviceName}`,
173
+ );
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Search all nearby devices in current subnet for an active devicelink instance.
179
+ * @param {Number} port The port devicelink will search in (defaults to current port).
180
+ * @returns {Promise<DevicelinkDevice[]>} A list of discovered active devices.
181
+ */
182
+ async search(port = this.port) {
183
+ const baseIp = getBaseIp(this.lanAddress);
184
+ const promises = [];
185
+ const timeoutMs = 1500;
186
+
187
+ for (let i = 1; i <= 254; i++) {
188
+ const targetIp = baseIp + i;
189
+ promises.push(this.#pingDevice(targetIp, port, timeoutMs));
190
+ }
191
+
192
+ const results = await Promise.allSettled(promises);
193
+
194
+ const foundDevices = results
195
+ .filter(
196
+ (result) => result.status === "fulfilled" && result.value !== null,
197
+ )
198
+ .map((result) => result.value);
199
+
200
+ return foundDevices;
201
+ }
202
+
203
+ async #pingDevice(ip, port, timeoutMs) {
204
+ const controller = new AbortController();
205
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
206
+ const host = `${ip}:${port}`;
207
+
208
+ try {
209
+ const url = `http://${host}/devicelink/status`;
210
+ const response = await fetch(url, { signal: controller.signal });
211
+
212
+ if (response.ok) {
213
+ const data = await response.json();
214
+
215
+ if (data) {
216
+ const device = new DevicelinkDevice(
217
+ host,
218
+ data["device_name"],
219
+ data["is_connected"],
220
+ data["is_active"],
221
+ );
222
+ return device;
223
+ }
224
+ }
225
+ } catch (error) {
226
+ } finally {
227
+ clearTimeout(timeoutId);
228
+ }
229
+
230
+ return null;
231
+ }
232
+ }
233
+
234
+ class DevicelinkDevice {
235
+ #host;
236
+ #deviceName;
237
+ #isConnected;
238
+ #isActive;
239
+
240
+ /**
241
+ * Creates a new Devicelink device instance.
242
+ *
243
+ * @param {string} host - The device host in `ip:port` format.
244
+ * @param {string} deviceName - The device name.
245
+ * @param {boolean} isConnected - Whether the device is connected.
246
+ * @param {boolean} isActive - Whether the device is active.
247
+ */
248
+ constructor(host, deviceName, isConnected, isActive) {
249
+ this.#host = host;
250
+ this.#deviceName = deviceName;
251
+ this.#isConnected = isConnected;
252
+ this.#isActive = isActive;
253
+ }
254
+
255
+ /**
256
+ * Gets the device host.
257
+ *
258
+ * @returns {string} The host in `ip:port` format.
259
+ */
260
+ get host() {
261
+ return this.#host;
262
+ }
263
+
264
+ /**
265
+ * Gets the device name.
266
+ *
267
+ * @returns {string} The device name.
268
+ */
269
+ get deviceName() {
270
+ return this.#deviceName;
271
+ }
272
+
273
+ /**
274
+ * Gets whether the device is connected.
275
+ *
276
+ * @returns {boolean} True when connected.
277
+ */
278
+ get isConnected() {
279
+ return this.#isConnected;
280
+ }
281
+
282
+ /**
283
+ * Gets whether the device is active.
284
+ *
285
+ * @returns {boolean} True when active.
286
+ */
287
+ get isActive() {
288
+ return this.#isActive;
289
+ }
290
+
291
+ /**
292
+ * Returns a custom Node.js inspection string.
293
+ *
294
+ * @returns {string} A formatted device description.
295
+ */
296
+ [Symbol.for("nodejs.util.inspect.custom")]() {
297
+ return `<DevicelinkDevice [${this.#deviceName}] at ${this.#host} (connected: ${this.#isConnected}, active: ${this.#isActive})>`;
298
+ }
299
+ }
300
+
301
+ function getLanAddress() {
302
+ const nets = os.networkInterfaces();
303
+ for (const name of Object.keys(nets)) {
304
+ for (const net of nets[name]) {
305
+ if (net.family === "IPv4" && !net.internal) {
306
+ return net.address;
307
+ }
308
+ }
309
+ }
310
+ return "127.0.0.1";
311
+ }
312
+
313
+ function getBaseIp(ip = "121.0.0.1") {
314
+ const parts = ip.split(".");
315
+ parts.pop();
316
+ return parts.join(".") + ".";
317
+ }
318
+
319
+ function convertToIpv4(ip = "::ffff:121.0.0.1") {
320
+ if (ip == "::1") return "127.0.0.1";
321
+ if (ip.substring(0, 7) === "::ffff:") {
322
+ ip = ip.substring(7);
323
+ }
324
+
325
+ return ip;
326
+ }
327
+
328
+ /**
329
+ * Builds a callback host string from the request IP and `x-callback-port` header.
330
+ * @param {Object} req - The request object.
331
+ * @param {string} [req.ip] - The request IP address.
332
+ * @param {Object} req.socket - The socket object.
333
+ * @param {string} req.socket.remoteAddress - The socket remote address.
334
+ * @returns {string} The formatted host address in `ip:port` format.
335
+ */
336
+ function getRequestHost(req) {
337
+ const ip = convertToIpv4(req.ip || req.socket.remoteAddress);
338
+ const port = req.headers["x-devicelink-port"];
339
+ const host = `${ip}:${port}`;
340
+ return host;
341
+ }
342
+
343
+ module.exports = Devicelink;
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "devicelink",
3
+ "version": "0.1.1",
4
+ "description": "A way to get multiple devices on the same network to talk with each other.",
5
+ "license": "ISC",
6
+ "author": "YasoXtreme",
7
+ "type": "commonjs",
8
+ "main": "index.js",
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1",
11
+ "start": "node index.js"
12
+ },
13
+ "dependencies": {
14
+ "express": "^5.2.1"
15
+ },
16
+ "files": [
17
+ "index.js",
18
+ "README.md"
19
+ ]
20
+ }