@linkshell/gateway 0.2.47 → 0.3.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 (40) hide show
  1. package/README.md +14 -13
  2. package/dist/gateway/src/agent-permission-http.d.ts +10 -37
  3. package/dist/gateway/src/agent-permission-http.js +16 -56
  4. package/dist/gateway/src/agent-permission-http.js.map +1 -1
  5. package/dist/gateway/src/embedded.js +121 -57
  6. package/dist/gateway/src/embedded.js.map +1 -1
  7. package/dist/gateway/src/index.js +161 -94
  8. package/dist/gateway/src/index.js.map +1 -1
  9. package/dist/gateway/src/pairings.d.ts +3 -3
  10. package/dist/gateway/src/pairings.js +4 -5
  11. package/dist/gateway/src/pairings.js.map +1 -1
  12. package/dist/gateway/src/relay.d.ts +2 -2
  13. package/dist/gateway/src/relay.js +27 -38
  14. package/dist/gateway/src/relay.js.map +1 -1
  15. package/dist/gateway/src/sessions.d.ts +31 -28
  16. package/dist/gateway/src/sessions.js +163 -145
  17. package/dist/gateway/src/sessions.js.map +1 -1
  18. package/dist/gateway/src/state-store.d.ts +9 -6
  19. package/dist/gateway/src/state-store.js +26 -19
  20. package/dist/gateway/src/state-store.js.map +1 -1
  21. package/dist/gateway/src/tokens.d.ts +27 -7
  22. package/dist/gateway/src/tokens.js +86 -60
  23. package/dist/gateway/src/tokens.js.map +1 -1
  24. package/dist/gateway/src/tunnel.d.ts +11 -13
  25. package/dist/gateway/src/tunnel.js +36 -36
  26. package/dist/gateway/src/tunnel.js.map +1 -1
  27. package/dist/gateway/tsconfig.tsbuildinfo +1 -1
  28. package/dist/shared-protocol/src/index.d.ts +3961 -5788
  29. package/dist/shared-protocol/src/index.js +19 -84
  30. package/dist/shared-protocol/src/index.js.map +1 -1
  31. package/package.json +10 -10
  32. package/src/agent-permission-http.ts +20 -63
  33. package/src/embedded.ts +124 -56
  34. package/src/index.ts +165 -94
  35. package/src/pairings.ts +6 -7
  36. package/src/relay.ts +38 -48
  37. package/src/sessions.ts +174 -150
  38. package/src/state-store.ts +41 -25
  39. package/src/tokens.ts +109 -63
  40. package/src/tunnel.ts +49 -43
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  PROTOCOL_VERSION,
10
10
  } from "@linkshell/protocol";
11
11
  import { z, ZodError } from "zod";
12
- import { SessionManager } from "./sessions.js";
12
+ import { DeviceManager } from "./sessions.js";
13
13
  import { PairingManager } from "./pairings.js";
14
14
  import { TokenManager } from "./tokens.js";
15
15
  import { createSupabaseStateStore } from "./state-store.js";
@@ -41,7 +41,7 @@ function log(level: "debug" | "info" | "warn" | "error", msg: string): void {
41
41
  }
42
42
 
43
43
  const stateStore = createSupabaseStateStore();
44
- const sessionManager = new SessionManager();
44
+ const sessionManager = new DeviceManager();
45
45
  const pairingManager = new PairingManager(stateStore);
46
46
  const tokenManager = new TokenManager(stateStore);
47
47
  await Promise.all([pairingManager.hydrate(), tokenManager.hydrate()]);
