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 +16 -3
- package/package.json +28 -8
- package/src/args.test.ts +18 -0
- package/src/args.ts +5 -1
- package/src/cli.ts +3 -1
- package/src/dev.ts +15 -4
- package/src/room-info.test.ts +21 -0
- package/src/room-info.ts +7 -3
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
|
|
46
|
-
--info-port <n> room-info port
|
|
47
|
-
--wt-host <host> WebTransport
|
|
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.
|
|
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.
|
|
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.
|
|
24
|
-
"@siltrun/room-server-darwin-x64": "0.
|
|
25
|
-
"@siltrun/room-server-linux-arm64": "0.
|
|
26
|
-
"@siltrun/room-server-linux-x64": "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
|
|
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
|
-
|
|
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 => ({
|
|
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.
|
|
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
|
-
|
|
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(
|
package/src/room-info.test.ts
CHANGED
|
@@ -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
|
-
|
|
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 });
|