siltrun 0.1.1 → 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/README.md CHANGED
@@ -42,12 +42,25 @@ restart (250ms → 5s cap), clearly logged, so a transient failure never ends yo
42
42
 
43
43
  ```
44
44
  siltrun dev <contract.ts> [options]
45
- --wt-port <n> WebTransport port (default 4433, env SILT_WT_PORT)
46
- --info-port <n> room-info port (default 4000, env SILT_INFO_PORT)
47
- --wt-host <host> WebTransport bind host (default 127.0.0.1)
45
+ --wt-port <n> WebTransport port (default 4433, env SILT_WT_PORT)
46
+ --info-port <n> room-info port (default 4000, env SILT_INFO_PORT)
47
+ --wt-host <host> advertised WebTransport host (default 127.0.0.1, env SILT_WT_HOST)
48
+ --wt-bind <addr> WebTransport bind address (default: --wt-host, env SILT_WT_BIND)
48
49
  --no-doctor skip the determinism check on (re)load
49
50
  ```
50
51
 
52
+ `--wt-host` is what the room *advertises*; `--wt-bind` is the address it *listens* on.
53
+ They are the same by default. To reach the room from a second device, bind wide and let
54
+ discovery answer for itself:
55
+
56
+ ```bash
57
+ npx siltrun dev room.ts --wt-bind 0.0.0.0
58
+ ```
59
+
60
+ room-info binds wherever WebTransport binds — a device that can reach the room can also
61
+ discover it — and the advertised `wtEndpoint` follows the host each request arrived on, so
62
+ a loopback fetch learns a loopback endpoint and a LAN or tailnet fetch learns that one.
63
+
51
64
  Path overrides (mainly for tests and for running against a local checkout):
52
65
  `SILT_ROOM_SERVER_DIR`, `SILT_ROOM_HOST_ENTRY`, `SILT_DOCTOR_ENTRY`.
53
66
 
package/package.json CHANGED
@@ -1,8 +1,29 @@
1
1
  {
2
2
  "name": "siltrun",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "The Silt CLI — run an authoritative multiplayer room locally with `siltrun dev`.",
6
+ "keywords": [
7
+ "multiplayer",
8
+ "webtransport",
9
+ "realtime",
10
+ "game-server",
11
+ "silt",
12
+ "cli",
13
+ "dev-server",
14
+ "netcode",
15
+ "gamedev",
16
+ "authoritative-server"
17
+ ],
18
+ "homepage": "https://silt.run",
19
+ "bugs": {
20
+ "url": "https://github.com/siltrun/silt/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/siltrun/silt.git",
25
+ "directory": "packages/cli"
26
+ },
6
27
  "type": "module",
7
28
  "bin": {
8
29
  "siltrun": "bin/silt.mjs"
@@ -16,14 +37,14 @@
16
37
  "typecheck": "tsc --noEmit"
17
38
  },
18
39
  "dependencies": {
19
- "@siltrun/room-host": "0.1.0",
40
+ "@siltrun/room-host": "0.2.0",
20
41
  "bun": "^1.3.0"
21
42
  },
22
43
  "optionalDependencies": {
23
- "@siltrun/room-server-darwin-arm64": "0.1.0",
24
- "@siltrun/room-server-darwin-x64": "0.1.0",
25
- "@siltrun/room-server-linux-arm64": "0.1.0",
26
- "@siltrun/room-server-linux-x64": "0.1.0"
44
+ "@siltrun/room-server-darwin-arm64": "0.2.0",
45
+ "@siltrun/room-server-darwin-x64": "0.2.0",
46
+ "@siltrun/room-server-linux-arm64": "0.2.0",
47
+ "@siltrun/room-server-linux-x64": "0.2.0"
27
48
  },
28
49
  "engines": {
29
50
  "node": ">=18"
@@ -31,6 +52,5 @@
31
52
  "devDependencies": {
32
53
  "@types/bun": "^1.2.0",
33
54
  "typescript": "^5.6.0"
34
- },
35
- "homepage": "https://silt.run"
55
+ }
36
56
  }
package/src/args.test.ts CHANGED
@@ -91,3 +91,21 @@ describe("deriveRoomName", () => {
91
91
  expect(deriveRoomName("examples/„…“/⚡.ts")).toBe("room");
92
92
  });
93
93
  });
