Settings: list, add, rename and remove passkeys #116

Closed
kayg wants to merge 0 commits from issue/99-passkeys-2 into dev
Owner

Closes #99.

What

Settings → Account → Passkeys: list each passkey (name, added, last used), add one, rename it in place, remove it (asks first, in place). The last passkey can never be removed.

Server (crates/auth, in the account plugin, all through the one action pipeline):

  • account.passkeys (list), account.passkey-add-start / account.passkey-add-finish, account.passkey-rename, account.passkey-remove.
  • Each change records account.passkey-added|renamed|removed in the journal, so every other open view updates (ADR 0012). Signing in doesn't touch the journal, so "last used" refreshes when Settings opens.
  • Never the last: the count and the delete run in one transaction; refusal is the new last_passkey error (409, plain message). Also refused: someone else's passkey, unknown id, empty name. An add prompt is tied to the person who started it and works once.
  • auth.db v2: passkeys.name. Existing passkeys get "Passkey"; ones made at setup are named after the device.

Web: PasskeySection.svelte + lib/passkeys.svelte.ts (list kept live from the journal in live.ts). New passkey is named after the device and opens for renaming. The remove button on the only passkey is dimmed and its warm tooltip says why. Escape backs out of "Remove it?" and returns focus. Also fixes Devices → Sign out, which sent id where the server wants installation.

Tests

  • Rust (crates/auth/tests/passkeys.rs, db migration test, name helpers): list, add/rename/remove with journal events, last passkey refused (and a removed passkey no longer signs in), unknown ids, name rules, adding needs sign-in and a sign-in prompt can't add, excluded credentials. cargo test -p calternal-auth -p calternal-core, clippy, fmt clean.
  • Playwright Chromium passkeys.spec.ts (own server, virtual authenticators): two windows of one browser; add on a second authenticator shows in both windows without reload; rename live; Escape cancels; remove asks; last one dimmed, direct API call gets 409 last_passkey; adding the same device again gives one calm line; the added passkey signs in; "Last used" appears.
  • Playwright passkeys-ui.spec.ts on Chromium and WebKit, 390 and 1440: the section renders, no sideways overflow, a failed add shows one calm line. WebKit gap: Playwright's WebKit has no virtual authenticator, so the add/rename/remove flow is Chromium only (Safari needs a hand check with a real passkey).

Evidence (screenshots, numbers) is on #99.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T

(Replaces #114, merged with current dev; I resolved the live.ts import conflict. Review is on #114.)

Closes #99. ## What Settings → Account → Passkeys: list each passkey (name, added, last used), add one, rename it in place, remove it (asks first, in place). The last passkey can never be removed. **Server** (`crates/auth`, in the `account` plugin, all through the one action pipeline): - `account.passkeys` (list), `account.passkey-add-start` / `account.passkey-add-finish`, `account.passkey-rename`, `account.passkey-remove`. - Each change records `account.passkey-added|renamed|removed` in the journal, so every other open view updates (ADR 0012). Signing in doesn't touch the journal, so "last used" refreshes when Settings opens. - Never the last: the count and the delete run in one transaction; refusal is the new `last_passkey` error (409, plain message). Also refused: someone else's passkey, unknown id, empty name. An add prompt is tied to the person who started it and works once. - `auth.db` v2: `passkeys.name`. Existing passkeys get "Passkey"; ones made at setup are named after the device. **Web**: `PasskeySection.svelte` + `lib/passkeys.svelte.ts` (list kept live from the journal in `live.ts`). New passkey is named after the device and opens for renaming. The remove button on the only passkey is dimmed and its warm tooltip says why. Escape backs out of "Remove it?" and returns focus. Also fixes Devices → Sign out, which sent `id` where the server wants `installation`. ## Tests - Rust (`crates/auth/tests/passkeys.rs`, db migration test, name helpers): list, add/rename/remove with journal events, last passkey refused (and a removed passkey no longer signs in), unknown ids, name rules, adding needs sign-in and a sign-in prompt can't add, excluded credentials. `cargo test -p calternal-auth -p calternal-core`, clippy, fmt clean. - Playwright Chromium `passkeys.spec.ts` (own server, virtual authenticators): two windows of one browser; add on a second authenticator shows in both windows without reload; rename live; Escape cancels; remove asks; last one dimmed, direct API call gets 409 `last_passkey`; adding the same device again gives one calm line; the added passkey signs in; "Last used" appears. - Playwright `passkeys-ui.spec.ts` on Chromium and WebKit, 390 and 1440: the section renders, no sideways overflow, a failed add shows one calm line. WebKit gap: Playwright's WebKit has no virtual authenticator, so the add/rename/remove flow is Chromium only (Safari needs a hand check with a real passkey). Evidence (screenshots, numbers) is on #99. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T (Replaces #114, merged with current dev; I resolved the live.ts import conflict. Review is on #114.)
Server: account.passkeys, passkey-add-start/finish, passkey-rename and
passkey-remove, each change a journal event so other open views update.
The last passkey can never be removed (count and delete in one
transaction, new last_passkey error). Passkeys get a name (auth.db v2).
Web: a Passkeys section in Settings > Account, live from the journal.
Tests: server integration tests, Chromium virtual authenticators in two
windows, and a WebKit/Chromium screen test.

Also fixes Devices > Sign out, which sent the wrong field name.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Passkeys screen test can save a picture of the section
Some checks failed
CI / rust (pull_request) Successful in 1m31s
CI / web (pull_request) Failing after 6m0s
2eb09c6b49
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Yyy8fQGxPLkLa8v8NXz7T
Merge remote-tracking branch 'origin/dev' into issue/99-passkeys
Some checks failed
CI / rust (pull_request) Successful in 2m6s
CI / web (pull_request) Failing after 10m47s
a5c2afdbb2
# Conflicts:
#	apps/web/src/lib/live.ts
Author
Owner

Closing: this branch is already fully contained in dev (it landed through a merge train). Branch cleanup, 2026-10-15.

Closing: this branch is already fully contained in `dev` (it landed through a merge train). Branch cleanup, 2026-10-15.
kayg closed this pull request 2026-10-15 08:35:02 +00:00
Some checks failed
CI / rust (pull_request) Successful in 2m6s
CI / web (pull_request) Failing after 10m47s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal!116
No description provided.