@@ -127,10 +127,12 @@ function setCors(res: ServerResponse): void {
127
127
 
128
128
  // ── HTTP API ────────────────────────────────────────────────────────
129
129
 
130
- const createPairingBody = z.object({ sessionId: z.string().optional() });
130
+ const createPairingBody = z.object({ hostDeviceId: z.string().min(1) });
131
131
  const claimPairingBody = z.object({
132
132
  pairingCode: z.string().length(6),
133
133
  deviceToken: z.string().min(1).optional(),
134
+ clientDeviceId: z.string().min(1).optional(),
135
+ clientName: z.string().min(1).optional(),
134
136
  });
135
137
 
136
138
  const server = createServer(async (req, res) => {
@@ -181,27 +183,27 @@ async function handleRequest(
181
183
  return;
182
184
  }
183
185
 
184
- // Sessions owned by authenticated user (before AUTH_REQUIRED guard — uses its own auth)
185
- if (method === "GET" && url.pathname === "/sessions/mine") {
186
+ // Devices owned by authenticated user (before AUTH_REQUIRED guard — uses its own auth)
187
+ if (method === "GET" && url.pathname === "/devices/mine") {
186
188
  const authResult = await validateRequest(req);
187
189
  if (!authResult || !authResult.userId) {
188
190
  json(res, 401, { error: "auth_required", message: "Authentication required" });
189
191
  return;
190
192
  }
191
- const sessions = sessionManager
193
+ const devices = sessionManager
192
194
  .listActive()
193
195
  .filter((s) => s.userId === authResult.userId)
194
196
  .map((s) => sessionManager.getSummary(s.id))
195
197
  .filter(Boolean);
196
- json(res, 200, { sessions });
198
+ json(res, 200, { devices });
197
199
  return;
198
200
  }
199
201
 
200
- // Delete a session owned by authenticated user
201
- if (method === "DELETE" && url.pathname.startsWith("/sessions/")) {
202
- const sessionId = url.pathname.split("/")[2];
203
- if (!sessionId) {
204
- json(res, 400, { error: "missing_session_id" });
202
+ // Delete a host device owned by authenticated user
203
+ if (method === "DELETE" && /^\/devices\/[^/]+$/.test(url.pathname)) {
204
+ const hostDeviceId = url.pathname.split("/")[2];
205
+ if (!hostDeviceId) {
206
+ json(res, 400, { error: "missing_host_device_id" });
205
207
  return;
206
208
  }
207
209
  const authResult = await validateRequest(req);
@@ -209,17 +211,17 @@ async function handleRequest(
209
211
  json(res, 401, { error: "auth_required", message: "Authentication required" });
210
212
  return;
211
213
  }
212
- const session = sessionManager.get(sessionId);
213
- if (!session) {
214
+ const device = sessionManager.get(hostDeviceId);
215
+ if (!device) {
214
216
  json(res, 404, { error: "not_found" });
215
217
  return;
216
218
  }
217
- if (session.userId && session.userId === authResult.userId) {
218
- sessionManager.forceDelete(sessionId);
219
+ if (device.userId && device.userId === authResult.userId) {
220
+ sessionManager.forceDelete(hostDeviceId);
219
221
  json(res, 200, { ok: true });
220
222
  return;
221
223
  }
222
- json(res, 403, { error: "forbidden", message: "You do not own this session" });
224
+ json(res, 403, { error: "forbidden", message: "You do not own this device" });
223
225
  return;
224
226
  }
225
227
 
@@ -234,7 +236,7 @@ async function handleRequest(
234
236
  const tunnelCookie = parseTunnelCookie(req);
235
237
  if (tunnelCookie) {
236
238
  const fallbackParsed = {
237
- sessionId: tunnelCookie.sessionId,
239
+ hostDeviceId: tunnelCookie.hostDeviceId,
238
240
  port: tunnelCookie.port,
239
241
  path: url.pathname,
240
242
  };
@@ -264,27 +266,27 @@ async function handleRequest(
264
266
  item.terminalId ? `${item.type}:${item.terminalId}` : item.type,
265
267
  ).join(",") ?? "none";
266
268
  const ack = result.ack ? ` resolved=${result.ack.resolved} delivered=${result.ack.delivered}` : "";
267
- log(result.status === 200 ? "info" : "warn", `agent permission respond protocol=${body.protocol} session=${body.sessionId} request=${body.requestId} status=${result.status} forwarded=${forwarded}${ack}`);
269
+ log(result.status === 200 ? "info" : "warn", `agent permission respond protocol=${body.protocol} hostDevice=${body.hostDeviceId} request=${body.requestId} status=${result.status} forwarded=${forwarded}${ack}`);
268
270
  json(res, result.status, result.body);
269
271
  return;
270
272
  }
271
273
 
272
- // Auth check for premium gateway (skip healthz, /sessions/mine, tunnel)
274
+ // Auth check for premium gateway (skip healthz, device-owned endpoints, tunnel)
273
275
  if (AUTH_REQUIRED) {
274
276
  const authResult = await requireAuth(req, res);
275
277
  if (!authResult) return; // response already sent
276
278
  }
277
279
 
278
- // Create pairing
280
+ // Create one-time pairing challenge for a host device
279
281
  if (method === "POST" && url.pathname === "/pairings") {
280
282
  if (!isRateLimitBypassed(ip) && !pairingLimiter.allow(ip)) {
281
283
  json(res, 429, { error: "rate_limited", message: "Too many requests" });
282
284
  return;
283
285
  }
284
286
  const body = createPairingBody.parse(await readJson(req));
285
- const record = pairingManager.create(body.sessionId);
287
+ const record = pairingManager.create(body.hostDeviceId);
286
288
  json(res, 201, {
287
- sessionId: record.sessionId,
289
+ hostDeviceId: record.hostDeviceId,
288
290
  pairingCode: record.pairingCode,
289
291
  expiresAt: new Date(record.expiresAt).toISOString(),
290
292
  });
@@ -304,44 +306,62 @@ async function handleRequest(
304
306
  return;
305
307
  }
306
308
  const token = tokenManager.register(body.deviceToken);
307
- tokenManager.bind(token, result.sessionId);
308
- json(res, 200, { sessionId: result.sessionId, deviceToken: token });
309
+ const authorization = tokenManager.authorize(token, result.hostDeviceId, {
310
+ clientDeviceId: body.clientDeviceId,
311
+ clientName: body.clientName,
312
+ });
313
+ json(res, 200, {
314
+ hostDeviceId: result.hostDeviceId,
315
+ deviceToken: token,
316
+ authorizationId: authorization?.authorizationId,
317
+ });
309
318
  return;
310
319
  }
311
320
 
312
- // Session list
313
- if (method === "GET" && url.pathname === "/sessions") {
321
+ // Authorized host device list
322
+ if (method === "GET" && url.pathname === "/devices") {
314
323
  const token = extractBearerToken(req);
315
- const allowedIds = token && tokenManager.validate(token)
316
- ? tokenManager.getSessionIds(token)
317
- : new Set<string>();
318
- const sessions = sessionManager
319
- .listActive()
320
- .filter((s) => allowedIds.has(s.id))
321
- .map((s) => ({
322
- id: s.id,
323
- state: s.state,
324
- hasHost: !!s.host && s.host.socket.readyState === s.host.socket.OPEN,
325
- clientCount: s.clients.size,
326
- controllerId: s.controllerId ?? null,
327
- lastActivity: s.lastActivity,
328
- createdAt: s.createdAt,
329
- provider: s.provider ?? null,
330
- machineId: s.machineId ?? null,
331
- hostname: s.hostname ?? null,
332
- platform: s.platform ?? null,
333
- cwd: s.cwd ?? null,
334
- projectName: s.projectName ?? null,
335
- }));
336
- json(res, 200, { sessions });
324
+ if (!token || !tokenManager.validate(token)) {
325
+ json(res, 401, {
326
+ error: "unauthorized",
327
+ message: "Valid device token required",
328
+ });
329
+ return;
330
+ }
331
+ const allowedIds = tokenManager.getHostDeviceIds(token);
332
+ const devices = [...allowedIds].map((hostDeviceId) => {
333
+ const summary = sessionManager.getSummary(hostDeviceId);
334
+ return summary ?? {
335
+ id: hostDeviceId,
336
+ hostDeviceId,
337
+ state: "host_disconnected",
338
+ online: false,
339
+ hasHost: false,
340
+ clientCount: 0,
341
+ controllerId: null,
342
+ lastActivity: null,
343
+ createdAt: null,
344
+ bufferSize: 0,
345
+ machineId: null,
346
+ hostname: null,
347
+ platform: null,
348
+ cwd: null,
349
+ capabilities: [],
350
+ authorizationId: tokenManager.getAuthorizationId(token, hostDeviceId) ?? null,
351
+ };
352
+ }).map((device) => ({
353
+ ...device,
354
+ authorizationId: tokenManager.getAuthorizationId(token, device.hostDeviceId) ?? null,
355
+ }));
356
+ json(res, 200, { devices });
337
357
  return;
338
358
  }
339
359
 
340
- // Session detail
341
- const sessionMatch = url.pathname.match(/^\/sessions\/([^/]+)$/);
342
- if (method === "GET" && sessionMatch) {
360
+ // Device detail
361
+ const deviceMatch = url.pathname.match(/^\/devices\/([^/]+)$/);
362
+ if (method === "GET" && deviceMatch) {
343
363
  const token = extractBearerToken(req);
344
- const targetId = sessionMatch[1]!;
364
+ const targetId = deviceMatch[1]!;
345
365
  if (!token || !tokenManager.owns(token, targetId)) {
346
366
  json(res, 401, {
347
367
  error: "unauthorized",
@@ -351,10 +371,51 @@ async function handleRequest(
351
371
  }
352
372
  const summary = sessionManager.getSummary(targetId);
353
373
  if (!summary) {
354
- json(res, 404, { error: "session_not_found" });
374
+ json(res, 200, {
375
+ id: targetId,
376
+ hostDeviceId: targetId,
377
+ state: "host_disconnected",
378
+ online: false,
379
+ hasHost: false,
380
+ clientCount: 0,
381
+ controllerId: null,
382
+ lastActivity: null,
383
+ createdAt: null,
384
+ bufferSize: 0,
385
+ machineId: null,
386
+ hostname: null,
387
+ platform: null,
388
+ cwd: null,
389
+ capabilities: [],
390
+ authorizationId: tokenManager.getAuthorizationId(token, targetId) ?? null,
391
+ });
392
+ return;
393
+ }
394
+ json(res, 200, {
395
+ ...summary,
396
+ authorizationId: tokenManager.getAuthorizationId(token, targetId) ?? null,
397
+ });
398
+ return;
399
+ }
400
+
401
+ const revokeMatch = url.pathname.match(/^\/devices\/([^/]+)\/authorizations\/([^/]+)$/);
402
+ if (method === "DELETE" && revokeMatch) {
403
+ const token = extractBearerToken(req);
404
+ const hostDeviceId = decodeURIComponent(revokeMatch[1]!);
405
+ const authorizationId = decodeURIComponent(revokeMatch[2]!);
406
+ if (
407
+ !token ||
408
+ tokenManager.getAuthorizationId(token, hostDeviceId) !== authorizationId ||
409
+ !tokenManager.revoke(token, hostDeviceId, authorizationId)
410
+ ) {
411
+ json(res, 401, {
412
+ error: "unauthorized",
413
+ message: "Valid device authorization required",
414
+ });
355
415
  return;
356
416
  }
357
- json(res, 200, summary);
417
+ sessionManager.disconnectAuthorization(hostDeviceId, authorizationId);
418
+ json(res, 200, { ok: true });
358
419
  return;
359
420
  }
360
421
 
@@ -405,7 +466,7 @@ server.on("upgrade", (request, socket, head) => {
405
466
  const tunnelCookie = parseTunnelCookie(request);
406
467
  if (tunnelCookie && url.pathname !== "/ws") {
407
468
  const fallbackParsed = {
408
- sessionId: tunnelCookie.sessionId,
469
+ hostDeviceId: tunnelCookie.hostDeviceId,
409
470
  port: tunnelCookie.port,
410
471
  path: url.pathname,
411
472
  };
@@ -459,30 +520,33 @@ server.on("upgrade", (request, socket, head) => {
459
520
  wss.on(
460
521
  "connection",
461
522
  (socket: WebSocket, _request: IncomingMessage, url: URL) => {
462
- const sessionId = url.searchParams.get("sessionId");
523
+ const hostDeviceId = url.searchParams.get("hostDeviceId");
463
524
  const role = url.searchParams.get("role") as "host" | "client" | null;
464
525
 
465
- if (!sessionId || !role || (role !== "host" && role !== "client")) {
466
- socket.close(1008, "missing sessionId or role");
526
+ if (!hostDeviceId || !role || (role !== "host" && role !== "client")) {
527
+ socket.close(1008, "missing hostDeviceId or role");
467
528
  return;
468
529
  }
469
530
 
470
531
  const deviceId = url.searchParams.get("deviceId") ?? randomUUID();
471
532
 
533
+ let clientToken: string | undefined;
534
+ let clientAuthorizationId: string | undefined;
535
+
472
536
  if (role === "client") {
473
537
  const token = url.searchParams.get("token");
474
538
  const authResult = (_request as any).__authResult as
475
539
  | { userId?: string }
476
540
  | undefined;
477
- const session = sessionManager.get(sessionId);
541
+ const device = sessionManager.get(hostDeviceId);
478
542
 
479
- // Allow if: device token owns session, OR auth user owns session
480
- const tokenOwns = token && tokenManager.owns(token, sessionId);
543
+ // Allow if: device token owns host device, OR auth user owns host device
544
+ const tokenOwns = Boolean(token && tokenManager.owns(token, hostDeviceId));
481
545
  const authOwns =
482
546
  AUTH_REQUIRED &&
483
547
  authResult?.userId &&
484
- session?.userId &&
485
- authResult.userId === session.userId;
548
+ device?.userId &&
549
+ authResult.userId === device.userId;
486
550
 
487
551
  if (!tokenOwns && !authOwns) {
488
552
  socket.close(4001, "unauthorized");
@@ -490,8 +554,12 @@ wss.on(
490
554
  }
491
555
  if (!tokenOwns && authOwns && token) {
492
556
  tokenManager.register(token);
493
- tokenManager.bind(token, sessionId);
494
- log("info", `bound authenticated device token to session ${sessionId}`);
557
+ tokenManager.bind(token, hostDeviceId);
558
+ log("info", `bound authenticated device token to host device ${hostDeviceId}`);
559
+ }
560
+ if (token && (tokenOwns || authOwns)) {
561
+ clientToken = token;
562
+ clientAuthorizationId = tokenManager.getAuthorizationId(token, hostDeviceId);
495
563
  }
496
564
  }
497
565
 
@@ -499,31 +567,33 @@ wss.on(
499
567
  socket,
500
568
  role,
501
569
  deviceId,
570
+ token: clientToken,
571
+ authorizationId: clientAuthorizationId,
502
572
  connectedAt: Date.now(),
503
573
  };
504
574
 
505
575
  if (role === "host") {
506
576
  // Check if this is a reconnect (session already exists with clients)
507
- const existingSession = sessionManager.get(sessionId);
577
+ const existingSession = sessionManager.get(hostDeviceId);
508
578
  const isReconnect =
509
579
  existingSession &&
510
580
  existingSession.clients.size > 0 &&
511
581
  existingSession.state === "host_disconnected";
512
- sessionManager.setHost(sessionId, device);
582
+ sessionManager.setHost(hostDeviceId, device);
513
583
 
514
584
  // Associate userId from auth (for AUTH_REQUIRED gateways)
515
585
  const authResult = (_request as any).__authResult as
516
586
  | { userId?: string }
517
587
  | undefined;
518
588
  if (authResult?.userId) {
519
- const session = sessionManager.get(sessionId);
520
- if (session) session.userId = authResult.userId;
589
+ const deviceRecord = sessionManager.get(hostDeviceId);
590
+ if (deviceRecord) deviceRecord.userId = authResult.userId;
521
591
  }
522
592
  if (isReconnect) {
523
593
  const notification = serializeEnvelope(
524
594
  createEnvelope({
525
- type: "session.host_reconnected",
526
- sessionId,
595
+ type: "device.host_reconnected",
596
+ hostDeviceId,
527
597
  payload: {},
528
598
  }),
529
599
  );
@@ -534,15 +604,15 @@ wss.on(
534
604
  }
535
605
  }
536
606
  } else {
537
- sessionManager.addClient(sessionId, device);
607
+ sessionManager.addClient(hostDeviceId, device);
538
608
  }
539
609
 
540
610
  // Send welcome with protocol version
541
611
  socket.send(
542
612
  serializeEnvelope(
543
613
  createEnvelope({
544
- type: "session.connect",
545
- sessionId,
614
+ type: "device.connect",
615
+ hostDeviceId,
546
616
  payload: {
547
617
  role,
548
618
  clientName: deviceId,
@@ -554,7 +624,7 @@ wss.on(
554
624
 
555
625
  // If client just joined and host is not connected, notify immediately
556
626
  if (role === "client") {
557
- const sessionAfterJoin = sessionManager.get(sessionId);
627
+ const sessionAfterJoin = sessionManager.get(hostDeviceId);
558
628
  if (sessionAfterJoin) {
559
629
  const hostGone =
560
630
  !sessionAfterJoin.host ||
@@ -565,8 +635,8 @@ wss.on(
565
635
  socket.send(
566
636
  serializeEnvelope(
567
637
  createEnvelope({
568
- type: "session.host_disconnected",
569
- sessionId,
638
+ type: "device.host_disconnected",
639
+ hostDeviceId,
570
640
  payload: { reason: "host not connected" },
571
641
  }),
572
642
  ),
@@ -588,18 +658,18 @@ wss.on(
588
658
  socket,
589
659
  data.toString(),
590
660
  role,
591
- sessionId,
661
+ hostDeviceId,
592
662
  deviceId,
593
663
  sessionManager,
594
664
  );
595
665
  } catch (err) {
596
- log("error", `unhandled websocket message error for session ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
666
+ log("error", `unhandled websocket message error for host device ${hostDeviceId}: ${err instanceof Error ? err.message : String(err)}`);
597
667
  if (socket.readyState === socket.OPEN) {
598
668
  socket.send(
599
669
  serializeEnvelope(
600
670
  createEnvelope({
601
- type: "session.error",
602
- sessionId,
671
+ type: "device.error",
672
+ hostDeviceId,
603
673
  payload: {
604
674
  code: "invalid_message",
605
675
  message: "Failed to handle message",
@@ -614,13 +684,14 @@ wss.on(
614
684
  socket.on("close", () => {
615
685
  clearInterval(pingTimer);
616
686
  if (role === "host") {
617
- const result = sessionManager.removeHost(sessionId);
687
+ const result = sessionManager.removeHost(hostDeviceId);
688
+ cleanupSessionTunnels(hostDeviceId);
618
689
  // Notify all clients that host disconnected
619
690
  if (result) {
620
691
  const notification = serializeEnvelope(
621
692
  createEnvelope({
622
- type: "session.host_disconnected",
623
- sessionId,
693
+ type: "device.host_disconnected",
694
+ hostDeviceId,
624
695
  payload: { reason: "host connection closed" },
625
696
  }),
626
697
  );
@@ -631,7 +702,7 @@ wss.on(
631
702
  }
632
703
  }
633
704
  } else {
634
- sessionManager.removeClient(sessionId, deviceId);
705
+ sessionManager.removeClient(hostDeviceId, deviceId);
635
706
  }
636
707
  });
637
708
 
@@ -670,18 +741,18 @@ if (AUTH_REQUIRED) {
670
741
  if (!session.userId || !session.host) continue;
671
742
  const subscription = await checkSubscriptionByUserId(session.userId);
672
743
  if (subscription.status === "unknown") {
673
- log("warn", `subscription check unknown for user ${session.userId}, keeping session ${session.id}${subscription.reason ? ` (${subscription.reason})` : ""}`);
744
+ log("warn", `subscription check unknown for user ${session.userId}, keeping host device ${session.id}${subscription.reason ? ` (${subscription.reason})` : ""}`);
674
745
  continue;
675
746
  }
676
747
  if (subscription.status === "inactive") {
677
- log("info", `subscription expired for user ${session.userId}, disconnecting session ${session.id}`);
748
+ log("info", `subscription expired for user ${session.userId}, disconnecting host device ${session.id}`);
678
749
  // Notify host
679
750
  try {
680
751
  session.host.socket.send(
681
752
  serializeEnvelope(
682
753
  createEnvelope({
683
- type: "session.error",
684
- sessionId: session.id,
754
+ type: "device.error",
755
+ hostDeviceId: session.id,
685
756
  payload: {
686
757
  code: "subscription_expired",
687
758
  message: "Your Pro subscription has expired. Renew at https://itool.tech",
@@ -697,12 +768,12 @@ if (AUTH_REQUIRED) {
697
768
  try {
698
769
  client.socket.send(
699
770
  serializeEnvelope(
700
- createEnvelope({
701
- type: "session.error",
702
- sessionId: session.id,
771
+ createEnvelope({
772
+ type: "device.error",
773
+ hostDeviceId: session.id,
703
774
  payload: {
704
775
  code: "subscription_expired",
705
- message: "Host subscription expired. Session ended.",
776
+ message: "Host subscription expired. HostDevice ended.",
706
777
  },
707
778
  }),
708
779
  ),
package/src/pairings.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { randomInt, randomUUID } from "node:crypto";
1
+ import { randomInt } from "node:crypto";
2
2
  import type { GatewayStateStore } from "./state-store.js";
3
3
 
4
4
  export interface PairingRecord {
5
- sessionId: string;
5
+ hostDeviceId: string;
6
6
  pairingCode: string;
7
7
  expiresAt: number; // unix ms
8
8
  claimed: boolean;
@@ -36,11 +36,10 @@ export class PairingManager {
36
36
  }
37
37
  }
38
38
 
39
- create(sessionId?: string): PairingRecord {
40
- const id = sessionId ?? randomUUID();
39
+ create(hostDeviceId: string): PairingRecord {
41
40
  const code = String(randomInt(100000, 999999));
42
41
  const record: PairingRecord = {
43
- sessionId: id,
42
+ hostDeviceId,
44
43
  pairingCode: code,
45
44
  expiresAt: Date.now() + PAIRING_TTL,
46
45
  claimed: false,
@@ -68,7 +67,7 @@ export class PairingManager {
68
67
  return record;
69
68
  }
70
69
 
71
- getStatus(pairingCode: string): { status: string; expiresAt: number; sessionId: string } | { error: string; httpStatus: number } {
70
+ getStatus(pairingCode: string): { status: string; expiresAt: number; hostDeviceId: string } | { error: string; httpStatus: number } {
72
71
  const record = this.pairings.get(pairingCode);
73
72
  if (!record) {
74
73
  return { error: "pairing_not_found", httpStatus: 404 };
@@ -81,7 +80,7 @@ export class PairingManager {
81
80
  return {
82
81
  status: record.claimed ? "claimed" : "waiting",
83
82
  expiresAt: record.expiresAt,
84
- sessionId: record.sessionId,
83
+ hostDeviceId: record.hostDeviceId,
85
84
  };
86
85
  }
87
86