@linkshell/gateway 0.1.2 → 0.1.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkshell/gateway",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "main": "dist/gateway/src/index.js",
6
6
  "exports": {
package/src/embedded.ts CHANGED
@@ -70,7 +70,9 @@ function getClientIp(req: IncomingMessage): string {
70
70
  * Start an embedded gateway. Returns a handle to get URLs and close it.
71
71
  * Used by CLI when no external --gateway is provided.
72
72
  */
73
- export function startEmbeddedGateway(options: EmbeddedGatewayOptions = {}): Promise<EmbeddedGateway> {
73
+ export function startEmbeddedGateway(
74
+ options: EmbeddedGatewayOptions = {},
75
+ ): Promise<EmbeddedGateway> {
74
76
  const targetPort = options.port ?? 0; // 0 = random available port
75
77
  const logLevel = options.logLevel ?? "warn";
76
78
  const silent = options.silent ?? false;
@@ -139,6 +141,7 @@ export function startEmbeddedGateway(options: EmbeddedGatewayOptions = {}): Prom
139
141
  createdAt: s.createdAt,
140
142
  provider: s.provider ?? null,
141
143
  hostname: s.hostname ?? null,
144
+ platform: s.platform ?? null,
142
145
  }));
143
146
  json(res, 200, { sessions });
144
147
  return;
@@ -169,19 +172,31 @@ export function startEmbeddedGateway(options: EmbeddedGatewayOptions = {}): Prom
169
172
  json(res, 404, { error: "not_found" });
170
173
  } catch (err) {
171
174
  if (err instanceof ZodError) {
172
- json(res, 400, { error: "invalid_message", message: err.errors[0]?.message ?? "Validation failed" });
175
+ json(res, 400, {
176
+ error: "invalid_message",
177
+ message: err.errors[0]?.message ?? "Validation failed",
178
+ });
173
179
  } else if (err instanceof BodyTooLargeError) {
174
- json(res, 413, { error: "body_too_large", message: "Request body exceeds limit" });
180
+ json(res, 413, {
181
+ error: "body_too_large",
182
+ message: "Request body exceeds limit",
183
+ });
175
184
  } else if (err instanceof SyntaxError) {
176
185
  json(res, 400, { error: "invalid_json", message: "Malformed JSON" });
177
186
  } else {
178
187
  log("error", `unhandled: ${err}`);
179
- json(res, 500, { error: "internal_error", message: "Internal server error" });
188
+ json(res, 500, {
189
+ error: "internal_error",
190
+ message: "Internal server error",
191
+ });
180
192
  }
181
193
  }
182
194
  });
183
195
 
184
- const wss = new WebSocketServer({ noServer: true, maxPayload: MAX_WS_MESSAGE_SIZE });
196
+ const wss = new WebSocketServer({
197
+ noServer: true,
198
+ maxPayload: MAX_WS_MESSAGE_SIZE,
199
+ });
185
200
 
186
201
  server.on("upgrade", (request, socket, head) => {
187
202
  const url = new URL(request.url ?? "/", `http://${request.headers.host}`);
@@ -194,88 +209,117 @@ export function startEmbeddedGateway(options: EmbeddedGatewayOptions = {}): Prom
194
209
  });
195
210
  });
196
211
 
