Version. 0.1
Date. 2026-06-12
Status. Scoping. Not yet built. This note scopes the Operator Layer surface for the recovery arc (item 4) and is the drafting basis for the build handoff to Claude Code.
Render-type. Scoping note. Operator-facing — HTML primary, Markdown source alongside.
Source. Seed v0.12 (Authentication framework — Account recovery); the recovery-completeness session handoff (2026-06-12); the two engine change requests just landed (loomworks-me-passkeys-endpoints-cr-v0_1, loomworks-me-security-recovery-codes-cr-v0_1); the Step-0 inspection of the Operator Layer settings structure (2026-06-12).
Repos this scopes work in. loomworks (Operator Layer) only. No engine change — the five /me/security/* routes already exist.
The two engine halves of account recovery are built and reachable by the API, but no person can use them — there is no screen. This note scopes that screen: one Security page where a signed-in person adds a backup passkey, sees the passkeys they have registered, sees how many recovery codes they have left, and regenerates their codes. It closes the recovery arc's build by making the engine work reachable.
Three things are settled going in: the screen is a sub-route under the existing Settings page; the screen is reachable by adding a Settings entry to the user menu (there is none today); and the one-time display of freshly-generated recovery codes is built as a guarded, reusable component — guarded because regenerating revokes the old codes the instant it runs, so a person who does not save the new set is locked out of that recovery route entirely.
One page, /settings/security, for the signed-in person. It calls the five /me/security/* routes that already exist:
GET /me/security/passkeys)POST /me/security/passkeys/begin then /complete)GET /me/security/recovery-codes)POST /me/security/recovery-codes/regenerate)It does not show existing recovery codes — they are stored hashed and cannot be retrieved. The surface offers a count and a regenerate, never a view. This is the same no-view-by-design property the engine change request recorded.
The screen mounts at /settings/security, a child of the existing /settings page — not as another card stacked on the Settings page itself.
The Settings page today holds two preference toggles in a card stack, and its own documented intent is to extend that page rather than fragment settings across parallel surfaces. A security sub-route honors that intent — it is a child of Settings, not a parallel surface — while giving security the room it needs. Security is categorically heavier than a toggle: a list, a browser passkey ceremony, a count, and a one-time-secret modal with a guard. Folding that into a card alongside two toggles would make the card the size of the rest of the page and mix "flip a preference" with "manage how I get into my account" — different weights of action.
The Settings page is reachable only by typing the URL today — the user menu holds only Sign Out, with no Settings link anywhere in the navigation. A security screen with no way to navigate to it is built-but-unreachable, the same shape as an engine endpoint with no surface. So this work adds a Settings entry to the user menu (above Sign Out), which is what makes the security screen — and the existing Settings page — reachable.
That the existing Settings page has been unreachable-by-navigation until now is a pre-existing gap this change happens to close, not one it created. Worth recording so the menu addition is understood as fixing a standing gap, not introducing a new surface.
Regenerating recovery codes revokes the old set the instant the engine responds. The fresh set is shown once, in that response. If the person closes the page, navigates away, or the render is interrupted before they save the new codes, they are worse off than before they regenerated — the old set is dead and the new set is lost, and that recovery route is gone.
The protection is a guard: the fresh codes display with copy and download affordances and an explicit "I have saved these codes" confirmation the person must give before the screen lets go of them. The Step-0 inspection found this guard does not exist anywhere — and that the signup flow shows recovery codes once with the same exposure (no copy, no download, no confirmation gate, one click navigates away permanently).
So the guarded display is built once as a reusable component, and both the security regenerate flow and the signup flow use it. The regenerate flow must have it — the consequence there is acute. The signup flow has the milder version of the same exposure (a fumble at signup does not lock the person out; they still have their passkey and can regenerate later), but it is the same gap with the same fix, and leaving the signup flow on its ungated inline version once the guarded component exists would mean two one-time-code displays in the codebase, one protected and one not, for no reason. One component, used in both, closes both.
The seed forbids disabled and grayed-out controls — only show what is available. The "I have saved these codes" confirmation is not that, and the distinction matters so it is not mistaken for a seed violation.
A forbidden disabled control is a grayed-out option the person cannot use. The confirmation gate is the opposite: it is the person taking an action — acknowledging they have captured a one-time secret — before the screen discards it. It is the machine-surfaces / human-confirms shape the seed holds to everywhere else, applied to a secret the human must capture before it is gone. The house style already admits exactly one use of disabled — the transient in-flight state during an action (a button disabled only while its request is in flight) — and the regenerate button follows that: fully enabled at all times, including at "0 unused codes" (which is precisely when regenerate matters most), disabled only for the moment its own request is in flight.
Reused directly, no extraction:
createWebAuthnAttestation) the signup flow already calls — the add-passkey ceremony is the same browser create() call. It slots between the new begin/complete API wrappers unchanged.api() with the session cookie) every /me/ call already uses. The security screen calls /me/security/ through it, mirroring the existing settings-API file.New, built here:
beginAddPasskey, completeAddPasskey, listPasskeys, getRecoveryCodeStatus, regenerateRecoveryCodes)./settings/security route and its screen.SECURITY.* strings group (all user-visible text centralized, per the house discipline).One verify-at-build item: confirm the engine's add-passkey begin-options payload matches the shape the WebAuthn registration helper decodes. The signup ceremony used a different begin shape; the helper is the same, but the options envelope must line up. Claude Code confirms this against the live engine response before wiring.
The regenerate flow's copy is load-bearing, because the person has to understand the consequence before they act, and has to be told plainly that the moment is one-time. Drafted here so the build uses considered language, not placeholder text. All in active voice, naming what the person controls, in the interface's own voice.
Before regenerating (the action the person is about to take):
The confirmation before the codes are discarded (the guard):
Recovery-code status (the count):
Add-passkey:
Passkey list empty/loading: the list is never truly empty (every person has at least their first passkey), but during load it shows the house loading treatment, and on fetch error it shows directing text in the interface's voice (what went wrong, how to retry), not an apology.
A suggested order, to be confirmed at handoff. The guarded component first (everything else displays results; this one protects a destructive action), then the screen, then the retrofit.
/settings/security screen — passkey list, add-passkey (reusing the WebAuthn helper), recovery-code status, regenerate (using the guarded component).SECURITY.* strings group, populated from the copy above.Per-step commits, halt before push, frontend test suite green at each step.
get_webauthn_config duplicate in the engine — a pre-existing engine-side oddity carried in the engine change requests; not this surface's concern.When this lands, every built recovery mechanism is reachable by a person: recovery codes (rotatable, with a guarded one-time display), and a backup passkey (enrollable). The acute lockout exposure — lose the authenticator and the codes, and you are locked out — is closed by two independent mechanisms a person can actually reach and manage.
What follows is the documentation step: carrying the seed's positive recovery commitment into the Companion functional specification (which states recovery only negatively today), written against the built reality. The operator-mediated-recovery decision — build it now as the third mechanism, or mark it committed-and-deferred — is taken there, with the two reachable mechanisms in full view.
DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — Operator Layer Security-Settings Surface — Scoping Note — v0.1 — 2026-06-12