fix: passkeys behind TLS reverse proxy (#225)

* fix: passkeys behind TLS reverse proxy

Add passkeyPublicOrigin and wire it through passkey routes so origin/rpId match
the browser when dev runs behind nginx. Expose dev-only /_emdash/api/dev/passkey-url,
add admin messaging for insecure WebAuthn contexts, nginx repro under demos/simple,
and direct kysely dependency for the simple demo Node adapter bundle.

Made-with: Cursor

* docs: add passkeyPublicOrigin to configuration reference

Adds the new passkeyPublicOrigin option and reverse proxy guidance
to the public-facing configuration docs as requested in PR review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update tests and more docs

* fix: add missing refresh-server-pat fixture and restore docs heading

---------

Co-authored-by: Joseph Eftekhari <jdeftekhari@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
seslly
2026-04-05 23:41:07 -07:00
committed by GitHub
parent 28f98fda4f
commit d2114523a5
25 changed files with 526 additions and 53 deletions

View File

@@ -0,0 +1,24 @@
/**
* Re-runs dev-bypass after a dev-reset so the server info file has a valid PAT
* and the fixture database is back in "setup complete" state.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const SERVER_INFO_PATH = join(tmpdir(), "emdash-pw-server.json");
export async function refreshServerPatAfterDevBypass(baseUrl: string): Promise<void> {
const res = await fetch(`${baseUrl}/_emdash/api/setup/dev-bypass?token=1`);
if (!res.ok) {
throw new Error(`dev-bypass failed (${res.status}): ${await res.text()}`);
}
const json: { data: { token?: string } } = await res.json();
const token = json.data.token;
if (!token) throw new Error("dev-bypass did not return a PAT token");
// Update the server info so subsequent tests use the fresh token
const info = JSON.parse(readFileSync(SERVER_INFO_PATH, "utf-8"));
info.token = token;
writeFileSync(SERVER_INFO_PATH, JSON.stringify(info, null, 2));
}

View File

@@ -0,0 +1,33 @@
/**
* Chrome DevTools virtual WebAuthn authenticator for passkey e2e.
* Chromium-only (CDP). See https://developer.chrome.com/docs/devtools/webauthn/
*/
import type { Page } from "@playwright/test";
export async function addVirtualWebAuthnAuthenticator(page: Page): Promise<() => Promise<void>> {
const session = await page.context().newCDPSession(page);
await session.send("WebAuthn.enable");
const { authenticatorId } = await session.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "internal",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
return async () => {
try {
await session.send("WebAuthn.removeVirtualAuthenticator", { authenticatorId });
} catch {
// session may already be closed
}
try {
await session.detach();
} catch {
// ignore
}
};
}