@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/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({
|
|
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}
|
|
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.
|
|
164
|
+
const record = pairingManager.create(body.hostDeviceId);
|
|
163
165
|
json(res, 201, {
|
|
164
|
-
|
|
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.
|
|
180
|
-
|
|
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 === "/
|
|
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.
|
|
194
|
-
const
|
|
195
|
-
.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
217
|
-
if (method === "GET" &&
|
|
230
|
+
const deviceMatch = url.pathname.match(/^\/devices\/([^/]+)$/);
|
|
231
|
+
if (method === "GET" && deviceMatch) {
|
|
218
232
|
const token = extractBearerToken(req);
|
|
219
|
-
const targetId =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
338
|
-
socket.close(1008, "missing
|
|
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,
|
|
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 = {
|
|
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(
|
|
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(
|
|
427
|
+
sessionManager.setHost(hostDeviceId, device);
|
|
361
428
|
if (isReconnect) {
|
|
362
429
|
const notification = serializeEnvelope(
|
|
363
430
|
createEnvelope({
|
|
364
|
-
type: "
|
|
365
|
-
|
|
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(
|
|
442
|
+
sessionManager.addClient(hostDeviceId, device);
|
|
376
443
|
}
|
|
377
444
|
|
|
378
445
|
socket.send(
|
|
379
446
|
serializeEnvelope(
|
|
380
447
|
createEnvelope({
|
|
381
|
-
type: "
|
|
382
|
-
|
|
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
|
-
|
|
469
|
+
hostDeviceId,
|
|
403
470
|
deviceId,
|
|
404
471
|
sessionManager,
|
|
405
472
|
);
|
|
406
473
|
} catch (err) {
|
|
407
|
-
log("error", `unhandled websocket message error for
|
|
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: "
|
|
413
|
-
|
|
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(
|
|
495
|
+
const result = sessionManager.removeHost(hostDeviceId);
|
|
496
|
+
cleanupSessionTunnels(hostDeviceId);
|
|
429
497
|
if (result) {
|
|
430
498
|
const notification = serializeEnvelope(
|
|
431
499
|
createEnvelope({
|
|
432
|
-
type: "
|
|
433
|
-
|
|
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(
|
|
511
|
+
sessionManager.removeClient(hostDeviceId, deviceId);
|
|
444
512
|
}
|
|
445
513
|
});
|
|
446
514
|
|