197
- wss.on("connection", (socket: WebSocket, _request: IncomingMessage, url: URL) => {
198
- const sessionId = url.searchParams.get("sessionId");
199
- const role = url.searchParams.get("role") as "host" | "client" | null;
200
-
201
- if (!sessionId || !role || (role !== "host" && role !== "client")) {
202
- socket.close(1008, "missing sessionId or role");
203
- return;
204
- }
212
+ wss.on(
213
+ "connection",
214
+ (socket: WebSocket, _request: IncomingMessage, url: URL) => {
215
+ const sessionId = url.searchParams.get("sessionId");
216
+ const role = url.searchParams.get("role") as "host" | "client" | null;
205
217
 
206
- const deviceId = url.searchParams.get("deviceId") ?? randomUUID();
207
- const device = { socket, role, deviceId, connectedAt: Date.now() };
208
-
209
- if (role === "host") {
210
- const existingSession = sessionManager.get(sessionId);
211
- const isReconnect = existingSession && existingSession.clients.size > 0 && existingSession.state === "host_disconnected";
212
- sessionManager.setHost(sessionId, device);
213
- if (isReconnect) {
214
- const notification = serializeEnvelope(
215
- createEnvelope({ type: "session.host_reconnected", sessionId, payload: {} }),
216
- );
217
- for (const [, client] of existingSession.clients) {
218
- if (client.socket.readyState === client.socket.OPEN) client.socket.send(notification);
219
- }
218
+ if (!sessionId || !role || (role !== "host" && role !== "client")) {
219
+ socket.close(1008, "missing sessionId or role");
220
+ return;
220
221
  }
221
- } else {
222
- sessionManager.addClient(sessionId, device);
223
- }
224
-
225
- socket.send(
226
- serializeEnvelope(
227
- createEnvelope({
228
- type: "session.connect",
229
- sessionId,
230
- payload: { role, clientName: deviceId, protocolVersion: PROTOCOL_VERSION },
231
- }),
232
- ),
233
- );
234
-
235
- const pingTimer = setInterval(() => {
236
- if (socket.readyState === socket.OPEN) socket.ping();
237
- }, PING_INTERVAL);
238
222
 
239
- socket.on("message", (data: WebSocket.RawData) => {
240
- handleSocketMessage(socket, data.toString(), role, sessionId, deviceId, sessionManager);
241
- });
223
+ const deviceId = url.searchParams.get("deviceId") ?? randomUUID();
224
+ const device = { socket, role, deviceId, connectedAt: Date.now() };
242
225
 
243
- socket.on("close", () => {
244
- clearInterval(pingTimer);
245
226
  if (role === "host") {
246
- const result = sessionManager.removeHost(sessionId);
247
- if (result) {
227
+ const existingSession = sessionManager.get(sessionId);
228
+ const isReconnect =
229
+ existingSession &&
230
+ existingSession.clients.size > 0 &&
231
+ existingSession.state === "host_disconnected";
232
+ sessionManager.setHost(sessionId, device);
233
+ if (isReconnect) {
248
234
  const notification = serializeEnvelope(
249
- createEnvelope({ type: "session.host_disconnected", sessionId, payload: { reason: "host connection closed" } }),
235
+ createEnvelope({
236
+ type: "session.host_reconnected",
237
+ sessionId,
238
+ payload: {},
239
+ }),
250
240
  );
251
- for (const [, client] of result.clients) {
252
- if (client.socket.readyState === client.socket.OPEN) client.socket.send(notification);
241
+ for (const [, client] of existingSession.clients) {
242
+ if (client.socket.readyState === client.socket.OPEN)
243
+ client.socket.send(notification);
253
244
  }
254
245
  }
255
246
  } else {
256
- sessionManager.removeClient(sessionId, deviceId);
247
+ sessionManager.addClient(sessionId, device);
257
248
  }
258
- });
259
249
 
260
- socket.on("error", () => {});
261
- });
250
+ socket.send(
251
+ serializeEnvelope(
252
+ createEnvelope({
253
+ type: "session.connect",
254
+ sessionId,
255
+ payload: {
256
+ role,
257
+ clientName: deviceId,
258
+ protocolVersion: PROTOCOL_VERSION,
259
+ },
260
+ }),
261
+ ),
262
+ );
263
+
264
+ const pingTimer = setInterval(() => {
265
+ if (socket.readyState === socket.OPEN) socket.ping();
266
+ }, PING_INTERVAL);
267
+
268
+ socket.on("message", (data: WebSocket.RawData) => {
269
+ handleSocketMessage(
270
+ socket,
271
+ data.toString(),
272
+ role,
273
+ sessionId,
274
+ deviceId,
275
+ sessionManager,
276
+ );
277
+ });
278
+
279
+ socket.on("close", () => {
280
+ clearInterval(pingTimer);
281
+ if (role === "host") {
282
+ const result = sessionManager.removeHost(sessionId);
283
+ if (result) {
284
+ const notification = serializeEnvelope(
285
+ createEnvelope({
286
+ type: "session.host_disconnected",
287
+ sessionId,
288
+ payload: { reason: "host connection closed" },
289
+ }),
290
+ );
291
+ for (const [, client] of result.clients) {
292
+ if (client.socket.readyState === client.socket.OPEN)
293
+ client.socket.send(notification);
294
+ }
295
+ }
296
+ } else {
297
+ sessionManager.removeClient(sessionId, deviceId);
298
+ }
299
+ });
300
+
301
+ socket.on("error", () => {});
302
+ },
303
+ );
262
304
 
263
305
  return new Promise<EmbeddedGateway>((resolve, reject) => {
264
306
  server.on("error", reject);
265
307
  server.listen(targetPort, () => {
266
308
  const addr = server.address();
267
- const actualPort = typeof addr === "object" && addr ? addr.port : targetPort;
309
+ const actualPort =
310
+ typeof addr === "object" && addr ? addr.port : targetPort;
268
311
  log("info", `embedded gateway on port ${actualPort}`);
269
312
  resolve({
270
313
  port: actualPort,
271
314
  httpUrl: `http://127.0.0.1:${actualPort}`,
272
315
  wsUrl: `ws://127.0.0.1:${actualPort}/ws`,
273
- close: () => new Promise<void>((res) => {
274
- wss.clients.forEach((ws) => ws.close(1001, "shutting down"));
275
- sessionManager.destroy();
276
- pairingManager.destroy();
277
- server.close(() => res());
278
- }),
316
+ close: () =>
317
+ new Promise<void>((res) => {
318
+ wss.clients.forEach((ws) => ws.close(1001, "shutting down"));
319
+ sessionManager.destroy();
320
+ pairingManager.destroy();
321
+ server.close(() => res());
322
+ }),
279
323
  });
280
324
  });
281
325
  });
package/src/index.ts CHANGED
@@ -199,6 +199,7 @@ async function handleRequest(
199
199
  createdAt: s.createdAt,
200
200
  provider: s.provider ?? null,
201
201
  hostname: s.hostname ?? null,
202
+ platform: s.platform ?? null,
202
203
  }));
203
204
  json(res, 200, { sessions });
204
205
  return;