@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/index.ts
CHANGED
|
@@ -127,10 +127,12 @@ function setCors(res: ServerResponse): void {
|
|
|
127
127
|
|
|
128
128
|
// ── HTTP API ────────────────────────────────────────────────────────
|
|
129
129
|
|
|
130
|
-
const createPairingBody = z.object({
|
|
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
|
-
//
|
|
185
|
-
if (method === "GET" && url.pathname === "/
|
|
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
|
|
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, {
|
|
198
|
+
json(res, 200, { devices });
|
|
197
199
|
return;
|
|
198
200
|
}
|
|
199
201
|
|
|
200
|
-
// Delete a
|
|
201
|
-
if (method === "DELETE" && url.pathname
|
|
202
|
-
const
|
|
203
|
-
if (!
|
|
204
|
-
json(res, 400, { error: "
|
|
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
|
|
213
|
-
if (!
|
|
214
|
+
const device = sessionManager.get(hostDeviceId);
|
|
215
|
+
if (!device) {
|
|
214
216
|
json(res, 404, { error: "not_found" });
|
|
215
217
|
return;
|
|
216
218
|
}
|
|
217
|
-
if (
|
|
218
|
-
sessionManager.forceDelete(
|
|
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
|
|
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
|
-
|
|
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}
|
|
269
|
+
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}`);
|
|
268
270
|
json(res, result.status, result.body);
|
|
269
271
|
return;
|
|
270
272
|
}
|
|
271
273
|
|
|
272
|
-
// Auth check for premium gateway (skip healthz,
|
|
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.
|
|
287
|
+
const record = pairingManager.create(body.hostDeviceId);
|
|
286
288
|
json(res, 201, {
|
|
287
|
-
|
|
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.
|
|
308
|
-
|
|
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
|
-
//
|
|
313
|
-
if (method === "GET" && url.pathname === "/
|
|
321
|
+
// Authorized host device list
|
|
322
|
+
if (method === "GET" && url.pathname === "/devices") {
|
|
314
323
|
const token = extractBearerToken(req);
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
//
|
|
341
|
-
const
|
|
342
|
-
if (method === "GET" &&
|
|
360
|
+
// Device detail
|
|
361
|
+
const deviceMatch = url.pathname.match(/^\/devices\/([^/]+)$/);
|
|
362
|
+
if (method === "GET" && deviceMatch) {
|
|
343
363
|
const token = extractBearerToken(req);
|
|
344
|
-
const targetId =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
523
|
+
const hostDeviceId = url.searchParams.get("hostDeviceId") ?? url.searchParams.get("sessionId");
|
|
463
524
|
const role = url.searchParams.get("role") as "host" | "client" | null;
|
|
464
525
|
|
|
465
|
-
if (!
|
|
466
|
-
socket.close(1008, "missing
|
|
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
|
|
541
|
+
const device = sessionManager.get(hostDeviceId);
|
|
478
542
|
|
|
479
|
-
// Allow if: device token owns
|
|
480
|
-
const tokenOwns = token && tokenManager.owns(token,
|
|
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
|
-
|
|
485
|
-
authResult.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,
|
|
494
|
-
log("info", `bound authenticated device token to
|
|
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(
|
|
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(
|
|
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
|
|
520
|
-
if (
|
|
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: "
|
|
526
|
-
|
|
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(
|
|
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: "
|
|
545
|
-
|
|
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(
|
|
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: "
|
|
569
|
-
|
|
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
|
-
|
|
661
|
+
hostDeviceId,
|
|
592
662
|
deviceId,
|
|
593
663
|
sessionManager,
|
|
594
664
|
);
|
|
595
665
|
} catch (err) {
|
|
596
|
-
log("error", `unhandled websocket message error for
|
|
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: "
|
|
602
|
-
|
|
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(
|
|
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: "
|
|
623
|
-
|
|
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(
|
|
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
|
|
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
|
|
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: "
|
|
684
|
-
|
|
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,9 +768,9 @@ if (AUTH_REQUIRED) {
|
|
|
697
768
|
try {
|
|
698
769
|
client.socket.send(
|
|
699
770
|
serializeEnvelope(
|
|
700
|
-
|
|
701
|
-
type: "
|
|
702
|
-
|
|
771
|
+
createEnvelope({
|
|
772
|
+
type: "device.error",
|
|
773
|
+
hostDeviceId: session.id,
|
|
703
774
|
payload: {
|
|
704
775
|
code: "subscription_expired",
|
|
705
776
|
message: "Host subscription expired. Session ended.",
|
package/src/pairings.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { randomInt
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
2
2
|
import type { GatewayStateStore } from "./state-store.js";
|
|
3
3
|
|
|
4
4
|
export interface PairingRecord {
|
|
5
|
-
|
|
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(
|
|
40
|
-
const id = sessionId ?? randomUUID();
|
|
39
|
+
create(hostDeviceId: string): PairingRecord {
|
|
41
40
|
const code = String(randomInt(100000, 999999));
|
|
42
41
|
const record: PairingRecord = {
|
|
43
|
-
|
|
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;
|
|
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
|
-
|
|
83
|
+
hostDeviceId: record.hostDeviceId,
|
|
85
84
|
};
|
|
86
85
|
}
|
|
87
86
|
|