@workerdeck/relay 3.3.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 Tobias Strebitzer
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 ADDED
@@ -0,0 +1,39 @@
1
+ # @workerdeck/relay
2
+
3
+ The WorkerDeck relay: one always-on process every gateway dials out to. It holds the cross-gateway
4
+ session registry, authenticates enrolled gateways, enforces the visibility rules, and routes peer
5
+ `peek` and `send` requests between gateways, so agents on separate machines can work together.
6
+
7
+ Part of [WorkerDeck](https://github.com/workerdeck/workerdeck). The `workerdeck` CLI runs it too, as
8
+ `workerdeck relay`.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install -g @workerdeck/relay
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```bash
19
+ workerdeck-relay enroll mac-mini # prints the gateway key once
20
+ workerdeck-relay serve --host 0.0.0.0 # plain ws://, or add --tls-cert/--tls-key
21
+ workerdeck-relay status # who is online
22
+ workerdeck-relay revoke mac-mini
23
+ ```
24
+
25
+ Rules live in `~/.workerdeck/relay/rules.json` and default to deny:
26
+
27
+ ```json
28
+ { "rules": [{ "from": "*", "to": "*" }] }
29
+ ```
30
+
31
+ A rule without `allow` permits `send` and `peek`. See the
32
+ [cross-gateway peers guide](https://workerdeck.github.io/workerdeck/guides/cross-gateway-peers/).
33
+
34
+ Embed it with `startRelay({ stateDir, host, port, tls })`.
35
+
36
+ ## License
37
+
38
+ MIT © Tobias Strebitzer - see
39
+ [LICENSE](https://github.com/workerdeck/workerdeck/blob/master/LICENSE).
@@ -0,0 +1 @@
1
+ export {}
package/build/cli.mjs ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import { i as runRelayCli, n as RelayUsageError, t as RELAY_HELP } from "./command-myavxZoY.mjs";
3
+ //#region src/cli.ts
4
+ runRelayCli(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
5
+ console.error(`workerdeck-relay: ${error instanceof Error ? error.message : String(error)}${error instanceof RelayUsageError ? `\n\n${RELAY_HELP}` : ""}`);
6
+ process.exit(error instanceof RelayUsageError ? 2 : 1);
7
+ });
8
+ //#endregion
9
+ export {};
10
+
11
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { RelayUsageError, RELAY_HELP, runRelayCli } from './command.ts'\n\nrunRelayCli(process.argv.slice(2)).then(\n (code) => process.exit(code),\n (error: unknown) => {\n console.error(\n `workerdeck-relay: ${error instanceof Error ? error.message : String(error)}${error instanceof RelayUsageError ? `\\n\\n${RELAY_HELP}` : ''}`,\n )\n process.exit(error instanceof RelayUsageError ? 2 : 1)\n },\n)\n"],"mappings":";;;AAGA,YAAY,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,MAChC,SAAS,QAAQ,KAAK,IAAI,IAC1B,UAAmB;CAClB,QAAQ,MACN,qBAAqB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,IAAI,iBAAiB,kBAAkB,OAAO,eAAe,IACzI;CACA,QAAQ,KAAK,iBAAiB,kBAAkB,IAAI,CAAC;AACvD,CACF"}
@@ -0,0 +1,637 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { RELAY_CLOSE, RELAY_FRAME_MAX_BYTES, RELAY_GATEWAY_NAME, RELAY_MAX_HOPS, RELAY_MESSAGE_MAX_CHARS, RELAY_OPS, RELAY_WIRE_VERSION, canonicalJson, decodeFrame, encodeFrame, isRelayOp, parseRelayPeerId, registryHash, relayPeerId } from "@workerdeck/relay-client";
5
+ import { createServer } from "node:http";
6
+ import { createServer as createServer$1 } from "node:https";
7
+ import { unwatchFile, watchFile } from "node:fs";
8
+ import { WebSocketServer } from "ws";
9
+ import { homedir } from "node:os";
10
+ //#region src/enrollment.ts
11
+ const ENROLLMENT_FILE = "gateways.json";
12
+ function enrollmentPath(stateDir) {
13
+ return join(stateDir, ENROLLMENT_FILE);
14
+ }
15
+ function hashKey(key) {
16
+ return createHash("sha256").update(key).digest("hex");
17
+ }
18
+ function newGatewayKey() {
19
+ return `wdr_${randomBytes(32).toString("base64url")}`;
20
+ }
21
+ function keyMatches(enrollment, key) {
22
+ const presented = Buffer.from(hashKey(key), "hex");
23
+ const stored = Buffer.from(enrollment?.hash ?? hashKey(""), "hex");
24
+ return presented.length === stored.length && timingSafeEqual(presented, stored) && enrollment !== void 0;
25
+ }
26
+ function checkGatewayName(name) {
27
+ return RELAY_GATEWAY_NAME.test(name) ? null : "a gateway name is 1 to 63 lowercase letters, digits or dashes, starting with a letter or digit";
28
+ }
29
+ async function readEnrollments(stateDir) {
30
+ let text;
31
+ try {
32
+ text = await readFile(enrollmentPath(stateDir), "utf8");
33
+ } catch (error) {
34
+ if (error.code === "ENOENT") return {
35
+ version: 1,
36
+ gateways: {}
37
+ };
38
+ throw error;
39
+ }
40
+ const parsed = JSON.parse(text);
41
+ const gateways = {};
42
+ for (const [name, entry] of Object.entries(parsed.gateways ?? {})) if (checkGatewayName(name) === null && typeof entry?.hash === "string" && /^[0-9a-f]{64}$/.test(entry.hash)) gateways[name] = {
43
+ hash: entry.hash,
44
+ enrolledAt: typeof entry.enrolledAt === "number" ? entry.enrolledAt : 0
45
+ };
46
+ return {
47
+ version: 1,
48
+ gateways
49
+ };
50
+ }
51
+ async function writeEnrollments(stateDir, file) {
52
+ const path = enrollmentPath(stateDir);
53
+ await mkdir(dirname(path), {
54
+ recursive: true,
55
+ mode: 448
56
+ });
57
+ const tmp = `${path}.${process.pid}.tmp`;
58
+ await writeFile(tmp, `${JSON.stringify(file, null, 2)}\n`, { mode: 384 });
59
+ await rename(tmp, path);
60
+ }
61
+ async function enrollGateway(stateDir, name, options) {
62
+ const invalid = checkGatewayName(name);
63
+ if (invalid) throw new Error(invalid);
64
+ const file = await readEnrollments(stateDir);
65
+ if (file.gateways[name] && !options?.rotate) throw new Error(`gateway ${name} is already enrolled; pass --rotate to issue a new key`);
66
+ const key = newGatewayKey();
67
+ file.gateways[name] = {
68
+ hash: hashKey(key),
69
+ enrolledAt: Date.now()
70
+ };
71
+ await writeEnrollments(stateDir, file);
72
+ return key;
73
+ }
74
+ async function revokeGateway(stateDir, name) {
75
+ const file = await readEnrollments(stateDir);
76
+ if (!file.gateways[name]) return false;
77
+ delete file.gateways[name];
78
+ await writeEnrollments(stateDir, file);
79
+ return true;
80
+ }
81
+ //#endregion
82
+ //#region src/rules.ts
83
+ const RULES_FILE = "rules.json";
84
+ function rulesPath(stateDir) {
85
+ return join(stateDir, RULES_FILE);
86
+ }
87
+ function parseRules(value) {
88
+ const list = value?.rules;
89
+ if (!Array.isArray(list)) throw new Error("rules file must be an object with a \"rules\" array");
90
+ return list.map((raw, index) => {
91
+ const rule = raw;
92
+ const where = `rules[${index}]`;
93
+ if (typeof rule?.from !== "string" || typeof rule.to !== "string" || !rule.from || !rule.to) throw new Error(`${where}: "from" and "to" must be gateway names or "*"`);
94
+ if (rule.allow !== void 0 && (!Array.isArray(rule.allow) || !rule.allow.every(isRelayOp))) throw new Error(`${where}: "allow" must list "send" and/or "peek"`);
95
+ const projects = rule.scope?.projects;
96
+ if (projects !== void 0 && (!Array.isArray(projects) || !projects.every((path) => typeof path === "string" && path.startsWith("/")))) throw new Error(`${where}: "scope.projects" must list absolute paths`);
97
+ return {
98
+ from: rule.from,
99
+ to: rule.to,
100
+ allow: rule.allow === void 0 ? [...RELAY_OPS] : [...new Set(rule.allow)],
101
+ ...projects ? { projects } : {}
102
+ };
103
+ });
104
+ }
105
+ async function readRules(stateDir) {
106
+ let text;
107
+ try {
108
+ text = await readFile(rulesPath(stateDir), "utf8");
109
+ } catch (error) {
110
+ if (error.code === "ENOENT") return [];
111
+ throw error;
112
+ }
113
+ return parseRules(JSON.parse(text));
114
+ }
115
+ function underPath(path, root) {
116
+ if (!path) return false;
117
+ const base = root.length > 1 && root.endsWith("/") ? root.slice(0, -1) : root;
118
+ return path === base || path.startsWith(base === "/" ? "/" : `${base}/`);
119
+ }
120
+ function inScope(rule, entry) {
121
+ return !rule.projects || rule.projects.some((root) => underPath(entry.cwd, root) || underPath(entry.project?.root, root));
122
+ }
123
+ function allowedOps(rules, from, to, entry, ceiling) {
124
+ const ops = /* @__PURE__ */ new Set();
125
+ for (const rule of rules) if ((rule.from === "*" || rule.from === from) && (rule.to === "*" || rule.to === to) && inScope(rule, entry)) for (const op of rule.allow) ops.add(op);
126
+ return RELAY_OPS.filter((op) => ops.has(op) && ceiling.has(op));
127
+ }
128
+ //#endregion
129
+ //#region src/relay.ts
130
+ const WINDOW_MS = 6e4;
131
+ function isLoopback(address) {
132
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
133
+ }
134
+ function readEntries(value) {
135
+ if (!Array.isArray(value)) return;
136
+ const entries = value.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string" && entry.id.length > 0 && typeof entry.cwd === "string");
137
+ return entries.length === value.length ? entries : void 0;
138
+ }
139
+ async function startRelay(options) {
140
+ const log = options.log ?? ((line) => console.log(line));
141
+ const requestTimeoutMs = options.requestTimeoutMs ?? 8e3;
142
+ const heartbeatMs = options.heartbeatMs ?? 15e3;
143
+ const helloTimeoutMs = options.helloTimeoutMs ?? 1e4;
144
+ const perMinute = options.perMinute ?? 10;
145
+ const maxHops = options.maxHops ?? RELAY_MAX_HOPS;
146
+ const maxChars = options.maxMessageChars ?? RELAY_MESSAGE_MAX_CHARS;
147
+ const watchIntervalMs = options.watchIntervalMs ?? 2e3;
148
+ let enrollments = await readEnrollments(options.stateDir);
149
+ let rules = await readRules(options.stateDir);
150
+ const gateways = /* @__PURE__ */ new Map();
151
+ const routed = /* @__PURE__ */ new Map();
152
+ const sent = /* @__PURE__ */ new Map();
153
+ let nextId = 0;
154
+ const drop = (gateway, code, reason) => {
155
+ if (gateways.get(gateway.name) === gateway) {
156
+ gateways.delete(gateway.name);
157
+ for (const [id, entry] of routed) if (entry.gateway === gateway.name) {
158
+ clearTimeout(entry.timer);
159
+ routed.delete(id);
160
+ entry.settle({
161
+ ok: false,
162
+ error: `gateway ${gateway.name} went offline`
163
+ });
164
+ }
165
+ log(`relay: ${gateway.name} offline (${reason})`);
166
+ }
167
+ if (gateway.socket.readyState === gateway.socket.OPEN || gateway.socket.readyState === gateway.socket.CONNECTING) gateway.socket.close(code, reason);
168
+ };
169
+ const reload = async () => {
170
+ try {
171
+ enrollments = await readEnrollments(options.stateDir);
172
+ } catch (error) {
173
+ log(`relay: keeping the previous enrollments, ${enrollmentPath(options.stateDir)} is unreadable: ${String(error)}`);
174
+ }
175
+ try {
176
+ rules = await readRules(options.stateDir);
177
+ } catch (error) {
178
+ log(`relay: keeping the previous rules, ${rulesPath(options.stateDir)} is invalid: ${error instanceof Error ? error.message : String(error)}`);
179
+ }
180
+ for (const gateway of gateways.values()) if (!enrollments.gateways[gateway.name]) drop(gateway, RELAY_CLOSE.revoked, "revoked");
181
+ };
182
+ const underRateLimit = (key) => {
183
+ const now = Date.now();
184
+ const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS);
185
+ const ok = recent.length < perMinute;
186
+ if (ok) recent.push(now);
187
+ sent.set(key, recent);
188
+ return ok;
189
+ };
190
+ const route = (target, frame) => {
191
+ const id = `r${++nextId}`;
192
+ return new Promise((resolve, reject) => {
193
+ const timer = setTimeout(() => {
194
+ routed.delete(id);
195
+ reject(/* @__PURE__ */ new Error(`gateway ${target.name} did not answer in time`));
196
+ }, requestTimeoutMs);
197
+ routed.set(id, {
198
+ gateway: target.name,
199
+ timer,
200
+ settle: (res) => res.ok ? resolve(res.result) : reject(new Error(res.error ?? "request failed"))
201
+ });
202
+ send(target.socket, {
203
+ ...frame,
204
+ id
205
+ });
206
+ });
207
+ };
208
+ const visible = (from, target, entry) => target === from ? [] : allowedOps(rules, from.name, target.name, entry, target.ops);
209
+ const list = (from) => {
210
+ const rows = [];
211
+ for (const target of gateways.values()) for (const entry of target.entries.values()) {
212
+ const allow = visible(from, target, entry);
213
+ if (allow.length > 0) rows.push({
214
+ ...entry,
215
+ gateway: target.name,
216
+ allow
217
+ });
218
+ }
219
+ return rows.sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt));
220
+ };
221
+ const resolveTarget = (from, to, op) => {
222
+ const where = to;
223
+ if (typeof where?.gateway !== "string" || typeof where.id !== "string") return;
224
+ const target = gateways.get(where.gateway);
225
+ const entry = target?.entries.get(where.id);
226
+ if (!target || !entry || !visible(from, target, entry).includes(op)) return;
227
+ return {
228
+ target,
229
+ entry
230
+ };
231
+ };
232
+ const peerRequest = async (from, frame) => {
233
+ const sender = typeof frame.from === "string" ? from.entries.get(frame.from) : void 0;
234
+ if (frame.t === "peer.list") return sender ? list(from) : [];
235
+ const to = frame.to;
236
+ const label = `${to?.gateway ?? "?"}:${to?.id ?? "?"}`;
237
+ if (frame.t === "peer.peek") {
238
+ const found = sender && resolveTarget(from, frame.to, "peek");
239
+ if (!found) return null;
240
+ const recent = typeof frame.recent === "number" ? frame.recent : void 0;
241
+ return route(found.target, {
242
+ t: "peer.peek",
243
+ origin: originOf(from, sender, []),
244
+ to: found.entry.id,
245
+ recent
246
+ });
247
+ }
248
+ const text = typeof frame.text === "string" ? frame.text : "";
249
+ const found = sender && resolveTarget(from, frame.to, "send");
250
+ if (!found || !sender) return {
251
+ delivered: false,
252
+ reason: `no such session: ${label}`
253
+ };
254
+ if (text.length === 0 || text.length > maxChars) return {
255
+ delivered: false,
256
+ reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`
257
+ };
258
+ const hops = (Array.isArray(frame.hops) ? frame.hops : []).filter((hop) => typeof hop === "string").map((hop) => parseRelayPeerId(hop) ? hop : relayPeerId(from.name, hop));
259
+ if (hops.length > maxHops) return {
260
+ delivered: false,
261
+ reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`
262
+ };
263
+ if (!underRateLimit(`${relayPeerId(from.name, sender.id)}->${relayPeerId(found.target.name, found.entry.id)}`)) return {
264
+ delivered: false,
265
+ reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.`
266
+ };
267
+ return route(found.target, {
268
+ t: "peer.send",
269
+ origin: originOf(from, sender, hops),
270
+ to: found.entry.id,
271
+ text
272
+ });
273
+ };
274
+ const onGatewayFrame = (gateway, frame) => {
275
+ switch (frame.t) {
276
+ case "registry.snapshot": {
277
+ const entries = readEntries(frame.entries);
278
+ if (!entries || typeof frame.seq !== "number") return;
279
+ gateway.entries = new Map(entries.map((entry) => [entry.id, entry]));
280
+ gateway.json = new Map(entries.map((entry) => [entry.id, canonicalJson(entry)]));
281
+ gateway.seq = frame.seq;
282
+ return;
283
+ }
284
+ case "registry.delta": {
285
+ const upsert = readEntries(frame.upsert);
286
+ const remove = Array.isArray(frame.remove) ? frame.remove.filter((id) => typeof id === "string") : void 0;
287
+ if (!upsert || !remove || frame.seq !== gateway.seq + 1) {
288
+ send(gateway.socket, { t: "registry.resync" });
289
+ return;
290
+ }
291
+ for (const id of remove) {
292
+ gateway.entries.delete(id);
293
+ gateway.json.delete(id);
294
+ }
295
+ for (const entry of upsert) {
296
+ gateway.entries.set(entry.id, entry);
297
+ gateway.json.set(entry.id, canonicalJson(entry));
298
+ }
299
+ gateway.seq = frame.seq;
300
+ return;
301
+ }
302
+ case "registry.digest":
303
+ if (frame.seq !== gateway.seq) return;
304
+ if (frame.count !== gateway.entries.size || frame.hash !== registryHash(gateway.json)) {
305
+ log(`relay: ${gateway.name} registry drifted; asking for a snapshot`);
306
+ send(gateway.socket, { t: "registry.resync" });
307
+ }
308
+ return;
309
+ case "res": {
310
+ const id = typeof frame.id === "string" ? frame.id : "";
311
+ const entry = routed.get(id);
312
+ if (!entry || entry.gateway !== gateway.name) return;
313
+ routed.delete(id);
314
+ clearTimeout(entry.timer);
315
+ entry.settle({
316
+ ok: frame.ok === true,
317
+ result: frame.result,
318
+ error: typeof frame.error === "string" ? frame.error : void 0
319
+ });
320
+ return;
321
+ }
322
+ case "peer.list":
323
+ case "peer.peek":
324
+ case "peer.send": {
325
+ const id = typeof frame.id === "string" ? frame.id : void 0;
326
+ if (!id) return;
327
+ peerRequest(gateway, frame).then((result) => send(gateway.socket, {
328
+ t: "res",
329
+ id,
330
+ ok: true,
331
+ result: result ?? null
332
+ }), (error) => send(gateway.socket, {
333
+ t: "res",
334
+ id,
335
+ ok: false,
336
+ error: error instanceof Error ? error.message : String(error)
337
+ }));
338
+ return;
339
+ }
340
+ }
341
+ };
342
+ const onHello = (socket, frame) => {
343
+ const name = typeof frame.gateway === "string" ? frame.gateway : "";
344
+ if (frame.t !== "hello" || !name || typeof frame.key !== "string") {
345
+ socket.close(RELAY_CLOSE.badHello, "expected hello");
346
+ return;
347
+ }
348
+ if (frame.version !== RELAY_WIRE_VERSION) {
349
+ socket.close(RELAY_CLOSE.versionMismatch, `relay speaks wire version ${RELAY_WIRE_VERSION}`);
350
+ return;
351
+ }
352
+ const enrollment = enrollments.gateways[name];
353
+ if (!keyMatches(enrollment, frame.key)) {
354
+ log(`relay: refused a hello for ${JSON.stringify(name.slice(0, 64))}`);
355
+ socket.close(RELAY_CLOSE.unauthorized, "unauthorized");
356
+ return;
357
+ }
358
+ const ceiling = frame.ceiling?.ops;
359
+ const previous = gateways.get(name);
360
+ if (previous) drop(previous, RELAY_CLOSE.replaced, "replaced by a newer connection");
361
+ const gateway = {
362
+ name,
363
+ socket,
364
+ ops: new Set(Array.isArray(ceiling) ? ceiling.filter(isRelayOp) : []),
365
+ connectedAt: Date.now(),
366
+ seq: 0,
367
+ entries: /* @__PURE__ */ new Map(),
368
+ json: /* @__PURE__ */ new Map(),
369
+ missed: 0
370
+ };
371
+ gateways.set(name, gateway);
372
+ log(`relay: ${name} online`);
373
+ send(socket, {
374
+ t: "welcome",
375
+ relayVersion: RELAY_WIRE_VERSION
376
+ });
377
+ return gateway;
378
+ };
379
+ const onConnection = (socket) => {
380
+ let gateway;
381
+ const helloTimer = setTimeout(() => socket.close(RELAY_CLOSE.timeout, "no hello"), helloTimeoutMs);
382
+ socket.on("message", (raw) => {
383
+ const frame = decodeFrame(Array.isArray(raw) ? Buffer.concat(raw) : raw instanceof ArrayBuffer ? Buffer.from(raw) : raw);
384
+ if (gateway) {
385
+ gateway.missed = 0;
386
+ if (frame) onGatewayFrame(gateway, frame);
387
+ return;
388
+ }
389
+ clearTimeout(helloTimer);
390
+ gateway = frame ? onHello(socket, frame) : void 0;
391
+ if (!gateway && socket.readyState === socket.OPEN) socket.close(RELAY_CLOSE.badHello, "expected hello");
392
+ });
393
+ socket.on("pong", () => {
394
+ if (gateway) gateway.missed = 0;
395
+ });
396
+ socket.on("error", () => {});
397
+ socket.on("close", () => {
398
+ clearTimeout(helloTimer);
399
+ if (gateway) drop(gateway, 1e3, "disconnected");
400
+ });
401
+ };
402
+ const serveStatus = (req, res) => {
403
+ if (req.url !== "/status" || req.method !== "GET" || !isLoopback(req.socket.remoteAddress)) {
404
+ res.writeHead(404).end();
405
+ return;
406
+ }
407
+ res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(status()));
408
+ };
409
+ const status = () => {
410
+ const names = /* @__PURE__ */ new Set([...Object.keys(enrollments.gateways), ...gateways.keys()]);
411
+ return {
412
+ version: RELAY_WIRE_VERSION,
413
+ gateways: [...names].sort().map((name) => {
414
+ const gateway = gateways.get(name);
415
+ return gateway ? {
416
+ name,
417
+ online: true,
418
+ sessions: gateway.entries.size,
419
+ connectedAt: gateway.connectedAt,
420
+ ops: [...gateway.ops]
421
+ } : {
422
+ name,
423
+ online: false,
424
+ sessions: 0,
425
+ ops: []
426
+ };
427
+ })
428
+ };
429
+ };
430
+ const server = options.tls ? createServer$1({
431
+ cert: options.tls.cert,
432
+ key: options.tls.key
433
+ }, serveStatus) : createServer(serveStatus);
434
+ const wss = new WebSocketServer({
435
+ server,
436
+ maxPayload: RELAY_FRAME_MAX_BYTES
437
+ });
438
+ wss.on("connection", onConnection);
439
+ const heartbeat = setInterval(() => {
440
+ for (const gateway of gateways.values()) {
441
+ gateway.missed += 1;
442
+ if (gateway.missed > 2) {
443
+ drop(gateway, RELAY_CLOSE.timeout, "heartbeat");
444
+ gateway.socket.terminate();
445
+ continue;
446
+ }
447
+ gateway.socket.ping();
448
+ }
449
+ }, heartbeatMs);
450
+ heartbeat.unref();
451
+ const watched = [enrollmentPath(options.stateDir), rulesPath(options.stateDir)];
452
+ for (const path of watched) watchFile(path, {
453
+ interval: watchIntervalMs,
454
+ persistent: false
455
+ }, () => void reload());
456
+ const host = options.host ?? "127.0.0.1";
457
+ await new Promise((resolve, reject) => {
458
+ server.once("error", reject);
459
+ server.listen(options.port ?? 7777, host, () => {
460
+ server.off("error", reject);
461
+ resolve();
462
+ });
463
+ });
464
+ const port = server.address().port;
465
+ const url = `${options.tls ? "wss" : "ws"}://${host.includes(":") ? `[${host}]` : host}:${port}`;
466
+ if (!options.tls && !isLoopback(host) && host !== "localhost") log(`relay: serving plain ws:// on ${host}; gateway keys cross the network unencrypted unless this is a tailnet or a TLS proxy`);
467
+ return {
468
+ url,
469
+ port,
470
+ status,
471
+ reload,
472
+ close: async () => {
473
+ clearInterval(heartbeat);
474
+ for (const path of watched) unwatchFile(path);
475
+ for (const gateway of gateways.values()) drop(gateway, 1001, "relay shutting down");
476
+ for (const client of wss.clients) client.terminate();
477
+ await new Promise((resolve) => wss.close(() => resolve()));
478
+ await new Promise((resolve) => server.close(() => resolve()));
479
+ }
480
+ };
481
+ }
482
+ function send(socket, frame) {
483
+ if (socket.readyState === socket.OPEN) socket.send(encodeFrame(frame));
484
+ }
485
+ function originOf(from, sender, hops) {
486
+ return {
487
+ gateway: from.name,
488
+ sessionId: sender.id,
489
+ name: sender.title,
490
+ engine: sender.engine,
491
+ hops
492
+ };
493
+ }
494
+ //#endregion
495
+ //#region src/command.ts
496
+ const RELAY_HELP = `workerdeck-relay - the cross-gateway peer relay. Also runs as \`workerdeck relay\`.
497
+
498
+ Usage
499
+ workerdeck-relay serve [options] run the relay
500
+ workerdeck-relay enroll <name> [--rotate] enroll a gateway and print its key once
501
+ workerdeck-relay revoke <name> revoke a gateway; a running relay drops it within seconds
502
+ workerdeck-relay list list enrolled gateways
503
+ workerdeck-relay status [options] ask a running relay on this machine who is online
504
+
505
+ Options
506
+ --state-dir <path> enrollments (gateways.json) and rules (rules.json); default ~/.workerdeck/relay
507
+ --host <addr> listen address (default 127.0.0.1)
508
+ --port <n> listen port (default 7777)
509
+ --tls-cert <file> serve wss:// with this certificate (optional, plain ws:// otherwise)
510
+ --tls-key <file> the certificate's private key
511
+ -h, --help this text
512
+ `;
513
+ var RelayUsageError = class extends Error {};
514
+ function parseRelayArgs(argv, env = process.env) {
515
+ const flags = { stateDir: env.WORKERDECK_RELAY_STATE_DIR ?? join(homedir(), ".workerdeck", "relay") };
516
+ const positional = [];
517
+ for (let i = 0; i < argv.length; i++) {
518
+ const arg = argv[i];
519
+ const value = () => {
520
+ const next = argv[++i];
521
+ if (next === void 0) throw new RelayUsageError(`${arg} needs a value`);
522
+ return next;
523
+ };
524
+ switch (arg) {
525
+ case "--state-dir":
526
+ flags.stateDir = resolve(value());
527
+ break;
528
+ case "--host":
529
+ flags.host = value();
530
+ break;
531
+ case "--port": {
532
+ const port = Number(value());
533
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new RelayUsageError("--port must be a port number");
534
+ flags.port = port;
535
+ break;
536
+ }
537
+ case "--tls-cert":
538
+ flags.tlsCert = resolve(value());
539
+ break;
540
+ case "--tls-key":
541
+ flags.tlsKey = resolve(value());
542
+ break;
543
+ case "--rotate":
544
+ flags.rotate = true;
545
+ break;
546
+ case "-h":
547
+ case "--help":
548
+ flags.help = true;
549
+ break;
550
+ default:
551
+ if (arg.startsWith("-")) throw new RelayUsageError(`unknown option: ${arg}`);
552
+ positional.push(arg);
553
+ }
554
+ }
555
+ flags.command = positional[0];
556
+ flags.name = positional[1];
557
+ if (Boolean(flags.tlsCert) !== Boolean(flags.tlsKey)) throw new RelayUsageError("--tls-cert and --tls-key go together");
558
+ return flags;
559
+ }
560
+ async function runRelayCli(argv) {
561
+ let flags;
562
+ try {
563
+ flags = parseRelayArgs(argv);
564
+ } catch (error) {
565
+ if (error instanceof RelayUsageError) {
566
+ console.error(`workerdeck-relay: ${error.message}\n\n${RELAY_HELP}`);
567
+ return 2;
568
+ }
569
+ throw error;
570
+ }
571
+ if (flags.help || !flags.command) {
572
+ console.log(RELAY_HELP);
573
+ return flags.help ? 0 : 2;
574
+ }
575
+ switch (flags.command) {
576
+ case "serve": {
577
+ await readRules(flags.stateDir);
578
+ const relay = await startRelay({
579
+ stateDir: flags.stateDir,
580
+ host: flags.host,
581
+ port: flags.port,
582
+ tls: flags.tlsCert && flags.tlsKey ? {
583
+ cert: await readFile(flags.tlsCert),
584
+ key: await readFile(flags.tlsKey)
585
+ } : void 0
586
+ });
587
+ const enrolled = Object.keys((await readEnrollments(flags.stateDir)).gateways).length;
588
+ console.log(`workerdeck-relay listening on ${relay.url} (${enrolled} gateway(s) enrolled, rules at ${rulesPath(flags.stateDir)})`);
589
+ await new Promise((done) => {
590
+ const stop = () => void relay.close().then(done);
591
+ process.once("SIGINT", stop);
592
+ process.once("SIGTERM", stop);
593
+ });
594
+ return 0;
595
+ }
596
+ case "enroll": {
597
+ if (!flags.name) throw new RelayUsageError("enroll needs a gateway name");
598
+ const key = await enrollGateway(flags.stateDir, flags.name, { rotate: flags.rotate });
599
+ console.log(`Enrolled ${flags.name}. Its key, shown once:\n\n ${key}\n\nSave it on that gateway (for example ~/.workerdeck/relay.key, mode 0600) and point relay.keyFile at it.`);
600
+ return 0;
601
+ }
602
+ case "revoke": {
603
+ if (!flags.name) throw new RelayUsageError("revoke needs a gateway name");
604
+ const removed = await revokeGateway(flags.stateDir, flags.name);
605
+ console.log(removed ? `Revoked ${flags.name}.` : `${flags.name} was not enrolled.`);
606
+ return removed ? 0 : 1;
607
+ }
608
+ case "list": {
609
+ const file = await readEnrollments(flags.stateDir);
610
+ const names = Object.entries(file.gateways).sort(([a], [b]) => a < b ? -1 : 1);
611
+ if (names.length === 0) console.log("No gateways enrolled.");
612
+ for (const [name, entry] of names) console.log(`${name}\tenrolled ${new Date(entry.enrolledAt).toISOString()}`);
613
+ return 0;
614
+ }
615
+ case "status": {
616
+ const url = `${flags.tlsCert ? "https" : "http"}://127.0.0.1:${flags.port ?? 7777}/status`;
617
+ let status;
618
+ try {
619
+ const res = await fetch(url);
620
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
621
+ status = await res.json();
622
+ } catch (error) {
623
+ console.error(`workerdeck-relay: no relay answered at ${url} (${error instanceof Error ? error.message : String(error)})`);
624
+ return 2;
625
+ }
626
+ for (const gateway of status.gateways) console.log(gateway.online ? `${gateway.name}\tonline\t${gateway.sessions} session(s)\taccepts ${gateway.ops.join(", ") || "nothing"}` : `${gateway.name}\toffline`);
627
+ return 0;
628
+ }
629
+ default:
630
+ console.error(`workerdeck-relay: unknown command ${flags.command}\n\n${RELAY_HELP}`);
631
+ return 2;
632
+ }
633
+ }
634
+ //#endregion
635
+ export { newGatewayKey as _, startRelay as a, parseRules as c, ENROLLMENT_FILE as d, checkGatewayName as f, keyMatches as g, hashKey as h, runRelayCli as i, readRules as l, enrollmentPath as m, RelayUsageError as n, RULES_FILE as o, enrollGateway as p, parseRelayArgs as r, allowedOps as s, RELAY_HELP as t, rulesPath as u, readEnrollments as v, revokeGateway as y };
636
+
637
+ //# sourceMappingURL=command-myavxZoY.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-myavxZoY.mjs","names":["createHttpsServer","createHttpServer"],"sources":["../src/enrollment.ts","../src/rules.ts","../src/relay.ts","../src/command.ts"],"sourcesContent":["import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'\nimport { mkdir, readFile, rename, writeFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { RELAY_GATEWAY_NAME } from '@workerdeck/relay-client'\n\nexport type Enrollment = { hash: string; enrolledAt: number }\n\nexport type EnrollmentFile = { version: 1; gateways: Record<string, Enrollment> }\n\nexport const ENROLLMENT_FILE = 'gateways.json'\n\nexport function enrollmentPath(stateDir: string): string {\n return join(stateDir, ENROLLMENT_FILE)\n}\n\nexport function hashKey(key: string): string {\n return createHash('sha256').update(key).digest('hex')\n}\n\nexport function newGatewayKey(): string {\n return `wdr_${randomBytes(32).toString('base64url')}`\n}\n\nexport function keyMatches(enrollment: Enrollment | undefined, key: string): boolean {\n const presented = Buffer.from(hashKey(key), 'hex')\n const stored = Buffer.from(enrollment?.hash ?? hashKey(''), 'hex')\n return presented.length === stored.length && timingSafeEqual(presented, stored) && enrollment !== undefined\n}\n\nexport function checkGatewayName(name: string): string | null {\n return RELAY_GATEWAY_NAME.test(name)\n ? null\n : 'a gateway name is 1 to 63 lowercase letters, digits or dashes, starting with a letter or digit'\n}\n\nexport async function readEnrollments(stateDir: string): Promise<EnrollmentFile> {\n let text: string\n try {\n text = await readFile(enrollmentPath(stateDir), 'utf8')\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return { version: 1, gateways: {} }\n }\n throw error\n }\n const parsed = JSON.parse(text) as Partial<EnrollmentFile>\n const gateways: Record<string, Enrollment> = {}\n for (const [name, entry] of Object.entries(parsed.gateways ?? {})) {\n if (checkGatewayName(name) === null && typeof entry?.hash === 'string' && /^[0-9a-f]{64}$/.test(entry.hash)) {\n gateways[name] = { hash: entry.hash, enrolledAt: typeof entry.enrolledAt === 'number' ? entry.enrolledAt : 0 }\n }\n }\n return { version: 1, gateways }\n}\n\nasync function writeEnrollments(stateDir: string, file: EnrollmentFile): Promise<void> {\n const path = enrollmentPath(stateDir)\n await mkdir(dirname(path), { recursive: true, mode: 0o700 })\n const tmp = `${path}.${process.pid}.tmp`\n await writeFile(tmp, `${JSON.stringify(file, null, 2)}\\n`, { mode: 0o600 })\n await rename(tmp, path)\n}\n\nexport async function enrollGateway(stateDir: string, name: string, options?: { rotate?: boolean }): Promise<string> {\n const invalid = checkGatewayName(name)\n if (invalid) {\n throw new Error(invalid)\n }\n const file = await readEnrollments(stateDir)\n if (file.gateways[name] && !options?.rotate) {\n throw new Error(`gateway ${name} is already enrolled; pass --rotate to issue a new key`)\n }\n const key = newGatewayKey()\n file.gateways[name] = { hash: hashKey(key), enrolledAt: Date.now() }\n await writeEnrollments(stateDir, file)\n return key\n}\n\nexport async function revokeGateway(stateDir: string, name: string): Promise<boolean> {\n const file = await readEnrollments(stateDir)\n if (!file.gateways[name]) {\n return false\n }\n delete file.gateways[name]\n await writeEnrollments(stateDir, file)\n return true\n}\n","import { readFile } from 'node:fs/promises'\nimport { join } from 'node:path'\nimport { RELAY_OPS, isRelayOp, type RelayOp, type RelaySessionEntry } from '@workerdeck/relay-client'\n\nexport type RelayRule = {\n from: string\n to: string\n allow: RelayOp[]\n projects?: string[]\n}\n\nexport const RULES_FILE = 'rules.json'\n\nexport function rulesPath(stateDir: string): string {\n return join(stateDir, RULES_FILE)\n}\n\nexport function parseRules(value: unknown): RelayRule[] {\n const list = (value as { rules?: unknown } | null)?.rules\n if (!Array.isArray(list)) {\n throw new Error('rules file must be an object with a \"rules\" array')\n }\n return list.map((raw, index) => {\n const rule = raw as { from?: unknown; to?: unknown; allow?: unknown; scope?: { projects?: unknown } }\n const where = `rules[${index}]`\n if (typeof rule?.from !== 'string' || typeof rule.to !== 'string' || !rule.from || !rule.to) {\n throw new Error(`${where}: \"from\" and \"to\" must be gateway names or \"*\"`)\n }\n if (rule.allow !== undefined && (!Array.isArray(rule.allow) || !rule.allow.every(isRelayOp))) {\n throw new Error(`${where}: \"allow\" must list \"send\" and/or \"peek\"`)\n }\n const projects = rule.scope?.projects\n if (\n projects !== undefined &&\n (!Array.isArray(projects) || !projects.every((path) => typeof path === 'string' && path.startsWith('/')))\n ) {\n throw new Error(`${where}: \"scope.projects\" must list absolute paths`)\n }\n return {\n from: rule.from,\n to: rule.to,\n allow: rule.allow === undefined ? [...RELAY_OPS] : [...new Set(rule.allow as RelayOp[])],\n ...(projects ? { projects: projects as string[] } : {}),\n }\n })\n}\n\nexport async function readRules(stateDir: string): Promise<RelayRule[]> {\n let text: string\n try {\n text = await readFile(rulesPath(stateDir), 'utf8')\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return []\n }\n throw error\n }\n return parseRules(JSON.parse(text))\n}\n\nfunction underPath(path: string | undefined, root: string): boolean {\n if (!path) {\n return false\n }\n const base = root.length > 1 && root.endsWith('/') ? root.slice(0, -1) : root\n return path === base || path.startsWith(base === '/' ? '/' : `${base}/`)\n}\n\nfunction inScope(rule: RelayRule, entry: RelaySessionEntry): boolean {\n return !rule.projects || rule.projects.some((root) => underPath(entry.cwd, root) || underPath(entry.project?.root, root))\n}\n\n// Union of every matching rule, intersected with what the target gateway accepts at all. Empty means\n// the session is invisible to the requester.\nexport function allowedOps(\n rules: readonly RelayRule[],\n from: string,\n to: string,\n entry: RelaySessionEntry,\n ceiling: ReadonlySet<RelayOp>,\n): RelayOp[] {\n const ops = new Set<RelayOp>()\n for (const rule of rules) {\n if ((rule.from === '*' || rule.from === from) && (rule.to === '*' || rule.to === to) && inScope(rule, entry)) {\n for (const op of rule.allow) {\n ops.add(op)\n }\n }\n }\n return RELAY_OPS.filter((op) => ops.has(op) && ceiling.has(op))\n}\n","import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'\nimport { createServer as createHttpsServer } from 'node:https'\nimport type { AddressInfo } from 'node:net'\nimport { unwatchFile, watchFile } from 'node:fs'\nimport { WebSocketServer, type RawData, type WebSocket } from 'ws'\nimport {\n RELAY_CLOSE,\n RELAY_FRAME_MAX_BYTES,\n RELAY_MAX_HOPS,\n RELAY_MESSAGE_MAX_CHARS,\n RELAY_WIRE_VERSION,\n canonicalJson,\n decodeFrame,\n encodeFrame,\n isRelayOp,\n parseRelayPeerId,\n registryHash,\n relayPeerId,\n type InboundPeek,\n type InboundSend,\n type RelayFrame,\n type RelayOp,\n type RelayOrigin,\n type RelayPeerRow,\n type RelaySendResult,\n type RelaySessionEntry,\n} from '@workerdeck/relay-client'\nimport { enrollmentPath, keyMatches, readEnrollments, type EnrollmentFile } from './enrollment.ts'\nimport { allowedOps, readRules, rulesPath, type RelayRule } from './rules.ts'\n\nexport type RelayOptions = {\n stateDir: string\n host?: string\n port?: number\n tls?: { cert: string | Buffer; key: string | Buffer }\n requestTimeoutMs?: number\n heartbeatMs?: number\n helloTimeoutMs?: number\n perMinute?: number\n maxHops?: number\n maxMessageChars?: number\n watchIntervalMs?: number\n log?: (line: string) => void\n}\n\nexport type RelayGatewayStatus = { name: string; online: boolean; sessions: number; connectedAt?: number; ops: RelayOp[] }\n\nexport type RelayStatus = { version: number; gateways: RelayGatewayStatus[] }\n\nexport type Relay = {\n url: string\n port: number\n status(): RelayStatus\n reload(): Promise<void>\n close(): Promise<void>\n}\n\ntype Gateway = {\n name: string\n socket: WebSocket\n ops: Set<RelayOp>\n connectedAt: number\n seq: number\n entries: Map<string, RelaySessionEntry>\n json: Map<string, string>\n missed: number\n}\n\ntype Routed = { gateway: string; timer: NodeJS.Timeout; settle: (frame: { ok: boolean; result?: unknown; error?: string }) => void }\n\ntype Frame = { t: string; [key: string]: unknown }\n\nconst WINDOW_MS = 60_000\n\nfunction isLoopback(address: string | undefined): boolean {\n return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'\n}\n\nfunction readEntries(value: unknown): RelaySessionEntry[] | undefined {\n if (!Array.isArray(value)) {\n return undefined\n }\n const entries = value.filter(\n (entry): entry is RelaySessionEntry =>\n typeof entry === 'object' && entry !== null && typeof entry.id === 'string' && entry.id.length > 0 && typeof entry.cwd === 'string',\n )\n return entries.length === value.length ? entries : undefined\n}\n\nexport async function startRelay(options: RelayOptions): Promise<Relay> {\n const log = options.log ?? ((line: string) => console.log(line))\n const requestTimeoutMs = options.requestTimeoutMs ?? 8_000\n const heartbeatMs = options.heartbeatMs ?? 15_000\n const helloTimeoutMs = options.helloTimeoutMs ?? 10_000\n const perMinute = options.perMinute ?? 10\n const maxHops = options.maxHops ?? RELAY_MAX_HOPS\n const maxChars = options.maxMessageChars ?? RELAY_MESSAGE_MAX_CHARS\n const watchIntervalMs = options.watchIntervalMs ?? 2_000\n\n let enrollments: EnrollmentFile = await readEnrollments(options.stateDir)\n let rules: RelayRule[] = await readRules(options.stateDir)\n const gateways = new Map<string, Gateway>()\n const routed = new Map<string, Routed>()\n const sent = new Map<string, number[]>()\n let nextId = 0\n\n const drop = (gateway: Gateway, code: number, reason: string): void => {\n if (gateways.get(gateway.name) === gateway) {\n gateways.delete(gateway.name)\n for (const [id, entry] of routed) {\n if (entry.gateway === gateway.name) {\n clearTimeout(entry.timer)\n routed.delete(id)\n entry.settle({ ok: false, error: `gateway ${gateway.name} went offline` })\n }\n }\n log(`relay: ${gateway.name} offline (${reason})`)\n }\n if (gateway.socket.readyState === gateway.socket.OPEN || gateway.socket.readyState === gateway.socket.CONNECTING) {\n gateway.socket.close(code, reason)\n }\n }\n\n const reload = async (): Promise<void> => {\n try {\n enrollments = await readEnrollments(options.stateDir)\n } catch (error) {\n log(`relay: keeping the previous enrollments, ${enrollmentPath(options.stateDir)} is unreadable: ${String(error)}`)\n }\n try {\n rules = await readRules(options.stateDir)\n } catch (error) {\n log(\n `relay: keeping the previous rules, ${rulesPath(options.stateDir)} is invalid: ${error instanceof Error ? error.message : String(error)}`,\n )\n }\n for (const gateway of gateways.values()) {\n if (!enrollments.gateways[gateway.name]) {\n drop(gateway, RELAY_CLOSE.revoked, 'revoked')\n }\n }\n }\n\n const underRateLimit = (key: string): boolean => {\n const now = Date.now()\n const recent = (sent.get(key) ?? []).filter((at) => now - at < WINDOW_MS)\n const ok = recent.length < perMinute\n if (ok) {\n recent.push(now)\n }\n sent.set(key, recent)\n return ok\n }\n\n const route = (target: Gateway, frame: Omit<InboundPeek, 'id'> | Omit<InboundSend, 'id'>): Promise<unknown> => {\n const id = `r${++nextId}`\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n routed.delete(id)\n reject(new Error(`gateway ${target.name} did not answer in time`))\n }, requestTimeoutMs)\n routed.set(id, {\n gateway: target.name,\n timer,\n settle: (res) => (res.ok ? resolve(res.result) : reject(new Error(res.error ?? 'request failed'))),\n })\n send(target.socket, { ...frame, id } as RelayFrame)\n })\n }\n\n const visible = (from: Gateway, target: Gateway, entry: RelaySessionEntry): RelayOp[] =>\n target === from ? [] : allowedOps(rules, from.name, target.name, entry, target.ops)\n\n const list = (from: Gateway): RelayPeerRow[] => {\n const rows: RelayPeerRow[] = []\n for (const target of gateways.values()) {\n for (const entry of target.entries.values()) {\n const allow = visible(from, target, entry)\n if (allow.length > 0) {\n rows.push({ ...entry, gateway: target.name, allow })\n }\n }\n }\n return rows.sort((a, b) => (b.lastActivityAt ?? b.createdAt) - (a.lastActivityAt ?? a.createdAt))\n }\n\n const resolveTarget = (from: Gateway, to: unknown, op: RelayOp): { target: Gateway; entry: RelaySessionEntry } | undefined => {\n const where = to as { gateway?: unknown; id?: unknown } | undefined\n if (typeof where?.gateway !== 'string' || typeof where.id !== 'string') {\n return undefined\n }\n const target = gateways.get(where.gateway)\n const entry = target?.entries.get(where.id)\n if (!target || !entry || !visible(from, target, entry).includes(op)) {\n return undefined\n }\n return { target, entry }\n }\n\n const peerRequest = async (from: Gateway, frame: Frame): Promise<unknown> => {\n const sender = typeof frame.from === 'string' ? from.entries.get(frame.from) : undefined\n if (frame.t === 'peer.list') {\n return sender ? list(from) : []\n }\n const to = frame.to as { gateway?: string; id?: string } | undefined\n const label = `${to?.gateway ?? '?'}:${to?.id ?? '?'}`\n if (frame.t === 'peer.peek') {\n const found = sender && resolveTarget(from, frame.to, 'peek')\n if (!found) {\n return null\n }\n const recent = typeof frame.recent === 'number' ? frame.recent : undefined\n return route(found.target, { t: 'peer.peek', origin: originOf(from, sender, []), to: found.entry.id, recent })\n }\n const text = typeof frame.text === 'string' ? frame.text : ''\n const found = sender && resolveTarget(from, frame.to, 'send')\n if (!found || !sender) {\n return { delivered: false, reason: `no such session: ${label}` } satisfies RelaySendResult\n }\n if (text.length === 0 || text.length > maxChars) {\n return {\n delivered: false,\n reason: `message is ${text.length} characters; the limit is ${maxChars}. Write it to a file and send the path.`,\n }\n }\n const hops = (Array.isArray(frame.hops) ? frame.hops : [])\n .filter((hop): hop is string => typeof hop === 'string')\n .map((hop) => (parseRelayPeerId(hop) ? hop : relayPeerId(from.name, hop)))\n if (hops.length > maxHops) {\n return {\n delivered: false,\n reason: `${hops.length} messages have passed between sessions without a person speaking; stop and ask your user before continuing.`,\n }\n }\n if (!underRateLimit(`${relayPeerId(from.name, sender.id)}->${relayPeerId(found.target.name, found.entry.id)}`)) {\n return { delivered: false, reason: `rate limit: at most ${perMinute} messages a minute to one session. Batch what you have to say.` }\n }\n return route(found.target, { t: 'peer.send', origin: originOf(from, sender, hops), to: found.entry.id, text })\n }\n\n const onGatewayFrame = (gateway: Gateway, frame: Frame): void => {\n switch (frame.t) {\n case 'registry.snapshot': {\n const entries = readEntries(frame.entries)\n if (!entries || typeof frame.seq !== 'number') {\n return\n }\n gateway.entries = new Map(entries.map((entry) => [entry.id, entry]))\n gateway.json = new Map(entries.map((entry) => [entry.id, canonicalJson(entry)]))\n gateway.seq = frame.seq\n return\n }\n case 'registry.delta': {\n const upsert = readEntries(frame.upsert)\n const remove = Array.isArray(frame.remove) ? frame.remove.filter((id): id is string => typeof id === 'string') : undefined\n if (!upsert || !remove || frame.seq !== gateway.seq + 1) {\n send(gateway.socket, { t: 'registry.resync' })\n return\n }\n for (const id of remove) {\n gateway.entries.delete(id)\n gateway.json.delete(id)\n }\n for (const entry of upsert) {\n gateway.entries.set(entry.id, entry)\n gateway.json.set(entry.id, canonicalJson(entry))\n }\n gateway.seq = frame.seq\n return\n }\n case 'registry.digest': {\n if (frame.seq !== gateway.seq) {\n return\n }\n if (frame.count !== gateway.entries.size || frame.hash !== registryHash(gateway.json)) {\n log(`relay: ${gateway.name} registry drifted; asking for a snapshot`)\n send(gateway.socket, { t: 'registry.resync' })\n }\n return\n }\n case 'res': {\n const id = typeof frame.id === 'string' ? frame.id : ''\n const entry = routed.get(id)\n if (!entry || entry.gateway !== gateway.name) {\n return\n }\n routed.delete(id)\n clearTimeout(entry.timer)\n entry.settle({ ok: frame.ok === true, result: frame.result, error: typeof frame.error === 'string' ? frame.error : undefined })\n return\n }\n case 'peer.list':\n case 'peer.peek':\n case 'peer.send': {\n const id = typeof frame.id === 'string' ? frame.id : undefined\n if (!id) {\n return\n }\n peerRequest(gateway, frame).then(\n (result) => send(gateway.socket, { t: 'res', id, ok: true, result: result ?? null }),\n (error: unknown) =>\n send(gateway.socket, { t: 'res', id, ok: false, error: error instanceof Error ? error.message : String(error) }),\n )\n return\n }\n }\n }\n\n const onHello = (socket: WebSocket, frame: Frame): Gateway | undefined => {\n const name = typeof frame.gateway === 'string' ? frame.gateway : ''\n if (frame.t !== 'hello' || !name || typeof frame.key !== 'string') {\n socket.close(RELAY_CLOSE.badHello, 'expected hello')\n return undefined\n }\n if (frame.version !== RELAY_WIRE_VERSION) {\n socket.close(RELAY_CLOSE.versionMismatch, `relay speaks wire version ${RELAY_WIRE_VERSION}`)\n return undefined\n }\n const enrollment = enrollments.gateways[name]\n if (!keyMatches(enrollment, frame.key)) {\n log(`relay: refused a hello for ${JSON.stringify(name.slice(0, 64))}`)\n socket.close(RELAY_CLOSE.unauthorized, 'unauthorized')\n return undefined\n }\n const ceiling = (frame.ceiling as { ops?: unknown } | undefined)?.ops\n const previous = gateways.get(name)\n if (previous) {\n drop(previous, RELAY_CLOSE.replaced, 'replaced by a newer connection')\n }\n const gateway: Gateway = {\n name,\n socket,\n ops: new Set(Array.isArray(ceiling) ? ceiling.filter(isRelayOp) : []),\n connectedAt: Date.now(),\n seq: 0,\n entries: new Map(),\n json: new Map(),\n missed: 0,\n }\n gateways.set(name, gateway)\n log(`relay: ${name} online`)\n send(socket, { t: 'welcome', relayVersion: RELAY_WIRE_VERSION })\n return gateway\n }\n\n const onConnection = (socket: WebSocket): void => {\n let gateway: Gateway | undefined\n const helloTimer = setTimeout(() => socket.close(RELAY_CLOSE.timeout, 'no hello'), helloTimeoutMs)\n socket.on('message', (raw: RawData) => {\n const frame = decodeFrame(Array.isArray(raw) ? Buffer.concat(raw) : raw instanceof ArrayBuffer ? Buffer.from(raw) : raw)\n if (gateway) {\n gateway.missed = 0\n if (frame) {\n onGatewayFrame(gateway, frame)\n }\n return\n }\n clearTimeout(helloTimer)\n gateway = frame ? onHello(socket, frame) : undefined\n if (!gateway && socket.readyState === socket.OPEN) {\n socket.close(RELAY_CLOSE.badHello, 'expected hello')\n }\n })\n socket.on('pong', () => {\n if (gateway) {\n gateway.missed = 0\n }\n })\n socket.on('error', () => {})\n socket.on('close', () => {\n clearTimeout(helloTimer)\n if (gateway) {\n drop(gateway, 1000, 'disconnected')\n }\n })\n }\n\n const serveStatus = (req: IncomingMessage, res: ServerResponse): void => {\n if (req.url !== '/status' || req.method !== 'GET' || !isLoopback(req.socket.remoteAddress)) {\n res.writeHead(404).end()\n return\n }\n res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(status()))\n }\n\n const status = (): RelayStatus => {\n const names = new Set([...Object.keys(enrollments.gateways), ...gateways.keys()])\n return {\n version: RELAY_WIRE_VERSION,\n gateways: [...names].sort().map((name) => {\n const gateway = gateways.get(name)\n return gateway\n ? { name, online: true, sessions: gateway.entries.size, connectedAt: gateway.connectedAt, ops: [...gateway.ops] }\n : { name, online: false, sessions: 0, ops: [] }\n }),\n }\n }\n\n const server: Server = options.tls\n ? createHttpsServer({ cert: options.tls.cert, key: options.tls.key }, serveStatus)\n : createHttpServer(serveStatus)\n const wss = new WebSocketServer({ server, maxPayload: RELAY_FRAME_MAX_BYTES })\n wss.on('connection', onConnection)\n\n const heartbeat = setInterval(() => {\n for (const gateway of gateways.values()) {\n gateway.missed += 1\n if (gateway.missed > 2) {\n drop(gateway, RELAY_CLOSE.timeout, 'heartbeat')\n gateway.socket.terminate()\n continue\n }\n gateway.socket.ping()\n }\n }, heartbeatMs)\n heartbeat.unref()\n\n const watched = [enrollmentPath(options.stateDir), rulesPath(options.stateDir)]\n for (const path of watched) {\n watchFile(path, { interval: watchIntervalMs, persistent: false }, () => void reload())\n }\n\n const host = options.host ?? '127.0.0.1'\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(options.port ?? 7777, host, () => {\n server.off('error', reject)\n resolve()\n })\n })\n const port = (server.address() as AddressInfo).port\n const scheme = options.tls ? 'wss' : 'ws'\n const url = `${scheme}://${host.includes(':') ? `[${host}]` : host}:${port}`\n if (!options.tls && !isLoopback(host) && host !== 'localhost') {\n log(`relay: serving plain ws:// on ${host}; gateway keys cross the network unencrypted unless this is a tailnet or a TLS proxy`)\n }\n\n return {\n url,\n port,\n status,\n reload,\n close: async () => {\n clearInterval(heartbeat)\n for (const path of watched) {\n unwatchFile(path)\n }\n for (const gateway of gateways.values()) {\n drop(gateway, 1001, 'relay shutting down')\n }\n for (const client of wss.clients) {\n client.terminate()\n }\n await new Promise<void>((resolve) => wss.close(() => resolve()))\n await new Promise<void>((resolve) => server.close(() => resolve()))\n },\n }\n}\n\nfunction send(socket: WebSocket, frame: RelayFrame): void {\n if (socket.readyState === socket.OPEN) {\n socket.send(encodeFrame(frame))\n }\n}\n\nfunction originOf(from: Gateway, sender: RelaySessionEntry, hops: string[]): RelayOrigin {\n return { gateway: from.name, sessionId: sender.id, name: sender.title, engine: sender.engine, hops }\n}\n","import { readFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport { enrollGateway, readEnrollments, revokeGateway } from './enrollment.ts'\nimport { startRelay, type RelayStatus } from './relay.ts'\nimport { readRules, rulesPath } from './rules.ts'\n\nexport const RELAY_HELP = `workerdeck-relay - the cross-gateway peer relay. Also runs as \\`workerdeck relay\\`.\n\nUsage\n workerdeck-relay serve [options] run the relay\n workerdeck-relay enroll <name> [--rotate] enroll a gateway and print its key once\n workerdeck-relay revoke <name> revoke a gateway; a running relay drops it within seconds\n workerdeck-relay list list enrolled gateways\n workerdeck-relay status [options] ask a running relay on this machine who is online\n\nOptions\n --state-dir <path> enrollments (gateways.json) and rules (rules.json); default ~/.workerdeck/relay\n --host <addr> listen address (default 127.0.0.1)\n --port <n> listen port (default 7777)\n --tls-cert <file> serve wss:// with this certificate (optional, plain ws:// otherwise)\n --tls-key <file> the certificate's private key\n -h, --help this text\n`\n\nexport type RelayFlags = {\n command?: string\n name?: string\n stateDir: string\n host?: string\n port?: number\n tlsCert?: string\n tlsKey?: string\n rotate?: boolean\n help?: boolean\n}\n\nexport class RelayUsageError extends Error {}\n\nexport function parseRelayArgs(argv: readonly string[], env: NodeJS.ProcessEnv = process.env): RelayFlags {\n const flags: RelayFlags = { stateDir: env.WORKERDECK_RELAY_STATE_DIR ?? join(homedir(), '.workerdeck', 'relay') }\n const positional: string[] = []\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i]!\n const value = (): string => {\n const next = argv[++i]\n if (next === undefined) {\n throw new RelayUsageError(`${arg} needs a value`)\n }\n return next\n }\n switch (arg) {\n case '--state-dir': {\n flags.stateDir = resolve(value())\n break\n }\n case '--host': {\n flags.host = value()\n break\n }\n case '--port': {\n const port = Number(value())\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new RelayUsageError('--port must be a port number')\n }\n flags.port = port\n break\n }\n case '--tls-cert': {\n flags.tlsCert = resolve(value())\n break\n }\n case '--tls-key': {\n flags.tlsKey = resolve(value())\n break\n }\n case '--rotate': {\n flags.rotate = true\n break\n }\n case '-h':\n case '--help': {\n flags.help = true\n break\n }\n default: {\n if (arg.startsWith('-')) {\n throw new RelayUsageError(`unknown option: ${arg}`)\n }\n positional.push(arg)\n }\n }\n }\n flags.command = positional[0]\n flags.name = positional[1]\n if (Boolean(flags.tlsCert) !== Boolean(flags.tlsKey)) {\n throw new RelayUsageError('--tls-cert and --tls-key go together')\n }\n return flags\n}\n\nexport async function runRelayCli(argv: readonly string[]): Promise<number> {\n let flags: RelayFlags\n try {\n flags = parseRelayArgs(argv)\n } catch (error) {\n if (error instanceof RelayUsageError) {\n console.error(`workerdeck-relay: ${error.message}\\n\\n${RELAY_HELP}`)\n return 2\n }\n throw error\n }\n if (flags.help || !flags.command) {\n console.log(RELAY_HELP)\n return flags.help ? 0 : 2\n }\n switch (flags.command) {\n case 'serve': {\n await readRules(flags.stateDir)\n const relay = await startRelay({\n stateDir: flags.stateDir,\n host: flags.host,\n port: flags.port,\n tls: flags.tlsCert && flags.tlsKey ? { cert: await readFile(flags.tlsCert), key: await readFile(flags.tlsKey) } : undefined,\n })\n const enrolled = Object.keys((await readEnrollments(flags.stateDir)).gateways).length\n console.log(`workerdeck-relay listening on ${relay.url} (${enrolled} gateway(s) enrolled, rules at ${rulesPath(flags.stateDir)})`)\n await new Promise<void>((done) => {\n const stop = () => void relay.close().then(done)\n process.once('SIGINT', stop)\n process.once('SIGTERM', stop)\n })\n return 0\n }\n case 'enroll': {\n if (!flags.name) {\n throw new RelayUsageError('enroll needs a gateway name')\n }\n const key = await enrollGateway(flags.stateDir, flags.name, { rotate: flags.rotate })\n console.log(\n `Enrolled ${flags.name}. Its key, shown once:\\n\\n ${key}\\n\\n` +\n `Save it on that gateway (for example ~/.workerdeck/relay.key, mode 0600) and point relay.keyFile at it.`,\n )\n return 0\n }\n case 'revoke': {\n if (!flags.name) {\n throw new RelayUsageError('revoke needs a gateway name')\n }\n const removed = await revokeGateway(flags.stateDir, flags.name)\n console.log(removed ? `Revoked ${flags.name}.` : `${flags.name} was not enrolled.`)\n return removed ? 0 : 1\n }\n case 'list': {\n const file = await readEnrollments(flags.stateDir)\n const names = Object.entries(file.gateways).sort(([a], [b]) => (a < b ? -1 : 1))\n if (names.length === 0) {\n console.log('No gateways enrolled.')\n }\n for (const [name, entry] of names) {\n console.log(`${name}\\tenrolled ${new Date(entry.enrolledAt).toISOString()}`)\n }\n return 0\n }\n case 'status': {\n const scheme = flags.tlsCert ? 'https' : 'http'\n const url = `${scheme}://127.0.0.1:${flags.port ?? 7777}/status`\n let status: RelayStatus\n try {\n const res = await fetch(url)\n if (!res.ok) {\n throw new Error(`HTTP ${res.status}`)\n }\n status = (await res.json()) as RelayStatus\n } catch (error) {\n console.error(`workerdeck-relay: no relay answered at ${url} (${error instanceof Error ? error.message : String(error)})`)\n return 2\n }\n for (const gateway of status.gateways) {\n console.log(\n gateway.online\n ? `${gateway.name}\\tonline\\t${gateway.sessions} session(s)\\taccepts ${gateway.ops.join(', ') || 'nothing'}`\n : `${gateway.name}\\toffline`,\n )\n }\n return 0\n }\n default: {\n console.error(`workerdeck-relay: unknown command ${flags.command}\\n\\n${RELAY_HELP}`)\n return 2\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import { RelayOp, RelaySessionEntry } from "@workerdeck/relay-client";
2
+ //#region src/enrollment.d.ts
3
+ type Enrollment = {
4
+ hash: string;
5
+ enrolledAt: number;
6
+ };
7
+ type EnrollmentFile = {
8
+ version: 1;
9
+ gateways: Record<string, Enrollment>;
10
+ };
11
+ declare const ENROLLMENT_FILE = "gateways.json";
12
+ declare function enrollmentPath(stateDir: string): string;
13
+ declare function hashKey(key: string): string;
14
+ declare function newGatewayKey(): string;
15
+ declare function keyMatches(enrollment: Enrollment | undefined, key: string): boolean;
16
+ declare function checkGatewayName(name: string): string | null;
17
+ declare function readEnrollments(stateDir: string): Promise<EnrollmentFile>;
18
+ declare function enrollGateway(stateDir: string, name: string, options?: {
19
+ rotate?: boolean;
20
+ }): Promise<string>;
21
+ declare function revokeGateway(stateDir: string, name: string): Promise<boolean>;
22
+ //#endregion
23
+ //#region src/rules.d.ts
24
+ type RelayRule = {
25
+ from: string;
26
+ to: string;
27
+ allow: RelayOp[];
28
+ projects?: string[];
29
+ };
30
+ declare const RULES_FILE = "rules.json";
31
+ declare function rulesPath(stateDir: string): string;
32
+ declare function parseRules(value: unknown): RelayRule[];
33
+ declare function readRules(stateDir: string): Promise<RelayRule[]>;
34
+ declare function allowedOps(rules: readonly RelayRule[], from: string, to: string, entry: RelaySessionEntry, ceiling: ReadonlySet<RelayOp>): RelayOp[];
35
+ //#endregion
36
+ //#region src/relay.d.ts
37
+ type RelayOptions = {
38
+ stateDir: string;
39
+ host?: string;
40
+ port?: number;
41
+ tls?: {
42
+ cert: string | Buffer;
43
+ key: string | Buffer;
44
+ };
45
+ requestTimeoutMs?: number;
46
+ heartbeatMs?: number;
47
+ helloTimeoutMs?: number;
48
+ perMinute?: number;
49
+ maxHops?: number;
50
+ maxMessageChars?: number;
51
+ watchIntervalMs?: number;
52
+ log?: (line: string) => void;
53
+ };
54
+ type RelayGatewayStatus = {
55
+ name: string;
56
+ online: boolean;
57
+ sessions: number;
58
+ connectedAt?: number;
59
+ ops: RelayOp[];
60
+ };
61
+ type RelayStatus = {
62
+ version: number;
63
+ gateways: RelayGatewayStatus[];
64
+ };
65
+ type Relay = {
66
+ url: string;
67
+ port: number;
68
+ status(): RelayStatus;
69
+ reload(): Promise<void>;
70
+ close(): Promise<void>;
71
+ };
72
+ declare function startRelay(options: RelayOptions): Promise<Relay>;
73
+ //#endregion
74
+ //#region src/command.d.ts
75
+ declare const RELAY_HELP = "workerdeck-relay - the cross-gateway peer relay. Also runs as `workerdeck relay`.\n\nUsage\n workerdeck-relay serve [options] run the relay\n workerdeck-relay enroll <name> [--rotate] enroll a gateway and print its key once\n workerdeck-relay revoke <name> revoke a gateway; a running relay drops it within seconds\n workerdeck-relay list list enrolled gateways\n workerdeck-relay status [options] ask a running relay on this machine who is online\n\nOptions\n --state-dir <path> enrollments (gateways.json) and rules (rules.json); default ~/.workerdeck/relay\n --host <addr> listen address (default 127.0.0.1)\n --port <n> listen port (default 7777)\n --tls-cert <file> serve wss:// with this certificate (optional, plain ws:// otherwise)\n --tls-key <file> the certificate's private key\n -h, --help this text\n";
76
+ type RelayFlags = {
77
+ command?: string;
78
+ name?: string;
79
+ stateDir: string;
80
+ host?: string;
81
+ port?: number;
82
+ tlsCert?: string;
83
+ tlsKey?: string;
84
+ rotate?: boolean;
85
+ help?: boolean;
86
+ };
87
+ declare class RelayUsageError extends Error {}
88
+ declare function parseRelayArgs(argv: readonly string[], env?: NodeJS.ProcessEnv): RelayFlags;
89
+ declare function runRelayCli(argv: readonly string[]): Promise<number>;
90
+ //#endregion
91
+ export { ENROLLMENT_FILE, Enrollment, EnrollmentFile, RELAY_HELP, RULES_FILE, Relay, RelayFlags, RelayGatewayStatus, RelayOptions, RelayRule, RelayStatus, RelayUsageError, allowedOps, checkGatewayName, enrollGateway, enrollmentPath, hashKey, keyMatches, newGatewayKey, parseRelayArgs, parseRules, readEnrollments, readRules, revokeGateway, rulesPath, runRelayCli, startRelay };
92
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,2 @@
1
+ import { _ as newGatewayKey, a as startRelay, c as parseRules, d as ENROLLMENT_FILE, f as checkGatewayName, g as keyMatches, h as hashKey, i as runRelayCli, l as readRules, m as enrollmentPath, n as RelayUsageError, o as RULES_FILE, p as enrollGateway, r as parseRelayArgs, s as allowedOps, t as RELAY_HELP, u as rulesPath, v as readEnrollments, y as revokeGateway } from "./command-myavxZoY.mjs";
2
+ export { ENROLLMENT_FILE, RELAY_HELP, RULES_FILE, RelayUsageError, allowedOps, checkGatewayName, enrollGateway, enrollmentPath, hashKey, keyMatches, newGatewayKey, parseRelayArgs, parseRules, readEnrollments, readRules, revokeGateway, rulesPath, runRelayCli, startRelay };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@workerdeck/relay",
3
+ "version": "3.3.0",
4
+ "type": "module",
5
+ "description": "The WorkerDeck relay: one always-on process every gateway dials out to. Holds the cross-gateway session registry, authenticates enrolled gateways, enforces the visibility rules and routes peer requests between them.",
6
+ "license": "MIT",
7
+ "main": "./build/index.mjs",
8
+ "types": "./build/index.d.mts",
9
+ "bin": {
10
+ "workerdeck-relay": "./build/cli.mjs"
11
+ },
12
+ "files": [
13
+ "build"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "@workerdeck/source": "./src/index.ts",
18
+ "types": "./build/index.d.mts",
19
+ "default": "./build/index.mjs"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "ws": "^8.21.3",
24
+ "@workerdeck/relay-client": "3.3.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^26.6.1",
28
+ "rimraf": "^6.1.3",
29
+ "tsdown": "^0.22.14",
30
+ "vitest": "^4.1.11",
31
+ "@types/ws": "^8.18.1"
32
+ },
33
+ "author": "Tobias Strebitzer",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/workerdeck/workerdeck.git",
37
+ "directory": "packages/relay"
38
+ },
39
+ "homepage": "https://workerdeck.github.io/workerdeck/",
40
+ "bugs": "https://github.com/workerdeck/workerdeck/issues",
41
+ "keywords": [
42
+ "workerdeck",
43
+ "relay",
44
+ "agent",
45
+ "peers",
46
+ "gateway"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "scripts": {
52
+ "clean": "rimraf build",
53
+ "build": "tsdown",
54
+ "typecheck": "tsgo -p tsconfig.json",
55
+ "test": "vitest run"
56
+ }
57
+ }