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

Closed
kayg wants to merge 0 commits from issue/99-passkeys 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

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
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
Author
Owner

Review: I looked at the Settings screenshots myself.

  • 1440 light: two passkeys plus Add. With only one passkey left, its remove button is dimmed.
  • 390 dark: reads cleanly, with no sideways overflow.

The server refuses to remove the last passkey inside a single transaction, which is right.

Merged with dev:

  • Conflict: the live.ts imports clashed with #100; I kept both.
  • Devices fix: this PR's Devices sign-out fix matches the one dev already has from #104, so they merged cleanly.
  • Checks after the merge: svelte-check reports 0 errors, and the calternal-auth and calternal tests pass.

CI: web is red with the known timeout family (see probe #115 and #11).

Replacing with #116 and merging.

Polish for later: when the display name equals the username, the You card shows kayg twice.

Review: I looked at the Settings screenshots myself. - **1440 light:** two passkeys plus Add. With only one passkey left, its remove button is dimmed. - **390 dark:** reads cleanly, with no sideways overflow. The server refuses to remove the last passkey inside a single transaction, which is right. **Merged with dev:** - **Conflict:** the `live.ts` imports clashed with #100; I kept both. - **Devices fix:** this PR's Devices sign-out fix matches the one dev already has from #104, so they merged cleanly. - **Checks after the merge:** `svelte-check` reports 0 errors, and the `calternal-auth` and `calternal` tests pass. **CI:** web is red with the known timeout family (see probe #115 and #11). Replacing with #116 and merging. **Polish for later:** when the display name equals the username, the You card shows `kayg` twice.
kayg closed this pull request 2026-10-14 04:12:59 +00:00
Some checks failed
CI / rust (pull_request) Successful in 1m31s
CI / web (pull_request) Failing after 6m0s

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!114
No description provided.