@linkshell/gateway 0.3.9 → 0.4.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.
Files changed (41) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +13 -14
  3. package/dist/gateway/src/agent-permission-http.d.ts +74 -19
  4. package/dist/gateway/src/agent-permission-http.js +56 -16
  5. package/dist/gateway/src/agent-permission-http.js.map +1 -1
  6. package/dist/gateway/src/embedded.js +61 -153
  7. package/dist/gateway/src/embedded.js.map +1 -1
  8. package/dist/gateway/src/index.js +98 -193
  9. package/dist/gateway/src/index.js.map +1 -1
  10. package/dist/gateway/src/pairings.d.ts +3 -3
  11. package/dist/gateway/src/pairings.js +5 -4
  12. package/dist/gateway/src/pairings.js.map +1 -1
  13. package/dist/gateway/src/relay.d.ts +2 -2
  14. package/dist/gateway/src/relay.js +85 -161
  15. package/dist/gateway/src/relay.js.map +1 -1
  16. package/dist/gateway/src/sessions.d.ts +28 -42
  17. package/dist/gateway/src/sessions.js +145 -200
  18. package/dist/gateway/src/sessions.js.map +1 -1
  19. package/dist/gateway/src/state-store.d.ts +6 -9
  20. package/dist/gateway/src/state-store.js +19 -26
  21. package/dist/gateway/src/state-store.js.map +1 -1
  22. package/dist/gateway/src/tokens.d.ts +7 -27
  23. package/dist/gateway/src/tokens.js +60 -86
  24. package/dist/gateway/src/tokens.js.map +1 -1
  25. package/dist/gateway/src/tunnel.d.ts +13 -11
  26. package/dist/gateway/src/tunnel.js +36 -36
  27. package/dist/gateway/src/tunnel.js.map +1 -1
  28. package/dist/gateway/tsconfig.tsbuildinfo +1 -1
  29. package/dist/shared-protocol/src/index.d.ts +11978 -3423
  30. package/dist/shared-protocol/src/index.js +114 -163
  31. package/dist/shared-protocol/src/index.js.map +1 -1
  32. package/package.json +11 -11
  33. package/src/agent-permission-http.ts +63 -20
  34. package/src/embedded.ts +60 -158
  35. package/src/index.ts +98 -199
  36. package/src/pairings.ts +7 -6
  37. package/src/relay.ts +97 -193
  38. package/src/sessions.ts +150 -213
  39. package/src/state-store.ts +25 -41
  40. package/src/tokens.ts +63 -109
  41. package/src/tunnel.ts +43 -49
@@ -1,16 +1,15 @@
1
1
  import { createServer } from "node:http";
2
2
  import { randomUUID } from "node:crypto";
3
- import { monitorEventLoopDelay } from "node:perf_hooks";
4
3
  import { WebSocketServer } from "ws";
5
4
  import { createEnvelope, serializeEnvelope, PROTOCOL_VERSION, } from "@linkshell/protocol";
6
5
  import { z, ZodError } from "zod";
7
- import { DeviceManager } from "./sessions.js";
6
+ import { SessionManager } from "./sessions.js";
8
7
  import { PairingManager } from "./pairings.js";
9
8
  import { TokenManager } from "./tokens.js";
10
9
  import { createSupabaseStateStore } from "./state-store.js";
11
10
  import { handleSocketMessage } from "./relay.js";
12
11
  import { agentPermissionHttpBodySchema, forwardAgentPermissionHttp, } from "./agent-permission-http.js";
13
- import { parseTunnelPath, parseTunnelCookie, handleTunnelRequest, cleanupSessionTunnels, } from "./tunnel.js";
12
+ import { parseTunnelPath, parseTunnelCookie, handleTunnelRequest, } from "./tunnel.js";
14
13
  import { AUTH_REQUIRED, requireAuth, checkWsAuth, validateRequest, checkSubscriptionByUserId } from "./auth-middleware.js";
15
14
  const port = Number(process.env.PORT ?? 8787);
16
15
  const logLevel = (process.env.LOG_LEVEL ?? "info");
@@ -21,20 +20,17 @@ function log(level, msg) {
21
20
  }
22
21
  }
23
22
  const stateStore = createSupabaseStateStore();
24
- const sessionManager = new DeviceManager();
23
+ const sessionManager = new SessionManager();
25
24
  const pairingManager = new PairingManager(stateStore);
