@workerdeck/relay 3.3.0 → 3.5.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/build/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as runRelayCli, n as RelayUsageError, t as RELAY_HELP } from "./command-
|
|
2
|
+
import { i as runRelayCli, n as RelayUsageError, t as RELAY_HELP } from "./command-_C1jiMvx.mjs";
|
|
3
3
|
//#region src/cli.ts
|
|
4
4
|
runRelayCli(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
5
5
|
console.error(`workerdeck-relay: ${error instanceof Error ? error.message : String(error)}${error instanceof RelayUsageError ? `\n\n${RELAY_HELP}` : ""}`);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
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";
|
|
5
|
+
import { createServer, request } from "node:http";
|
|
6
6
|
import { createServer as createServer$1 } from "node:https";
|
|
7
7
|
import { unwatchFile, watchFile } from "node:fs";
|
|
8
8
|
import { WebSocketServer } from "ws";
|
|
9
|
+
import { createConnection } from "node:net";
|
|
9
10
|
import { homedir } from "node:os";
|
|
10
11
|
//#region src/enrollment.ts
|
|
11
12
|
const ENROLLMENT_FILE = "gateways.json";
|
|
@@ -126,8 +127,81 @@ function allowedOps(rules, from, to, entry, ceiling) {
|
|
|
126
127
|
return RELAY_OPS.filter((op) => ops.has(op) && ceiling.has(op));
|
|
127
128
|
}
|
|
128
129
|
//#endregion
|
|
130
|
+
//#region src/status.ts
|
|
131
|
+
function statusSocketPath(stateDir) {
|
|
132
|
+
return join(stateDir, "relay.sock");
|
|
133
|
+
}
|
|
134
|
+
function socketAnswers(path) {
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const probe = createConnection(path);
|
|
137
|
+
probe.once("connect", () => {
|
|
138
|
+
probe.end();
|
|
139
|
+
resolve(true);
|
|
140
|
+
});
|
|
141
|
+
probe.once("error", () => resolve(false));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function serveStatusSocket(stateDir, status) {
|
|
145
|
+
const path = statusSocketPath(stateDir);
|
|
146
|
+
await mkdir(stateDir, {
|
|
147
|
+
recursive: true,
|
|
148
|
+
mode: 448
|
|
149
|
+
});
|
|
150
|
+
if (await socketAnswers(path)) throw new Error(`another relay is already serving ${stateDir} (${path} answers)`);
|
|
151
|
+
await rm(path, { force: true });
|
|
152
|
+
const server = createServer((req, res) => {
|
|
153
|
+
if (req.url !== "/status" || req.method !== "GET") {
|
|
154
|
+
res.writeHead(404).end();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
res.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(status()));
|
|
158
|
+
});
|
|
159
|
+
await new Promise((resolve, reject) => {
|
|
160
|
+
server.once("error", reject);
|
|
161
|
+
server.listen(path, () => {
|
|
162
|
+
server.off("error", reject);
|
|
163
|
+
resolve();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
await chmod(path, 384);
|
|
167
|
+
return server;
|
|
168
|
+
}
|
|
169
|
+
function fetchRelayStatus(stateDir, timeoutMs = 5e3) {
|
|
170
|
+
const socketPath = statusSocketPath(stateDir);
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const req = request({
|
|
173
|
+
socketPath,
|
|
174
|
+
path: "/status",
|
|
175
|
+
method: "GET",
|
|
176
|
+
timeout: timeoutMs
|
|
177
|
+
}, (res) => {
|
|
178
|
+
if (res.statusCode !== 200) {
|
|
179
|
+
res.resume();
|
|
180
|
+
reject(/* @__PURE__ */ new Error(`HTTP ${res.statusCode}`));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let body = "";
|
|
184
|
+
res.setEncoding("utf8");
|
|
185
|
+
res.on("data", (chunk) => body += chunk);
|
|
186
|
+
res.on("end", () => {
|
|
187
|
+
try {
|
|
188
|
+
resolve(JSON.parse(body));
|
|
189
|
+
} catch (error) {
|
|
190
|
+
reject(error);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
req.on("timeout", () => req.destroy(/* @__PURE__ */ new Error("timed out")));
|
|
195
|
+
req.on("error", reject);
|
|
196
|
+
req.end();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
129
200
|
//#region src/relay.ts
|
|
130
201
|
const WINDOW_MS = 6e4;
|
|
202
|
+
function notFound(_req, res) {
|
|
203
|
+
res.writeHead(404).end();
|
|
204
|
+
}
|
|
131
205
|
function isLoopback(address) {
|
|
132
206
|
return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
|
|
133
207
|
}
|
|
@@ -399,13 +473,6 @@ async function startRelay(options) {
|
|
|
399
473
|
if (gateway) drop(gateway, 1e3, "disconnected");
|
|
400
474
|
});
|
|
401
475
|
};
|
|
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
476
|
const status = () => {
|
|
410
477
|
const names = /* @__PURE__ */ new Set([...Object.keys(enrollments.gateways), ...gateways.keys()]);
|
|
411
478
|
return {
|
|
@@ -430,7 +497,7 @@ async function startRelay(options) {
|
|
|
430
497
|
const server = options.tls ? createServer$1({
|
|
431
498
|
cert: options.tls.cert,
|
|
432
499
|
key: options.tls.key
|
|
433
|
-
},
|
|
500
|
+
}, notFound) : createServer(notFound);
|
|
434
501
|
const wss = new WebSocketServer({
|
|
435
502
|
server,
|
|
436
503
|
maxPayload: RELAY_FRAME_MAX_BYTES
|
|
@@ -454,13 +521,23 @@ async function startRelay(options) {
|
|
|
454
521
|
persistent: false
|
|
455
522
|
}, () => void reload());
|
|
456
523
|
const host = options.host ?? "127.0.0.1";
|
|
457
|
-
await
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
524
|
+
const statusServer = await serveStatusSocket(options.stateDir, status);
|
|
525
|
+
const closeStatus = async () => {
|
|
526
|
+
await new Promise((resolve) => statusServer.close(() => resolve()));
|
|
527
|
+
await rm(statusSocketPath(options.stateDir), { force: true });
|
|
528
|
+
};
|
|
529
|
+
try {
|
|
530
|
+
await new Promise((resolve, reject) => {
|
|
531
|
+
server.once("error", reject);
|
|
532
|
+
server.listen(options.port ?? 7777, host, () => {
|
|
533
|
+
server.off("error", reject);
|
|
534
|
+
resolve();
|
|
535
|
+
});
|
|
462
536
|
});
|
|
463
|
-
})
|
|
537
|
+
} catch (error) {
|
|
538
|
+
await closeStatus();
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
464
541
|
const port = server.address().port;
|
|
465
542
|
const url = `${options.tls ? "wss" : "ws"}://${host.includes(":") ? `[${host}]` : host}:${port}`;
|
|
466
543
|
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`);
|
|
@@ -476,6 +553,7 @@ async function startRelay(options) {
|
|
|
476
553
|
for (const client of wss.clients) client.terminate();
|
|
477
554
|
await new Promise((resolve) => wss.close(() => resolve()));
|
|
478
555
|
await new Promise((resolve) => server.close(() => resolve()));
|
|
556
|
+
await closeStatus();
|
|
479
557
|
}
|
|
480
558
|
};
|
|
481
559
|
}
|
|
@@ -500,10 +578,10 @@ Usage
|
|
|
500
578
|
workerdeck-relay enroll <name> [--rotate] enroll a gateway and print its key once
|
|
501
579
|
workerdeck-relay revoke <name> revoke a gateway; a running relay drops it within seconds
|
|
502
580
|
workerdeck-relay list list enrolled gateways
|
|
503
|
-
workerdeck-relay status
|
|
581
|
+
workerdeck-relay status ask the relay serving this state dir who is online
|
|
504
582
|
|
|
505
583
|
Options
|
|
506
|
-
--state-dir <path> enrollments
|
|
584
|
+
--state-dir <path> enrollments, rules and the status socket (relay.sock); default ~/.workerdeck/relay
|
|
507
585
|
--host <addr> listen address (default 127.0.0.1)
|
|
508
586
|
--port <n> listen port (default 7777)
|
|
509
587
|
--tls-cert <file> serve wss:// with this certificate (optional, plain ws:// otherwise)
|
|
@@ -613,14 +691,12 @@ async function runRelayCli(argv) {
|
|
|
613
691
|
return 0;
|
|
614
692
|
}
|
|
615
693
|
case "status": {
|
|
616
|
-
const url = `${flags.tlsCert ? "https" : "http"}://127.0.0.1:${flags.port ?? 7777}/status`;
|
|
617
694
|
let status;
|
|
618
695
|
try {
|
|
619
|
-
|
|
620
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
621
|
-
status = await res.json();
|
|
696
|
+
status = await fetchRelayStatus(flags.stateDir);
|
|
622
697
|
} catch (error) {
|
|
623
|
-
|
|
698
|
+
const socket = statusSocketPath(flags.stateDir);
|
|
699
|
+
console.error(`workerdeck-relay: no relay answered on ${socket} (${error instanceof Error ? error.message : String(error)})`);
|
|
624
700
|
return 2;
|
|
625
701
|
}
|
|
626
702
|
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`);
|
|
@@ -632,6 +708,6 @@ async function runRelayCli(argv) {
|
|
|
632
708
|
}
|
|
633
709
|
}
|
|
634
710
|
//#endregion
|
|
635
|
-
export {
|
|
711
|
+
export { revokeGateway as S, enrollmentPath as _, startRelay as a, newGatewayKey as b, statusSocketPath as c, parseRules as d, readRules as f, enrollGateway as g, checkGatewayName as h, runRelayCli as i, RULES_FILE as l, ENROLLMENT_FILE as m, RelayUsageError as n, fetchRelayStatus as o, rulesPath as p, parseRelayArgs as r, serveStatusSocket as s, RELAY_HELP as t, allowedOps as u, hashKey as v, readEnrollments as x, keyMatches as y };
|
|
636
712
|
|
|
637
|
-
//# sourceMappingURL=command-
|
|
713
|
+
//# sourceMappingURL=command-_C1jiMvx.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-_C1jiMvx.mjs","names":["createHttpsServer","createHttpServer"],"sources":["../src/enrollment.ts","../src/rules.ts","../src/status.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 { chmod, mkdir, rm } from 'node:fs/promises'\nimport { createServer, request, type Server } from 'node:http'\nimport { createConnection } from 'node:net'\nimport { join } from 'node:path'\nimport type { RelayStatus } from './relay.ts'\n\nexport function statusSocketPath(stateDir: string): string {\n return join(stateDir, 'relay.sock')\n}\n\nfunction socketAnswers(path: string): Promise<boolean> {\n return new Promise((resolve) => {\n const probe = createConnection(path)\n probe.once('connect', () => {\n probe.end()\n resolve(true)\n })\n probe.once('error', () => resolve(false))\n })\n}\n\nexport async function serveStatusSocket(stateDir: string, status: () => RelayStatus): Promise<Server> {\n const path = statusSocketPath(stateDir)\n await mkdir(stateDir, { recursive: true, mode: 0o700 })\n if (await socketAnswers(path)) {\n throw new Error(`another relay is already serving ${stateDir} (${path} answers)`)\n }\n await rm(path, { force: true })\n const server = createServer((req, res) => {\n if (req.url !== '/status' || req.method !== 'GET') {\n res.writeHead(404).end()\n return\n }\n res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(status()))\n })\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject)\n server.listen(path, () => {\n server.off('error', reject)\n resolve()\n })\n })\n await chmod(path, 0o600)\n return server\n}\n\nexport function fetchRelayStatus(stateDir: string, timeoutMs = 5_000): Promise<RelayStatus> {\n const socketPath = statusSocketPath(stateDir)\n return new Promise((resolve, reject) => {\n const req = request({ socketPath, path: '/status', method: 'GET', timeout: timeoutMs }, (res) => {\n if (res.statusCode !== 200) {\n res.resume()\n reject(new Error(`HTTP ${res.statusCode}`))\n return\n }\n let body = ''\n res.setEncoding('utf8')\n res.on('data', (chunk: string) => (body += chunk))\n res.on('end', () => {\n try {\n resolve(JSON.parse(body) as RelayStatus)\n } catch (error) {\n reject(error)\n }\n })\n })\n req.on('timeout', () => req.destroy(new Error('timed out')))\n req.on('error', reject)\n req.end()\n })\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 { rm } from 'node:fs/promises'\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'\nimport { serveStatusSocket, statusSocketPath } from './status.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 notFound(_req: IncomingMessage, res: ServerResponse): void {\n res.writeHead(404).end()\n}\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 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 }, notFound)\n : createHttpServer(notFound)\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 const statusServer = await serveStatusSocket(options.stateDir, status)\n const closeStatus = async (): Promise<void> => {\n await new Promise<void>((resolve) => statusServer.close(() => resolve()))\n await rm(statusSocketPath(options.stateDir), { force: true })\n }\n try {\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 } catch (error) {\n await closeStatus()\n throw error\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 await closeStatus()\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 { fetchRelayStatus, statusSocketPath } from './status.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 ask the relay serving this state dir who is online\n\nOptions\n --state-dir <path> enrollments, rules and the status socket (relay.sock); 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 let status: RelayStatus\n try {\n status = await fetchRelayStatus(flags.stateDir)\n } catch (error) {\n const socket = statusSocketPath(flags.stateDir)\n console.error(`workerdeck-relay: no relay answered on ${socket} (${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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"}
|
package/build/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RelayOp, RelaySessionEntry } from "@workerdeck/relay-client";
|
|
2
|
+
import { Server } from "node:http";
|
|
2
3
|
//#region src/enrollment.d.ts
|
|
3
4
|
type Enrollment = {
|
|
4
5
|
hash: string;
|
|
@@ -71,8 +72,13 @@ type Relay = {
|
|
|
71
72
|
};
|
|
72
73
|
declare function startRelay(options: RelayOptions): Promise<Relay>;
|
|
73
74
|
//#endregion
|
|
75
|
+
//#region src/status.d.ts
|
|
76
|
+
declare function statusSocketPath(stateDir: string): string;
|
|
77
|
+
declare function serveStatusSocket(stateDir: string, status: () => RelayStatus): Promise<Server>;
|
|
78
|
+
declare function fetchRelayStatus(stateDir: string, timeoutMs?: number): Promise<RelayStatus>;
|
|
79
|
+
//#endregion
|
|
74
80
|
//#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
|
|
81
|
+
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 ask the relay serving this state dir who is online\n\nOptions\n --state-dir <path> enrollments, rules and the status socket (relay.sock); 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
82
|
type RelayFlags = {
|
|
77
83
|
command?: string;
|
|
78
84
|
name?: string;
|
|
@@ -88,5 +94,5 @@ declare class RelayUsageError extends Error {}
|
|
|
88
94
|
declare function parseRelayArgs(argv: readonly string[], env?: NodeJS.ProcessEnv): RelayFlags;
|
|
89
95
|
declare function runRelayCli(argv: readonly string[]): Promise<number>;
|
|
90
96
|
//#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 };
|
|
97
|
+
export { ENROLLMENT_FILE, Enrollment, EnrollmentFile, RELAY_HELP, RULES_FILE, Relay, RelayFlags, RelayGatewayStatus, RelayOptions, RelayRule, RelayStatus, RelayUsageError, allowedOps, checkGatewayName, enrollGateway, enrollmentPath, fetchRelayStatus, hashKey, keyMatches, newGatewayKey, parseRelayArgs, parseRules, readEnrollments, readRules, revokeGateway, rulesPath, runRelayCli, serveStatusSocket, startRelay, statusSocketPath };
|
|
92
98
|
//# sourceMappingURL=index.d.mts.map
|
package/build/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { _ as
|
|
2
|
-
export { ENROLLMENT_FILE, RELAY_HELP, RULES_FILE, RelayUsageError, allowedOps, checkGatewayName, enrollGateway, enrollmentPath, hashKey, keyMatches, newGatewayKey, parseRelayArgs, parseRules, readEnrollments, readRules, revokeGateway, rulesPath, runRelayCli, startRelay };
|
|
1
|
+
import { S as revokeGateway, _ as enrollmentPath, a as startRelay, b as newGatewayKey, c as statusSocketPath, d as parseRules, f as readRules, g as enrollGateway, h as checkGatewayName, i as runRelayCli, l as RULES_FILE, m as ENROLLMENT_FILE, n as RelayUsageError, o as fetchRelayStatus, p as rulesPath, r as parseRelayArgs, s as serveStatusSocket, t as RELAY_HELP, u as allowedOps, v as hashKey, x as readEnrollments, y as keyMatches } from "./command-_C1jiMvx.mjs";
|
|
2
|
+
export { ENROLLMENT_FILE, RELAY_HELP, RULES_FILE, RelayUsageError, allowedOps, checkGatewayName, enrollGateway, enrollmentPath, fetchRelayStatus, hashKey, keyMatches, newGatewayKey, parseRelayArgs, parseRules, readEnrollments, readRules, revokeGateway, rulesPath, runRelayCli, serveStatusSocket, startRelay, statusSocketPath };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workerdeck/relay",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"type": "module",
|
|
5
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
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"ws": "^8.21.3",
|
|
24
|
-
"@workerdeck/relay-client": "3.
|
|
24
|
+
"@workerdeck/relay-client": "3.5.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.6.1",
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|