@linkshell/gateway 0.2.47 → 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 +23 -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 +271 -223
- package/dist/shared-protocol/src/index.js +31 -15
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +1 -1
- 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 +26 -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": {
|
|
@@ -213,8 +216,8 @@ function handleClientMessage(
|
|
|
213
216
|
socket.send(
|
|
214
217
|
serializeEnvelope(
|
|
215
218
|
createEnvelope({
|
|
216
|
-
type: "
|
|
217
|
-
|
|
219
|
+
type: "device.error",
|
|
220
|
+
hostDeviceId: session.id,
|
|
218
221
|
payload: {
|
|
219
222
|
code: "control_conflict",
|
|
220
223
|
message: "Not the controller",
|
|
@@ -236,11 +239,13 @@ function handleClientMessage(
|
|
|
236
239
|
sendToHost(session, envelope);
|
|
237
240
|
break;
|
|
238
241
|
}
|
|
242
|
+
case "device.ack":
|
|
239
243
|
case "session.ack": {
|
|
240
244
|
// Forward ACK to host
|
|
241
245
|
sendToHost(session, envelope);
|
|
242
246
|
break;
|
|
243
247
|
}
|
|
248
|
+
case "device.resume":
|
|
244
249
|
case "session.resume": {
|
|
245
250
|
const p = parseTypedPayload("session.resume", envelope.payload);
|
|
246
251
|
// Replay from gateway buffer first
|
|
@@ -255,7 +260,7 @@ function handleClientMessage(
|
|
|
255
260
|
serializeEnvelope(
|
|
256
261
|
createEnvelope({
|
|
257
262
|
type: "terminal.output",
|
|
258
|
-
|
|
263
|
+
hostDeviceId: session.id,
|
|
259
264
|
terminalId: msg.terminalId,
|
|
260
265
|
seq: msg.seq,
|
|
261
266
|
payload: { ...payload, isReplay: true },
|
|
@@ -284,7 +289,7 @@ function handleClientMessage(
|
|
|
284
289
|
sessions.claimControl(session.id, deviceId);
|
|
285
290
|
const grantMsg = createEnvelope({
|
|
286
291
|
type: "control.grant",
|
|
287
|
-
|
|
292
|
+
hostDeviceId: session.id,
|
|
288
293
|
payload: { deviceId },
|
|
289
294
|
});
|
|
290
295
|
// Broadcast to ALL clients so previous controller updates its state
|
|
@@ -296,13 +301,14 @@ function handleClientMessage(
|
|
|
296
301
|
sessions.releaseControl(session.id, deviceId);
|
|
297
302
|
const releaseMsg = createEnvelope({
|
|
298
303
|
type: "control.release",
|
|
299
|
-
|
|
304
|
+
hostDeviceId: session.id,
|
|
300
305
|
payload: { deviceId },
|
|
301
306
|
});
|
|
302
307
|
broadcastToClients(session, releaseMsg);
|
|
303
308
|
sendToHost(session, releaseMsg);
|
|
304
309
|
break;
|
|
305
310
|
}
|
|
311
|
+
case "device.heartbeat":
|
|
306
312
|
case "session.heartbeat":
|
|
307
313
|
break;
|
|
308
314
|
// Screen sharing: client → host
|
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 {}
|