@linkshell/gateway 0.1.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.
@@ -0,0 +1,332 @@
1
+ import { createServer } from "node:http";
2
+ import { randomUUID } from "node:crypto";
3
+ import { WebSocketServer } from "ws";
4
+ import { createEnvelope, serializeEnvelope, PROTOCOL_VERSION, } from "@linkshell/protocol";
5
+ import { z, ZodError } from "zod";
6
+ import { SessionManager } from "./sessions.js";
7
+ import { PairingManager } from "./pairings.js";
8
+ import { handleSocketMessage } from "./relay.js";
9
+ const port = Number(process.env.PORT ?? 8787);
10
+ const logLevel = (process.env.LOG_LEVEL ?? "info");
11
+ const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
12
+ function log(level, msg) {
13
+ if (LOG_LEVELS[level] >= LOG_LEVELS[logLevel]) {
14
+ process.stdout.write(`[gateway:${level}] ${msg}\n`);
15
+ }
16
+ }
17
+ const sessionManager = new SessionManager();
18
+ const pairingManager = new PairingManager();
19
+ const PING_INTERVAL = 20_000;
20
+ const MAX_BODY_SIZE = 4096;
21
+ const MAX_WS_MESSAGE_SIZE = 64 * 1024; // 64KB
22
+ const PAIRING_RATE_LIMIT_MAX = Number(process.env.PAIRING_RATE_LIMIT_MAX ?? 30);
23
+ const PAIRING_RATE_LIMIT_WINDOW_MS = Number(process.env.PAIRING_RATE_LIMIT_WINDOW_MS ?? 60_000);
24
+ const WS_CONNECT_RATE_LIMIT_MAX = Number(process.env.WS_CONNECT_RATE_LIMIT_MAX ?? 20);
25
+ const WS_CONNECT_RATE_LIMIT_WINDOW_MS = Number(process.env.WS_CONNECT_RATE_LIMIT_WINDOW_MS ?? 60_000);
26
+ // ── Rate limiter ────────────────────────────────────────────────────
27
+ class RateLimiter {
28
+ maxHits;
29
+ windowMs;
30
+ hits = new Map();
31
+ constructor(maxHits, windowMs) {
32
+ this.maxHits = maxHits;
33
+ this.windowMs = windowMs;
34
+ }
35
+ allow(key) {
36
+ const now = Date.now();
37
+ const entry = this.hits.get(key);
38
+ if (!entry || now >= entry.resetAt) {
39
+ this.hits.set(key, { count: 1, resetAt: now + this.windowMs });
40
+ return true;
41
+ }
42
+ entry.count++;
43
+ return entry.count <= this.maxHits;
44
+ }
45
+ }
46
+ const pairingLimiter = new RateLimiter(PAIRING_RATE_LIMIT_MAX, PAIRING_RATE_LIMIT_WINDOW_MS);
47
+ const wsConnectLimiter = new RateLimiter(WS_CONNECT_RATE_LIMIT_MAX, WS_CONNECT_RATE_LIMIT_WINDOW_MS);
48
+ function isLoopbackIp(ip) {
49
+ const normalized = ip.trim().toLowerCase();
50
+ return (normalized === "127.0.0.1" ||
51
+ normalized === "::1" ||
52
+ normalized === "::ffff:127.0.0.1");
53
+ }
54
+ function isRateLimitBypassed(ip) {
55
+ return isLoopbackIp(ip);
56
+ }
57
+ function getClientIp(req) {
58
+ const forwarded = req.headers["x-forwarded-for"];
59
+ if (typeof forwarded === "string")
60
+ return forwarded.split(",")[0].trim();
61
+ return req.socket.remoteAddress ?? "unknown";
62
+ }
63
+ // ── CORS ────────────────────────────────────────────────────────────
64
+ function setCors(res) {
65
+ res.setHeader("Access-Control-Allow-Origin", "*");
66
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
67
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
68
+ res.setHeader("Access-Control-Max-Age", "86400");
69
+ }
70
+ // ── HTTP API ────────────────────────────────────────────────────────
71
+ const createPairingBody = z.object({ sessionId: z.string().optional() });
72
+ const claimPairingBody = z.object({ pairingCode: z.string().length(6) });
73
+ const server = createServer(async (req, res) => {
74
+ setCors(res);
75
+ if (req.method === "OPTIONS") {
76
+ res.writeHead(204);
77
+ res.end();
78
+ return;
79
+ }
80
+ try {
81
+ await handleRequest(req, res);
82
+ }
83
+ catch (err) {
84
+ if (err instanceof ZodError) {
85
+ json(res, 400, {
86
+ error: "invalid_message",
87
+ message: err.errors[0]?.message ?? "Validation failed",
88
+ });
89
+ }
90
+ else if (err instanceof BodyTooLargeError) {
91
+ json(res, 413, {
92
+ error: "body_too_large",
93
+ message: "Request body exceeds limit",
94
+ });
95
+ }
96
+ else if (err instanceof SyntaxError) {
97
+ json(res, 400, { error: "invalid_json", message: "Malformed JSON" });
98
+ }
99
+ else {
100
+ process.stderr.write(`[gateway] unhandled error: ${err}\n`);
101
+ json(res, 500, {
102
+ error: "internal_error",
103
+ message: "Internal server error",
104
+ });
105
+ }
106
+ }
107
+ });
108
+ async function handleRequest(req, res) {
109
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
110
+ const method = req.method ?? "GET";
111
+ const ip = getClientIp(req);
112
+ // Health check
113
+ if (method === "GET" && url.pathname === "/healthz") {
114
+ json(res, 200, { ok: true });
115
+ return;
116
+ }
117
+ // Create pairing
118
+ if (method === "POST" && url.pathname === "/pairings") {
119
+ if (!isRateLimitBypassed(ip) && !pairingLimiter.allow(ip)) {
120
+ json(res, 429, { error: "rate_limited", message: "Too many requests" });
121
+ return;
122
+ }
123
+ const body = createPairingBody.parse(await readJson(req));
124
+ const record = pairingManager.create(body.sessionId);
125
+ json(res, 201, {
126
+ sessionId: record.sessionId,
127
+ pairingCode: record.pairingCode,
128
+ expiresAt: new Date(record.expiresAt).toISOString(),
129
+ });
130
+ return;
131
+ }
132
+ // Claim pairing
133
+ if (method === "POST" && url.pathname === "/pairings/claim") {
134
+ if (!isRateLimitBypassed(ip) && !pairingLimiter.allow(ip)) {
135
+ json(res, 429, { error: "rate_limited", message: "Too many requests" });
136
+ return;
137
+ }
138
+ const body = claimPairingBody.parse(await readJson(req));
139
+ const result = pairingManager.claim(body.pairingCode);
140
+ if ("error" in result) {
141
+ json(res, result.status, { error: result.error });
142
+ return;
143
+ }
144
+ json(res, 200, { sessionId: result.sessionId });
145
+ return;
146
+ }
147
+ // Session list
148
+ if (method === "GET" && url.pathname === "/sessions") {
149
+ const sessions = sessionManager.listActive().map((s) => ({
150
+ id: s.id,
151
+ state: s.state,
152
+ hasHost: !!s.host,
153
+ clientCount: s.clients.size,
154
+ controllerId: s.controllerId ?? null,
155
+ lastActivity: s.lastActivity,
156
+ createdAt: s.createdAt,
157
+ provider: s.provider ?? null,
158
+ hostname: s.hostname ?? null,
159
+ }));
160
+ json(res, 200, { sessions });
161
+ return;
162
+ }
163
+ // Session detail
164
+ const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
165
+ if (method === "GET" && sessionMatch) {
166
+ const summary = sessionManager.getSummary(sessionMatch[1]);
167
+ if (!summary) {
168
+ json(res, 404, { error: "session_not_found" });
169
+ return;
170
+ }
171
+ json(res, 200, summary);
172
+ return;
173
+ }
174
+ // Pairing status (for CLI polling)
175
+ const pairingMatch = url.pathname.match(/^\/pairings\/(\d{6})\/status$/);
176
+ if (method === "GET" && pairingMatch) {
177
+ const result = pairingManager.getStatus(pairingMatch[1]);
178
+ if ("error" in result) {
179
+ json(res, result.httpStatus, { error: result.error });
180
+ return;
181
+ }
182
+ json(res, 200, result);
183
+ return;
184
+ }
185
+ json(res, 404, { error: "not_found" });
186
+ }
187
+ // ── WebSocket ───────────────────────────────────────────────────────
188
+ const wss = new WebSocketServer({
189
+ noServer: true,
190
+ maxPayload: MAX_WS_MESSAGE_SIZE,
191
+ });
192
+ server.on("upgrade", (request, socket, head) => {
193
+ const url = new URL(request.url ?? "/", `http://${request.headers.host}`);
194
+ if (url.pathname !== "/ws") {
195
+ socket.destroy();
196
+ return;
197
+ }
198
+ const ip = getClientIp(request);
199
+ if (!isRateLimitBypassed(ip) && !wsConnectLimiter.allow(ip)) {
200
+ socket.write("HTTP/1.1 429 Too Many Requests\r\n\r\n");
201
+ socket.destroy();
202
+ return;
203
+ }
204
+ wss.handleUpgrade(request, socket, head, (ws) => {
205
+ wss.emit("connection", ws, request, url);
206
+ });
207
+ });
208
+ wss.on("connection", (socket, _request, url) => {
209
+ const sessionId = url.searchParams.get("sessionId");
210
+ const role = url.searchParams.get("role");
211
+ if (!sessionId || !role || (role !== "host" && role !== "client")) {
212
+ socket.close(1008, "missing sessionId or role");
213
+ return;
214
+ }
215
+ const deviceId = url.searchParams.get("deviceId") ?? randomUUID();
216
+ const device = {
217
+ socket,
218
+ role,
219
+ deviceId,
220
+ connectedAt: Date.now(),
221
+ };
222
+ if (role === "host") {
223
+ // Check if this is a reconnect (session already exists with clients)
224
+ const existingSession = sessionManager.get(sessionId);
225
+ const isReconnect = existingSession &&
226
+ existingSession.clients.size > 0 &&
227
+ existingSession.state === "host_disconnected";
228
+ sessionManager.setHost(sessionId, device);
229
+ if (isReconnect) {
230
+ const notification = serializeEnvelope(createEnvelope({
231
+ type: "session.host_reconnected",
232
+ sessionId,
233
+ payload: {},
234
+ }));
235
+ for (const [, client] of existingSession.clients) {
236
+ if (client.socket.readyState === client.socket.OPEN) {
237
+ client.socket.send(notification);
238
+ }
239
+ }
240
+ }
241
+ }
242
+ else {
243
+ sessionManager.addClient(sessionId, device);
244
+ }
245
+ // Send welcome with protocol version
246
+ socket.send(serializeEnvelope(createEnvelope({
247
+ type: "session.connect",
248
+ sessionId,
249
+ payload: {
250
+ role,
251
+ clientName: deviceId,
252
+ protocolVersion: PROTOCOL_VERSION,
253
+ },
254
+ })));
255
+ // Ping/pong for liveness
256
+ const pingTimer = setInterval(() => {
257
+ if (socket.readyState === socket.OPEN) {
258
+ socket.ping();
259
+ }
260
+ }, PING_INTERVAL);
261
+ socket.on("message", (data) => {
262
+ handleSocketMessage(socket, data.toString(), role, sessionId, deviceId, sessionManager);
263
+ });
264
+ socket.on("close", () => {
265
+ clearInterval(pingTimer);
266
+ if (role === "host") {
267
+ const result = sessionManager.removeHost(sessionId);
268
+ // Notify all clients that host disconnected
269
+ if (result) {
270
+ const notification = serializeEnvelope(createEnvelope({
271
+ type: "session.host_disconnected",
272
+ sessionId,
273
+ payload: { reason: "host connection closed" },
274
+ }));
275
+ for (const [, client] of result.clients) {
276
+ if (client.socket.readyState === client.socket.OPEN) {
277
+ client.socket.send(notification);
278
+ }
279
+ }
280
+ }
281
+ }
282
+ else {
283
+ sessionManager.removeClient(sessionId, deviceId);
284
+ }
285
+ });
286
+ socket.on("error", () => {
287
+ // close will fire
288
+ });
289
+ });
290
+ // ── Graceful shutdown ───────────────────────────────────────────────
291
+ function shutdown() {
292
+ process.stdout.write("[gateway] shutting down...\n");
293
+ wss.clients.forEach((ws) => ws.close(1001, "server shutting down"));
294
+ sessionManager.destroy();
295
+ pairingManager.destroy();
296
+ server.close(() => {
297
+ process.stdout.write("[gateway] stopped\n");
298
+ process.exit(0);
299
+ });
300
+ // Force exit after 5s
301
+ setTimeout(() => process.exit(1), 5000).unref();
302
+ }
303
+ process.on("SIGINT", shutdown);
304
+ process.on("SIGTERM", shutdown);
305
+ // ── Start ───────────────────────────────────────────────────────────
306
+ server.listen(port, () => {
307
+ log("info", `LinkShell Gateway v0.1.0`);
308
+ log("info", `listening on http://0.0.0.0:${port}`);
309
+ log("info", `log level: ${logLevel}`);
310
+ });
311
+ // ── Helpers ─────────────────────────────────────────────────────────
312
+ class BodyTooLargeError extends Error {
313
+ }
314
+ function json(res, status, body) {
315
+ res.writeHead(status, { "content-type": "application/json" });
316
+ res.end(JSON.stringify(body));
317
+ }
318
+ async function readJson(req) {
319
+ const chunks = [];
320
+ let size = 0;
321
+ for await (const chunk of req) {
322
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
323
+ size += buf.length;
324
+ if (size > MAX_BODY_SIZE)
325
+ throw new BodyTooLargeError();
326
+ chunks.push(buf);
327
+ }
328
+ if (chunks.length === 0)
329
+ return {};
330
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
331
+ }
332
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export interface PairingRecord {
2
+ sessionId: string;
3
+ pairingCode: string;
4
+ expiresAt: number;
5
+ claimed: boolean;
6
+ }
7
+ export declare class PairingManager {
8
+ private pairings;
9
+ private cleanupTimer;
10
+ constructor();
11
+ create(sessionId?: string): PairingRecord;
12
+ claim(pairingCode: string): PairingRecord | {
13
+ error: string;
14
+ status: number;
15
+ };
16
+ getStatus(pairingCode: string): {
17
+ status: string;
18
+ expiresAt: number;
19
+ sessionId: string;
20
+ } | {
21
+ error: string;
22
+ httpStatus: number;
23
+ };
24
+ private cleanup;
25
+ destroy(): void;
26
+ }
@@ -0,0 +1,61 @@
1
+ import { randomInt, randomUUID } from "node:crypto";
2
+ const PAIRING_TTL = 10 * 60_000; // 10 minutes
3
+ const CLEANUP_INTERVAL = 60_000;
4
+ export class PairingManager {
5
+ pairings = new Map();
6
+ cleanupTimer;
7
+ constructor() {
8
+ this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
9
+ }
10
+ create(sessionId) {
11
+ const id = sessionId ?? randomUUID();
12
+ const code = String(randomInt(100000, 999999));
13
+ const record = {
14
+ sessionId: id,
15
+ pairingCode: code,
16
+ expiresAt: Date.now() + PAIRING_TTL,
17
+ claimed: false,
18
+ };
19
+ this.pairings.set(code, record);
20
+ return record;
21
+ }
22
+ claim(pairingCode) {
23
+ const record = this.pairings.get(pairingCode);
24
+ if (!record) {
25
+ return { error: "pairing_not_found", status: 404 };
26
+ }
27
+ if (record.expiresAt < Date.now()) {
28
+ this.pairings.delete(pairingCode);
29
+ return { error: "pairing_expired", status: 410 };
30
+ }
31
+ record.claimed = true;
32
+ return record;
33
+ }
34
+ getStatus(pairingCode) {
35
+ const record = this.pairings.get(pairingCode);
36
+ if (!record) {
37
+ return { error: "pairing_not_found", httpStatus: 404 };
38
+ }
39
+ if (record.expiresAt < Date.now()) {
40
+ this.pairings.delete(pairingCode);
41
+ return { error: "pairing_expired", httpStatus: 410 };
42
+ }
43
+ return {
44
+ status: record.claimed ? "claimed" : "waiting",
45
+ expiresAt: record.expiresAt,
46
+ sessionId: record.sessionId,
47
+ };
48
+ }
49
+ cleanup() {
50
+ const now = Date.now();
51
+ for (const [code, record] of this.pairings) {
52
+ if (record.expiresAt < now) {
53
+ this.pairings.delete(code);
54
+ }
55
+ }
56
+ }
57
+ destroy() {
58
+ clearInterval(this.cleanupTimer);
59
+ }
60
+ }
61
+ //# sourceMappingURL=pairings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairings.js","sourceRoot":"","sources":["../../../src/pairings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AASpD,MAAM,WAAW,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,aAAa;AAC9C,MAAM,gBAAgB,GAAG,MAAM,CAAC;AAEhC,MAAM,OAAO,cAAc;IACjB,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAC5C,YAAY,CAAiC;IAErD;QACE,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,gBAAgB,CAAC,CAAC;IAC1E,CAAC;IAED,MAAM,CAAC,SAAkB;QACvB,MAAM,EAAE,GAAG,SAAS,IAAI,UAAU,EAAE,CAAC;QACrC,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAkB;YAC5B,SAAS,EAAE,EAAE;YACb,WAAW,EAAE,IAAI;YACjB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW;YACnC,OAAO,EAAE,KAAK;SACf,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAChC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,WAAmB;QACvB,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QACrD,CAAC;QACD,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAClC,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QACnD,CAAC;QACD,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,SAAS,CAAC,WAAmB;QAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;QACzD,CAAC;QACD,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;YAClC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAClC,OAAO,EAAE,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC;QACvD,CAAC;QACD,OAAO;YACL,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;YAC9C,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;SAC5B,CAAC;IACJ,CAAC;IAEO,OAAO;QACb,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3C,IAAI,MAAM,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC;gBAC3B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO;QACL,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACnC,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ import type WebSocket from "ws";
2
+ import type { SessionManager } from "./sessions.js";
3
+ export declare function handleSocketMessage(socket: WebSocket, raw: string, role: "host" | "client", sessionId: string, deviceId: string, sessions: SessionManager): void;
@@ -0,0 +1,156 @@
1
+ import { parseEnvelope, parseTypedPayload, serializeEnvelope, createEnvelope, } from "@linkshell/protocol";
2
+ export function handleSocketMessage(socket, raw, role, sessionId, deviceId, sessions) {
3
+ let envelope;
4
+ try {
5
+ envelope = parseEnvelope(raw);
6
+ }
7
+ catch {
8
+ socket.send(serializeEnvelope(createEnvelope({
9
+ type: "session.error",
10
+ sessionId,
11
+ payload: { code: "invalid_message", message: "Failed to parse envelope" },
12
+ })));
13
+ return;
14
+ }
15
+ const session = sessions.get(sessionId);
16
+ if (!session) {
17
+ socket.send(serializeEnvelope(createEnvelope({
18
+ type: "session.error",
19
+ sessionId,
20
+ payload: { code: "session_not_found", message: "Session not found" },
21
+ })));
22
+ return;
23
+ }
24
+ if (role === "host") {
25
+ handleHostMessage(envelope, session, sessions);
26
+ }
27
+ else {
28
+ handleClientMessage(envelope, socket, session, deviceId, sessions);
29
+ }
30
+ }
31
+ function handleHostMessage(envelope, session, sessions) {
32
+ switch (envelope.type) {
33
+ case "session.connect": {
34
+ // Extract metadata from host's connect message
35
+ const p = parseTypedPayload("session.connect", envelope.payload);
36
+ if (p.provider || p.hostname) {
37
+ sessions.setMetadata(session.id, p.provider ?? undefined, p.hostname ?? undefined);
38
+ }
39
+ break;
40
+ }
41
+ case "terminal.output": {
42
+ sessions.bufferOutput(session.id, envelope);
43
+ broadcastToClients(session, envelope);
44
+ break;
45
+ }
46
+ case "terminal.exit": {
47
+ sessions.terminate(session.id);
48
+ broadcastToClients(session, envelope);
49
+ break;
50
+ }
51
+ case "session.heartbeat":
52
+ break;
53
+ case "control.grant":
54
+ case "control.reject":
55
+ broadcastToClients(session, envelope);
56
+ break;
57
+ default:
58
+ broadcastToClients(session, envelope);
59
+ break;
60
+ }
61
+ }
62
+ function handleClientMessage(envelope, socket, session, deviceId, sessions) {
63
+ switch (envelope.type) {
64
+ case "terminal.input": {
65
+ // Only controller can send input
66
+ if (session.controllerId !== deviceId) {
67
+ socket.send(serializeEnvelope(createEnvelope({
68
+ type: "session.error",
69
+ sessionId: session.id,
70
+ payload: { code: "control_conflict", message: "Not the controller" },
71
+ })));
72
+ return;
73
+ }
74
+ sendToHost(session, envelope);
75
+ break;
76
+ }
77
+ case "terminal.resize": {
78
+ if (session.controllerId !== deviceId)
79
+ return;
80
+ sendToHost(session, envelope);
81
+ break;
82
+ }
83
+ case "session.ack": {
84
+ // Forward ACK to host
85
+ sendToHost(session, envelope);
86
+ break;
87
+ }
88
+ case "session.resume": {
89
+ const p = parseTypedPayload("session.resume", envelope.payload);
90
+ // Replay from gateway buffer first
91
+ const replay = sessions.getReplayFrom(session.id, p.lastAckedSeq);
92
+ for (const msg of replay) {
93
+ const payload = msg.payload;
94
+ socket.send(serializeEnvelope(createEnvelope({
95
+ type: "terminal.output",
96
+ sessionId: session.id,
97
+ seq: msg.seq,
98
+ payload: { ...payload, isReplay: true },
99
+ })));
100
+ }
101
+ // Also forward resume to host so it can fill gaps beyond gateway buffer
102
+ sendToHost(session, envelope);
103
+ break;
104
+ }
105
+ case "control.claim": {
106
+ const granted = sessions.claimControl(session.id, deviceId);
107
+ if (granted) {
108
+ const grantMsg = createEnvelope({
109
+ type: "control.grant",
110
+ sessionId: session.id,
111
+ payload: { deviceId },
112
+ });
113
+ socket.send(serializeEnvelope(grantMsg));
114
+ sendToHost(session, grantMsg);
115
+ }
116
+ else {
117
+ socket.send(serializeEnvelope(createEnvelope({
118
+ type: "control.reject",
119
+ sessionId: session.id,
120
+ payload: { deviceId, reason: "Another device holds control" },
121
+ })));
122
+ }
123
+ break;
124
+ }
125
+ case "control.release": {
126
+ sessions.releaseControl(session.id, deviceId);
127
+ const releaseMsg = createEnvelope({
128
+ type: "control.release",
129
+ sessionId: session.id,
130
+ payload: { deviceId },
131
+ });
132
+ broadcastToClients(session, releaseMsg);
133
+ sendToHost(session, releaseMsg);
134
+ break;
135
+ }
136
+ case "session.heartbeat":
137
+ break;
138
+ default:
139
+ sendToHost(session, envelope);
140
+ break;
141
+ }
142
+ }
143
+ function broadcastToClients(session, envelope) {
144
+ const data = serializeEnvelope(envelope);
145
+ for (const [, client] of session.clients) {
146
+ if (client.socket.readyState === client.socket.OPEN) {
147
+ client.socket.send(data);
148
+ }
149
+ }
150
+ }
151
+ function sendToHost(session, envelope) {
152
+ if (session.host && session.host.socket.readyState === session.host.socket.OPEN) {
153
+ session.host.socket.send(serializeEnvelope(envelope));
154
+ }
155
+ }
156
+ //# sourceMappingURL=relay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay.js","sourceRoot":"","sources":["../../../src/relay.ts"],"names":[],"mappings":"AACA,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,iBAAiB,EACjB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAI7B,MAAM,UAAU,mBAAmB,CACjC,MAAiB,EACjB,GAAW,EACX,IAAuB,EACvB,SAAiB,EACjB,QAAgB,EAChB,QAAwB;IAExB,IAAI,QAAkB,CAAC;IACvB,IAAI,CAAC;QACH,QAAQ,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,IAAI,CACT,iBAAiB,CACf,cAAc,CAAC;YACb,IAAI,EAAE,eAAe;YACrB,SAAS;YACT,OAAO,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,OAAO,EAAE,0BAA0B,EAAE;SAC1E,CAAC,CACH,CACF,CAAC;QACF,OAAO;IACT,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACxC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CACT,iBAAiB,CACf,cAAc,CAAC;YACb,IAAI,EAAE,eAAe;YACrB,SAAS;YACT,OAAO,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,mBAAmB,EAAE;SACrE,CAAC,CACH,CACF,CAAC;QACF,OAAO;IACT,CAAC;IAED,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QACpB,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;IACjD,CAAC;SAAM,CAAC;QACN,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CACxB,QAAkB,EAClB,OAA+C,EAC/C,QAAwB;IAExB,QAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,+CAA+C;YAC/C,MAAM,CAAC,GAAG,iBAAiB,CAAC,iBAAiB,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;YACjE,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;gBAC7B,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,IAAI,SAAS,EAAE,CAAC,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC;YACrF,CAAC;YACD,MAAM;QACR,CAAC;QACD,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YAC5C,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YACtC,MAAM;QACR,CAAC;QACD,KAAK,eAAe,CAAC,CAAC,CAAC;YACrB,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;YAC/B,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YACtC,MAAM;QACR,CAAC;QACD,KAAK,mBAAmB;YACtB,MAAM;QACR,KAAK,eAAe,CAAC;QACrB,KAAK,gBAAgB;YACnB,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YACtC,MAAM;QACR;YACE,kBAAkB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YACtC,MAAM;IACV,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAC1B,QAAkB,EAClB,MAAiB,EACjB,OAA+C,EAC/C,QAAgB,EAChB,QAAwB;IAExB,QAAQ,QAAQ,CAAC,IAAI,EAAE,CAAC;QACtB,KAAK,gBAAgB,CAAC,CAAC,CAAC;YACtB,iCAAiC;YACjC,IAAI,OAAO,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;gBACtC,MAAM,CAAC,IAAI,CACT,iBAAiB,CACf,cAAc,CAAC;oBACb,IAAI,EAAE,eAAe;oBACrB,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,OAAO,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,oBAAoB,EAAE;iBACrE,CAAC,CACH,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YACD,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC9B,MAAM;QACR,CAAC;QACD,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,IAAI,OAAO,CAAC,YAAY,KAAK,QAAQ;gBAAE,OAAO;YAC9C,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC9B,MAAM;QACR,CAAC;QACD,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,sBAAsB;YACtB,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC9B,MAAM;QACR,CAAC;QACD,KAAK,gBAAgB,CAAC,CAAC,CAAC;YACtB,MAAM,CAAC,GAAG,iBAAiB,CAAC,gBAAgB,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;YAChE,mCAAmC;YACnC,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;YAClE,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;gBACzB,MAAM,OAAO,GAAG,GAAG,CAAC,OAAkC,CAAC;gBACvD,MAAM,CAAC,IAAI,CACT,iBAAiB,CACf,cAAc,CAAC;oBACb,IAAI,EAAE,iBAAiB;oBACvB,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,GAAG,EAAE,GAAG,CAAC,GAAG;oBACZ,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE;iBACxC,CAAC,CACH,CACF,CAAC;YACJ,CAAC;YACD,wEAAwE;YACxE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC9B,MAAM;QACR,CAAC;QACD,KAAK,eAAe,CAAC,CAAC,CAAC;YACrB,MAAM,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YAC5D,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,QAAQ,GAAG,cAAc,CAAC;oBAC9B,IAAI,EAAE,eAAe;oBACrB,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,OAAO,EAAE,EAAE,QAAQ,EAAE;iBACtB,CAAC,CAAC;gBACH,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACzC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAChC,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CACT,iBAAiB,CACf,cAAc,CAAC;oBACb,IAAI,EAAE,gBAAgB;oBACtB,SAAS,EAAE,OAAO,CAAC,EAAE;oBACrB,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,8BAA8B,EAAE;iBAC9D,CAAC,CACH,CACF,CAAC;YACJ,CAAC;YACD,MAAM;QACR,CAAC;QACD,KAAK,iBAAiB,CAAC,CAAC,CAAC;YACvB,QAAQ,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;YAC9C,MAAM,UAAU,GAAG,cAAc,CAAC;gBAChC,IAAI,EAAE,iBAAiB;gBACvB,SAAS,EAAE,OAAO,CAAC,EAAE;gBACrB,OAAO,EAAE,EAAE,QAAQ,EAAE;aACtB,CAAC,CAAC;YACH,kBAAkB,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;YACxC,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;YAChC,MAAM;QACR,CAAC;QACD,KAAK,mBAAmB;YACtB,MAAM;QACR;YACE,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;YAC9B,MAAM;IACV,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CACzB,OAA+C,EAC/C,QAAkB;IAElB,MAAM,IAAI,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;IACzC,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;QACzC,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YACpD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CACjB,OAA+C,EAC/C,QAAkB;IAElB,IAAI,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAChF,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAC;IACxD,CAAC;AACH,CAAC"}