@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/embedded.ts CHANGED
@@ -44,10 +44,12 @@ const MAX_WS_MESSAGE_SIZE = 50 * 1024 * 1024; // 50MB (supports base64 image upl
44
44
 
45
45
  const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
46
46
 
47
- const createPairingBody = z.object({ sessionId: z.string().optional() });
47
+ const createPairingBody = z.object({ hostDeviceId: z.string().min(1) });
48
48
  const claimPairingBody = z.object({
49
49
  pairingCode: z.string().length(6),
50
50
  deviceToken: z.string().min(1).optional(),
51
+ clientDeviceId: z.string().min(1).optional(),
52
+ clientName: z.string().min(1).optional(),
51
53
  });
52
54
 
53
55
  class BodyTooLargeError extends Error {}
@@ -152,16 +154,16 @@ export function startEmbeddedGateway(
152
154
  item.terminalId ? `${item.type}:${item.terminalId}` : item.type,
153
155
  ).join(",") ?? "none";
154
156
  const ack = result.ack ? ` resolved=${result.ack.resolved} delivered=${result.ack.delivered}` : "";
155
- log(result.status === 200 ? "info" : "warn", `agent permission respond protocol=${body.protocol} session=${body.sessionId} request=${body.requestId} status=${result.status} forwarded=${forwarded}${ack}`);
157
+ log(result.status === 200 ? "info" : "warn", `agent permission respond protocol=${body.protocol} hostDevice=${body.hostDeviceId ?? body.sessionId ?? "unknown"} request=${body.requestId} status=${result.status} forwarded=${forwarded}${ack}`);
156
158
  json(res, result.status, result.body);
157
159
  return;
158
160
  }
159
161
 
160
162
  if (method === "POST" && url.pathname === "/pairings") {
161
163
  const body = createPairingBody.parse(await readJson(req));
162
- const record = pairingManager.create(body.sessionId);
164
+ const record = pairingManager.create(body.hostDeviceId);
163
165
  json(res, 201, {
164
- sessionId: record.sessionId,
166
+ hostDeviceId: record.hostDeviceId,
165
167
  pairingCode: record.pairingCode,
166
168
  expiresAt: new Date(record.expiresAt).toISOString(),
167
169
  });
@@ -176,12 +178,19 @@ export function startEmbeddedGateway(
176
178
  return;
177
179
  }
178
180
  const token = tokenManager.register(body.deviceToken);
179
- tokenManager.bind(token, result.sessionId);
180
- json(res, 200, { sessionId: result.sessionId, deviceToken: token });
181
+ const authorization = tokenManager.authorize(token, result.hostDeviceId, {
182
+ clientDeviceId: body.clientDeviceId,
183
+ clientName: body.clientName,
184
+ });
185
+ json(res, 200, {
186
+ hostDeviceId: result.hostDeviceId,
187
+ deviceToken: token,
188
+ authorizationId: authorization?.authorizationId,
189
+ });
181
190
  return;
182
191
  }
183
192
 
