@linkshell/gateway 0.1.2 → 0.1.3
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 +1 -1
- package/src/embedded.ts +109 -65
- package/src/index.ts +1 -0
package/package.json
CHANGED
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(
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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({
|
|
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(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
|
247
|
-
|
|
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({
|
|
235
|
+
createEnvelope({
|
|
236
|
+
type: "session.host_reconnected",
|
|
237
|
+
sessionId,
|
|
238
|
+
payload: {},
|
|
239
|
+
}),
|
|
250
240
|
);
|
|
251
|
-
for (const [, client] of
|
|
252
|
-
if (client.socket.readyState === client.socket.OPEN)
|
|
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.
|
|
247
|
+
sessionManager.addClient(sessionId, device);
|
|
257
248
|
}
|
|
258
|
-
});
|
|
259
249
|
|
|
260
|
-
|
|
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 =
|
|
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: () =>
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
});
|