94
+
95
+ describe("wt-bind", () => {
96
+ const noEnv = {};
97
+ test("defaults to wtHost", () => {
98
+ const o = parseDevArgs(["room.ts", "--wt-host", "100.64.0.5"], noEnv);
99
+ expect(o.wtBind).toBe("100.64.0.5");
100
+ });
101
+ test("splits bind from advertised host", () => {
102
+ const o = parseDevArgs(["room.ts", "--wt-host", "100.64.0.5", "--wt-bind", "0.0.0.0"], noEnv);
103
+ expect(o.wtBind).toBe("0.0.0.0");
104
+ expect(o.wtHost).toBe("100.64.0.5");
105
+ });
106
+ test("reads SILT_WT_BIND from env", () => {
107
+ const o = parseDevArgs(["room.ts"], { SILT_WT_BIND: "0.0.0.0" });
108
+ expect(o.wtBind).toBe("0.0.0.0");
109
+ expect(o.wtHost).toBe("127.0.0.1");
110
+ });
111
+ });
package/src/args.ts CHANGED
@@ -9,6 +9,9 @@ export interface DevOptions {
9
9
  wtPort: number;
10
10
  infoPort: number;
11
11
  wtHost: string;
12
+ /** WT bind address; defaults to wtHost. Split so a host can bind 0.0.0.0 (serve
13
+ * loopback AND a LAN/tailnet interface) while advertising one reachable host. */
14
+ wtBind: string;
12
15
  doctor: boolean;
13
16
  }
14
17
 
@@ -105,6 +108,7 @@ export function parseDevArgs(argv: string[], env: Record<string, string | undefi
105
108
  }
106
109
 
107
110
  const wtHost = (flags.get("wt-host") as string) || env.SILT_WT_HOST || "127.0.0.1";
111
+ const wtBind = (flags.get("wt-bind") as string) || env.SILT_WT_BIND || wtHost;
108
112
  const wtPort = intOpt((flags.get("wt-port") as string) ?? env.SILT_WT_PORT, "--wt-port", 4433);
109
113
  const infoPort = intOpt(
110
114
  (flags.get("info-port") as string) ?? env.SILT_INFO_PORT,
@@ -113,5 +117,5 @@ export function parseDevArgs(argv: string[], env: Record<string, string | undefi
113
117
  );
114
118
  const doctor = flags.get("no-doctor") !== true;
115
119
 
116
- return { contract, room, wtPort, infoPort, wtHost, doctor };
120
+ return { contract, room, wtPort, infoPort, wtHost, wtBind, doctor };
117
121
  }
package/src/cli.ts CHANGED
@@ -26,7 +26,9 @@ ${paint.bold("dev options")}
26
26
  generic like room.ts/index.ts; env SILT_ROOM)
27
27
  --wt-port <n> WebTransport port (default: 4433, env SILT_WT_PORT)
28
28
  --info-port <n> room-info port (default: 4000, env SILT_INFO_PORT)
29
- --wt-host <host> WebTransport bind host (default: 127.0.0.1)
29
+ --wt-host <host> advertised WebTransport + room-info host (default: 127.0.0.1)
30
+ --wt-bind <addr> WT bind address (default: --wt-host; use 0.0.0.0 to serve
31
+ loopback and a LAN/tailnet interface at once)
30
32
  --no-doctor skip the determinism check on (re)load
31
33
 
32
34
  ${paint.bold("deploy options")}