26
25
  const tokenManager = new TokenManager(stateStore);
27
26
  await Promise.all([pairingManager.hydrate(), tokenManager.hydrate()]);
28
27
  const PING_INTERVAL = 20_000;
29
28
  const MAX_BODY_SIZE = 4096;
30
- const MAX_WS_MESSAGE_SIZE = Number(process.env.MAX_WS_MESSAGE_SIZE ?? 16 * 1024 * 1024);
29
+ const MAX_WS_MESSAGE_SIZE = 50 * 1024 * 1024; // 50MB (supports base64 image uploads)
31
30
  const PAIRING_RATE_LIMIT_MAX = Number(process.env.PAIRING_RATE_LIMIT_MAX ?? 30);
32
31
  const PAIRING_RATE_LIMIT_WINDOW_MS = Number(process.env.PAIRING_RATE_LIMIT_WINDOW_MS ?? 60_000);
33
32
  const WS_CONNECT_RATE_LIMIT_MAX = Number(process.env.WS_CONNECT_RATE_LIMIT_MAX ?? 20);
34
33
  const WS_CONNECT_RATE_LIMIT_WINDOW_MS = Number(process.env.WS_CONNECT_RATE_LIMIT_WINDOW_MS ?? 60_000);
35
- const DETAILED_HEALTH = process.env.DETAILED_HEALTH === "true";
36
- const eventLoopDelay = monitorEventLoopDelay({ resolution: 20 });
37
- eventLoopDelay.enable();
38
34
  // ── Rate limiter ────────────────────────────────────────────────────
