@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.
Files changed (39) hide show
  1. package/dist/gateway/src/agent-permission-http.d.ts +18 -9
  2. package/dist/gateway/src/agent-permission-http.js +18 -10
  3. package/dist/gateway/src/agent-permission-http.js.map +1 -1
  4. package/dist/gateway/src/embedded.js +119 -55
  5. package/dist/gateway/src/embedded.js.map +1 -1
  6. package/dist/gateway/src/index.js +158 -91
  7. package/dist/gateway/src/index.js.map +1 -1
  8. package/dist/gateway/src/pairings.d.ts +3 -3
  9. package/dist/gateway/src/pairings.js +4 -5
  10. package/dist/gateway/src/pairings.js.map +1 -1
  11. package/dist/gateway/src/relay.d.ts +1 -1
  12. package/dist/gateway/src/relay.js +23 -18
  13. package/dist/gateway/src/relay.js.map +1 -1
  14. package/dist/gateway/src/sessions.d.ts +35 -28
  15. package/dist/gateway/src/sessions.js +165 -145
  16. package/dist/gateway/src/sessions.js.map +1 -1
  17. package/dist/gateway/src/state-store.d.ts +9 -6
  18. package/dist/gateway/src/state-store.js +26 -19
  19. package/dist/gateway/src/state-store.js.map +1 -1
  20. package/dist/gateway/src/tokens.d.ts +27 -7
  21. package/dist/gateway/src/tokens.js +86 -60
  22. package/dist/gateway/src/tokens.js.map +1 -1
  23. package/dist/gateway/src/tunnel.d.ts +11 -10
  24. package/dist/gateway/src/tunnel.js +46 -35
  25. package/dist/gateway/src/tunnel.js.map +1 -1
  26. package/dist/gateway/tsconfig.tsbuildinfo +1 -1
  27. package/dist/shared-protocol/src/index.d.ts +271 -223
  28. package/dist/shared-protocol/src/index.js +31 -15
  29. package/dist/shared-protocol/src/index.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/agent-permission-http.ts +18 -10
  32. package/src/embedded.ts +122 -54
  33. package/src/index.ts +162 -91
  34. package/src/pairings.ts +6 -7
  35. package/src/relay.ts +26 -20
  36. package/src/sessions.ts +179 -150
  37. package/src/state-store.ts +41 -25
  38. package/src/tokens.ts +109 -63
  39. 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
- sessionId: string,
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, sessionId, "invalid_message", "Failed to parse envelope");
31
+ sendSessionError(socket, hostDeviceId, "invalid_message", "Failed to parse envelope");
32
32
  return;
33
33
  }
34
34
 
