openclaw-navigator 5.2.0 → 5.2.2

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.
Files changed (3) hide show
  1. package/cli.mjs +33 -7
  2. package/mcp.mjs +54 -34
  3. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -755,6 +755,7 @@ function handleRequest(req, res) {
755
755
  // Keep /ui prefix — Next.js basePath: "/ui" expects it
756
756
  if (path === "/ui" || path.startsWith("/ui/")) {
757
757
  const targetURL = `${path}${url.search}`;
758
+ const incomingHost = req.headers.host || "localhost";
758
759
 
759
760
  const proxyOpts = {
760
761
  hostname: "127.0.0.1",
@@ -764,15 +765,38 @@ function handleRequest(req, res) {
764
765
  headers: {
765
766
  ...req.headers,
766
767
  host: `127.0.0.1:${ocUIPort}`,
768
+ "x-forwarded-host": incomingHost,
769
+ "x-forwarded-proto": activeTunnelURL ? "https" : "http",
770
+ "x-forwarded-for": req.socket.remoteAddress || "127.0.0.1",
767
771
  },
768
772
  };
769
773
 
770
774
  const proxyReq = httpRequest(proxyOpts, (proxyRes) => {
771
- // Forward status + headers (add CORS for cross-origin tunnel access)
772
775
  const headers = { ...proxyRes.headers };
776
+
777
+ // CORS for cross-origin tunnel access
773
778
  headers["access-control-allow-origin"] = "*";
774
779
  headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, OPTIONS";
775
- headers["access-control-allow-headers"] = "Content-Type, Authorization";
780
+ headers["access-control-allow-headers"] = "Content-Type, Authorization, Cookie";
781
+ headers["access-control-allow-credentials"] = "true";
782
+
783
+ // Fix redirects — rewrite Location from localhost:4000 to tunnel URL
784
+ if (headers.location) {
785
+ headers.location = headers.location
786
+ .replace(`http://127.0.0.1:${ocUIPort}`, "")
787
+ .replace(`http://localhost:${ocUIPort}`, "");
788
+ }
789
+
790
+ // Fix cookies — remove domain restriction so they work through tunnel
791
+ if (headers["set-cookie"]) {
792
+ const cookies = Array.isArray(headers["set-cookie"])
793
+ ? headers["set-cookie"]
794
+ : [headers["set-cookie"]];
795
+ headers["set-cookie"] = cookies.map((c) =>
796
+ c.replace(/;\s*domain=[^;]*/gi, "").replace(/;\s*secure/gi, ""),
797
+ );
798
+ }
799
+
776
800
  res.writeHead(proxyRes.statusCode ?? 502, headers);
777
801
  proxyRes.pipe(res, { end: true });
778
802
  });
@@ -1119,13 +1143,14 @@ module.exports = {
1119
1143
  });
1120
1144
 
1121
1145
  // ── WebSocket proxy — forward /ws connections to OC gateway ─────────
1122
- // The bridge is a transparent relay: Navigator connects here, we pipe to
1123
- // the OC gateway WebSocket at ws://localhost:ocGatewayPort/ws.
1146
+ // The bridge is a transparent relay: Navigator connects to /ws here,
1147
+ // we pipe to the OC gateway WebSocket at ws://localhost:ocGatewayPort/
1148
+ // (OC Core accepts WebSocket at root /, not /ws)
1124
1149
  server.on("upgrade", (req, socket, head) => {
1125
1150
  const reqUrl = new URL(req.url ?? "/", "http://localhost");
1126
1151
  const reqPath = reqUrl.pathname;
1127
1152
 
1128
- // Only handle /ws paths
1153
+ // Only handle /ws paths (Navigator connects to /ws)
1129
1154
  if (reqPath !== "/ws" && !reqPath.startsWith("/ws/")) {
1130
1155
  socket.destroy();
1131
1156
  return;
@@ -1133,9 +1158,10 @@ module.exports = {
1133
1158
 
1134
1159
  // Connect to the gateway WebSocket
1135
1160
  const gwSocket = netConnect(ocGatewayPort, "127.0.0.1", () => {
1136
- // Forward the HTTP upgrade request to the gateway
1161
+ // Rewrite path: Navigator uses /ws but OC Core expects / (root)
1162
+ const gwPath = reqPath === "/ws" ? "/" : reqPath.replace(/^\/ws/, "");
1137
1163
  const upgradeHeaders = [
1138
- `${req.method} ${req.url} HTTP/${req.httpVersion}`,
1164
+ `${req.method} ${gwPath || "/"} HTTP/${req.httpVersion}`,
1139
1165
  ...Object.entries(req.headers).map(([k, v]) => `${k}: ${v}`),
1140
1166
  "",
1141
1167
  "",
package/mcp.mjs CHANGED
@@ -63,46 +63,66 @@ function authHeaders() {
63
63
 
64
64
  // ── HTTP helpers ──────────────────────────────────────────────────────────
65
65
 
66
- async function bridgeGet(path) {
67
- const res = await fetch(`${BRIDGE_URL}${path}`, {
68
- headers: authHeaders(),
69
- });
70
- if (!res.ok) {
71
- // If 401, try reloading the token (bridge may have restarted with new identity)
72
- if (res.status === 401 && !bridgeGet._retrying) {
73
- bridgeGet._retrying = true;
74
- bridgeToken = loadBridgeToken();
75
- const retry = await bridgeGet(path);
76
- bridgeGet._retrying = false;
77
- return retry;
66
+ const MAX_RETRIES = 3;
67
+ const RETRY_DELAYS = [500, 1500, 3000]; // ms — escalating backoff
68
+
69
+ /** Retry wrapper for bridge HTTP calls — handles ECONNREFUSED and 401 */
70
+ async function withRetry(fn, label) {
71
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
72
+ try {
73
+ return await fn();
74
+ } catch (err) {
75
+ const isConnErr =
76
+ err.cause?.code === "ECONNREFUSED" ||
77
+ err.message?.includes("ECONNREFUSED") ||
78
+ err.message?.includes("fetch failed");
79
+ const is401 = err.message?.includes("returned 401");
80
+
81
+ if (is401 && attempt === 0) {
82
+ // Auth mismatch — bridge may have restarted with new token
83
+ log(`${label}: 401 — reloading bridge token and retrying`);
84
+ bridgeToken = loadBridgeToken();
85
+ continue;
86
+ }
87
+
88
+ if (isConnErr && attempt < MAX_RETRIES) {
89
+ const delay = RETRY_DELAYS[attempt] || 3000;
90
+ log(`${label}: bridge unreachable — retry ${attempt + 1}/${MAX_RETRIES} in ${delay}ms`);
91
+ await new Promise((r) => setTimeout(r, delay));
92
+ // Also reload token in case bridge restarted with new identity
93
+ bridgeToken = loadBridgeToken();
94
+ continue;
95
+ }
96
+
97
+ throw err;
78
98
  }
79
- bridgeGet._retrying = false;
80
- throw new Error(`Bridge GET ${path} returned ${res.status}`);
81
99
  }
82
- bridgeGet._retrying = false;
83
- return res.json();
100
+ }
101
+
102
+ async function bridgeGet(path) {
103
+ return withRetry(async () => {
104
+ const res = await fetch(`${BRIDGE_URL}${path}`, {
105
+ headers: authHeaders(),
106
+ });
107
+ if (!res.ok) {
108
+ throw new Error(`Bridge GET ${path} returned ${res.status}`);
109
+ }
110
+ return res.json();
111
+ }, `GET ${path}`);
84
112
  }
85
113
 
86
114
  async function bridgePost(path, body) {
87
- const res = await fetch(`${BRIDGE_URL}${path}`, {
88
- method: "POST",
89
- headers: authHeaders(),
90
- body: JSON.stringify(body),
91
- });
92
- if (!res.ok) {
93
- // If 401, try reloading the token (bridge may have restarted with new identity)
94
- if (res.status === 401 && !bridgePost._retrying) {
95
- bridgePost._retrying = true;
96
- bridgeToken = loadBridgeToken();
97
- const retry = await bridgePost(path, body);
98
- bridgePost._retrying = false;
99
- return retry;
115
+ return withRetry(async () => {
116
+ const res = await fetch(`${BRIDGE_URL}${path}`, {
117
+ method: "POST",
118
+ headers: authHeaders(),
119
+ body: JSON.stringify(body),
120
+ });
121
+ if (!res.ok) {
122
+ throw new Error(`Bridge POST ${path} returned ${res.status}`);
100
123
  }
101
- bridgePost._retrying = false;
102
- throw new Error(`Bridge POST ${path} returned ${res.status}`);
103
- }
104
- bridgePost._retrying = false;
105
- return res.json();
124
+ return res.json();
125
+ }, `POST ${path}`);
106
126
  }
107
127
 
108
128
  // ── Event polling engine ──────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-navigator",
3
- "version": "5.2.0",
3
+ "version": "5.2.2",
4
4
  "description": "One-command bridge + tunnel for the Navigator browser — works on any machine, any OS",
5
5
  "keywords": [
6
6
  "browser",