DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path scoping-notes/loomworks-ol-security-surface-scoping-note-v0_1.html
LoomworksScoping Notev0.12026-06-12Scoping — not yet built

Operator Layer Security-Settings Surface

Scoping the one Operator Layer screen that makes both built recovery halves reachable — add a backup passkey, regenerate recovery codes — and the guarded one-time display a destructive, irreversible rotation demands.

Version   0.1
Date   2026-06-12
Status   Scoping — not yet built. Scopes the Operator Layer surface for the recovery arc (item 4); 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 (Account recovery); recovery-completeness handoff (2026-06-12); the two engine CRs just landed (me-passkeys-endpoints, me-security-recovery-codes); the Step-0 OL settings inspection (2026-06-12).
Repos this scopes   loomworks (Operator Layer) only — no engine change; the five /me/security/* routes already exist

Plain-language summary

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.

What this surface does

One page, /settings/security, for the signed-in person. It calls the five /me/security/* routes that already exist:

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 three settled build-shaping decisions

1. A sub-route under Settings, not a card

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.

2. The user-menu entry is in scope, and is not optional

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.

3. The guarded one-time display is built once and used in both places

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 guard is not a forbidden disabled control

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.

What is reuse and what is new (from the Step-0 inspection)

Reused directly, no extraction:

New, built here:

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.

Copy — the words the destructive action depends on

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.

Build sequence (for the handoff)

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.

  1. The guarded one-time-secret display component (codes + copy + download + confirmation gate), with its own tests.
  2. The settings-security API file (the five route wrappers) + the verify-at-build options-shape check.
  3. The /settings/security screen — passkey list, add-passkey (reusing the WebAuthn helper), recovery-code status, regenerate (using the guarded component).
  4. The user-menu Settings entry.
  5. The signup-flow retrofit onto the guarded component.
  6. The SECURITY.* strings group, populated from the copy above.

Per-step commits, halt before push, frontend test suite green at each step.

Out of scope

Delete / revoke of passkeys and the last-credential guard — still deferred to where the recovery arc settles what "last credential" means.

Operator-mediated recovery (seed mechanism 3) — still deferred; its build-now-or-defer decision is pending at the documentation step that follows this surface.

Engine changes — none. The five routes exist; this is Operator Layer only.

The get_webauthn_config duplicate in the engine — a pre-existing engine-side oddity carried in the engine change requests; not this surface's concern.

What this closes, and what follows

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