184
- if (method === "GET" && url.pathname === "/sessions") {
193
+ if (method === "GET" && url.pathname === "/devices") {
185
194
  const token = extractBearerToken(req);
186
195
  if (!token || !tokenManager.validate(token)) {
187
196
  json(res, 401, {
@@ -190,33 +199,38 @@ export function startEmbeddedGateway(
190
199
  });
191
200
  return;
192
201
  }
193
- const allowedIds = tokenManager.getSessionIds(token);
194
- const sessions = sessionManager
195
- .listActive()
196
- .filter((s) => allowedIds.has(s.id))
197
- .map((s) => ({
198
- id: s.id,
199
- state: s.state,
200
- hasHost: !!s.host && s.host.socket.readyState === s.host.socket.OPEN,
201
- clientCount: s.clients.size,
202
- controllerId: s.controllerId ?? null,
203
- lastActivity: s.lastActivity,
204
- createdAt: s.createdAt,
205
- provider: s.provider ?? null,
206
- machineId: s.machineId ?? null,
207
- hostname: s.hostname ?? null,
208
- platform: s.platform ?? null,
209
- cwd: s.cwd ?? null,
210
- projectName: s.projectName ?? null,
211
- }));
212
- json(res, 200, { sessions });
202
+ const allowedIds = tokenManager.getHostDeviceIds(token);
203
+ const devices = [...allowedIds].map((hostDeviceId) => {
204
+ const summary = sessionManager.getSummary(hostDeviceId);
205
+ return {
206
+ ...(summary ?? {
207
+ id: hostDeviceId,
208
+ hostDeviceId,
209
+ state: "host_disconnected",
210
+ online: false,
211
+ hasHost: false,
212
+ clientCount: 0,
213
+ controllerId: null,
214
+ lastActivity: null,
215
+ createdAt: null,
216
+ bufferSize: 0,
217
+ machineId: null,
218
+ hostname: null,
219
+ platform: null,
220
+ cwd: null,
221
+ capabilities: [],
222
+ }),
223
+ authorizationId: tokenManager.getAuthorizationId(token, hostDeviceId) ?? null,
224
+ };
225
+ });
226
+ json(res, 200, { devices });
213
227
  return;
214
228
  }
215
229
 
216
- const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
217
- if (method === "GET" && sessionMatch) {
230
+ const deviceMatch = url.pathname.match(/^\/devices\/([^/]+)$/);
231
+ if (method === "GET" && deviceMatch) {
218
232
  const token = extractBearerToken(req);
219
- const targetId = sessionMatch[1]!;
233
+ const targetId = deviceMatch[1]!;
220
234
  if (!token || !tokenManager.owns(token, targetId)) {
221
235
  json(res, 401, {
222
236
  error: "unauthorized",
@@ -226,10 +240,51 @@ export function startEmbeddedGateway(
226
240
  }
227
241
  const summary = sessionManager.getSummary(targetId);
228
242
  if (!summary) {
229
- json(res, 404, { error: "session_not_found" });
243
+ json(res, 200, {
244
+ id: targetId,
245
+ hostDeviceId: targetId,
246
+ state: "host_disconnected",
247
+ online: false,
248
+ hasHost: false,
249
+ clientCount: 0,
250
+ controllerId: null,
251
+ lastActivity: null,
252
+ createdAt: null,
253
+ bufferSize: 0,
254
+ machineId: null,
255
+ hostname: null,
256
+ platform: null,
257
+ cwd: null,
258
+ capabilities: [],
259
+ authorizationId: tokenManager.getAuthorizationId(token, targetId) ?? null,
260
+ });
261
+ return;
262
+ }
263
+ json(res, 200, {
264
+ ...summary,
265
+ authorizationId: tokenManager.getAuthorizationId(token, targetId) ?? null,
266
+ });
267
+ return;
268
+ }
269
+
270
+ const revokeMatch = url.pathname.match(/^\/devices\/([^/]+)\/authorizations\/([^/]+)$/);
271
+ if (method === "DELETE" && revokeMatch) {
272
+ const token = extractBearerToken(req);
273
+ const hostDeviceId = decodeURIComponent(revokeMatch[1]!);
274
+ const authorizationId = decodeURIComponent(revokeMatch[2]!);
275
+ if (
276
+ !token ||
277
+ tokenManager.getAuthorizationId(token, hostDeviceId) !== authorizationId ||
278
+ !tokenManager.revoke(token, hostDeviceId, authorizationId)
279
+ ) {
280
+ json(res, 401, {
281
+ error: "unauthorized",
282
+ message: "Valid device authorization required",
283
+ });
230
284
  return;
231
285
  }
232
- json(res, 200, summary);
286
+ sessionManager.disconnectAuthorization(hostDeviceId, authorizationId);
287
+ json(res, 200, { ok: true });
233
288
  return;
234
289
  }
235
290
 
@@ -255,7 +310,7 @@ export function startEmbeddedGateway(
255
310
  const tunnelCookie = parseTunnelCookie(req);
256
311
  if (tunnelCookie) {
257
312
  const fallbackParsed = {
258
- sessionId: tunnelCookie.sessionId,
313
+ hostDeviceId: tunnelCookie.hostDeviceId,
259
314
  port: tunnelCookie.port,
260
315
  path: url.pathname,
261
316
  };
@@ -308,7 +363,7 @@ export function startEmbeddedGateway(
308
363
  const tunnelCookie = parseTunnelCookie(request);
309
364
  if (tunnelCookie && url.pathname !== "/ws") {
310
365
  const fallbackParsed = {
311
- sessionId: tunnelCookie.sessionId,
366
+ hostDeviceId: tunnelCookie.hostDeviceId,
312
367
  port: tunnelCookie.port,
313
368
  path: url.pathname,
314
369
  };
@@ -331,38 +386,50 @@ export function startEmbeddedGateway(
331
386
  wss.on(
332
387
  "connection",
333
388
  (socket: WebSocket, _request: IncomingMessage, url: URL) => {
334
- const sessionId = url.searchParams.get("sessionId");
389
+ const hostDeviceId = url.searchParams.get("hostDeviceId") ?? url.searchParams.get("sessionId");
335
390
  const role = url.searchParams.get("role") as "host" | "client" | null;
336
391
 
337
- if (!sessionId || !role || (role !== "host" && role !== "client")) {
338
- socket.close(1008, "missing sessionId or role");
392
+ if (!hostDeviceId || !role || (role !== "host" && role !== "client")) {
393
+ socket.close(1008, "missing hostDeviceId or role");
339
394
  return;
340
395
  }
341
396
 
342
397
  const deviceId = url.searchParams.get("deviceId") ?? randomUUID();
343
398
 
399
+ let clientToken: string | undefined;
400
+ let clientAuthorizationId: string | undefined;
401
+
344
402
  if (role === "client") {
345
403
  const token = url.searchParams.get("token");
346
- if (!token || !tokenManager.owns(token, sessionId)) {
404
+ if (!token || !tokenManager.owns(token, hostDeviceId)) {
347
405
  socket.close(4001, "unauthorized");
348
406
  return;
349
407
  }
408
+ clientToken = token;
409
+ clientAuthorizationId = tokenManager.getAuthorizationId(token, hostDeviceId);
350
410
  }
351
411
 
352
- const device = { socket, role, deviceId, connectedAt: Date.now() };
412
+ const device = {
413
+ socket,
414
+ role,
415
+ deviceId,
416
+ token: clientToken,
417
+ authorizationId: clientAuthorizationId,
418
+ connectedAt: Date.now(),
419
+ };
353
420
 
354
421
  if (role === "host") {
355
- const existingSession = sessionManager.get(sessionId);
422
+ const existingSession = sessionManager.get(hostDeviceId);
356
423
  const isReconnect =
357
424
  existingSession &&
358
425
  existingSession.clients.size > 0 &&
359
426
  existingSession.state === "host_disconnected";
360
- sessionManager.setHost(sessionId, device);
427
+ sessionManager.setHost(hostDeviceId, device);
361
428
  if (isReconnect) {
362
429
  const notification = serializeEnvelope(
363
430
  createEnvelope({
364
- type: "session.host_reconnected",
365
- sessionId,
431
+ type: "device.host_reconnected",
432
+ hostDeviceId,
366
433
  payload: {},
367
434
  }),
368
435
  );
@@ -372,14 +439,14 @@ export function startEmbeddedGateway(
372
439
  }
373
440
  }
374
441
  } else {
375
- sessionManager.addClient(sessionId, device);
442
+ sessionManager.addClient(hostDeviceId, device);
376
443
  }
377
444
 
378
445
  socket.send(
379
446
  serializeEnvelope(
380
447
  createEnvelope({
381
- type: "session.connect",
382
- sessionId,
448
+ type: "device.connect",
449
+ hostDeviceId,
383
450
  payload: {
384
451
  role,
385
452
  clientName: deviceId,
@@ -399,18 +466,18 @@ export function startEmbeddedGateway(
399
466
  socket,
400
467
  data.toString(),
401
468
  role,
402
- sessionId,
469
+ hostDeviceId,
403
470
  deviceId,
404
471
  sessionManager,
405
472
  );
406
473
  } catch (err) {
407
- log("error", `unhandled websocket message error for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
474
+ log("error", `unhandled websocket message error for host device ${hostDeviceId}: ${err instanceof Error ? err.message : String(err)}`);
408
475
  if (socket.readyState === socket.OPEN) {
409
476
  socket.send(
410
477
  serializeEnvelope(
411
478
  createEnvelope({
412
- type: "session.error",
413
- sessionId,
479
+ type: "device.error",
480
+ hostDeviceId,
414
481
  payload: {
415
482
  code: "invalid_message",
416
483
  message: "Failed to handle message",
@@ -425,12 +492,13 @@ export function startEmbeddedGateway(
425
492
  socket.on("close", () => {
426
493
  clearInterval(pingTimer);
427
494
  if (role === "host") {
428
- const result = sessionManager.removeHost(sessionId);
495
+ const result = sessionManager.removeHost(hostDeviceId);
496
+ cleanupSessionTunnels(hostDeviceId);
429
497
  if (result) {
430
498
  const notification = serializeEnvelope(
431
499
  createEnvelope({
432
- type: "session.host_disconnected",
433
- sessionId,
500
+ type: "device.host_disconnected",
501
+ hostDeviceId,
434
502
  payload: { reason: "host connection closed" },
435
503
  }),
436
504
  );
@@ -440,7 +508,7 @@ export function startEmbeddedGateway(
440
508
  }
441
509
  }
442
510
  } else {
443
- sessionManager.removeClient(sessionId, deviceId);
511
+ sessionManager.removeClient(hostDeviceId, deviceId);
444
512
  }
445
513
  });
446
514