poltertab 1.8.0-beta.2 → 1.8.0-beta.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.
@@ -54,7 +54,7 @@ let isSecondary = false;
54
54
  let nodeId = null;
55
55
  let wss = null;
56
56
  let httpServer = null;
57
- let httpServer6 = null; // IPv6 loopback companion; null if host has no IPv6
57
+ // (httpServer6 removed — wildcard bind covers ::1; see fix/legacy-bind)
58
58
 
59
59
  // Secondary-side retry state. One counter for the whole reconnect/probe/promote
60
60
  // cycle, reset only by a Primary's secondary_accepted; `refusal` and
@@ -216,8 +216,14 @@ function allowOrigin(info, cb) {
216
216
 
217
217
  function setupPrimaryServer() {
218
218
  const http = require("http");
219
+ const LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
219
220
  const handleRequest = (req, res) => {
220
221
  if (req.method === "GET" && req.url === "/status") {
222
+ if (!LOOPBACK.has(req.socket.remoteAddress)) {
223
+ res.writeHead(404);
224
+ res.end();
225
+ return;
226
+ }
221
227
  const now = Date.now();
222
228
  const secondaries = [];
223
229
  for (const [nid, ws] of secondaryClients.entries()) {
@@ -268,26 +274,20 @@ function setupPrimaryServer() {
268
274
  httpServer.close();
269
275
  httpServer = null;
270
276
  startSecondaryMode();
271
- // NICE-2 residual race: a legacy (wildcard-binding) PolterTab that starts
272
- // in the ms between a successful probe and our listen() call will bind
273
- // undetected on macOS — SO_REUSEADDR lets 127.0.0.1 bind over 0.0.0.0,
274
- // so the new loopback Primary dual-binds silently. That window is
275
- // milliseconds and requires two simultaneous starts; not worth a retry
276
- // loop, but do not remove the probe and rely on this handler alone.
277
277
  } else {
278
278
  console.error(`[PolterTab MCP] HTTP server error: ${e.message}`);
279
279
  }
280
280
  });
281
281
 
282
- // Why probe-first (SHOULD-1 narrative, real reason for this change):
283
- // PR-3 routes /status to loopback only, so the whole Primary now binds
284
- // 127.0.0.1 instead of 0.0.0.0. On macOS, libuv sets SO_REUSEADDR, and
285
- // BSD/macOS lets a specific-address bind succeed silently over a live
286
- // wildcard listener — so the new loopback-binding Primary would start beside
287
- // a pre-1.8 (wildcard) PolterTab without getting EADDRINUSE, then steal all
288
- // loopback traffic. S11 is the regression test: it places a wildcard fixture
289
- // and asserts the bridge-under-test demotes instead of dual-binding. The
290
- // EADDRINUSE handler above remains as the fallback for same-host races.
282
+ // Why probe-first (SHOULD-1 narrative):
283
+ // We bind :: (IPv6 wildcard, dual-stack) so a second wildcard bind from a
284
+ // legacy 1.7.x/beta.1 server gets EADDRINUSE on every platform. But on
285
+ // macOS SO_REUSEADDR, a wildcard bind does NOT give EADDRINUSE when the
286
+ // existing listener is a specific-address bind (127.0.0.1/::1) — the old
287
+ // regression this PR fixes. The probe detects a real primary already
288
+ // serving 127.0.0.1 and demotes us before we call listen(), avoiding a
289
+ // race in the milliseconds between a successful probe and our own bind.
290
+ // The EADDRINUSE handler above covers the symmetric case.
291
291
  probePrimary().then((alive) => {
292
292
  if (alive) {
293
293
  console.error(
@@ -298,7 +298,7 @@ function setupPrimaryServer() {
298
298
  startSecondaryMode();
299
299
  return;
300
300
  }
301
- httpServer.listen(WS_PORT, "127.0.0.1", () => {
301
+ httpServer.listen(WS_PORT, () => {
302
302
  wss = new WebSocket.WebSocketServer({
303
303
  server: httpServer,
304
304
  verifyClient: allowOrigin,
@@ -409,35 +409,13 @@ function setupPrimaryServer() {
409
409
 
410
410
  setupPrimaryWss(wss);
411
411
 
412
- // Also listen on ::1 (IPv6 loopback) so Node 18 clients work: Node 18
413
- // resolves 'localhost' to ::1 first with no happy-eyeballs fallback.
414
- // Skip cleanly when the host has no IPv6 stack (EADDRNOTAVAIL/EAFNOSUPPORT).
415
- // On macOS, SO_REUSEADDR lets a specific-address ::1 bind succeed silently
416
- // over a live :: wildcard listener — but we only set up httpServer6 AFTER
417
- // probing 127.0.0.1, so a false-primary that owns :: would be caught by that
418
- // probe (it also owns 127.0.0.1) and we would have gone Secondary already.
419
- //
420
- // The "listening" log is delayed until ::1 is ready (or skipped) so that
421
- // any client connecting on the log signal can reach both addresses.
422
- const SKIP_IPV6 = new Set(["EADDRNOTAVAIL", "EAFNOSUPPORT", "EPROTONOSUPPORT"]);
423
- const logListening = () =>
424
- console.error(
425
- `[PolterTab MCP] Primary WebSocket server listening on ws://localhost:${WS_PORT}`,
426
- );
427
- httpServer6 = http.createServer(handleRequest);
428
- httpServer6.on("error", (e) => {
429
- if (SKIP_IPV6.has(e.code)) {
430
- httpServer6 = null;
431
- } else {
432
- console.error(`[PolterTab MCP] IPv6 loopback error: ${e.message}`);
433
- }
434
- logListening();
435
- });
436
- httpServer6.once("listening", logListening);
437
- httpServer6.on("upgrade", (req, socket, head) => {
438
- wss.handleUpgrade(req, socket, head, (client) => wss.emit("connection", client, req));
439
- });
440
- httpServer6.listen(WS_PORT, "::1");
412
+ // Wildcard bind (:: dual-stack) covers both 127.0.0.1 and ::1, so Node 18
413
+ // clients that resolve localhost → ::1 reach the primary without a separate
414
+ // httpServer6. A legacy server attempting listen(port) with no host gets
415
+ // EADDRINUSE because two wildcard binds on the same port always collide.
416
+ console.error(
417
+ `[PolterTab MCP] Primary WebSocket server listening on ws://localhost:${WS_PORT}`,
418
+ );
441
419
  });
442
420
  });
443
421
  }
@@ -1125,7 +1103,6 @@ function shutdown() {
1125
1103
  console.error("[PolterTab MCP] Shutting down...");
1126
1104
  if (wss) wss.close();
1127
1105
  if (httpServer) httpServer.close();
1128
- if (httpServer6) httpServer6.close();
1129
1106
  process.exit(0);
1130
1107
  }
1131
1108
  // Helper to wait for WebSocket connection
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poltertab",
3
- "version": "1.8.0-beta.2",
3
+ "version": "1.8.0-beta.3",
4
4
  "description": "Let any AI assistant drive and scrape your existing Chrome profile — no headless browser, no complex setup.",
5
5
  "bin": {
6
6
  "poltertab": "./bin/poltertab.js"