package/src/dev.ts CHANGED
@@ -93,9 +93,16 @@ export async function dev(opts: DevOptions): Promise<void> {
93
93
  // room-info state — cert refreshes on each (re)boot. The room name (SEAM §4:
94
94
  // /room/<name>) comes from --room, or is derived from the contract path (args.ts);
95
95
  // the server serves exactly this path and loudly rejects anything else.
96
- const wtEndpoint = `https://${opts.wtHost}:${opts.wtPort}/room/${opts.room}`;
96
+ // The advertised WT host follows the host the info request arrived on (a loopback
97
+ // fetch gets a loopback endpoint, a tailnet fetch the tailnet one — cert pinning
98
+ // makes the hostname interchangeable); --wt-host is the fallback for callers with
99
+ // no request in hand (boot banner).
97
100
  let certHash: string | null = null;
98
- const getInfo = (): RoomInfo => ({ wtEndpoint, certHash, mode });
101
+ const getInfo = (requestHost?: string): RoomInfo => ({
102
+ wtEndpoint: `https://${requestHost ?? opts.wtHost}:${opts.wtPort}/room/${opts.room}`,
103
+ certHash,
104
+ mode,
105
+ });
99
106
 
100
107
  // 4. supervise the room-server.
101
108
  // The server treats a non-empty --contract as "enable compute mode"; empty = relay.
@@ -107,7 +114,7 @@ export async function dev(opts: DevOptions): Promise<void> {
107
114
  cwd: existsSync(paths.roomServerDir) ? paths.roomServerDir : undefined,
108
115
  // SILT_BUN: the server spawns the Bun room-host itself (compute.go) — hand it the
109
116
  // exact runtime running this CLI so it never depends on `bun` being on PATH.
110
- env: { WT_BIND: `${opts.wtHost}:${opts.wtPort}`, SILT_BUN: bunBin() },
117
+ env: { WT_BIND: `${opts.wtBind}:${opts.wtPort}`, SILT_BUN: bunBin() },
111
118
  onCert: (h) => {
112
119
  certHash = h;
113
120
  },
@@ -134,7 +141,11 @@ export async function dev(opts: DevOptions): Promise<void> {
134
141
  // 5. room-info endpoint
135
142
  let roomInfo: RoomInfoServer;
136
143
  try {
137
- roomInfo = startRoomInfoServer({ port: opts.infoPort, hostname: "127.0.0.1", getInfo });
144
+ // Bind room-info wherever WebTransport binds: a device that can reach the room
145
+ // must also be able to discover it (the /.well-known/silt fetch carries the cert
146
+ // hash), and with --wt-bind 0.0.0.0 that includes loopback AND the external
147
+ // interface. Default flags keep this loopback-only, exactly as before.
148
+ roomInfo = startRoomInfoServer({ port: opts.infoPort, hostname: opts.wtBind, getInfo });
138
149
  } catch (e) {
139
150
  log.error(`could not bind room-info on :${opts.infoPort}: ${String(e)}`);
140
151
  log.plain(
@@ -78,3 +78,24 @@ describe("room-info endpoint (SEAM §4)", () => {
78
78
  expect(res.status).toBe(404);
79
79
  });
80
80
  });
81
+
82
+ describe("request-host advertisement", () => {
83
+ test("getInfo receives the hostname the request arrived on", async () => {
84
+ let seen: string | undefined = "unset" as string | undefined;
85
+ const s = startRoomInfoServer({
86
+ port: 0,
87
+ getInfo: (requestHost) => {
88
+ seen = requestHost;
89
+ return { wtEndpoint: `https://${requestHost}:4433/room/x`, certHash: null, mode: "compute" };
90
+ },
91
+ });
92
+ try {
93
+ const res = await fetch(`http://127.0.0.1:${s.port}${WELL_KNOWN_PATH}`);
94
+ const body = (await res.json()) as { wtEndpoint: string };
95
+ expect(seen).toBe("127.0.0.1");
96
+ expect(body.wtEndpoint).toBe("https://127.0.0.1:4433/room/x");
97
+ } finally {
98
+ s.stop();
99
+ }
100
+ });
101
+ });
package/src/room-info.ts CHANGED
@@ -31,12 +31,15 @@ export interface RoomInfoServer {
31
31
 
32
32
  /**
33
33
  * Serve room-info on `port`. `getInfo` is called per request so the served cert hash
34
- * tracks the live room-server across restarts (the cert changes each boot).
34
+ * tracks the live room-server across restarts (the cert changes each boot). It receives
35
+ * the hostname the request arrived on, so a multi-homed dev server (--wt-bind 0.0.0.0)
36
+ * can advertise a WT endpoint the asking client can actually reach — a loopback fetch
37
+ * learns a loopback endpoint, a tailnet fetch learns the tailnet one.
35
38
  */
36
39
  export function startRoomInfoServer(opts: {
37
40
  port: number;
38
41
  hostname?: string;
39
- getInfo: () => RoomInfo;
42
+ getInfo: (requestHost?: string) => RoomInfo;
40
43
  }): RoomInfoServer {
41
44
  const hostname = opts.hostname ?? "127.0.0.1";
42
45
  const server = Bun.serve({
@@ -53,7 +56,8 @@ export function startRoomInfoServer(opts: {
53
56
  if (req.method !== "GET") {
54
57
  return new Response("method not allowed", { status: 405, headers: CORS });
55
58
  }
56
- return Response.json(opts.getInfo(), { headers: CORS });
59
+ const requestHost = new URL(req.url).hostname || undefined;
60
+ return Response.json(opts.getInfo(requestHost), { headers: CORS });
57
61
  }
58
62
 
59
63
  return new Response("not found", { status: 404, headers: CORS });