@linkshell/gateway 0.2.46 → 0.2.48
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/dist/gateway/src/agent-permission-http.d.ts +18 -9
- package/dist/gateway/src/agent-permission-http.js +18 -10
- package/dist/gateway/src/agent-permission-http.js.map +1 -1
- package/dist/gateway/src/embedded.js +119 -55
- package/dist/gateway/src/embedded.js.map +1 -1
- package/dist/gateway/src/index.js +158 -91
- package/dist/gateway/src/index.js.map +1 -1
- package/dist/gateway/src/pairings.d.ts +3 -3
- package/dist/gateway/src/pairings.js +4 -5
- package/dist/gateway/src/pairings.js.map +1 -1
- package/dist/gateway/src/relay.d.ts +1 -1
- package/dist/gateway/src/relay.js +25 -18
- package/dist/gateway/src/relay.js.map +1 -1
- package/dist/gateway/src/sessions.d.ts +35 -28
- package/dist/gateway/src/sessions.js +165 -145
- package/dist/gateway/src/sessions.js.map +1 -1
- package/dist/gateway/src/state-store.d.ts +9 -6
- package/dist/gateway/src/state-store.js +26 -19
- package/dist/gateway/src/state-store.js.map +1 -1
- package/dist/gateway/src/tokens.d.ts +27 -7
- package/dist/gateway/src/tokens.js +86 -60
- package/dist/gateway/src/tokens.js.map +1 -1
- package/dist/gateway/src/tunnel.d.ts +11 -10
- package/dist/gateway/src/tunnel.js +46 -35
- package/dist/gateway/src/tunnel.js.map +1 -1
- package/dist/gateway/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +662 -494
- package/dist/shared-protocol/src/index.js +52 -15
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +2 -2
- package/src/agent-permission-http.ts +18 -10
- package/src/embedded.ts +122 -54
- package/src/index.ts +162 -91
- package/src/pairings.ts +6 -7
- package/src/relay.ts +28 -20
- package/src/sessions.ts +179 -150
- package/src/state-store.ts +41 -25
- package/src/tokens.ts +109 -63
- package/src/tunnel.ts +57 -39
package/src/relay.ts
CHANGED
|
@@ -20,7 +20,7 @@ export function handleSocketMessage(
|
|
|
20
20
|
socket: WebSocket,
|
|
21
21
|
raw: string,
|
|
22
22
|
role: "host" | "client",
|
|
23
|
-
|
|
23
|
+
hostDeviceId: string,
|
|
24
24
|
deviceId: string,
|
|
25
25
|
sessions: SessionManager,
|
|
26
26
|
): void {
|
|
@@ -28,23 +28,23 @@ export function handleSocketMessage(
|
|
|
28
28
|
try {
|
|
29
29
|
envelope = parseEnvelope(raw);
|
|
30
30
|
} catch {
|
|
31
|
-
sendSessionError(socket,
|
|
31
|
+
sendSessionError(socket, hostDeviceId, "invalid_message", "Failed to parse envelope");
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
if (envelope.
|
|
35
|
+
if (envelope.hostDeviceId !== hostDeviceId) {
|
|
36
36
|
sendSessionError(
|
|
37
37
|
socket,
|
|
38
|
-
|
|
38
|
+
hostDeviceId,
|
|
39
39
|
"invalid_message",
|
|
40
|
-
"Envelope
|
|
40
|
+
"Envelope hostDeviceId does not match connection hostDeviceId",
|
|
41
41
|
);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
const session = sessions.get(
|
|
45
|
+
const session = sessions.get(hostDeviceId);
|
|
46
46
|
if (!session) {
|
|
47
|
-
sendSessionError(socket,
|
|
47
|
+
sendSessionError(socket, hostDeviceId, "device_not_found", "Device not found");
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -65,13 +65,13 @@ export function handleSocketMessage(
|
|
|
65
65
|
if (error instanceof ZodError) {
|
|
66
66
|
sendSessionError(
|
|
67
67
|
socket,
|
|
68
|
-
|
|
68
|
+
hostDeviceId,
|
|
69
69
|
"invalid_message",
|
|
70
70
|
error.errors[0]?.message ?? "Invalid message payload",
|
|
71
71
|
);
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
-
sendSessionError(socket,
|
|
74
|
+
sendSessionError(socket, hostDeviceId, "invalid_message", "Failed to handle message");
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -81,7 +81,7 @@ function isProtocolMessageType(type: string): type is ProtocolMessageType {
|
|
|
81
81
|
|
|
82
82
|
function sendSessionError(
|
|
83
83
|
socket: WebSocket,
|
|
84
|
-
|
|
84
|
+
hostDeviceId: string,
|
|
85
85
|
code: string,
|
|
86
86
|
message: string,
|
|
87
87
|
): void {
|
|
@@ -89,8 +89,8 @@ function sendSessionError(
|
|
|
89
89
|
socket.send(
|
|
90
90
|
serializeEnvelope(
|
|
91
91
|
createEnvelope({
|
|
92
|
-
type: "
|
|
93
|
-
|
|
92
|
+
type: "device.error",
|
|
93
|
+
hostDeviceId,
|
|
94
94
|
payload: { code, message },
|
|
95
95
|
}),
|
|
96
96
|
),
|
|
@@ -103,18 +103,20 @@ function handleHostMessage(
|
|
|
103
103
|
sessions: SessionManager,
|
|
104
104
|
): void {
|
|
105
105
|
switch (envelope.type) {
|
|
106
|
+
case "device.connect":
|
|
106
107
|
case "session.connect": {
|
|
107
108
|
// Extract metadata from host's connect message
|
|
108
109
|
const p = parseTypedPayload("session.connect", envelope.payload);
|
|
109
|
-
if (p.
|
|
110
|
+
if (p.machineId || p.hostname || p.platform || p.cwd || p.capabilities) {
|
|
110
111
|
sessions.setMetadata(
|
|
111
112
|
session.id,
|
|
112
|
-
|
|
113
|
+
undefined,
|
|
113
114
|
p.machineId ?? undefined,
|
|
114
115
|
p.hostname ?? undefined,
|
|
115
116
|
p.platform ?? undefined,
|
|
116
117
|
p.cwd ?? undefined,
|
|
117
|
-
|
|
118
|
+
undefined,
|
|
119
|
+
p.capabilities ?? undefined,
|
|
118
120
|
);
|
|
119
121
|
}
|
|
120
122
|
break;
|
|
@@ -129,6 +131,7 @@ function handleHostMessage(
|
|
|
129
131
|
broadcastToClients(session, envelope);
|
|
130
132
|
break;
|
|
131
133
|
}
|
|
134
|
+
case "device.heartbeat":
|
|
132
135
|
case "session.heartbeat":
|
|
133
136
|
break;
|
|
134
137
|
case "permission.decision.result": {
|
|
@@ -187,6 +190,7 @@ function handleHostMessage(
|
|
|
187
190
|
case "terminal.spawned":
|
|
188
191
|
case "terminal.list":
|
|
189
192
|
case "terminal.browse.result":
|
|
193
|
+
case "terminal.file.read.result":
|
|
190
194
|
broadcastToClients(session, envelope);
|
|
191
195
|
break;
|
|
192
196
|
// Structured status from hooks
|
|
@@ -212,8 +216,8 @@ function handleClientMessage(
|
|
|
212
216
|
socket.send(
|
|
213
217
|
serializeEnvelope(
|
|
214
218
|
createEnvelope({
|
|
215
|
-
type: "
|
|
216
|
-
|
|
219
|
+
type: "device.error",
|
|
220
|
+
hostDeviceId: session.id,
|
|
217
221
|
payload: {
|
|
218
222
|
code: "control_conflict",
|
|
219
223
|
message: "Not the controller",
|
|
@@ -235,11 +239,13 @@ function handleClientMessage(
|
|
|
235
239
|
sendToHost(session, envelope);
|
|
236
240
|
break;
|
|
237
241
|
}
|
|
242
|
+
case "device.ack":
|
|
238
243
|
case "session.ack": {
|
|
239
244
|
// Forward ACK to host
|
|
240
245
|
sendToHost(session, envelope);
|
|
241
246
|
break;
|
|
242
247
|
}
|
|
248
|
+
case "device.resume":
|
|
243
249
|
case "session.resume": {
|
|
244
250
|
const p = parseTypedPayload("session.resume", envelope.payload);
|
|
245
251
|
// Replay from gateway buffer first
|
|
@@ -254,7 +260,7 @@ function handleClientMessage(
|
|
|
254
260
|
serializeEnvelope(
|
|
255
261
|
createEnvelope({
|
|
256
262
|
type: "terminal.output",
|
|
257
|
-
|
|
263
|
+
hostDeviceId: session.id,
|
|
258
264
|
terminalId: msg.terminalId,
|
|
259
265
|
seq: msg.seq,
|
|
260
266
|
payload: { ...payload, isReplay: true },
|
|
@@ -283,7 +289,7 @@ function handleClientMessage(
|
|
|
283
289
|
sessions.claimControl(session.id, deviceId);
|
|
284
290
|
const grantMsg = createEnvelope({
|
|
285
291
|
type: "control.grant",
|
|
286
|
-
|
|
292
|
+
hostDeviceId: session.id,
|
|
287
293
|
payload: { deviceId },
|
|
288
294
|
});
|
|
289
295
|
// Broadcast to ALL clients so previous controller updates its state
|
|
@@ -295,13 +301,14 @@ function handleClientMessage(
|
|
|
295
301
|
sessions.releaseControl(session.id, deviceId);
|
|
296
302
|
const releaseMsg = createEnvelope({
|
|
297
303
|
type: "control.release",
|
|
298
|
-
|
|
304
|
+
hostDeviceId: session.id,
|
|
299
305
|
payload: { deviceId },
|
|
300
306
|
});
|
|
301
307
|
broadcastToClients(session, releaseMsg);
|
|
302
308
|
sendToHost(session, releaseMsg);
|
|
303
309
|
break;
|
|
304
310
|
}
|
|
311
|
+
case "device.heartbeat":
|
|
305
312
|
case "session.heartbeat":
|
|
306
313
|
break;
|
|
307
314
|
// Screen sharing: client → host
|
|
@@ -325,6 +332,7 @@ function handleClientMessage(
|
|
|
325
332
|
case "terminal.kill":
|
|
326
333
|
case "terminal.list":
|
|
327
334
|
case "terminal.browse":
|
|
335
|
+
case "terminal.file.read":
|
|
328
336
|
case "terminal.mkdir":
|
|
329
337
|
case "terminal.history.request":
|
|
330
338
|
case "file.upload":
|
package/src/sessions.ts
CHANGED
|
@@ -1,54 +1,58 @@
|
|
|
1
1
|
import type WebSocket from "ws";
|
|
2
2
|
import type { Envelope } from "@linkshell/protocol";
|
|
3
3
|
|
|
4
|
-
export type
|
|
4
|
+
export type DeviceState = "active" | "host_disconnected" | "terminated";
|
|
5
|
+
export type SessionState = DeviceState;
|
|
5
6
|
|
|
6
7
|
export interface ConnectedDevice {
|
|
7
8
|
socket: WebSocket;
|
|
8
9
|
role: "host" | "client";
|
|
9
10
|
deviceId: string;
|
|
11
|
+
token?: string;
|
|
12
|
+
authorizationId?: string;
|
|
10
13
|
connectedAt: number;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
export interface
|
|
16
|
+
export interface HostDevice {
|
|
14
17
|
id: string;
|
|
15
|
-
|
|
18
|
+
hostDeviceId: string;
|
|
19
|
+
state: DeviceState;
|
|
16
20
|
host: ConnectedDevice | undefined;
|
|
17
21
|
clients: Map<string, ConnectedDevice>;
|
|
18
22
|
controllerId: string | undefined;
|
|
19
23
|
lastActivity: number;
|
|
20
24
|
createdAt: number;
|
|
21
|
-
outputBuffers: Map<string, Envelope[]>;
|
|
22
|
-
lastStatusByTerminal: Map<string, Envelope>;
|
|
25
|
+
outputBuffers: Map<string, Envelope[]>;
|
|
26
|
+
lastStatusByTerminal: Map<string, Envelope>;
|
|
23
27
|
hostDisconnectedAt: number | undefined;
|
|
24
|
-
// Metadata from host's session.connect
|
|
25
|
-
provider: string | undefined;
|
|
26
28
|
machineId: string | undefined;
|
|
27
29
|
hostname: string | undefined;
|
|
28
30
|
platform: string | undefined;
|
|
29
31
|
cwd: string | undefined;
|
|
30
|
-
|
|
31
|
-
// Auth: user who owns this session (set on AUTH_REQUIRED gateways)
|
|
32
|
+
capabilities: string[];
|
|
32
33
|
userId: string | undefined;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export type Session = HostDevice;
|
|
37
|
+
|
|
35
38
|
const OUTPUT_BUFFER_CAPACITY = 200;
|
|
36
|
-
const HOST_RECONNECT_WINDOW = 60_000;
|
|
39
|
+
const HOST_RECONNECT_WINDOW = 60_000;
|
|
37
40
|
const CLEANUP_INTERVAL = 30_000;
|
|
38
41
|
|
|
39
|
-
export class
|
|
40
|
-
private
|
|
42
|
+
export class DeviceManager {
|
|
43
|
+
private devices = new Map<string, HostDevice>();
|
|
41
44
|
private cleanupTimer: ReturnType<typeof setInterval>;
|
|
42
45
|
|
|
43
46
|
constructor() {
|
|
44
47
|
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
getOrCreate(
|
|
48
|
-
let
|
|
49
|
-
if (!
|
|
50
|
-
|
|
51
|
-
id:
|
|
50
|
+
getOrCreate(hostDeviceId: string): HostDevice {
|
|
51
|
+
let device = this.devices.get(hostDeviceId);
|
|
52
|
+
if (!device) {
|
|
53
|
+
device = {
|
|
54
|
+
id: hostDeviceId,
|
|
55
|
+
hostDeviceId,
|
|
52
56
|
state: "active",
|
|
53
57
|
host: undefined,
|
|
54
58
|
clients: new Map(),
|
|
@@ -58,119 +62,142 @@ export class SessionManager {
|
|
|
58
62
|
outputBuffers: new Map(),
|
|
59
63
|
lastStatusByTerminal: new Map(),
|
|
60
64
|
hostDisconnectedAt: undefined,
|
|
61
|
-
provider: undefined,
|
|
62
65
|
machineId: undefined,
|
|
63
66
|
hostname: undefined,
|
|
64
67
|
platform: undefined,
|
|
65
68
|
cwd: undefined,
|
|
66
|
-
|
|
69
|
+
capabilities: [],
|
|
67
70
|
userId: undefined,
|
|
68
71
|
};
|
|
69
|
-
this.
|
|
72
|
+
this.devices.set(hostDeviceId, device);
|
|
70
73
|
}
|
|
71
|
-
return
|
|
74
|
+
return device;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
get(
|
|
75
|
-
return this.
|
|
77
|
+
get(hostDeviceId: string): HostDevice | undefined {
|
|
78
|
+
return this.devices.get(hostDeviceId);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
|
-
setHost(
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
setHost(hostDeviceId: string, socketDevice: ConnectedDevice): void {
|
|
82
|
+
const device = this.getOrCreate(hostDeviceId);
|
|
83
|
+
device.host = socketDevice;
|
|
84
|
+
device.state = "active";
|
|
85
|
+
device.hostDisconnectedAt = undefined;
|
|
86
|
+
device.lastActivity = Date.now();
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
addClient(
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
if (!
|
|
90
|
-
|
|
89
|
+
addClient(hostDeviceId: string, socketDevice: ConnectedDevice): void {
|
|
90
|
+
const device = this.getOrCreate(hostDeviceId);
|
|
91
|
+
device.clients.set(socketDevice.deviceId, socketDevice);
|
|
92
|
+
if (!device.controllerId) {
|
|
93
|
+
device.controllerId = socketDevice.deviceId;
|
|
91
94
|
}
|
|
92
|
-
|
|
95
|
+
device.lastActivity = Date.now();
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
removeHost(
|
|
96
|
-
|
|
99
|
+
hostDeviceId: string,
|
|
97
100
|
): { clients: Map<string, ConnectedDevice> } | undefined {
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return { clients:
|
|
101
|
+
const device = this.devices.get(hostDeviceId);
|
|
102
|
+
if (!device) return undefined;
|
|
103
|
+
device.host = undefined;
|
|
104
|
+
device.state = "host_disconnected";
|
|
105
|
+
device.hostDisconnectedAt = Date.now();
|
|
106
|
+
return { clients: device.clients };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
removeClient(hostDeviceId: string, deviceId: string): void {
|
|
110
|
+
const device = this.devices.get(hostDeviceId);
|
|
111
|
+
if (!device) return;
|
|
112
|
+
device.clients.delete(deviceId);
|
|
113
|
+
if (device.controllerId === deviceId) {
|
|
114
|
+
const next = device.clients.keys().next();
|
|
115
|
+
device.controllerId = next.done ? undefined : next.value;
|
|
116
|
+
}
|
|
117
|
+
this.maybeDelete(hostDeviceId);
|
|
104
118
|
}
|
|
105
119
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
if (!
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
disconnectAuthorization(hostDeviceId: string, authorizationId: string): number {
|
|
121
|
+
const device = this.devices.get(hostDeviceId);
|
|
122
|
+
if (!device) return 0;
|
|
123
|
+
let closed = 0;
|
|
124
|
+
for (const [deviceId, client] of device.clients) {
|
|
125
|
+
if (client.authorizationId !== authorizationId) continue;
|
|
126
|
+
try {
|
|
127
|
+
client.socket.close(4001, "authorization revoked");
|
|
128
|
+
} catch {}
|
|
129
|
+
device.clients.delete(deviceId);
|
|
130
|
+
closed++;
|
|
131
|
+
}
|
|
132
|
+
if (device.controllerId && !device.clients.has(device.controllerId)) {
|
|
133
|
+
const next = device.clients.keys().next();
|
|
134
|
+
device.controllerId = next.done ? undefined : next.value;
|
|
114
135
|
}
|
|
115
|
-
this.maybeDelete(
|
|
136
|
+
this.maybeDelete(hostDeviceId);
|
|
137
|
+
return closed;
|
|
116
138
|
}
|
|
117
139
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
forceDelete(hostDeviceId: string): boolean {
|
|
141
|
+
const device = this.devices.get(hostDeviceId);
|
|
142
|
+
if (!device) return false;
|
|
143
|
+
if (device.host) {
|
|
144
|
+
try {
|
|
145
|
+
device.host.socket.close(1000, "device deleted");
|
|
146
|
+
} catch {}
|
|
124
147
|
}
|
|
125
|
-
for (const [, client] of
|
|
126
|
-
try {
|
|
148
|
+
for (const [, client] of device.clients) {
|
|
149
|
+
try {
|
|
150
|
+
client.socket.close(1000, "device deleted");
|
|
151
|
+
} catch {}
|
|
127
152
|
}
|
|
128
|
-
this.
|
|
153
|
+
this.devices.delete(hostDeviceId);
|
|
129
154
|
return true;
|
|
130
155
|
}
|
|
131
156
|
|
|
132
|
-
bufferOutput(
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
135
|
-
const
|
|
136
|
-
let
|
|
137
|
-
if (!
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
bufferOutput(hostDeviceId: string, envelope: Envelope): void {
|
|
158
|
+
const device = this.devices.get(hostDeviceId);
|
|
159
|
+
if (!device) return;
|
|
160
|
+
const terminalId = envelope.terminalId ?? "default";
|
|
161
|
+
let buffer = device.outputBuffers.get(terminalId);
|
|
162
|
+
if (!buffer) {
|
|
163
|
+
buffer = [];
|
|
164
|
+
device.outputBuffers.set(terminalId, buffer);
|
|
140
165
|
}
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
166
|
+
buffer.push(envelope);
|
|
167
|
+
if (buffer.length > OUTPUT_BUFFER_CAPACITY) {
|
|
168
|
+
buffer.shift();
|
|
144
169
|
}
|
|
145
|
-
|
|
170
|
+
device.lastActivity = Date.now();
|
|
146
171
|
}
|
|
147
172
|
|
|
148
|
-
cacheStatus(
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
173
|
+
cacheStatus(hostDeviceId: string, envelope: Envelope): void {
|
|
174
|
+
const device = this.devices.get(hostDeviceId);
|
|
175
|
+
if (!device) return;
|
|
176
|
+
const terminalId = envelope.terminalId ?? "default";
|
|
177
|
+
device.lastStatusByTerminal.set(terminalId, envelope);
|
|
178
|
+
device.lastActivity = Date.now();
|
|
154
179
|
}
|
|
155
180
|
|
|
156
|
-
getStatusReplay(
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
159
|
-
return [...
|
|
181
|
+
getStatusReplay(hostDeviceId: string): Envelope[] {
|
|
182
|
+
const device = this.devices.get(hostDeviceId);
|
|
183
|
+
if (!device) return [];
|
|
184
|
+
return [...device.lastStatusByTerminal.values()];
|
|
160
185
|
}
|
|
161
186
|
|
|
162
187
|
getReplayFrom(
|
|
163
|
-
|
|
188
|
+
hostDeviceId: string,
|
|
164
189
|
afterSeqByTerminal: Record<string, number>,
|
|
165
190
|
fallbackAfterSeq = -1,
|
|
166
191
|
): Envelope[] {
|
|
167
|
-
const
|
|
168
|
-
if (!
|
|
192
|
+
const device = this.devices.get(hostDeviceId);
|
|
193
|
+
if (!device) return [];
|
|
169
194
|
const result: Envelope[] = [];
|
|
170
|
-
for (const [terminalId,
|
|
195
|
+
for (const [terminalId, buffer] of device.outputBuffers) {
|
|
171
196
|
const afterSeq = afterSeqByTerminal[terminalId] ?? fallbackAfterSeq;
|
|
172
|
-
for (const
|
|
173
|
-
if (
|
|
197
|
+
for (const envelope of buffer) {
|
|
198
|
+
if (envelope.seq !== undefined && envelope.seq > afterSeq) {
|
|
199
|
+
result.push(envelope);
|
|
200
|
+
}
|
|
174
201
|
}
|
|
175
202
|
}
|
|
176
203
|
return result.sort((a, b) => {
|
|
@@ -181,101 +208,101 @@ export class SessionManager {
|
|
|
181
208
|
});
|
|
182
209
|
}
|
|
183
210
|
|
|
184
|
-
claimControl(
|
|
185
|
-
const
|
|
186
|
-
if (!
|
|
187
|
-
|
|
188
|
-
session.controllerId = deviceId;
|
|
211
|
+
claimControl(hostDeviceId: string, deviceId: string): boolean {
|
|
212
|
+
const device = this.devices.get(hostDeviceId);
|
|
213
|
+
if (!device) return false;
|
|
214
|
+
device.controllerId = deviceId;
|
|
189
215
|
return true;
|
|
190
216
|
}
|
|
191
217
|
|
|
192
|
-
releaseControl(
|
|
193
|
-
const
|
|
194
|
-
if (!
|
|
195
|
-
if (
|
|
196
|
-
|
|
218
|
+
releaseControl(hostDeviceId: string, deviceId: string): boolean {
|
|
219
|
+
const device = this.devices.get(hostDeviceId);
|
|
220
|
+
if (!device) return false;
|
|
221
|
+
if (device.controllerId !== deviceId) return false;
|
|
222
|
+
device.controllerId = undefined;
|
|
197
223
|
return true;
|
|
198
224
|
}
|
|
199
225
|
|
|
200
|
-
terminate(
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
|
|
226
|
+
terminate(hostDeviceId: string): void {
|
|
227
|
+
const device = this.devices.get(hostDeviceId);
|
|
228
|
+
if (!device) return;
|
|
229
|
+
device.state = "terminated";
|
|
204
230
|
}
|
|
205
231
|
|
|
206
|
-
listActive():
|
|
207
|
-
return [...this.
|
|
232
|
+
listActive(): HostDevice[] {
|
|
233
|
+
return [...this.devices.values()].filter((device) => device.state !== "terminated");
|
|
208
234
|
}
|
|
209
235
|
|
|
210
|
-
getSummary(
|
|
211
|
-
const
|
|
212
|
-
if (!
|
|
236
|
+
getSummary(hostDeviceId: string) {
|
|
237
|
+
const device = this.devices.get(hostDeviceId);
|
|
238
|
+
if (!device) return undefined;
|
|
213
239
|
return {
|
|
214
|
-
id:
|
|
215
|
-
|
|
240
|
+
id: device.hostDeviceId,
|
|
241
|
+
hostDeviceId: device.hostDeviceId,
|
|
242
|
+
state: device.state,
|
|
243
|
+
online:
|
|
244
|
+
!!device.host &&
|
|
245
|
+
device.host.socket.readyState === device.host.socket.OPEN,
|
|
216
246
|
hasHost:
|
|
217
|
-
!!
|
|
218
|
-
|
|
219
|
-
clientCount:
|
|
220
|
-
controllerId:
|
|
221
|
-
lastActivity:
|
|
222
|
-
createdAt:
|
|
223
|
-
bufferSize: [...
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
userId: session.userId ?? null,
|
|
247
|
+
!!device.host &&
|
|
248
|
+
device.host.socket.readyState === device.host.socket.OPEN,
|
|
249
|
+
clientCount: device.clients.size,
|
|
250
|
+
controllerId: device.controllerId ?? null,
|
|
251
|
+
lastActivity: device.lastActivity,
|
|
252
|
+
createdAt: device.createdAt,
|
|
253
|
+
bufferSize: [...device.outputBuffers.values()].reduce((sum, buf) => sum + buf.length, 0),
|
|
254
|
+
machineId: device.machineId ?? null,
|
|
255
|
+
hostname: device.hostname ?? null,
|
|
256
|
+
platform: device.platform ?? null,
|
|
257
|
+
cwd: device.cwd ?? null,
|
|
258
|
+
capabilities: device.capabilities,
|
|
259
|
+
userId: device.userId ?? null,
|
|
231
260
|
};
|
|
232
261
|
}
|
|
233
262
|
|
|
234
263
|
setMetadata(
|
|
235
|
-
|
|
236
|
-
|
|
264
|
+
hostDeviceId: string,
|
|
265
|
+
_provider?: string,
|
|
237
266
|
machineId?: string,
|
|
238
267
|
hostname?: string,
|
|
239
268
|
platform?: string,
|
|
240
269
|
cwd?: string,
|
|
241
|
-
|
|
270
|
+
_projectName?: string,
|
|
271
|
+
capabilities?: string[],
|
|
242
272
|
): void {
|
|
243
|
-
const
|
|
244
|
-
if (!
|
|
245
|
-
if (
|
|
246
|
-
if (
|
|
247
|
-
if (
|
|
248
|
-
if (
|
|
249
|
-
if (
|
|
250
|
-
if (projectName) session.projectName = projectName;
|
|
273
|
+
const device = this.devices.get(hostDeviceId);
|
|
274
|
+
if (!device) return;
|
|
275
|
+
if (machineId) device.machineId = machineId;
|
|
276
|
+
if (hostname) device.hostname = hostname;
|
|
277
|
+
if (platform) device.platform = platform;
|
|
278
|
+
if (cwd) device.cwd = cwd;
|
|
279
|
+
if (capabilities) device.capabilities = capabilities;
|
|
251
280
|
}
|
|
252
281
|
|
|
253
|
-
private maybeDelete(
|
|
254
|
-
const
|
|
255
|
-
if (!
|
|
256
|
-
if (!
|
|
257
|
-
this.
|
|
282
|
+
private maybeDelete(hostDeviceId: string): void {
|
|
283
|
+
const device = this.devices.get(hostDeviceId);
|
|
284
|
+
if (!device) return;
|
|
285
|
+
if (!device.host && device.clients.size === 0) {
|
|
286
|
+
this.devices.delete(hostDeviceId);
|
|
258
287
|
}
|
|
259
288
|
}
|
|
260
289
|
|
|
261
290
|
private cleanup(): void {
|
|
262
291
|
const now = Date.now();
|
|
263
|
-
for (const [
|
|
264
|
-
// Remove sessions where host disconnected and window expired
|
|
292
|
+
for (const [hostDeviceId, device] of this.devices) {
|
|
265
293
|
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
now -
|
|
294
|
+
device.state === "host_disconnected" &&
|
|
295
|
+
device.hostDisconnectedAt &&
|
|
296
|
+
now - device.hostDisconnectedAt > HOST_RECONNECT_WINDOW
|
|
269
297
|
) {
|
|
270
|
-
|
|
298
|
+
device.state = "terminated";
|
|
271
299
|
}
|
|
272
|
-
// Clean up terminated sessions with no connections
|
|
273
300
|
if (
|
|
274
|
-
|
|
275
|
-
!
|
|
276
|
-
|
|
301
|
+
device.state === "terminated" &&
|
|
302
|
+
!device.host &&
|
|
303
|
+
device.clients.size === 0
|
|
277
304
|
) {
|
|
278
|
-
this.
|
|
305
|
+
this.devices.delete(hostDeviceId);
|
|
279
306
|
}
|
|
280
307
|
}
|
|
281
308
|
}
|
|
@@ -284,3 +311,5 @@ export class SessionManager {
|
|
|
284
311
|
clearInterval(this.cleanupTimer);
|
|
285
312
|
}
|
|
286
313
|
}
|
|
314
|
+
|
|
315
|
+
export class SessionManager extends DeviceManager {}
|