@linkshell/gateway 0.3.2 → 0.3.3
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/Dockerfile +1 -0
- package/dist/gateway/src/embedded.js +1 -1
- package/dist/gateway/src/embedded.js.map +1 -1
- package/dist/gateway/src/index.js +13 -2
- package/dist/gateway/src/index.js.map +1 -1
- package/dist/gateway/src/relay.js +33 -25
- package/dist/gateway/src/relay.js.map +1 -1
- package/dist/gateway/src/sessions.d.ts +7 -0
- package/dist/gateway/src/sessions.js +24 -0
- package/dist/gateway/src/sessions.js.map +1 -1
- package/dist/gateway/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +24 -24
- package/dist/shared-protocol/src/index.js +4 -4
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +2 -2
- package/src/embedded.ts +3 -1
- package/src/index.ts +15 -2
- package/src/relay.ts +34 -21
- package/src/sessions.ts +29 -0
package/src/index.ts
CHANGED
|
@@ -48,7 +48,9 @@ await Promise.all([pairingManager.hydrate(), tokenManager.hydrate()]);
|
|
|
48
48
|
|
|
49
49
|
const PING_INTERVAL = 20_000;
|
|
50
50
|
const MAX_BODY_SIZE = 4096;
|
|
51
|
-
const MAX_WS_MESSAGE_SIZE =
|
|
51
|
+
const MAX_WS_MESSAGE_SIZE = Number(
|
|
52
|
+
process.env.MAX_WS_MESSAGE_SIZE ?? 16 * 1024 * 1024,
|
|
53
|
+
);
|
|
52
54
|
const PAIRING_RATE_LIMIT_MAX = Number(process.env.PAIRING_RATE_LIMIT_MAX ?? 30);
|
|
53
55
|
const PAIRING_RATE_LIMIT_WINDOW_MS = Number(
|
|
54
56
|
process.env.PAIRING_RATE_LIMIT_WINDOW_MS ?? 60_000,
|
|
@@ -179,7 +181,18 @@ async function handleRequest(
|
|
|
179
181
|
|
|
180
182
|
// Health check
|
|
181
183
|
if (method === "GET" && url.pathname === "/healthz") {
|
|
182
|
-
|
|
184
|
+
const memory = process.memoryUsage();
|
|
185
|
+
json(res, 200, {
|
|
186
|
+
ok: true,
|
|
187
|
+
uptime: Math.round(process.uptime()),
|
|
188
|
+
memory: {
|
|
189
|
+
rss: memory.rss,
|
|
190
|
+
heapUsed: memory.heapUsed,
|
|
191
|
+
heapTotal: memory.heapTotal,
|
|
192
|
+
external: memory.external,
|
|
193
|
+
},
|
|
194
|
+
sessions: sessionManager.getStats(),
|
|
195
|
+
});
|
|
183
196
|
return;
|
|
184
197
|
}
|
|
185
198
|
|
package/src/relay.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type WebSocket from "ws";
|
|
|
2
2
|
import {
|
|
3
3
|
parseEnvelope,
|
|
4
4
|
parseTypedPayload,
|
|
5
|
-
protocolMessageSchemas,
|
|
6
5
|
serializeEnvelope,
|
|
7
6
|
createEnvelope,
|
|
8
7
|
} from "@linkshell/protocol";
|
|
@@ -49,7 +48,7 @@ export function handleSocketMessage(
|
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
try {
|
|
52
|
-
if (
|
|
51
|
+
if (shouldValidatePayload(envelope.type)) {
|
|
53
52
|
envelope = {
|
|
54
53
|
...envelope,
|
|
55
54
|
payload: parseTypedPayload(envelope.type, envelope.payload),
|
|
@@ -57,9 +56,9 @@ export function handleSocketMessage(
|
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
if (role === "host") {
|
|
60
|
-
handleHostMessage(envelope, session, sessions);
|
|
59
|
+
handleHostMessage(envelope, raw, session, sessions);
|
|
61
60
|
} else {
|
|
62
|
-
handleClientMessage(envelope, socket, session, deviceId, sessions);
|
|
61
|
+
handleClientMessage(envelope, raw, socket, session, deviceId, sessions);
|
|
63
62
|
}
|
|
64
63
|
} catch (error) {
|
|
65
64
|
if (error instanceof ZodError) {
|
|
@@ -75,8 +74,18 @@ export function handleSocketMessage(
|
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
function
|
|
79
|
-
return
|
|
77
|
+
function shouldValidatePayload(type: string): type is ProtocolMessageType {
|
|
78
|
+
return (
|
|
79
|
+
type === "device.connect" ||
|
|
80
|
+
type === "device.ack" ||
|
|
81
|
+
type === "device.resume" ||
|
|
82
|
+
type === "terminal.input" ||
|
|
83
|
+
type === "terminal.resize" ||
|
|
84
|
+
type === "permission.decision" ||
|
|
85
|
+
type === "permission.decision.result" ||
|
|
86
|
+
type === "control.claim" ||
|
|
87
|
+
type === "control.release"
|
|
88
|
+
);
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
function sendSessionError(
|
|
@@ -99,6 +108,7 @@ function sendSessionError(
|
|
|
99
108
|
|
|
100
109
|
function handleHostMessage(
|
|
101
110
|
envelope: Envelope,
|
|
111
|
+
raw: string,
|
|
102
112
|
session: ReturnType<DeviceManager["get"]> & {},
|
|
103
113
|
sessions: DeviceManager,
|
|
104
114
|
): void {
|
|
@@ -122,12 +132,12 @@ function handleHostMessage(
|
|
|
122
132
|
}
|
|
123
133
|
case "terminal.output": {
|
|
124
134
|
sessions.bufferOutput(session.id, envelope);
|
|
125
|
-
broadcastToClients(session, envelope);
|
|
135
|
+
broadcastToClients(session, envelope, raw);
|
|
126
136
|
break;
|
|
127
137
|
}
|
|
128
138
|
case "terminal.exit": {
|
|
129
139
|
// Don't terminate session — other terminals may still be running
|
|
130
|
-
broadcastToClients(session, envelope);
|
|
140
|
+
broadcastToClients(session, envelope, raw);
|
|
131
141
|
break;
|
|
132
142
|
}
|
|
133
143
|
case "device.heartbeat":
|
|
@@ -145,7 +155,7 @@ function handleHostMessage(
|
|
|
145
155
|
message: p.message,
|
|
146
156
|
},
|
|
147
157
|
});
|
|
148
|
-
broadcastToClients(session, envelope);
|
|
158
|
+
broadcastToClients(session, envelope, raw);
|
|
149
159
|
break;
|
|
150
160
|
}
|
|
151
161
|
// Tunnel: host → gateway (not broadcast to clients)
|
|
@@ -166,7 +176,7 @@ function handleHostMessage(
|
|
|
166
176
|
}
|
|
167
177
|
case "control.grant":
|
|
168
178
|
case "control.reject":
|
|
169
|
-
broadcastToClients(session, envelope);
|
|
179
|
+
broadcastToClients(session, envelope, raw);
|
|
170
180
|
break;
|
|
171
181
|
// Screen sharing: host → clients
|
|
172
182
|
case "screen.frame":
|
|
@@ -185,21 +195,22 @@ function handleHostMessage(
|
|
|
185
195
|
case "terminal.list":
|
|
186
196
|
case "terminal.browse.result":
|
|
187
197
|
case "terminal.file.read.result":
|
|
188
|
-
broadcastToClients(session, envelope);
|
|
198
|
+
broadcastToClients(session, envelope, raw);
|
|
189
199
|
break;
|
|
190
200
|
// Structured status from hooks
|
|
191
201
|
case "terminal.status":
|
|
192
202
|
sessions.cacheStatus(session.id, envelope);
|
|
193
|
-
broadcastToClients(session, envelope);
|
|
203
|
+
broadcastToClients(session, envelope, raw);
|
|
194
204
|
break;
|
|
195
205
|
default:
|
|
196
|
-
broadcastToClients(session, envelope);
|
|
206
|
+
broadcastToClients(session, envelope, raw);
|
|
197
207
|
break;
|
|
198
208
|
}
|
|
199
209
|
}
|
|
200
210
|
|
|
201
211
|
function handleClientMessage(
|
|
202
212
|
envelope: Envelope,
|
|
213
|
+
raw: string,
|
|
203
214
|
socket: WebSocket,
|
|
204
215
|
session: ReturnType<DeviceManager["get"]> & {},
|
|
205
216
|
deviceId: string,
|
|
@@ -225,17 +236,17 @@ function handleClientMessage(
|
|
|
225
236
|
switch (envelope.type) {
|
|
226
237
|
case "terminal.input": {
|
|
227
238
|
if (!requireController()) return;
|
|
228
|
-
sendToHost(session, envelope);
|
|
239
|
+
sendToHost(session, envelope, raw);
|
|
229
240
|
break;
|
|
230
241
|
}
|
|
231
242
|
case "terminal.resize": {
|
|
232
243
|
if (!requireController()) return;
|
|
233
|
-
sendToHost(session, envelope);
|
|
244
|
+
sendToHost(session, envelope, raw);
|
|
234
245
|
break;
|
|
235
246
|
}
|
|
236
247
|
case "device.ack": {
|
|
237
248
|
// Forward ACK to host
|
|
238
|
-
sendToHost(session, envelope);
|
|
249
|
+
sendToHost(session, envelope, raw);
|
|
239
250
|
break;
|
|
240
251
|
}
|
|
241
252
|
case "device.resume": {
|
|
@@ -324,15 +335,15 @@ function handleClientMessage(
|
|
|
324
335
|
case "file.upload":
|
|
325
336
|
case "permission.decision":
|
|
326
337
|
if (!requireController()) return;
|
|
327
|
-
sendToHost(session, envelope);
|
|
338
|
+
sendToHost(session, envelope, raw);
|
|
328
339
|
break;
|
|
329
340
|
case "agent.v2.capabilities.request":
|
|
330
341
|
case "agent.v2.conversation.list":
|
|
331
342
|
case "agent.v2.snapshot.request":
|
|
332
|
-
sendToHost(session, envelope);
|
|
343
|
+
sendToHost(session, envelope, raw);
|
|
333
344
|
break;
|
|
334
345
|
default:
|
|
335
|
-
sendToHost(session, envelope);
|
|
346
|
+
sendToHost(session, envelope, raw);
|
|
336
347
|
break;
|
|
337
348
|
}
|
|
338
349
|
}
|
|
@@ -340,8 +351,9 @@ function handleClientMessage(
|
|
|
340
351
|
function broadcastToClients(
|
|
341
352
|
session: ReturnType<DeviceManager["get"]> & {},
|
|
342
353
|
envelope: Envelope,
|
|
354
|
+
raw?: string,
|
|
343
355
|
): void {
|
|
344
|
-
const data = serializeEnvelope(envelope);
|
|
356
|
+
const data = raw ?? serializeEnvelope(envelope);
|
|
345
357
|
for (const [, client] of session.clients) {
|
|
346
358
|
if (client.socket.readyState === client.socket.OPEN) {
|
|
347
359
|
client.socket.send(data);
|
|
@@ -352,11 +364,12 @@ function broadcastToClients(
|
|
|
352
364
|
function sendToHost(
|
|
353
365
|
session: ReturnType<DeviceManager["get"]> & {},
|
|
354
366
|
envelope: Envelope,
|
|
367
|
+
raw?: string,
|
|
355
368
|
): void {
|
|
356
369
|
if (
|
|
357
370
|
session.host &&
|
|
358
371
|
session.host.socket.readyState === session.host.socket.OPEN
|
|
359
372
|
) {
|
|
360
|
-
session.host.socket.send(serializeEnvelope(envelope));
|
|
373
|
+
session.host.socket.send(raw ?? serializeEnvelope(envelope));
|
|
361
374
|
}
|
|
362
375
|
}
|
package/src/sessions.ts
CHANGED
|
@@ -33,6 +33,9 @@ export interface HostDevice {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const OUTPUT_BUFFER_CAPACITY = 200;
|
|
36
|
+
const OUTPUT_BUFFER_MAX_PAYLOAD_BYTES = Number(
|
|
37
|
+
process.env.OUTPUT_BUFFER_MAX_PAYLOAD_BYTES ?? 64 * 1024,
|
|
38
|
+
);
|
|
36
39
|
const HOST_RECONNECT_WINDOW = 60_000;
|
|
37
40
|
const CLEANUP_INTERVAL = 30_000;
|
|
38
41
|
|
|
@@ -154,6 +157,14 @@ export class DeviceManager {
|
|
|
154
157
|
bufferOutput(hostDeviceId: string, envelope: Envelope): void {
|
|
155
158
|
const device = this.devices.get(hostDeviceId);
|
|
156
159
|
if (!device) return;
|
|
160
|
+
const payload = envelope.payload as { data?: unknown } | undefined;
|
|
161
|
+
if (
|
|
162
|
+
typeof payload?.data === "string" &&
|
|
163
|
+
Buffer.byteLength(payload.data, "utf8") > OUTPUT_BUFFER_MAX_PAYLOAD_BYTES
|
|
164
|
+
) {
|
|
165
|
+
device.lastActivity = Date.now();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
157
168
|
const terminalId = envelope.terminalId ?? "default";
|
|
158
169
|
let buffer = device.outputBuffers.get(terminalId);
|
|
159
170
|
if (!buffer) {
|
|
@@ -257,6 +268,24 @@ export class DeviceManager {
|
|
|
257
268
|
};
|
|
258
269
|
}
|
|
259
270
|
|
|
271
|
+
getStats() {
|
|
272
|
+
let clientCount = 0;
|
|
273
|
+
let bufferedTerminalFrames = 0;
|
|
274
|
+
let terminalCount = 0;
|
|
275
|
+
for (const device of this.devices.values()) {
|
|
276
|
+
clientCount += device.clients.size;
|
|
277
|
+
terminalCount += device.outputBuffers.size;
|
|
278
|
+
bufferedTerminalFrames += [...device.outputBuffers.values()].reduce((sum, buf) => sum + buf.length, 0);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
devices: this.devices.size,
|
|
282
|
+
activeDevices: this.listActive().length,
|
|
283
|
+
clients: clientCount,
|
|
284
|
+
terminalsWithReplay: terminalCount,
|
|
285
|
+
bufferedTerminalFrames,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
260
289
|
setMetadata(
|
|
261
290
|
hostDeviceId: string,
|
|
262
291
|
_provider?: string,
|