39
35
  class RateLimiter {
40
36
  maxHits;
@@ -87,12 +83,10 @@ function setCors(res) {
87
83
  res.setHeader("Access-Control-Max-Age", "86400");
88
84
  }
89
85
  // ── HTTP API ────────────────────────────────────────────────────────
90
- const createPairingBody = z.object({ hostDeviceId: z.string().min(1) });
86
+ const createPairingBody = z.object({ sessionId: z.string().optional() });
91
87
  const claimPairingBody = z.object({
92
88
  pairingCode: z.string().length(6),
93
89
  deviceToken: z.string().min(1).optional(),
94
- clientDeviceId: z.string().min(1).optional(),
95
- clientName: z.string().min(1).optional(),
96
90
  });
97
91
  const server = createServer(async (req, res) => {
98
92
  setCors(res);
@@ -108,7 +102,7 @@ const server = createServer(async (req, res) => {
108
102
  if (err instanceof ZodError) {
109
103
  json(res, 400, {
110
104
  error: "invalid_message",
111
- message: err.issues[0]?.message ?? "Validation failed",
105
+ message: err.errors[0]?.message ?? "Validation failed",
112
106
  });
113
107
  }
114
108
  else if (err instanceof BodyTooLargeError) {
@@ -135,52 +129,29 @@ async function handleRequest(req, res) {
135
129
  const ip = getClientIp(req);
136
130
  // Health check
137
131
  if (method === "GET" && url.pathname === "/healthz") {
138
- const memory = process.memoryUsage();
139
- const detailed = DETAILED_HEALTH || url.searchParams.get("detail") === "1" || url.searchParams.get("detailed") === "true";
140
- json(res, 200, {
141
- ok: true,
142
- uptime: Math.round(process.uptime()),
143
- memory: {
144
- rss: memory.rss,
145
- heapUsed: memory.heapUsed,
146
- heapTotal: memory.heapTotal,
147
- external: memory.external,
148
- },
149
- sessions: sessionManager.getStats(),
150
- ...(detailed ? {
151
- eventLoop: {
152
- delayMeanMs: Number((eventLoopDelay.mean / 1_000_000).toFixed(2)),
153
- delayMaxMs: Number((eventLoopDelay.max / 1_000_000).toFixed(2)),
154
- delayP99Ms: Number((eventLoopDelay.percentile(99) / 1_000_000).toFixed(2)),
155
- },
156
- websocket: {
157
- maxPayloadBytes: MAX_WS_MESSAGE_SIZE,
158
- perMessageDeflate: false,
159
- },
160
- } : {}),
161
- });
132
+ json(res, 200, { ok: true });
162
133
  return;
163
134
  }
164
- // Devices owned by authenticated user (before AUTH_REQUIRED guard — uses its own auth)
165
- if (method === "GET" && url.pathname === "/devices/mine") {
135
+ // Sessions owned by authenticated user (before AUTH_REQUIRED guard — uses its own auth)
136
+ if (method === "GET" && url.pathname === "/sessions/mine") {
166
137
  const authResult = await validateRequest(req);
167
138
  if (!authResult || !authResult.userId) {
168
139
  json(res, 401, { error: "auth_required", message: "Authentication required" });
169
140
  return;
170
141
  }
171
- const devices = sessionManager
142
+ const sessions = sessionManager
172
143
  .listActive()
173
144
  .filter((s) => s.userId === authResult.userId)
174
145
  .map((s) => sessionManager.getSummary(s.id))
175
146
  .filter(Boolean);
176
- json(res, 200, { devices });
147
+ json(res, 200, { sessions });
177
148
  return;
178
149
  }
179
- // Delete a host device owned by authenticated user
180
- if (method === "DELETE" && /^\/devices\/[^/]+$/.test(url.pathname)) {
181
- const hostDeviceId = url.pathname.split("/")[2];
182
- if (!hostDeviceId) {
183
- json(res, 400, { error: "missing_host_device_id" });
150
+ // Delete a session owned by authenticated user
151
+ if (method === "DELETE" && url.pathname.startsWith("/sessions/")) {
152
+ const sessionId = url.pathname.split("/")[2];
153
+ if (!sessionId) {
154
+ json(res, 400, { error: "missing_session_id" });
184
155
  return;
185
156
  }
186
157
  const authResult = await validateRequest(req);
@@ -188,17 +159,17 @@ async function handleRequest(req, res) {
188
159
  json(res, 401, { error: "auth_required", message: "Authentication required" });
189
160
  return;
190
161
  }
191
- const device = sessionManager.get(hostDeviceId);
192
- if (!device) {
162
+ const session = sessionManager.get(sessionId);
163
+ if (!session) {
193
164
  json(res, 404, { error: "not_found" });
194
165
  return;
195
166
  }
196
- if (device.userId && device.userId === authResult.userId) {
197
- sessionManager.forceDelete(hostDeviceId);
167
+ if (session.userId && session.userId === authResult.userId) {
168
+ sessionManager.forceDelete(sessionId);
198
169
  json(res, 200, { ok: true });
199
170
  return;
200
171
  }
201
- json(res, 403, { error: "forbidden", message: "You do not own this device" });
172
+ json(res, 403, { error: "forbidden", message: "You do not own this session" });
202
173
  return;
203
174
  }
204
175
  // Tunnel HTTP proxy (explicit path) — before AUTH_REQUIRED, tunnel has its own auth
@@ -211,7 +182,7 @@ async function handleRequest(req, res) {
211
182
  const tunnelCookie = parseTunnelCookie(req);
212
183
  if (tunnelCookie) {
213
184
  const fallbackParsed = {
214
- hostDeviceId: tunnelCookie.hostDeviceId,
185
+ sessionId: tunnelCookie.sessionId,
215
186
  port: tunnelCookie.port,
216
187
  path: url.pathname,
217
188
  };
@@ -225,7 +196,7 @@ async function handleRequest(req, res) {
225
196
  if (!parsed.success) {
226
197
  json(res, 400, {
227
198
  error: "invalid_payload",
228
- message: parsed.error.issues[0]?.message ?? "Invalid permission response payload",
199
+ message: parsed.error.errors[0]?.message ?? "Invalid permission response payload",
229
200
  });
230
201
  return;
231
202
  }
@@ -238,26 +209,26 @@ async function handleRequest(req, res) {
238
209
  });
239
210
  const forwarded = result.forwarded?.map((item) => item.terminalId ? `${item.type}:${item.terminalId}` : item.type).join(",") ?? "none";
240
211
  const ack = result.ack ? ` resolved=${result.ack.resolved} delivered=${result.ack.delivered}` : "";
241
- log(result.status === 200 ? "info" : "warn", `agent permission respond protocol=${body.protocol} hostDevice=${body.hostDeviceId} request=${body.requestId} status=${result.status} forwarded=${forwarded}${ack}`);
212
+ log(result.status === 200 ? "info" : "warn", `agent permission respond protocol=${body.protocol} session=${body.sessionId} request=${body.requestId} status=${result.status} forwarded=${forwarded}${ack}`);
242
213
  json(res, result.status, result.body);
243
214
  return;
244
215
  }
245
- // Auth check for premium gateway (skip healthz, device-owned endpoints, tunnel)
216
+ // Auth check for premium gateway (skip healthz, /sessions/mine, tunnel)
246
217
  if (AUTH_REQUIRED) {
247
218
  const authResult = await requireAuth(req, res);
248
219
  if (!authResult)
249
220
  return; // response already sent
250
221
  }
251
- // Create one-time pairing challenge for a host device
222
+ // Create pairing
252
223
  if (method === "POST" && url.pathname === "/pairings") {
253
224
  if (!isRateLimitBypassed(ip) && !pairingLimiter.allow(ip)) {
254
225
  json(res, 429, { error: "rate_limited", message: "Too many requests" });
255
226
  return;
256
227
  }
257
228
  const body = createPairingBody.parse(await readJson(req));
258
- const record = pairingManager.create(body.hostDeviceId);
229
+ const record = pairingManager.create(body.sessionId);
259
230
  json(res, 201, {
260
- hostDeviceId: record.hostDeviceId,
231
+ sessionId: record.sessionId,
261
232
  pairingCode: record.pairingCode,
262
233
  expiresAt: new Date(record.expiresAt).toISOString(),
263
234
  });
@@ -276,60 +247,42 @@ async function handleRequest(req, res) {
276
247
  return;
277
248
  }
278
249
  const token = tokenManager.register(body.deviceToken);
279
- const authorization = tokenManager.authorize(token, result.hostDeviceId, {
280
- clientDeviceId: body.clientDeviceId,
281
- clientName: body.clientName,
282
- });
283
- json(res, 200, {
284
- hostDeviceId: result.hostDeviceId,
285
- deviceToken: token,
286
- authorizationId: authorization?.authorizationId,
287
- });
250
+ tokenManager.bind(token, result.sessionId);
251
+ json(res, 200, { sessionId: result.sessionId, deviceToken: token });
288
252
  return;
289
253
  }
290
- // Authorized host device list
291
- if (method === "GET" && url.pathname === "/devices") {
254
+ // Session list
255
+ if (method === "GET" && url.pathname === "/sessions") {
292
256
  const token = extractBearerToken(req);
293
- if (!token || !tokenManager.validate(token)) {
294
- json(res, 401, {
295
- error: "unauthorized",
296
- message: "Valid device token required",
297
- });
298
- return;
299
- }
300
- const allowedIds = tokenManager.getHostDeviceIds(token);
301
- const devices = [...allowedIds].map((hostDeviceId) => {
302
- const summary = sessionManager.getSummary(hostDeviceId);
303
- return summary ?? {
304
- id: hostDeviceId,
305
- hostDeviceId,
306
- state: "host_disconnected",
307
- online: false,
308
- hasHost: false,
309
- clientCount: 0,
310
- controllerId: null,
311
- lastActivity: null,
312
- createdAt: null,
313
- bufferSize: 0,
314
- machineId: null,
315
- hostname: null,
316
- platform: null,
317
- cwd: null,
318
- capabilities: [],
319
- authorizationId: tokenManager.getAuthorizationId(token, hostDeviceId) ?? null,
320
- };
321
- }).map((device) => ({
322
- ...device,
323
- authorizationId: tokenManager.getAuthorizationId(token, device.hostDeviceId) ?? null,
257
+ const allowedIds = token && tokenManager.validate(token)
258
+ ? tokenManager.getSessionIds(token)
259
+ : new Set();
260
+ const sessions = sessionManager
261
+ .listActive()
262
+ .filter((s) => allowedIds.has(s.id))
263
+ .map((s) => ({
264
+ id: s.id,
265
+ state: s.state,
266
+ hasHost: !!s.host && s.host.socket.readyState === s.host.socket.OPEN,
267
+ clientCount: s.clients.size,
268
+ controllerId: s.controllerId ?? null,
269
+ lastActivity: s.lastActivity,
270
+ createdAt: s.createdAt,
271
+ provider: s.provider ?? null,
272
+ machineId: s.machineId ?? null,
273
+ hostname: s.hostname ?? null,
274
+ platform: s.platform ?? null,
275
+ cwd: s.cwd ?? null,
276
+ projectName: s.projectName ?? null,
324
277
  }));
325
- json(res, 200, { devices });
278
+ json(res, 200, { sessions });
326
279
  return;
327
280
  }
328
- // Device detail
329
- const deviceMatch = url.pathname.match(/^\/devices\/([^/]+)$/);
330
- if (method === "GET" && deviceMatch) {
281
+ // Session detail
282
+ const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
283
+ if (method === "GET" && sessionMatch) {
331
284
  const token = extractBearerToken(req);
332
- const targetId = deviceMatch[1];
285
+ const targetId = sessionMatch[1];
333
286
  if (!token || !tokenManager.owns(token, targetId)) {
334
287
  json(res, 401, {
335
288
  error: "unauthorized",
@@ -339,48 +292,10 @@ async function handleRequest(req, res) {
339
292
  }
340
293
  const summary = sessionManager.getSummary(targetId);
341
294
  if (!summary) {
342
- json(res, 200, {
343
- id: targetId,
344
- hostDeviceId: targetId,
345
- state: "host_disconnected",
346
- online: false,
347
- hasHost: false,
348
- clientCount: 0,
349
- controllerId: null,
350
- lastActivity: null,
351
- createdAt: null,
352
- bufferSize: 0,
353
- machineId: null,
354
- hostname: null,
355
- platform: null,
356
- cwd: null,
357
- capabilities: [],
358
- authorizationId: tokenManager.getAuthorizationId(token, targetId) ?? null,
359
- });
295
+ json(res, 404, { error: "session_not_found" });
360
296
  return;
361
297
  }
362
- json(res, 200, {
363
- ...summary,
364
- authorizationId: tokenManager.getAuthorizationId(token, targetId) ?? null,
365
- });
366
- return;
367
- }
368
- const revokeMatch = url.pathname.match(/^\/devices\/([^/]+)\/authorizations\/([^/]+)$/);
369
- if (method === "DELETE" && revokeMatch) {
370
- const token = extractBearerToken(req);
371
- const hostDeviceId = decodeURIComponent(revokeMatch[1]);
372
- const authorizationId = decodeURIComponent(revokeMatch[2]);
373
- if (!token ||
374
- tokenManager.getAuthorizationId(token, hostDeviceId) !== authorizationId ||
375
- !tokenManager.revoke(token, hostDeviceId, authorizationId)) {
376
- json(res, 401, {
377
- error: "unauthorized",
378
- message: "Valid device authorization required",
379
- });
380
- return;
381
- }
382
- sessionManager.disconnectAuthorization(hostDeviceId, authorizationId);
383
- json(res, 200, { ok: true });
298
+ json(res, 200, summary);
384
299
  return;
385
300
  }
386
301
  // Pairing status (for CLI polling)
@@ -400,7 +315,6 @@ async function handleRequest(req, res) {
400
315
  const wss = new WebSocketServer({
401
316
  noServer: true,
402
317
  maxPayload: MAX_WS_MESSAGE_SIZE,
403
- perMessageDeflate: false,
404
318
  });
405
319
  server.on("upgrade", (request, socket, head) => {
406
320
  const url = new URL(request.url ?? "/", `http://${request.headers.host}`);
@@ -425,7 +339,7 @@ server.on("upgrade", (request, socket, head) => {
425
339
  const tunnelCookie = parseTunnelCookie(request);
426
340
  if (tunnelCookie && url.pathname !== "/ws") {
427
341
  const fallbackParsed = {
428
- hostDeviceId: tunnelCookie.hostDeviceId,
342
+ sessionId: tunnelCookie.sessionId,
429
343
  port: tunnelCookie.port,
430
344
  path: url.pathname,
431
345
  };
@@ -472,65 +386,57 @@ server.on("upgrade", (request, socket, head) => {
472
386
  });
473
387
  });
474
388
  wss.on("connection", (socket, _request, url) => {
475
- const hostDeviceId = url.searchParams.get("hostDeviceId");
389
+ const sessionId = url.searchParams.get("sessionId");
476
390
  const role = url.searchParams.get("role");
477
- if (!hostDeviceId || !role || (role !== "host" && role !== "client")) {
478
- socket.close(1008, "missing hostDeviceId or role");
391
+ if (!sessionId || !role || (role !== "host" && role !== "client")) {
392
+ socket.close(1008, "missing sessionId or role");
479
393
  return;
480
394
  }
481
395
  const deviceId = url.searchParams.get("deviceId") ?? randomUUID();
482
- let clientToken;
483
- let clientAuthorizationId;
484
396
  if (role === "client") {
485
397
  const token = url.searchParams.get("token");
486
398
  const authResult = _request.__authResult;
487
- const device = sessionManager.get(hostDeviceId);
488
- // Allow if: device token owns host device, OR auth user owns host device
489
- const tokenOwns = Boolean(token && tokenManager.owns(token, hostDeviceId));
399
+ const session = sessionManager.get(sessionId);
400
+ // Allow if: device token owns session, OR auth user owns session
401
+ const tokenOwns = token && tokenManager.owns(token, sessionId);
490
402
  const authOwns = AUTH_REQUIRED &&
491
403
  authResult?.userId &&
492
- device?.userId &&
493
- authResult.userId === device.userId;
404
+ session?.userId &&
405
+ authResult.userId === session.userId;
494
406
  if (!tokenOwns && !authOwns) {
495
407
  socket.close(4001, "unauthorized");
496
408
  return;
497
409
  }
498
410
  if (!tokenOwns && authOwns && token) {
499
411
  tokenManager.register(token);
500
- tokenManager.bind(token, hostDeviceId);
501
- log("info", `bound authenticated device token to host device ${hostDeviceId}`);
502
- }
503
- if (token && (tokenOwns || authOwns)) {
504
- clientToken = token;
505
- clientAuthorizationId = tokenManager.getAuthorizationId(token, hostDeviceId);
412
+ tokenManager.bind(token, sessionId);
413
+ log("info", `bound authenticated device token to session ${sessionId}`);
506
414
  }
507
415
  }
508
416
  const device = {
509
417
  socket,
510
418
  role,
511
419
  deviceId,
512
- token: clientToken,
513
- authorizationId: clientAuthorizationId,
514
420
  connectedAt: Date.now(),
515
421
  };
516
422
  if (role === "host") {
517
423
  // Check if this is a reconnect (session already exists with clients)
518
- const existingSession = sessionManager.get(hostDeviceId);
424
+ const existingSession = sessionManager.get(sessionId);
519
425
  const isReconnect = existingSession &&
520
426
  existingSession.clients.size > 0 &&
521
427
  existingSession.state === "host_disconnected";
522
- sessionManager.setHost(hostDeviceId, device);
428
+ sessionManager.setHost(sessionId, device);
523
429
  // Associate userId from auth (for AUTH_REQUIRED gateways)
524
430
  const authResult = _request.__authResult;
525
431
  if (authResult?.userId) {
526
- const deviceRecord = sessionManager.get(hostDeviceId);
527
- if (deviceRecord)
528
- deviceRecord.userId = authResult.userId;
432
+ const session = sessionManager.get(sessionId);
433
+ if (session)
434
+ session.userId = authResult.userId;
529
435
  }
530
436
  if (isReconnect) {
531
437
  const notification = serializeEnvelope(createEnvelope({
532
- type: "device.host_reconnected",
533
- hostDeviceId,
438
+ type: "session.host_reconnected",
439
+ sessionId,
534
440
  payload: {},
535
441
  }));
536
442
  for (const [, client] of existingSession.clients) {
@@ -541,12 +447,12 @@ wss.on("connection", (socket, _request, url) => {
541
447
  }
542
448
  }
543
449
  else {
544
- sessionManager.addClient(hostDeviceId, device);
450
+ sessionManager.addClient(sessionId, device);
545
451
  }
546
452
  // Send welcome with protocol version
547
453
  socket.send(serializeEnvelope(createEnvelope({
548
- type: "device.connect",
549
- hostDeviceId,
454
+ type: "session.connect",
455
+ sessionId,
550
456
  payload: {
551
457
  role,
552
458
  clientName: deviceId,
@@ -555,7 +461,7 @@ wss.on("connection", (socket, _request, url) => {
555
461
  })));
556
462
  // If client just joined and host is not connected, notify immediately
557
463
  if (role === "client") {
558
- const sessionAfterJoin = sessionManager.get(hostDeviceId);
464
+ const sessionAfterJoin = sessionManager.get(sessionId);
559
465
  if (sessionAfterJoin) {
560
466
  const hostGone = !sessionAfterJoin.host ||
561
467
  sessionAfterJoin.state === "host_disconnected" ||
@@ -563,8 +469,8 @@ wss.on("connection", (socket, _request, url) => {
563
469
  sessionAfterJoin.host.socket.OPEN;
564
470
  if (hostGone) {
565
471
  socket.send(serializeEnvelope(createEnvelope({
566
- type: "device.host_disconnected",
567
- hostDeviceId,
472
+ type: "session.host_disconnected",
473
+ sessionId,
568
474
  payload: { reason: "host not connected" },
569
475
  })));
570
476
  }
@@ -578,14 +484,14 @@ wss.on("connection", (socket, _request, url) => {
578
484
  }, PING_INTERVAL);
579
485
  socket.on("message", (data) => {
580
486
  try {
581
- handleSocketMessage(socket, data.toString(), role, hostDeviceId, deviceId, sessionManager);
487
+ handleSocketMessage(socket, data.toString(), role, sessionId, deviceId, sessionManager);
582
488
  }
583
489
  catch (err) {
584
- log("error", `unhandled websocket message error for host device ${hostDeviceId}: ${err instanceof Error ? err.message : String(err)}`);
490
+ log("error", `unhandled websocket message error for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
585
491
  if (socket.readyState === socket.OPEN) {
586
492
  socket.send(serializeEnvelope(createEnvelope({
587
- type: "device.error",
588
- hostDeviceId,
493
+ type: "session.error",
494
+ sessionId,
589
495
  payload: {
590
496
  code: "invalid_message",
591
497
  message: "Failed to handle message",
@@ -597,13 +503,12 @@ wss.on("connection", (socket, _request, url) => {
597
503
  socket.on("close", () => {
598
504
  clearInterval(pingTimer);
599
505
  if (role === "host") {
600
- const result = sessionManager.removeHost(hostDeviceId);
601
- cleanupSessionTunnels(hostDeviceId);
506
+ const result = sessionManager.removeHost(sessionId);
602
507
  // Notify all clients that host disconnected
603
508
  if (result) {
604
509
  const notification = serializeEnvelope(createEnvelope({
605
- type: "device.host_disconnected",
606
- hostDeviceId,
510
+ type: "session.host_disconnected",
511
+ sessionId,
607
512
  payload: { reason: "host connection closed" },
608
513
  }));
609
514
  for (const [, client] of result.clients) {
@@ -614,7 +519,7 @@ wss.on("connection", (socket, _request, url) => {
614
519
  }
615
520
  }
616
521
  else {
617
- sessionManager.removeClient(hostDeviceId, deviceId);
522
+ sessionManager.removeClient(sessionId, deviceId);
618
523
  }
619
524
  });
620
525
  socket.on("error", () => {
@@ -646,16 +551,16 @@ if (AUTH_REQUIRED) {
646
551
  continue;
647
552
  const subscription = await checkSubscriptionByUserId(session.userId);
648
553
  if (subscription.status === "unknown") {
649
- log("warn", `subscription check unknown for user ${session.userId}, keeping host device ${session.id}${subscription.reason ? ` (${subscription.reason})` : ""}`);
554
+ log("warn", `subscription check unknown for user ${session.userId}, keeping session ${session.id}${subscription.reason ? ` (${subscription.reason})` : ""}`);
650
555
  continue;
651
556
  }
652
557
  if (subscription.status === "inactive") {
653
- log("info", `subscription expired for user ${session.userId}, disconnecting host device ${session.id}`);
558
+ log("info", `subscription expired for user ${session.userId}, disconnecting session ${session.id}`);
654
559
  // Notify host
655
560
  try {
656
561
  session.host.socket.send(serializeEnvelope(createEnvelope({
657
- type: "device.error",
658
- hostDeviceId: session.id,
562
+ type: "session.error",
563
+ sessionId: session.id,
659
564
  payload: {
660
565
  code: "subscription_expired",
661
566
  message: "Your Pro subscription has expired. Renew at https://itool.tech",
@@ -669,11 +574,11 @@ if (AUTH_REQUIRED) {
669
574
  for (const [, client] of session.clients) {
670
575
  try {
671
576
  client.socket.send(serializeEnvelope(createEnvelope({
672
- type: "device.error",
673
- hostDeviceId: session.id,
577
+ type: "session.error",
578
+ sessionId: session.id,
674
579
  payload: {
675
580
  code: "subscription_expired",
676
- message: "Host subscription expired. HostDevice ended.",
581
+ message: "Host subscription expired. Session ended.",
677
582
  },
678
583
  })));
679
584
  client.socket.close(4003, "subscription_expired");