@stim-cli/transport-local 0.0.0 → 1.29.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/LICENSE +21 -0
- package/README.md +13 -1
- package/dist/index.d.mts +37 -0
- package/dist/index.mjs +96 -0
- package/package.json +27 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Janic Duplessis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
# @stim-cli/transport-local
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The local network transport for stim-server. It listens on the private IPv4 addresses the machine setting
|
|
4
|
+
`server.lanAddresses` names, each an address or a network interface name such as `en0`, so machines and phones on the
|
|
5
|
+
same network reach stim-server without Tailscale. It implements the [`@stim-cli/transport`](../transport/README.md)
|
|
6
|
+
contract.
|
|
7
|
+
|
|
8
|
+
- **Off by default.** With `server.lanAddresses` empty, stim-server listens on no local network address. Wildcard
|
|
9
|
+
(`0.0.0.0`), loopback, public and IPv6 addresses are refused.
|
|
10
|
+
- **Plain WebSocket, no TLS.** Every connection runs stim-server's protocol 2 Noise handshake before any request, which
|
|
11
|
+
authenticates both sides by their keys and encrypts the connection. A network address never identifies a client.
|
|
12
|
+
- **Live.** The setting and the interfaces are read again every 5 seconds, so a changed setting or a new DHCP lease
|
|
13
|
+
applies without a restart.
|
|
14
|
+
|
|
15
|
+
stim-server depends on it and loads it at start.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NetworkInterfaceInfo } from "node:os";
|
|
2
|
+
import { TRANSPORT_API_VERSION, Transport, TransportInput } from "@stim-cli/transport";
|
|
3
|
+
//#region index.d.ts
|
|
4
|
+
declare const apiVersion: typeof TRANSPORT_API_VERSION;
|
|
5
|
+
declare const id = "local";
|
|
6
|
+
type Interfaces = NodeJS.Dict<NetworkInterfaceInfo[]>;
|
|
7
|
+
/** What `server.lanAddresses` names now: the addresses to listen on, and why each other entry is left out. */
|
|
8
|
+
interface LanAddresses {
|
|
9
|
+
addresses: string[];
|
|
10
|
+
problems: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The private IPv4 addresses that `entries` name on this machine's interfaces now. An entry is an IPv4 address or an
|
|
14
|
+
* interface name such as `en0`; an interface contributes each private IPv4 address it has. Wildcard, loopback, public
|
|
15
|
+
* and IPv6 addresses are refused, and an address no interface holds now is left out until one does.
|
|
16
|
+
*/
|
|
17
|
+
declare function resolveLanAddresses(entries: readonly string[], interfaces: Interfaces): LanAddresses;
|
|
18
|
+
/** The local network transport, plus the reasons stim-server cannot listen on some of the entries it was given. */
|
|
19
|
+
interface LocalTransport extends Transport {
|
|
20
|
+
id: typeof id;
|
|
21
|
+
lan(): LanAddresses;
|
|
22
|
+
bindAddresses(): string[];
|
|
23
|
+
watch(listener: () => void): () => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Listens on the local network addresses `entries` names, read again on every poll so a changed setting or a new
|
|
27
|
+
* DHCP lease applies without a restart. It carries plain WebSocket: stim-server's Noise handshake authenticates every
|
|
28
|
+
* client by its key and encrypts the connection, so the transport needs no TLS and never identifies anyone.
|
|
29
|
+
*/
|
|
30
|
+
declare function localTransport({ entries, interfaces, pollMs }: {
|
|
31
|
+
entries: () => readonly string[];
|
|
32
|
+
interfaces?: () => Interfaces;
|
|
33
|
+
pollMs?: number;
|
|
34
|
+
}): LocalTransport;
|
|
35
|
+
declare function createTransport({ options }: TransportInput): LocalTransport;
|
|
36
|
+
//#endregion
|
|
37
|
+
export { LanAddresses, LocalTransport, apiVersion, createTransport, id, localTransport, resolveLanAddresses };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isIPv4 } from "node:net";
|
|
2
|
+
import { networkInterfaces } from "node:os";
|
|
3
|
+
import { TRANSPORT_API_VERSION } from "@stim-cli/transport";
|
|
4
|
+
//#region index.ts
|
|
5
|
+
const apiVersion = TRANSPORT_API_VERSION;
|
|
6
|
+
const id = "local";
|
|
7
|
+
const POLL_MS = 5e3;
|
|
8
|
+
/** RFC 1918 and link-local IPv4 addresses: a local network, never the public internet or a loopback. */
|
|
9
|
+
function isPrivateIPv4(address) {
|
|
10
|
+
const [a = 0, b = 0] = address.split(".").map(Number);
|
|
11
|
+
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The private IPv4 addresses that `entries` name on this machine's interfaces now. An entry is an IPv4 address or an
|
|
15
|
+
* interface name such as `en0`; an interface contributes each private IPv4 address it has. Wildcard, loopback, public
|
|
16
|
+
* and IPv6 addresses are refused, and an address no interface holds now is left out until one does.
|
|
17
|
+
*/
|
|
18
|
+
function resolveLanAddresses(entries, interfaces) {
|
|
19
|
+
const held = new Set(Object.values(interfaces).flatMap((each) => (each ?? []).filter((info) => info.family === "IPv4")));
|
|
20
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
21
|
+
const problems = [];
|
|
22
|
+
for (const raw of entries) {
|
|
23
|
+
const entry = raw.trim();
|
|
24
|
+
if (isIPv4(entry)) {
|
|
25
|
+
if (!isPrivateIPv4(entry)) problems.push(`${entry} is not a private local network address (10/8, 172.16/12, 192.168/16 or 169.254/16)`);
|
|
26
|
+
else if (![...held].some((info) => info.address === entry)) problems.push(`no network interface has ${entry} now`);
|
|
27
|
+
else addresses.add(entry);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const named = interfaces[entry];
|
|
31
|
+
if (!/^[A-Za-z][A-Za-z0-9._-]*$/.test(entry)) problems.push(`${entry} is neither an IPv4 address nor a network interface name`);
|
|
32
|
+
else if (!named) problems.push(`no network interface is named ${entry} now`);
|
|
33
|
+
else {
|
|
34
|
+
const found = named.filter((info) => info.family === "IPv4" && !info.internal && isPrivateIPv4(info.address));
|
|
35
|
+
if (!found.length) problems.push(`${entry} has no private IPv4 address now`);
|
|
36
|
+
for (const info of found) addresses.add(info.address);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
addresses: [...addresses],
|
|
41
|
+
problems
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Listens on the local network addresses `entries` names, read again on every poll so a changed setting or a new
|
|
46
|
+
* DHCP lease applies without a restart. It carries plain WebSocket: stim-server's Noise handshake authenticates every
|
|
47
|
+
* client by its key and encrypts the connection, so the transport needs no TLS and never identifies anyone.
|
|
48
|
+
*/
|
|
49
|
+
function localTransport({ entries, interfaces = networkInterfaces, pollMs = POLL_MS }) {
|
|
50
|
+
const read = () => {
|
|
51
|
+
try {
|
|
52
|
+
return resolveLanAddresses(entries(), interfaces());
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return {
|
|
55
|
+
addresses: [],
|
|
56
|
+
problems: [`could not read the local network addresses: ${error instanceof Error ? error.message : String(error)}`]
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const timers = /* @__PURE__ */ new Set();
|
|
61
|
+
return {
|
|
62
|
+
id,
|
|
63
|
+
traits: {
|
|
64
|
+
privateNetwork: false,
|
|
65
|
+
tls: "none",
|
|
66
|
+
peerIdentity: false
|
|
67
|
+
},
|
|
68
|
+
lan: read,
|
|
69
|
+
bindAddresses: () => read().addresses,
|
|
70
|
+
watch(listener) {
|
|
71
|
+
let last = JSON.stringify(read());
|
|
72
|
+
const timer = setInterval(() => {
|
|
73
|
+
const next = JSON.stringify(read());
|
|
74
|
+
if (next === last) return;
|
|
75
|
+
last = next;
|
|
76
|
+
listener();
|
|
77
|
+
}, pollMs);
|
|
78
|
+
timer.unref();
|
|
79
|
+
timers.add(timer);
|
|
80
|
+
return () => {
|
|
81
|
+
clearInterval(timer);
|
|
82
|
+
timers.delete(timer);
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
close() {
|
|
86
|
+
for (const timer of timers) clearInterval(timer);
|
|
87
|
+
timers.clear();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function createTransport({ options }) {
|
|
92
|
+
const addresses = (Array.isArray(options.addresses) ? options.addresses : []).filter((entry) => typeof entry === "string");
|
|
93
|
+
return localTransport({ entries: () => addresses });
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { apiVersion, createTransport, id, localTransport, resolveLanAddresses };
|
package/package.json
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/transport-local",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.29.0",
|
|
4
|
+
"description": "Local network transport for stim-server: plain WebSocket on chosen private addresses, protected by Stim's Noise session.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/appandflow/stim.git",
|
|
9
9
|
"directory": "packages/transport-local"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "dist/index.mjs",
|
|
18
|
+
"types": "dist/index.d.mts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.mts",
|
|
22
|
+
"module-sync": "./dist/index.mjs",
|
|
23
|
+
"default": "./dist/index.mjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@stim-cli/transport": "1.29.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.12.0"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsdown"
|
|
10
34
|
}
|
|
11
|
-
}
|
|
35
|
+
}
|