@linkshell/gateway 0.4.25 → 0.4.28

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.4.25",
3
+ "version": "0.4.28",
4
4
  "type": "module",
5
5
  "main": "dist/gateway/src/index.js",
6
6
  "exports": {
package/src/embedded.ts CHANGED
@@ -25,6 +25,7 @@ import {
25
25
  handleTunnelWsUpgrade,
26
26
  cleanupSessionTunnels,
27
27
  } from "./tunnel.js";
28
+ import { serveWeb, serveWebAsset, webEnabled, webDistPath } from "./static-web.js";
28
29
 
29
30
  export interface EmbeddedGatewayOptions {
30
31
  port?: number;
@@ -252,6 +253,11 @@ export function startEmbeddedGateway(
252
253
  return;
253
254
  }
254
255
 
256
+ // Serve the gateway's OWN built web assets (no SPA fallback) before the
257
+ // tunnel cookie fallback, so a real asset like /assets/index-xxx.js is
258
+ // served locally instead of being proxied via a stale tunnel cookie.
259
+ if (await serveWebAsset(req, res)) return;
260
+
255
261
  // Tunnel fallback: cookie-based routing for sub-resources (e.g. /_next/static/...)
256
262
  const tunnelCookie = parseTunnelCookie(req);
257
263
  if (tunnelCookie && shouldUseTunnelCookieFallback(req, url.pathname, (pathname) =>
@@ -272,6 +278,11 @@ export function startEmbeddedGateway(
272
278
  return;
273
279
  }
274
280
 
281
+ // Web SPA: last GET fallback, after every API route, so it never shadows
282
+ // them. Serves index.html for "/" and client-side routes; a no-op (false)
283
+ // when no web dist is bundled (e.g. an API-only embedded gateway).
284
+ if (await serveWeb(req, res)) return;
285
+
275
286
  json(res, 404, { error: "not_found" });
276
287
  } catch (err) {
277
288
  if (err instanceof ZodError) {
@@ -479,6 +490,14 @@ export function startEmbeddedGateway(
479
490
  const actualPort =
480
491
  typeof addr === "object" && addr ? addr.port : targetPort;
481
492
  log("info", `embedded gateway on port ${actualPort}`);
493
+ // Surface whether the bundled web SPA was found — the in-app WebView loads
494
+ // this gateway's "/", so a missing dist means a blank screen. Makes the
495
+ // LAN/self-hosted "WebView is blank" failure mode diagnosable from logs.
496
+ if (webEnabled()) {
497
+ log("info", `web UI served from ${webDistPath()}`);
498
+ } else {
499
+ log("warn", `web UI not bundled (WEB_DIST=${webDistPath()} has no index.html) — in-app agent console will be blank`);
500
+ }
482
501
  resolve({
483
502
  port: actualPort,
484
503
  httpUrl: `http://127.0.0.1:${actualPort}`,
package/src/index.ts CHANGED
@@ -774,7 +774,13 @@ wss.on(
774
774
  }
775
775
 
776
776
  // Ping/pong for liveness — terminate connections that stop responding
777
- // to pings (dead/half-open sockets) instead of leaking them.
777
+ // (dead/half-open sockets) instead of leaking them. Liveness is proven by
778
+ // EITHER a WS pong OR any inbound message: React Native's WebSocket does
779
+ // not reliably auto-reply to server ping control frames, so a perfectly
780
+ // healthy native client (terminal) would otherwise be terminated every
781
+ // ~40s, silently dropping its keystrokes into the reconnect queue. Active
782
+ // clients send a heartbeat every 15s (< PING_INTERVAL), so they always
783
+ // refresh isAlive; only a genuinely silent socket gets reaped.
778
784
  const liveSocket = socket as WebSocket & { isAlive?: boolean };
779
785
  liveSocket.isAlive = true;
780
786
  socket.on("pong", () => {
@@ -791,6 +797,8 @@ wss.on(
791
797
  }, PING_INTERVAL);
792
798
 
793
799
  socket.on("message", (data: WebSocket.RawData) => {
800
+ // Any inbound traffic counts as proof of life (see ping/pong note above).
801
+ liveSocket.isAlive = true;
794
802
  try {
795
803
  handleSocketMessage(
796
804
  socket,