import { describe, it, expect } from "vitest"; import { getPasskeyConfig } from "../../../src/auth/passkey-config.js"; /** URL shape from `new URL(request.url)` after trusted proxy + Astro `security.allowedDomains`. */ function urlAfterTrustedProxy(path: string, host: string, proto: "http" | "https"): URL { return new URL(path, `${proto}://${host}`); } describe("passkey-config", () => { describe("getPasskeyConfig() via emulated reverse proxy URL", () => { const internalDevUrl = "http://127.0.0.1:4321/_emdash/api/auth/passkey/register/options"; it("loopback URL alone matches Node before rewrite — rpId is not the public host", () => { const url = new URL(internalDevUrl); expect(getPasskeyConfig(url).rpId).toBe("127.0.0.1"); }); it("emits a single-element origins array by default", () => { const url = new URL(internalDevUrl); expect(getPasskeyConfig(url).origins).toEqual(["http://127.0.0.1:4321"]); }); it("forwarded Host/Proto yield the URL handlers see; rp matches HTTP reverse-proxy edge", () => { const url = urlAfterTrustedProxy( "/_emdash/api/auth/passkey/register/options", "emdash.local:8080", "http", ); const config = getPasskeyConfig(url, "My Site"); expect(config.rpId).toBe("emdash.local"); expect(config.rpName).toBe("My Site"); expect(config.origins[0]).toBe("http://emdash.local:8080"); }); it("HTTPS listener on proxy with HTTP upstream: siteUrl aligns origin with browser", () => { const urlAstroSeesFromForwardedHttp = urlAfterTrustedProxy( "/_emdash/api/setup/admin", "emdash.local:8080", "http", ); const browserOrigin = "https://emdash.local:8443"; const config = getPasskeyConfig(urlAstroSeesFromForwardedHttp, "My Site", browserOrigin); expect(config.rpId).toBe("emdash.local"); expect(config.rpName).toBe("My Site"); expect(config.origins[0]).toBe(browserOrigin); }); }); describe("getPasskeyConfig()", () => { it("throws when siteUrl is not a valid URL", () => { const url = new URL("http://localhost:4321/admin"); expect(() => getPasskeyConfig(url, "Site", "::not-a-url")).toThrow("Invalid siteUrl"); }); it("extracts rpId from localhost URL", () => { const url = new URL("http://localhost:4321/admin"); const config = getPasskeyConfig(url); expect(config.rpId).toBe("localhost"); }); it("extracts rpId from production URL", () => { const url = new URL("https://example.com/admin"); const config = getPasskeyConfig(url); expect(config.rpId).toBe("example.com"); }); it("extracts rpId from subdomain URL", () => { const url = new URL("https://admin.example.com/dashboard"); const config = getPasskeyConfig(url); expect(config.rpId).toBe("admin.example.com"); }); it("returns correct origin for http", () => { const url = new URL("http://localhost:4321/admin"); const config = getPasskeyConfig(url); expect(config.origins[0]).toBe("http://localhost:4321"); }); it("returns correct origin for https", () => { const url = new URL("https://example.com/admin"); const config = getPasskeyConfig(url); expect(config.origins[0]).toBe("https://example.com"); }); it("handles port numbers correctly", () => { const url = new URL("http://localhost:3000/setup"); const config = getPasskeyConfig(url); expect(config.rpId).toBe("localhost"); expect(config.origins[0]).toBe("http://localhost:3000"); }); it("handles https with non-standard port", () => { const url = new URL("https://staging.example.com:8443/admin"); const config = getPasskeyConfig(url); expect(config.rpId).toBe("staging.example.com"); expect(config.origins[0]).toBe("https://staging.example.com:8443"); }); it("uses hostname as rpName by default", () => { const url = new URL("https://example.com/admin"); const config = getPasskeyConfig(url); expect(config.rpName).toBe("example.com"); }); it("uses provided siteName for rpName", () => { const url = new URL("https://example.com/admin"); const config = getPasskeyConfig(url, "My Cool Site"); expect(config.rpName).toBe("My Cool Site"); expect(config.rpId).toBe("example.com"); }); it("ignores path and query params for origin", () => { const url = new URL("https://example.com:443/admin/setup?foo=bar#section"); const config = getPasskeyConfig(url); // Standard https port 443 is omitted from origin expect(config.origins[0]).toBe("https://example.com"); expect(config.rpId).toBe("example.com"); }); it("documents HTTPS reverse-proxy dev pitfall: server URL scheme must match the browser", () => { const serverDevUrl = new URL("http://emdash.local:8443/_emdash/api/setup/admin"); const browserPageOrigin = new URL("https://emdash.local:8443/_emdash/admin/setup"); const fromServer = getPasskeyConfig(serverDevUrl); const fromBrowser = getPasskeyConfig(browserPageOrigin); expect(fromServer.rpId).toBe(fromBrowser.rpId); expect(fromServer.origins[0]).toBe("http://emdash.local:8443"); expect(fromBrowser.origins[0]).toBe("https://emdash.local:8443"); // verifyRegistrationResponse requires clientData.origin === config.origins[0] (see @emdash-cms/auth/passkey) expect(fromServer.origins[0]).not.toBe(fromBrowser.origins[0]); }); it("siteUrl overrides origin and rpId (TLS termination and loopback request URL)", () => { const fromForwardedHttp = getPasskeyConfig( new URL("http://emdash.local:8443/_emdash/api/setup/admin"), "My Site", "https://emdash.local:8443", ); expect(fromForwardedHttp.rpName).toBe("My Site"); expect(fromForwardedHttp.rpId).toBe("emdash.local"); expect(fromForwardedHttp.origins[0]).toBe("https://emdash.local:8443"); const fromLoopback = getPasskeyConfig( new URL("http://127.0.0.1:4321/_emdash/api/setup/admin"), "My CMS", "https://public.example:8443", ); expect(fromLoopback.rpId).toBe("public.example"); expect(fromLoopback.rpName).toBe("My CMS"); expect(fromLoopback.origins[0]).toBe("https://public.example:8443"); const hostnameOnly = getPasskeyConfig( new URL("http://127.0.0.1:4321/x"), undefined, "https://public.example:8443", ); expect(hostnameOnly.rpName).toBe("public.example"); expect(hostnameOnly.rpId).toBe("public.example"); }); }); describe("getPasskeyConfig() multi-origin", () => { it("appends allowedOrigins after the canonical origin", () => { const url = new URL("http://localhost:4321/admin"); const config = getPasskeyConfig(url, "Site", "https://example.com", [ "https://preview.example.com", "https://staging.example.com", ]); expect(config.rpId).toBe("example.com"); expect(config.origins).toEqual([ "https://example.com", "https://preview.example.com", "https://staging.example.com", ]); }); it("places the canonical origin first when no siteUrl is set", () => { const url = new URL("https://preview.example.com/admin"); const config = getPasskeyConfig(url, undefined, undefined, ["https://example.com"]); // rpId is preview.example.com (no siteUrl); allow apex as second origin expect(config.rpId).toBe("preview.example.com"); expect(config.origins).toEqual(["https://preview.example.com", "https://example.com"]); }); it("dedupes if allowedOrigins repeats the canonical origin", () => { const url = new URL("http://localhost:4321/admin"); const config = getPasskeyConfig(url, undefined, "https://example.com", [ "https://example.com", "https://preview.example.com", ]); expect(config.origins).toEqual(["https://example.com", "https://preview.example.com"]); }); it("ignores empty/falsy entries in allowedOrigins", () => { const url = new URL("http://localhost:4321/admin"); const config = getPasskeyConfig(url, undefined, "https://example.com", [ "", "https://preview.example.com", ]); expect(config.origins).toEqual(["https://example.com", "https://preview.example.com"]); }); it("yields a single-element origins array when allowedOrigins is empty", () => { const url = new URL("http://localhost:4321/admin"); const config = getPasskeyConfig(url, undefined, "https://example.com", []); expect(config.origins).toEqual(["https://example.com"]); }); }); });