@mh-gg/host-config 0.1.1-alpha.20260613T085325975Z

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/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@mh-gg/host-config",
3
+ "version": "0.1.1-alpha.20260613T085325975Z",
4
+ "description": "Host CLI and environment config parsing for matterhorn.",
5
+ "type": "commonjs",
6
+ "main": "src/index.cjs",
7
+ "exports": {
8
+ ".": "./src/index.cjs"
9
+ },
10
+ "dependencies": {
11
+ "@mh-gg/room-link": "^0.1.1-alpha.20260613T085325975Z"
12
+ },
13
+ "engines": {
14
+ "node": ">=22.12"
15
+ },
16
+ "scripts": {
17
+ "test": "node --test test/*.test.cjs",
18
+ "coverage": "node --test --experimental-test-coverage --test-coverage-lines=80 --test-coverage-functions=80 --test-coverage-branches=80 --test-coverage-include=src/*.cjs test/*.test.cjs"
19
+ }
20
+ }
package/src/index.cjs ADDED
@@ -0,0 +1,296 @@
1
+ const path = require("node:path");
2
+ const { DEFAULT_APP_URL, slugifyRoom } = require("@mh-gg/room-link");
3
+
4
+ const PROTOCOL = 1;
5
+ const DEFAULT_RELAY_MAX_EVENTS = 10000;
6
+ const DEFAULT_RELAY_MAX_EVENTS_PER_ROOM = 2500;
7
+ const DEFAULT_RELAY_MAX_BYTES_PER_ROOM = 8 * 1024 * 1024;
8
+ const DEFAULT_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW = 240;
9
+ const DEFAULT_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS = 10 * 60;
10
+ const DEFAULT_RELAY_MAX_EVENT_BYTES = 8 * 1024;
11
+ const DEFAULT_RELAY_MAX_FUTURE_SECONDS = 10 * 60;
12
+ const DEFAULT_RELAY_ACTIVE_FANOUT = 4;
13
+ const DEFAULT_RELAY_SFU_MAX_PARTICIPANTS = 16;
14
+ const DEFAULT_RELAY_MAX_HOST_IPC_MESSAGE_BYTES = 256 * 1024;
15
+ const DEFAULT_RELAY_MAX_ROOM_MESSAGE_BYTES = 64 * 1024;
16
+ const DEFAULT_RELAY_MAX_MESH_MESSAGE_BYTES = 128 * 1024;
17
+ const DEFAULT_RELAY_MAX_NOSTR_MESSAGE_BYTES = 64 * 1024;
18
+ const DEFAULT_RELAY_MAX_CALL_SIGNAL_BYTES = 32 * 1024;
19
+ const DEFAULT_RELAY_RATE_WINDOW_MS = 10 * 1000;
20
+ const DEFAULT_RELAY_MAX_UNAUTHENTICATED_MESSAGES_PER_WINDOW = 20;
21
+ const DEFAULT_RELAY_MAX_ROOM_MESSAGES_PER_WINDOW = 120;
22
+ const DEFAULT_RELAY_MAX_MESH_MESSAGES_PER_WINDOW = 240;
23
+ const DEFAULT_RELAY_MAX_HOST_IPC_MESSAGES_PER_WINDOW = 240;
24
+ const DEFAULT_RELAY_HEARTBEAT_MS = 1000;
25
+ const DEFAULT_RELAY_HEARTBEAT_TIMEOUT_MS = 2500;
26
+ const DEFAULT_RELAY_FAILOVER_MS = 3500;
27
+
28
+ function parseRelayPeers(value) {
29
+ return String(value || "")
30
+ .split(",")
31
+ .map((item) => item.trim())
32
+ .filter(Boolean);
33
+ }
34
+
35
+ function parseIceServers(value) {
36
+ return String(value || "")
37
+ .split(",")
38
+ .map((item) => item.trim())
39
+ .filter(Boolean)
40
+ .map((urls) => ({ urls }));
41
+ }
42
+
43
+ function parseBool(value) {
44
+ return ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
45
+ }
46
+
47
+ function defaultHostDataDir() {
48
+ return path.resolve("host", "data");
49
+ }
50
+
51
+ function defaultRelayStoragePath(dataDir) {
52
+ return path.join(dataDir, "relay");
53
+ }
54
+
55
+ function resolveStoragePath(value, fallback) {
56
+ const raw = String(value || fallback || "");
57
+ if (raw === "~") return require("node:os").homedir();
58
+ if (raw.startsWith("~/") || raw.startsWith("~\\")) {
59
+ return path.join(require("node:os").homedir(), raw.slice(2));
60
+ }
61
+ return path.resolve(raw);
62
+ }
63
+
64
+ function parseArgs(argv, options = {}) {
65
+ const env = options.env || process.env;
66
+ const warn = options.warn || console.warn;
67
+ const args = {
68
+ room: env.ROOM || "rooftop-disco",
69
+ title: env.TITLE || "Rooftop Disco",
70
+ date: env.DATE || "2026-06-20",
71
+ time: env.TIME || "8:00 PM",
72
+ location: env.LOCATION || "Brooklyn rooftop",
73
+ description: env.DESCRIPTION || "Bring a friend, a snack, and one song for the shared playlist.",
74
+ appUrl: env.APP_URL || env.HOST_URL || DEFAULT_APP_URL,
75
+ roomSecret: env.ROOM_SECRET || "",
76
+ dataDir: env.MATTERHORN_DATA_DIR || options.defaultDataDir || defaultHostDataDir(),
77
+ theme: env.THEME || "sunset",
78
+ iceServers: env.ICE_SERVERS || "stun:stun.l.google.com:19302",
79
+ relayPeers: parseRelayPeers(env.MATTERHORN_RELAY_PEERS),
80
+ localPeerjs: parseBool(env.MATTERHORN_PEERJS_LOCAL)
81
+ };
82
+
83
+ for (let index = 0; index < argv.length; index += 1) {
84
+ const token = argv[index];
85
+ if (!token.startsWith("--")) continue;
86
+ const key = token.slice(2);
87
+ const value = argv[index + 1];
88
+ switch (key) {
89
+ case "room": args.room = value; index += 1; break;
90
+ case "title": args.title = value; index += 1; break;
91
+ case "date": args.date = value; index += 1; break;
92
+ case "time": args.time = value; index += 1; break;
93
+ case "location": args.location = value; index += 1; break;
94
+ case "description": args.description = value; index += 1; break;
95
+ case "app-url": args.appUrl = value; index += 1; break;
96
+ case "host-url": args.appUrl = value; index += 1; break;
97
+ case "secret": args.roomSecret = value; index += 1; break;
98
+ case "data-dir": args.dataDir = value; index += 1; break;
99
+ case "ice": args.iceServers = value; index += 1; break;
100
+ case "relay-peer": args.relayPeers.push(value); index += 1; break;
101
+ case "local-peerjs":
102
+ case "peerjs-local":
103
+ args.localPeerjs = true;
104
+ break;
105
+ case "no-local-peerjs":
106
+ args.localPeerjs = false;
107
+ break;
108
+ default:
109
+ warn(`Unknown option: ${token}`);
110
+ }
111
+ }
112
+
113
+ args.room = slugifyRoom(args.room);
114
+ args.dataDir = path.resolve(args.dataDir);
115
+ return args;
116
+ }
117
+
118
+ function parseRelayArgs(argv, options = {}) {
119
+ const env = options.env || process.env;
120
+ const warn = options.warn || console.warn;
121
+ const defaultStoragePath = options.defaultStoragePath || defaultRelayStoragePath(options.defaultDataDir || defaultHostDataDir());
122
+ const args = {
123
+ ipcHost: env.MATTERHORN_RELAY_IPC_HOST || options.defaultIpcHost || "127.0.0.1",
124
+ ipcPort: Number(env.MATTERHORN_RELAY_IPC_PORT || options.defaultIpcPort || 42777),
125
+ storagePath: env.MATTERHORN_RELAY_STORAGE || defaultStoragePath,
126
+ relayPeerId: env.MATTERHORN_RELAY_PEER_ID || undefined,
127
+ relayName: env.MATTERHORN_RELAY_NAME || undefined,
128
+ relayPeers: parseRelayPeers(env.MATTERHORN_RELAY_PEERS),
129
+ relayRefs: parseRelayPeers(env.MATTERHORN_RELAY_REFS),
130
+ activeRelayFanout: Number(env.MATTERHORN_RELAY_ACTIVE_FANOUT || DEFAULT_RELAY_ACTIVE_FANOUT),
131
+ relayHeartbeatMs: Number(env.MATTERHORN_RELAY_HEARTBEAT_MS || DEFAULT_RELAY_HEARTBEAT_MS),
132
+ relayHeartbeatTimeoutMs: Number(env.MATTERHORN_RELAY_HEARTBEAT_TIMEOUT_MS || DEFAULT_RELAY_HEARTBEAT_TIMEOUT_MS),
133
+ relayFailoverMs: Number(env.MATTERHORN_RELAY_FAILOVER_MS || DEFAULT_RELAY_FAILOVER_MS),
134
+ maxEvents: Number(env.MATTERHORN_RELAY_MAX_EVENTS || DEFAULT_RELAY_MAX_EVENTS),
135
+ maxEventsPerRoom: Number(env.MATTERHORN_RELAY_MAX_EVENTS_PER_ROOM || DEFAULT_RELAY_MAX_EVENTS_PER_ROOM),
136
+ maxBytesPerRoom: Number(env.MATTERHORN_RELAY_MAX_BYTES_PER_ROOM || DEFAULT_RELAY_MAX_BYTES_PER_ROOM),
137
+ maxEventsPerPubkeyWindow: Number(env.MATTERHORN_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW || DEFAULT_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW),
138
+ pubkeyQuotaWindowSeconds: Number(env.MATTERHORN_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS || DEFAULT_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS),
139
+ maxEventBytes: Number(env.MATTERHORN_RELAY_MAX_EVENT_BYTES || DEFAULT_RELAY_MAX_EVENT_BYTES),
140
+ maxFutureSeconds: Number(env.MATTERHORN_RELAY_MAX_FUTURE_SECONDS || DEFAULT_RELAY_MAX_FUTURE_SECONDS),
141
+ sfuEnabled: parseBool(env.MATTERHORN_RELAY_SFU),
142
+ sfuPeerId: env.MATTERHORN_RELAY_SFU_PEER_ID || undefined,
143
+ sfuMaxParticipants: Number(env.MATTERHORN_RELAY_SFU_MAX_PARTICIPANTS || DEFAULT_RELAY_SFU_MAX_PARTICIPANTS),
144
+ iceServers: env.ICE_SERVERS || "stun:stun.l.google.com:19302",
145
+ localPeerjs: parseBool(env.MATTERHORN_PEERJS_LOCAL)
146
+ };
147
+
148
+ for (let index = 0; index < argv.length; index += 1) {
149
+ const token = argv[index];
150
+ if (!token.startsWith("--")) continue;
151
+ const key = token.slice(2);
152
+ const value = argv[index + 1];
153
+ switch (key) {
154
+ case "host":
155
+ case "ipc-host":
156
+ args.ipcHost = value;
157
+ index += 1;
158
+ break;
159
+ case "port":
160
+ case "ipc-port":
161
+ args.ipcPort = Number(value);
162
+ index += 1;
163
+ break;
164
+ case "storage":
165
+ args.storagePath = value;
166
+ index += 1;
167
+ break;
168
+ case "relay-peer-id":
169
+ args.relayPeerId = value;
170
+ index += 1;
171
+ break;
172
+ case "name":
173
+ case "relay-name":
174
+ args.relayName = value;
175
+ index += 1;
176
+ break;
177
+ case "relay-peer":
178
+ args.relayPeers.push(value);
179
+ index += 1;
180
+ break;
181
+ case "addRelay":
182
+ case "add-relay":
183
+ args.relayRefs.push(value);
184
+ index += 1;
185
+ break;
186
+ case "active-relay-fanout":
187
+ args.activeRelayFanout = Number(value);
188
+ index += 1;
189
+ break;
190
+ case "relay-heartbeat-ms":
191
+ args.relayHeartbeatMs = Number(value);
192
+ index += 1;
193
+ break;
194
+ case "relay-heartbeat-timeout-ms":
195
+ args.relayHeartbeatTimeoutMs = Number(value);
196
+ index += 1;
197
+ break;
198
+ case "relay-failover-ms":
199
+ args.relayFailoverMs = Number(value);
200
+ index += 1;
201
+ break;
202
+ case "max-events":
203
+ args.maxEvents = Number(value);
204
+ index += 1;
205
+ break;
206
+ case "max-events-per-room":
207
+ args.maxEventsPerRoom = Number(value);
208
+ index += 1;
209
+ break;
210
+ case "max-bytes-per-room":
211
+ args.maxBytesPerRoom = Number(value);
212
+ index += 1;
213
+ break;
214
+ case "max-events-per-pubkey-window":
215
+ args.maxEventsPerPubkeyWindow = Number(value);
216
+ index += 1;
217
+ break;
218
+ case "pubkey-quota-window-seconds":
219
+ args.pubkeyQuotaWindowSeconds = Number(value);
220
+ index += 1;
221
+ break;
222
+ case "max-event-bytes":
223
+ args.maxEventBytes = Number(value);
224
+ index += 1;
225
+ break;
226
+ case "max-future-seconds":
227
+ args.maxFutureSeconds = Number(value);
228
+ index += 1;
229
+ break;
230
+ case "sfu":
231
+ args.sfuEnabled = true;
232
+ break;
233
+ case "no-sfu":
234
+ args.sfuEnabled = false;
235
+ break;
236
+ case "sfu-peer-id":
237
+ args.sfuPeerId = value;
238
+ index += 1;
239
+ break;
240
+ case "sfu-max-participants":
241
+ args.sfuMaxParticipants = Number(value);
242
+ index += 1;
243
+ break;
244
+ case "ice":
245
+ args.iceServers = value;
246
+ index += 1;
247
+ break;
248
+ case "local-peerjs":
249
+ case "peerjs-local":
250
+ args.localPeerjs = true;
251
+ break;
252
+ case "no-local-peerjs":
253
+ args.localPeerjs = false;
254
+ break;
255
+ default:
256
+ warn(`Unknown relay option: ${token}`);
257
+ }
258
+ }
259
+
260
+ args.storagePath = resolveStoragePath(args.storagePath, defaultStoragePath);
261
+ return args;
262
+ }
263
+
264
+ module.exports = {
265
+ DEFAULT_RELAY_ACTIVE_FANOUT,
266
+ DEFAULT_RELAY_FAILOVER_MS,
267
+ DEFAULT_RELAY_HEARTBEAT_MS,
268
+ DEFAULT_RELAY_HEARTBEAT_TIMEOUT_MS,
269
+ DEFAULT_RELAY_MAX_CALL_SIGNAL_BYTES,
270
+ DEFAULT_RELAY_MAX_BYTES_PER_ROOM,
271
+ DEFAULT_RELAY_MAX_EVENT_BYTES,
272
+ DEFAULT_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW,
273
+ DEFAULT_RELAY_MAX_EVENTS_PER_ROOM,
274
+ DEFAULT_RELAY_MAX_EVENTS,
275
+ DEFAULT_RELAY_MAX_FUTURE_SECONDS,
276
+ DEFAULT_RELAY_MAX_HOST_IPC_MESSAGE_BYTES,
277
+ DEFAULT_RELAY_MAX_HOST_IPC_MESSAGES_PER_WINDOW,
278
+ DEFAULT_RELAY_MAX_MESH_MESSAGE_BYTES,
279
+ DEFAULT_RELAY_MAX_MESH_MESSAGES_PER_WINDOW,
280
+ DEFAULT_RELAY_MAX_NOSTR_MESSAGE_BYTES,
281
+ DEFAULT_RELAY_MAX_ROOM_MESSAGE_BYTES,
282
+ DEFAULT_RELAY_MAX_ROOM_MESSAGES_PER_WINDOW,
283
+ DEFAULT_RELAY_MAX_UNAUTHENTICATED_MESSAGES_PER_WINDOW,
284
+ DEFAULT_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS,
285
+ DEFAULT_RELAY_RATE_WINDOW_MS,
286
+ DEFAULT_RELAY_SFU_MAX_PARTICIPANTS,
287
+ PROTOCOL,
288
+ defaultHostDataDir,
289
+ defaultRelayStoragePath,
290
+ parseArgs,
291
+ parseBool,
292
+ parseIceServers,
293
+ parseRelayArgs,
294
+ parseRelayPeers,
295
+ resolveStoragePath
296
+ };
@@ -0,0 +1,225 @@
1
+ const assert = require("node:assert/strict");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const test = require("node:test");
5
+ const {
6
+ DEFAULT_RELAY_MAX_CALL_SIGNAL_BYTES,
7
+ DEFAULT_RELAY_MAX_BYTES_PER_ROOM,
8
+ DEFAULT_RELAY_MAX_EVENT_BYTES,
9
+ DEFAULT_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW,
10
+ DEFAULT_RELAY_MAX_EVENTS_PER_ROOM,
11
+ DEFAULT_RELAY_MAX_FUTURE_SECONDS,
12
+ DEFAULT_RELAY_MAX_HOST_IPC_MESSAGE_BYTES,
13
+ DEFAULT_RELAY_MAX_MESH_MESSAGE_BYTES,
14
+ DEFAULT_RELAY_MAX_NOSTR_MESSAGE_BYTES,
15
+ DEFAULT_RELAY_MAX_ROOM_MESSAGE_BYTES,
16
+ DEFAULT_RELAY_MAX_UNAUTHENTICATED_MESSAGES_PER_WINDOW,
17
+ DEFAULT_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS,
18
+ DEFAULT_RELAY_RATE_WINDOW_MS,
19
+ parseArgs,
20
+ parseIceServers,
21
+ parseRelayArgs,
22
+ parseRelayPeers
23
+ } = require("..");
24
+
25
+ test("parses host args with normalized room and data dir", () => {
26
+ const warnings = [];
27
+ const args = parseArgs([
28
+ "--room", " Rooftop Party!! ",
29
+ "--data-dir", path.join(os.tmpdir(), "matterhorn-data"),
30
+ "--relay-peer", "peerjs:first",
31
+ "--relay-peer", "peerjs:second",
32
+ "--secret", "cli-secret",
33
+ "--unknown", "value"
34
+ ], {
35
+ env: {},
36
+ warn: (message) => warnings.push(message)
37
+ });
38
+
39
+ assert.equal(args.room, "rooftop-party");
40
+ assert.equal(args.roomSecret, "cli-secret");
41
+ assert.deepEqual(args.relayPeers, ["peerjs:first", "peerjs:second"]);
42
+ assert.equal(args.dataDir, path.resolve(os.tmpdir(), "matterhorn-data"));
43
+ assert.deepEqual(warnings, ["Unknown option: --unknown"]);
44
+ });
45
+
46
+ test("reads environment defaults and resolves the configured default data dir", () => {
47
+ const args = parseArgs([], {
48
+ defaultDataDir: path.join(os.tmpdir(), "default-matterhorn-data"),
49
+ env: {
50
+ ROOM: "env-room",
51
+ MATTERHORN_RELAY_PEERS: " peerjs:a, ,peerjs:b ",
52
+ ROOM_SECRET: "env-secret"
53
+ }
54
+ });
55
+
56
+ assert.equal(args.room, "env-room");
57
+ assert.equal(args.roomSecret, "env-secret");
58
+ assert.deepEqual(args.relayPeers, ["peerjs:a", "peerjs:b"]);
59
+ assert.equal(args.dataDir, path.resolve(os.tmpdir(), "default-matterhorn-data"));
60
+ });
61
+
62
+ test("command line values override env defaults", () => {
63
+ const args = parseArgs([
64
+ "ignored-position",
65
+ "--title", "CLI Title",
66
+ "--date", "2026-07-01",
67
+ "--time", "9:00 PM",
68
+ "--location", "CLI Venue",
69
+ "--description", "CLI Description",
70
+ "--app-url", "https://app-url.test/",
71
+ "--host-url", "https://example.test/",
72
+ "--ice", "stun:test"
73
+ ], {
74
+ env: {
75
+ APP_URL: "https://env.test/",
76
+ ICE_SERVERS: "stun:env"
77
+ }
78
+ });
79
+
80
+ assert.equal(args.title, "CLI Title");
81
+ assert.equal(args.date, "2026-07-01");
82
+ assert.equal(args.time, "9:00 PM");
83
+ assert.equal(args.location, "CLI Venue");
84
+ assert.equal(args.description, "CLI Description");
85
+ assert.equal(args.appUrl, "https://example.test/");
86
+ assert.equal(args.iceServers, "stun:test");
87
+ });
88
+
89
+ test("parseRelayPeers trims empty entries", () => {
90
+ assert.deepEqual(parseRelayPeers(" peerjs:a, ,peerjs:b "), ["peerjs:a", "peerjs:b"]);
91
+ });
92
+
93
+ test("parseRelayPeers preserves endpoint-aware PeerJS relay addresses", () => {
94
+ assert.deepEqual(
95
+ parseRelayPeers(" peerjs:a-relay@ws://127.0.0.1:9000/peerjs, peerjs:b-relay@wss://peer.yage.games:443/ "),
96
+ ["peerjs:a-relay@ws://127.0.0.1:9000/peerjs", "peerjs:b-relay@wss://peer.yage.games:443/"]
97
+ );
98
+ });
99
+
100
+ test("parses relay args with SFU and storage settings", () => {
101
+ const warnings = [];
102
+ const args = parseRelayArgs([
103
+ "--host", "0.0.0.0",
104
+ "--port", "1234",
105
+ "--storage", "~/.matterhorn/relay",
106
+ "--name", "relay-a",
107
+ "--relay-peer-id", "matterhorn-room",
108
+ "--relay-peer", "peerjs:relay-a",
109
+ "--addRelay", "matterhorn-relay:token",
110
+ "--add-relay", "peerjs:relay-b",
111
+ "--active-relay-fanout", "6",
112
+ "--max-events", "25",
113
+ "--max-events-per-room", "10",
114
+ "--max-bytes-per-room", "40960",
115
+ "--max-events-per-pubkey-window", "7",
116
+ "--pubkey-quota-window-seconds", "30",
117
+ "--max-event-bytes", "4096",
118
+ "--max-future-seconds", "120",
119
+ "--sfu",
120
+ "--sfu-peer-id", "matterhorn-room-sfu",
121
+ "--sfu-max-participants", "24",
122
+ "--ice", "stun:test",
123
+ "--unknown"
124
+ ], {
125
+ env: {},
126
+ defaultIpcHost: "127.0.0.1",
127
+ defaultIpcPort: 42777,
128
+ defaultStoragePath: path.join(os.tmpdir(), "relay-default"),
129
+ warn: (message) => warnings.push(message)
130
+ });
131
+
132
+ assert.equal(args.ipcHost, "0.0.0.0");
133
+ assert.equal(args.ipcPort, 1234);
134
+ assert.equal(args.storagePath, path.join(os.homedir(), ".matterhorn", "relay"));
135
+ assert.equal(args.relayName, "relay-a");
136
+ assert.equal(args.relayPeerId, "matterhorn-room");
137
+ assert.deepEqual(args.relayPeers, ["peerjs:relay-a"]);
138
+ assert.deepEqual(args.relayRefs, ["matterhorn-relay:token", "peerjs:relay-b"]);
139
+ assert.equal(args.activeRelayFanout, 6);
140
+ assert.equal(args.maxEvents, 25);
141
+ assert.equal(args.maxEventsPerRoom, 10);
142
+ assert.equal(args.maxBytesPerRoom, 40960);
143
+ assert.equal(args.maxEventsPerPubkeyWindow, 7);
144
+ assert.equal(args.pubkeyQuotaWindowSeconds, 30);
145
+ assert.equal(args.maxEventBytes, 4096);
146
+ assert.equal(args.maxFutureSeconds, 120);
147
+ assert.equal(args.sfuEnabled, true);
148
+ assert.equal(args.sfuPeerId, "matterhorn-room-sfu");
149
+ assert.equal(args.sfuMaxParticipants, 24);
150
+ assert.equal(args.iceServers, "stun:test");
151
+ assert.deepEqual(warnings, ["Unknown relay option: --unknown"]);
152
+ });
153
+
154
+ test("relay args read environment defaults and no-sfu override", () => {
155
+ const args = parseRelayArgs(["--no-sfu"], {
156
+ defaultStoragePath: path.join(os.tmpdir(), "relay-default"),
157
+ env: {
158
+ MATTERHORN_RELAY_IPC_HOST: "127.0.0.2",
159
+ MATTERHORN_RELAY_IPC_PORT: "5555",
160
+ MATTERHORN_RELAY_STORAGE: "relay-store",
161
+ MATTERHORN_RELAY_NAME: "relay-name",
162
+ MATTERHORN_RELAY_PEER_ID: "relay-peer",
163
+ MATTERHORN_RELAY_PEERS: " peerjs:a, ,peerjs:b ",
164
+ MATTERHORN_RELAY_REFS: " relay-a, ,matterhorn-relay:token ",
165
+ MATTERHORN_RELAY_ACTIVE_FANOUT: "5",
166
+ MATTERHORN_RELAY_MAX_EVENTS: "50",
167
+ MATTERHORN_RELAY_MAX_EVENTS_PER_ROOM: "20",
168
+ MATTERHORN_RELAY_MAX_BYTES_PER_ROOM: "81920",
169
+ MATTERHORN_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW: "9",
170
+ MATTERHORN_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS: "45",
171
+ MATTERHORN_RELAY_MAX_EVENT_BYTES: "2048",
172
+ MATTERHORN_RELAY_MAX_FUTURE_SECONDS: "30",
173
+ MATTERHORN_RELAY_SFU: "1",
174
+ MATTERHORN_RELAY_SFU_PEER_ID: "relay-sfu",
175
+ MATTERHORN_RELAY_SFU_MAX_PARTICIPANTS: "12",
176
+ ICE_SERVERS: "stun:env"
177
+ }
178
+ });
179
+
180
+ assert.equal(args.ipcHost, "127.0.0.2");
181
+ assert.equal(args.ipcPort, 5555);
182
+ assert.equal(args.storagePath, path.resolve("relay-store"));
183
+ assert.equal(args.relayName, "relay-name");
184
+ assert.equal(args.relayPeerId, "relay-peer");
185
+ assert.deepEqual(args.relayPeers, ["peerjs:a", "peerjs:b"]);
186
+ assert.deepEqual(args.relayRefs, ["relay-a", "matterhorn-relay:token"]);
187
+ assert.equal(args.activeRelayFanout, 5);
188
+ assert.equal(args.maxEvents, 50);
189
+ assert.equal(args.maxEventsPerRoom, 20);
190
+ assert.equal(args.maxBytesPerRoom, 81920);
191
+ assert.equal(args.maxEventsPerPubkeyWindow, 9);
192
+ assert.equal(args.pubkeyQuotaWindowSeconds, 45);
193
+ assert.equal(args.maxEventBytes, 2048);
194
+ assert.equal(args.maxFutureSeconds, 30);
195
+ assert.equal(args.sfuEnabled, false);
196
+ assert.equal(args.sfuPeerId, "relay-sfu");
197
+ assert.equal(args.sfuMaxParticipants, 12);
198
+ });
199
+
200
+ test("relay security bounds have explicit defaults", () => {
201
+ const args = parseRelayArgs([], { env: {}, defaultStoragePath: path.join(os.tmpdir(), "relay-default") });
202
+ assert.equal(DEFAULT_RELAY_MAX_EVENTS_PER_ROOM, 2500);
203
+ assert.equal(DEFAULT_RELAY_MAX_BYTES_PER_ROOM, 8 * 1024 * 1024);
204
+ assert.equal(DEFAULT_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW, 240);
205
+ assert.equal(DEFAULT_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS, 10 * 60);
206
+ assert.equal(DEFAULT_RELAY_MAX_EVENT_BYTES, 8 * 1024);
207
+ assert.equal(DEFAULT_RELAY_MAX_FUTURE_SECONDS, 10 * 60);
208
+ assert.equal(DEFAULT_RELAY_MAX_HOST_IPC_MESSAGE_BYTES, 256 * 1024);
209
+ assert.equal(DEFAULT_RELAY_MAX_ROOM_MESSAGE_BYTES, 64 * 1024);
210
+ assert.equal(DEFAULT_RELAY_MAX_MESH_MESSAGE_BYTES, 128 * 1024);
211
+ assert.equal(DEFAULT_RELAY_MAX_NOSTR_MESSAGE_BYTES, 64 * 1024);
212
+ assert.equal(DEFAULT_RELAY_MAX_CALL_SIGNAL_BYTES, 32 * 1024);
213
+ assert.equal(DEFAULT_RELAY_RATE_WINDOW_MS, 10 * 1000);
214
+ assert.equal(DEFAULT_RELAY_MAX_UNAUTHENTICATED_MESSAGES_PER_WINDOW, 20);
215
+ assert.equal(args.maxEventsPerRoom, DEFAULT_RELAY_MAX_EVENTS_PER_ROOM);
216
+ assert.equal(args.maxBytesPerRoom, DEFAULT_RELAY_MAX_BYTES_PER_ROOM);
217
+ assert.equal(args.maxEventsPerPubkeyWindow, DEFAULT_RELAY_MAX_EVENTS_PER_PUBKEY_WINDOW);
218
+ assert.equal(args.pubkeyQuotaWindowSeconds, DEFAULT_RELAY_PUBKEY_QUOTA_WINDOW_SECONDS);
219
+ assert.equal(args.maxEventBytes, DEFAULT_RELAY_MAX_EVENT_BYTES);
220
+ assert.equal(args.maxFutureSeconds, DEFAULT_RELAY_MAX_FUTURE_SECONDS);
221
+ });
222
+
223
+ test("parseIceServers returns RTC server objects", () => {
224
+ assert.deepEqual(parseIceServers(" stun:a, ,turn:b "), [{ urls: "stun:a" }, { urls: "turn:b" }]);
225
+ });