@stim-cli/transport-local 0.0.0-stage → 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 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
- # Temporary Holding Version
1
+ # @stim-cli/transport-local
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
@@ -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,6 +1,35 @@
1
1
  {
2
2
  "name": "@stim-cli/transport-local",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/appandflow/stim.git",
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"
34
+ }
6
35
  }