fix(gateway): allow HMR sockets on every configured domain
Quality / quality (ubuntu-latest) (push) Failing after 13m52s
Quality / quality (windows-latest) (push) Canceled after 0s

The WebSocket origin check compared the browser's Origin host, which
carries the port, against configured domains, which do not. publicOrigin
only ever matches domains[0], so every other domain fell through to that
comparison and was denied purely on the port: web.localhost:3000 never
matched web.localhost.

The result was a 403 on the HMR upgrade and a client reconnecting
forever, while the page itself loaded fine because HTTP routing resolves
the Host separately.

Compares hostnames now. Unrelated and lookalike-suffix origins are still
denied, and both cases are covered by tests.

Verified through a real gateway: the HMR socket opens on both localhost
and web.localhost, and a live edit reaches the browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 19:53:27 +05:30
co-authored by Claude Opus 5
parent b3b65dddd8
commit e66d2425aa
2 changed files with 61 additions and 2 deletions
@@ -0,0 +1,55 @@
import { expect, test } from "bun:test";
import { gatewayWebSocketOriginAllowed } from "../src/gateway.ts";
const target = {
name: "web",
origin: "http://127.0.0.1:3101",
domains: ["localhost", "web.localhost"],
publicOrigin: "http://localhost:3000",
} as any;
function upgrade(origin: string, host: string): Request {
return new Request("http://" + host + "/__wrnexus/hmr", {
headers: { origin, host, upgrade: "websocket" },
});
}
test("allows an upgrade from the app's primary domain", () => {
expect(
gatewayWebSocketOriginAllowed(upgrade("http://localhost:3000", "localhost:3000"), target, []),
).toBe(true);
});
test("allows an upgrade from a secondary domain on a non-default port", () => {
// publicOrigin is built from domains[0], so a browser on web.localhost falls
// through to the domain list — where the origin host still carries :3000 and
// the configured domain does not. That mismatch denied every HMR socket on
// any domain but the first, leaving the client reconnecting forever.
expect(
gatewayWebSocketOriginAllowed(
upgrade("http://web.localhost:3000", "web.localhost:3000"),
target,
[],
),
).toBe(true);
});
test("still denies an unrelated origin", () => {
expect(
gatewayWebSocketOriginAllowed(
upgrade("http://evil.example:3000", "web.localhost:3000"),
target,
[],
),
).toBe(false);
});
test("still denies a lookalike suffix domain", () => {
expect(
gatewayWebSocketOriginAllowed(
upgrade("http://notweb.localhost:3000", "web.localhost:3000"),
target,
[],
),
).toBe(false);
});