Rename realms to domains; object-keyed origins/related config format

Finish the realm→domain terminology removal across source, tests, e2e
and docs. The stored config drops all lists: Config.domains is keyed by
rp-id, DomainConfig.origins/related are objects keyed by host (https://
omitted), values True or OriginEntry(auth_host=True). The default/primary
domain concept is gone; ordering is display-time. Tests and e2e updated
to the new API shapes (not run). Database re-migrated from the legacy
backup into the new format.
This commit is contained in:
2026-09-07 02:05:12 +00:00
parent 80d55679fb
commit f2e6f5784e
33 changed files with 627 additions and 651 deletions
@@ -11,33 +11,33 @@ import {
} from './fixtures/remote-auth'
/**
* Multi-realm E2E tests.
* Multi-domain E2E tests.
*
* The server is bootstrapped with two realms: localhost (default) and
* The server is bootstrapped with two domains: localhost (default) and
* test.localhost. Chrome resolves any *.localhost hostname to loopback, so
* both realms are reachable over real HTTP from the browser.
* both domains are reachable over real HTTP from the browser.
*
* Covers:
* - Host-based realm dispatch (settings, 421 for unknown hosts)
* - Related Origin Requests well-known endpoint + admin realm API
* - Cross-realm remote login: a passkey registered on localhost permits a
* - Host-based domain dispatch (settings, 421 for unknown hosts)
* - Related Origin Requests well-known endpoint + admin domain API
* - Cross-domain remote login: a passkey registered on localhost permits a
* session on test.localhost via pairing code
* - The profile enrollment prompt on a realm where the user has no passkey
* - The profile enrollment prompt on a domain where the user has no passkey
*/
test.describe('Multi-realm E2E', () => {
test.describe('Multi-domain E2E', () => {
test.describe.configure({ mode: 'serial' })
const baseUrl = process.env.BASE_URL || 'http://localhost:4404'
const realmUrl = 'http://test.localhost:4404'
const domainUrl = 'http://test.localhost:4404'
test('dispatches realms by host header', async ({ page }) => {
test('dispatches domains by host header', async ({ page }) => {
// Browser navigation: Chrome maps *.localhost to loopback
const realmResp = await page.goto(`${realmUrl}/auth/api/settings`)
expect(realmResp?.status()).toBe(200)
const realmSettings = await realmResp?.json()
expect(realmSettings.rp_id).toBe('test.localhost')
expect(realmSettings.own_auth_host).toBeNull()
const domainResp = await page.goto(`${domainUrl}/auth/api/settings`)
expect(domainResp?.status()).toBe(200)
const domainSettings = await domainResp?.json()
expect(domainSettings.rp_id).toBe('test.localhost')
expect(domainSettings.own_auth_host).toBeNull()
const defaultResp = await page.goto(`${baseUrl}/auth/api/settings`)
expect(defaultResp?.status()).toBe(200)
@@ -58,8 +58,8 @@ test.describe('Multi-realm E2E', () => {
expect(before.status()).toBe(404)
})
test('master admin manages realms and related origins via API', async ({ page, virtualAuthenticator }) => {
// Fresh session via device token (realm writes require recent auth)
test('master admin manages domains and related origins via API', async ({ page, virtualAuthenticator }) => {
// Fresh session via device token (domain writes require recent auth)
const deviceToken = popDeviceToken()
test.skip(!deviceToken, 'No device tokens available')
await page.goto('/auth/')
@@ -68,18 +68,18 @@ test.describe('Multi-realm E2E', () => {
const headers = { Cookie: `${getSessionCookieName()}=${reg.session_token}` }
// List realms
const list = await page.request.get(`${baseUrl}/auth/api/admin/realms/`, { headers })
// List domains
const list = await page.request.get(`${baseUrl}/auth/api/admin/domains/`, { headers })
expect(list.ok()).toBeTruthy()
const realms = await list.json()
expect(realms.map((r: any) => r.rp_id).sort()).toEqual(['localhost', 'test.localhost'])
const localhostRealm = realms.find((r: any) => r.rp_id === 'localhost')
expect(localhostRealm.is_default).toBe(true)
const domains = await list.json()
expect(domains.map((r: any) => r.rp_id).sort()).toEqual(['localhost', 'test.localhost'])
const localhostDomain = domains.find((r: any) => r.rp_id === 'localhost')
expect(localhostDomain.origins).toEqual({})
// Add a related origin (unrelated domain) to the localhost realm
const patch = await page.request.patch(`${baseUrl}/auth/api/admin/realms/localhost`, {
// Add a related origin (unrelated domain) to the localhost domain
const patch = await page.request.patch(`${baseUrl}/auth/api/admin/domains/localhost`, {
headers,
data: { rp_name: '', auth_host: '', origins: [], related_origins: ['https://app.example.com'] },
data: { rp_name: '', origins: {}, related: { 'app.example.com': true } },
})
expect(patch.ok()).toBeTruthy()
@@ -90,16 +90,16 @@ test.describe('Multi-realm E2E', () => {
expect(wkJson.origins).toContain('https://app.example.com')
// Restore: remove related origins again so later tests see the pristine state
const restore = await page.request.patch(`${baseUrl}/auth/api/admin/realms/localhost`, {
const restore = await page.request.patch(`${baseUrl}/auth/api/admin/domains/localhost`, {
headers,
data: { rp_name: '', auth_host: '', origins: [], related_origins: [] },
data: { rp_name: '', origins: {}, related: {} },
})
expect(restore.ok()).toBeTruthy()
const after = await page.request.get(`${baseUrl}/.well-known/webauthn`)
expect(after.status()).toBe(404)
})
test('cross-realm remote login via pairing code', async ({ page, virtualAuthenticator }) => {
test('cross-domain remote login via pairing code', async ({ page, virtualAuthenticator }) => {
// Register a fresh passkey on localhost (this test's virtual authenticator)
const deviceToken = popDeviceToken()
test.skip(!deviceToken, 'No device tokens available')
@@ -107,19 +107,19 @@ test.describe('Multi-realm E2E', () => {
const reg = await registerPasskey(page, baseUrl, { resetToken: deviceToken })
expect(reg.session_token).toBeTruthy()
// Requester page on the other realm (no session there)
// Requester page on the other domain (no session there)
const reqPage = await page.context().newPage()
await reqPage.goto(`${realmUrl}/auth/`)
await reqPage.goto(`${domainUrl}/auth/`)
const pairingCode = await startRemoteAuthRequest(reqPage)
expect(pairingCode.split('.')).toHaveLength(3)
// Approver permits with the localhost passkey; the "found" message names
// the requesting realm
// the requesting domain
const found = await permitRemoteAuth(page, pairingCode)
expect(found.rp_id).toBe('test.localhost')
// The requester redeems the exchange code on its own realm and the
// The requester redeems the exchange code on its own domain and the
// session validates there for the same user
const validation = await awaitRemoteAuthSession(reqPage)
expect(validation.ctx.user.uuid).toBe(reg.user)
@@ -133,13 +133,13 @@ test.describe('Multi-realm E2E', () => {
const current = Object.values(userInfo.sessions as any[]).find((s: any) => s.is_current) as any
expect(current.host).toContain('test.localhost')
// The profile on test.localhost prompts adding a passkey for this realm,
// and the existing localhost passkey carries a realm badge
await reqPage.goto(`${realmUrl}/auth/`)
const notice = reqPage.locator('.realm-enroll-notice')
// The profile on test.localhost prompts adding a passkey for this domain,
// and the existing localhost passkey carries a domain badge
await reqPage.goto(`${domainUrl}/auth/`)
const notice = reqPage.locator('.domain-enroll-notice')
await expect(notice).toBeVisible({ timeout: 15000 })
await expect(notice).toContainText('test.localhost')
await expect(reqPage.locator('.badge-realm').first()).toHaveText('localhost')
await expect(reqPage.locator('.badge-domain').first()).toHaveText('localhost')
await reqPage.close()
})
+3 -3
View File
@@ -32,7 +32,7 @@ const b64helpersSource = `
/**
* Start a remote auth request on the given page (the device wanting to log in).
* The page must already be navigated to the requesting realm's origin.
* The page must already be navigated to the requesting domain's origin.
* Keeps the WebSocket open on window.__raWs and collects later messages into
* window.__raMsgs; resolves with the pairing code.
*/
@@ -107,8 +107,8 @@ export async function awaitRemoteAuthSession(page: Page, timeoutMs = 90000): Pro
/**
* Permit a remote auth request from the given page (the authenticating device).
* The page must be on the approver's origin with a valid session cookie and a
* virtual authenticator holding a credential for that realm.
* Resolves with the "found" message (includes the requesting realm's rp_id).
* virtual authenticator holding a credential for that domain.
* Resolves with the "found" message (includes the requesting domain's rp_id).
*/
export async function permitRemoteAuth(page: Page, code: string): Promise<any> {
return page.evaluate(async ({ code, powSrc, b64src }) => {
+3 -3
View File
@@ -20,7 +20,7 @@ interface TestState {
/**
* Global setup for E2E tests.
*
* Bootstraps a fresh combined database (paskia.kantadb) with two realms —
* Bootstraps a fresh combined database (paskia.kantadb) with two domains —
* localhost (default) and test.localhost — then starts the server with the
* test data directory as its working directory. Captures the bootstrap reset
* token from 'paskia init' output for initial user registration.
@@ -44,7 +44,7 @@ export default async function globalSetup() {
const state: TestState = {}
// Bootstrap the database: two realms, localhost (default) and test.localhost
// Bootstrap the database: two domains, localhost (default) and test.localhost
console.log(' Bootstrapping database with paskia init...')
const initResult = spawnSync(
'uv',
@@ -125,7 +125,7 @@ export default async function globalSetup() {
}
state.sessionCookie = settings.session_cookie
console.log(` ✅ Session cookie name: ${state.sessionCookie}`)
console.log(`Realm: ${settings.rp_id} (${settings.rp_name})\n`)
console.log(`Domain: ${settings.rp_id} (${settings.rp_name})\n`)
// Save state for tests
writeFileSync(stateFile, JSON.stringify(state, null, 2))