35
- if (envelope.sessionId !== sessionId) {
35
+ if (envelope.hostDeviceId !== hostDeviceId) {
36
36
  sendSessionError(
37
37
  socket,
38
- sessionId,
38
+ hostDeviceId,
39
39
  "invalid_message",
40
- "Envelope sessionId does not match connection sessionId",
40
+ "Envelope hostDeviceId does not match connection hostDeviceId",
41
41
  );
42
42
  return;
43
43
  }
44
44
 
45
- const session = sessions.get(sessionId);
45
+ const session = sessions.get(hostDeviceId);
46
46
  if (!session) {
47
- sendSessionError(socket, sessionId, "session_not_found", "Session not found");
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
- sessionId,
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, sessionId, "invalid_message", "Failed to handle message");
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
- sessionId: string,
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: "session.error",
93
- sessionId,
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.provider || p.machineId || p.hostname || p.platform || p.cwd || p.projectName) {
110
+ if (p.machineId || p.hostname || p.platform || p.cwd || p.capabilities) {
110
111
  sessions.setMetadata(
111
112
  session.id,
112
- p.provider ?? undefined,
113
+ undefined,
113
114
  p.machineId ?? undefined,
114
115
  p.hostname ?? undefined,
115
116
  p.platform ?? undefined,
116
117
  p.cwd ?? undefined,
117
- p.projectName ?? undefined,
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: "session.error",
217
- sessionId: session.id,
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
- sessionId: session.id,
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
- sessionId: session.id,
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
- sessionId: session.id,
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 SessionState = "active" | "host_disconnected" | "terminated";
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 Session {
16
+ export interface HostDevice {
14
17
  id: string;
15
- state: SessionState;
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[]>; // keyed by terminalId
22
- lastStatusByTerminal: Map<string, Envelope>; // last terminal.status per terminal
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
- projectName: string | undefined;
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; // 60s
39
+ const HOST_RECONNECT_WINDOW = 60_000;
37
40
  const CLEANUP_INTERVAL = 30_000;
38
41
 
39
- export class SessionManager {
40
- private sessions = new Map<string, Session>();
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(sessionId: string): Session {
48
- let session = this.sessions.get(sessionId);
49
- if (!session) {
50
- session = {
51
- id: sessionId,
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
- projectName: undefined,
69
+ capabilities: [],
67
70
  userId: undefined,
68
71
  };
69
- this.sessions.set(sessionId, session);
72
+ this.devices.set(hostDeviceId, device);
70
73
  }
71
- return session;
74
+ return device;
72
75
  }
73
76
 
74
- get(sessionId: string): Session | undefined {
75
- return this.sessions.get(sessionId);
77
+ get(hostDeviceId: string): HostDevice | undefined {
78
+ return this.devices.get(hostDeviceId);
76
79
  }
77
80
 
78
- setHost(sessionId: string, device: ConnectedDevice): void {
79
- const session = this.getOrCreate(sessionId);
80
- session.host = device;
81
- session.state = "active";
82
- session.hostDisconnectedAt = undefined;
83
- session.lastActivity = Date.now();
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(sessionId: string, device: ConnectedDevice): void {
87
- const session = this.getOrCreate(sessionId);
88
- session.clients.set(device.deviceId, device);
89
- if (!session.controllerId) {
90
- session.controllerId = device.deviceId;
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
- session.lastActivity = Date.now();
95
+ device.lastActivity = Date.now();
93
96
  }
94
97
 
95
98
  removeHost(
96
- sessionId: string,
99
+ hostDeviceId: string,
97
100
  ): { clients: Map<string, ConnectedDevice> } | undefined {
98
- const session = this.sessions.get(sessionId);
99
- if (!session) return undefined;
100
- session.host = undefined;
101
- session.state = "host_disconnected";
102
- session.hostDisconnectedAt = Date.now();
103
- return { clients: session.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
- removeClient(sessionId: string, deviceId: string): void {
107
- const session = this.sessions.get(sessionId);
108
- if (!session) return;
109
- session.clients.delete(deviceId);
110
- if (session.controllerId === deviceId) {
111
- // Transfer control to next client or clear
112
- const next = session.clients.keys().next();
113
- session.controllerId = next.done ? undefined : next.value;
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(sessionId);
136
+ this.maybeDelete(hostDeviceId);
137
+ return closed;
116
138
  }
117
139
 
118
- /** Force-delete a session: disconnect host + all clients, remove from map. */
119
- forceDelete(sessionId: string): boolean {
120
- const session = this.sessions.get(sessionId);
121
- if (!session) return false;
122
- if (session.host) {
123
- try { session.host.socket.close(1000, "session deleted"); } catch {}
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 session.clients) {
126
- try { client.socket.close(1000, "session deleted"); } catch {}
148
+ for (const [, client] of device.clients) {
149
+ try {
150
+ client.socket.close(1000, "device deleted");
151
+ } catch {}
127
152
  }
128
- this.sessions.delete(sessionId);
153
+ this.devices.delete(hostDeviceId);
129
154
  return true;
130
155
  }
131
156
 
132
- bufferOutput(sessionId: string, envelope: Envelope): void {
133
- const session = this.sessions.get(sessionId);
134
- if (!session) return;
135
- const tid = (envelope as any).terminalId ?? "default";
136
- let buf = session.outputBuffers.get(tid);
137
- if (!buf) {
138
- buf = [];
139
- session.outputBuffers.set(tid, buf);
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
- buf.push(envelope);
142
- if (buf.length > OUTPUT_BUFFER_CAPACITY) {
143
- buf.shift();
166
+ buffer.push(envelope);
167
+ if (buffer.length > OUTPUT_BUFFER_CAPACITY) {
168
+ buffer.shift();
144
169
  }
145
- session.lastActivity = Date.now();
170
+ device.lastActivity = Date.now();
146
171
  }
147
172
 
148
- cacheStatus(sessionId: string, envelope: Envelope): void {
149
- const session = this.sessions.get(sessionId);
150
- if (!session) return;
151
- const tid = (envelope as any).terminalId ?? "default";
152
- session.lastStatusByTerminal.set(tid, envelope);
153
- session.lastActivity = Date.now();
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(sessionId: string): Envelope[] {
157
- const session = this.sessions.get(sessionId);
158
- if (!session) return [];
159
- return [...session.lastStatusByTerminal.values()];
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
- sessionId: string,
188
+ hostDeviceId: string,
164
189
  afterSeqByTerminal: Record<string, number>,
165
190
  fallbackAfterSeq = -1,
166
191
  ): Envelope[] {
167
- const session = this.sessions.get(sessionId);
168
- if (!session) return [];
192
+ const device = this.devices.get(hostDeviceId);
193
+ if (!device) return [];
169
194
  const result: Envelope[] = [];
170
- for (const [terminalId, buf] of session.outputBuffers) {
195
+ for (const [terminalId, buffer] of device.outputBuffers) {
171
196
  const afterSeq = afterSeqByTerminal[terminalId] ?? fallbackAfterSeq;
172
- for (const e of buf) {
173
- if (e.seq !== undefined && e.seq > afterSeq) result.push(e);
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(sessionId: string, deviceId: string): boolean {
185
- const session = this.sessions.get(sessionId);
186
- if (!session) return false;
187
- // Always allow takeover – last claimer wins
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(sessionId: string, deviceId: string): boolean {
193
- const session = this.sessions.get(sessionId);
194
- if (!session) return false;
195
- if (session.controllerId !== deviceId) return false;
196
- session.controllerId = undefined;
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(sessionId: string): void {
201
- const session = this.sessions.get(sessionId);
202
- if (!session) return;
203
- session.state = "terminated";
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(): Session[] {
207
- return [...this.sessions.values()].filter((s) => s.state !== "terminated");
232
+ listActive(): HostDevice[] {
233
+ return [...this.devices.values()].filter((device) => device.state !== "terminated");
208
234
  }
209
235
 
210
- getSummary(sessionId: string) {
211
- const session = this.sessions.get(sessionId);
212
- if (!session) return undefined;
236
+ getSummary(hostDeviceId: string) {
237
+ const device = this.devices.get(hostDeviceId);
238
+ if (!device) return undefined;
213
239
  return {
214
- id: session.id,
215
- state: session.state,
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
- !!session.host &&
218
- session.host.socket.readyState === session.host.socket.OPEN,
219
- clientCount: session.clients.size,
220
- controllerId: session.controllerId ?? null,
221
- lastActivity: session.lastActivity,
222
- createdAt: session.createdAt,
223
- bufferSize: [...session.outputBuffers.values()].reduce((sum, buf) => sum + buf.length, 0),
224
- provider: session.provider ?? null,
225
- machineId: session.machineId ?? null,
226
- hostname: session.hostname ?? null,
227
- platform: session.platform ?? null,
228
- cwd: session.cwd ?? null,
229
- projectName: session.projectName ?? null,
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
- sessionId: string,
236
- provider?: string,
264
+ hostDeviceId: string,
265
+ _provider?: string,
237
266
  machineId?: string,
238
267
  hostname?: string,
239
268
  platform?: string,
240
269
  cwd?: string,
241
- projectName?: string,
270
+ _projectName?: string,
271
+ capabilities?: string[],
242
272
  ): void {
243
- const session = this.sessions.get(sessionId);
244
- if (!session) return;
245
- if (provider) session.provider = provider;
246
- if (machineId) session.machineId = machineId;
247
- if (hostname) session.hostname = hostname;
248
- if (platform) session.platform = platform;
249
- if (cwd) session.cwd = cwd;
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(sessionId: string): void {
254
- const session = this.sessions.get(sessionId);
255
- if (!session) return;
256
- if (!session.host && session.clients.size === 0) {
257
- this.sessions.delete(sessionId);
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 [id, session] of this.sessions) {
264
- // Remove sessions where host disconnected and window expired
292
+ for (const [hostDeviceId, device] of this.devices) {
265
293
  if (
266
- session.state === "host_disconnected" &&
267
- session.hostDisconnectedAt &&
268
- now - session.hostDisconnectedAt > HOST_RECONNECT_WINDOW
294
+ device.state === "host_disconnected" &&
295
+ device.hostDisconnectedAt &&
296
+ now - device.hostDisconnectedAt > HOST_RECONNECT_WINDOW
269
297
  ) {
270
- session.state = "terminated";
298
+ device.state = "terminated";
271
299
  }
272
- // Clean up terminated sessions with no connections
273
300
  if (
274
- session.state === "terminated" &&
275
- !session.host &&
276
- session.clients.size === 0
301
+ device.state === "terminated" &&
302
+ !device.host &&
303
+ device.clients.size === 0
277
304
  ) {
278
- this.sessions.delete(id);
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 {}