logicsrc/apps/pwa/test/origin.test.mjs
Anthony Ettinger 60c0cbfbce
Some checks are pending
CI / build (push) Waiting to run
test / test (push) Waiting to run
fix(pwa): return the caller's own host in the CLI device-flow URLs (#105)
`logicsrc login --device` told users to open
https://logicsrc-credentials-production.up.railway.app/cli/device even when they
had reached the app on the real domain. /cli/device/code built verification_uri
from `config.origin`, which is a single fixed value read from $PUBLIC_ORIGIN, so
the response was wrong for every hostname except the one that variable happened
to name.

Derive the origin from the request instead: whatever host the CLI called is the
host it gets sent back to. Express honours X-Forwarded-Proto/Host here because
server.mjs sets `trust proxy` behind Railway's TLS terminator.

Deliberately scoped to the two device-flow URLs. The WebAuthn expectedOrigin in
passkey.mjs stays pinned to config.origin — validating a signature against a
host the caller supplied would defeat the check.

Note this fixes which URL is *printed*; the host still has to route to this
service for the link to load.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 09:19:37 -07:00

47 lines
1.9 KiB
JavaScript

// `logicsrc login --device` printed a generated Railway hostname to users on the
// real domain, because /cli/device/code echoed $PUBLIC_ORIGIN instead of the host
// the CLI had just called. These pin the replacement behaviour.
import assert from "node:assert/strict";
import test from "node:test";
import { requestOrigin } from "../src/lib/origin.mjs";
/** A minimal stand-in for the Express request surface requestOrigin touches. */
const req = (host, protocol = "https") => ({
protocol,
headers: { host },
get: (h) => (h.toLowerCase() === "host" ? host : undefined),
});
const FALLBACK = "https://logicsrc-credentials-production.up.railway.app";
test("uses the host the caller actually reached", () => {
assert.equal(requestOrigin(req("logicsrc.com"), FALLBACK), "https://logicsrc.com");
// The same deployment answering on its Railway hostname still self-describes
// correctly — this is not a hardcode swap, it follows the request.
assert.equal(
requestOrigin(req("logicsrc-credentials-production.up.railway.app"), FALLBACK),
FALLBACK,
);
});
test("keeps the forwarded protocol and any explicit port", () => {
assert.equal(requestOrigin(req("localhost:8080", "http"), FALLBACK), "http://localhost:8080");
});
test("falls back to the configured origin when there is no Host header", () => {
assert.equal(requestOrigin({ protocol: "https" }, FALLBACK), FALLBACK);
assert.equal(requestOrigin({}, `${FALLBACK}/`), FALLBACK, "trailing slash is trimmed");
});
test("reads the header directly when req.get is unavailable", () => {
// Some middleware stacks (and our own tests) pass a bare object.
assert.equal(
requestOrigin({ protocol: "https", headers: { host: "logicsrc.com" } }, FALLBACK),
"https://logicsrc.com",
);
});
test("defaults to https when the request carries no protocol", () => {
assert.equal(requestOrigin({ headers: { host: "logicsrc.com" } }, FALLBACK), "https://logicsrc.com");
});