From 1b51b7dbabeaabc34fdc9cdbc51bfdd528d4d0e3 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Thu, 23 Apr 2026 18:08:43 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20Plan=201C-=CE=B2=E2=82=81=20(typed-item?= =?UTF-8?q?=20forms)=20design=20spec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second sub-plan after 1C-α. Adds the 5 remaining typed-item forms (SecureNote, Identity, Card, Key, Totp) so the extension can daily- drive every typed item the Rust core supports — Document deferred to γ for attachment dependencies. Form style: muted "signature block + uniform rows" pattern (per-type accent panel + plain rows for the rest). Login is refactored onto a shared field-helper module as the reference implementation. Totp covers `kind: 'totp'` and `kind: 'steam'`. The latter requires a Rust-core fix (Slice 1) — `compute_totp_code` currently produces decimal output for Steam but Steam Guard uses a 5-char alphabet (`23456789BCDFGHJKMNPQRTVWXY`). Plan ships the alphabet patch and RFC-style test vectors. Five-slice sequencing: Rust Steam → shared helpers + Login refactor → SecureNote+Identity → Card+Key → Totp. Custom fields editor, vault-settings view, advanced generator UI all moved to β₂. Hotp counter UI deferred. Document type stays in γ. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...-22-relicario-extension-1c-beta1-design.md | 401 ++++++++++++++++++ 1 file changed, 401 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-22-relicario-extension-1c-beta1-design.md diff --git a/docs/superpowers/specs/2026-04-22-relicario-extension-1c-beta1-design.md b/docs/superpowers/specs/2026-04-22-relicario-extension-1c-beta1-design.md new file mode 100644 index 0000000..45be2b5 --- /dev/null +++ b/docs/superpowers/specs/2026-04-22-relicario-extension-1c-beta1-design.md @@ -0,0 +1,401 @@ +# relicario — Extension Plan 1C-β₁ (Typed-Item Forms) Design + +Second of three sub-plans porting the extension to the typed-item core. 1C-α (foundation) shipped Login-parity; 1C-β₁ adds the **other 5 typed-item forms** so the extension can daily-drive every typed item the Rust core knows about (except Document, deferred to γ for attachment dependencies). Custom-fields editor, vault-settings view, and advanced generator UI move to **β₂**. + +Reference: 1C-α design `docs/superpowers/specs/2026-04-20-relicario-extension-1c-alpha-design.md` (commits `a1d733d`, `ad6d8af`); 1C-α implementation merged 2026-04-22 (`2b83105`, tag `plan-1c-alpha-complete`). + +## Plan 1C decomposition (post-α refinement) + +| Sub-plan | Status | Scope | +|---|---|---| +| 1C-α | shipped 2026-04-22 | WASM rebuild, shared TS types, SessionHandle SW, split router with sender checks, full security architecture, Login-parity popup, zxcvbn setup gate | +| **1C-β₁ (this spec)** | proposed | 5 typed-item forms (SecureNote / Identity / Card / Key / Totp) + Rust Steam encoding fix | +| 1C-β₂ | proposed | Custom fields editor, full vault-settings view, advanced generator-request UI | +| 1C-γ | proposed | Attachments (with `putBlob` Git-Data-API fallback), Document type, trash view, field-history view, device management | + +## Design Decisions (from brainstorming) + +| Question | Decision | Why | +|---|---|---| +| Does β stay one plan or split? | **β₁ + β₂** | Settings view + custom-fields editor are heavy independently; splitting unlocks daily-driver typed items as soon as β₁ ships | +| Document type in β₁? | **Defer to γ** | `DocumentCore.primary_attachment` is required; without attachment upload there's nothing to attach | +| Form visual style? | **Type-flavored, muted** | "Signature block + uniform rows" pattern: each type gets one accent panel + plain rows for the rest. Lower contrast than vivid v1 mockup, sits with the dark-terminal aesthetic | +| Totp variants in β₁? | **TOTP + Steam** (Hotp deferred) | Steam Guard is widely used; Hotp is rare and needs counter-persistence UX | +| Steam encoding in Rust core? | **Yes — fix as Slice 1** | Existing `compute_totp_code` returns decimal output for `kind: 'steam'`, which doesn't match Steam Guard. ~30 line patch + test vectors | +| Sequencing? | **5 slices: Rust Steam → shared helpers + Login refactor → SecureNote+Identity → Card+Key → Totp** | Helper extraction pays off across 5 forms; pairing trivial types together; Totp last because it depends on Steam fix | +| Custom fields in β₁? | **No — β₂** | Custom fields are the single hardest UI in β; deserves its own focused cycle | + +## Scope + +### In +- 5 typed-item forms wired end-to-end (view + add + edit + delete): SecureNote, Identity, Card, Key, Totp. +- Form style: muted "signature block + uniform rows" with thin left-border accent per type. +- **Steam Guard** support on Totp items: `kind: 'totp'` and `kind: 'steam'` selectable in the form; UI toggle (no dropdown). +- **Rust core fix**: `compute_totp_code` learns the Steam alphabet (`23456789BCDFGHJKMNPQRTVWXY`, 5-char output). +- Concealed-with-reveal+copy pattern applied to: `Card.number`, `Card.cvv`, `Card.pin`, `Key.key_material`, `Totp.secret` (rendered as base32). Re-uses Login's existing convention via a new shared helper. +- Shared helper module `extension/src/popup/components/fields.ts` for row / concealed-row / signature-block primitives. **Login refactored onto it** as the reference implementation (net code reduction even before adding 5 new types). +- `item-detail.ts` and `item-form.ts` collapse to thin dispatchers calling `types/.renderDetail()` / `renderForm()`. +- "New…" picker on the toolbar's `+ New` button, listing all 7 types (Document greyed/disabled with "coming in γ" tooltip). +- Per-type Vitest unit tests for the form→Item transform. + +### Out (→ β₂ / γ) +- Custom fields editor (sections + per-field add/rename/remove/reorder). β₂. +- Vault-settings view (retention, generator defaults, attachment caps). β₂. +- Advanced generator-request UI (BIP39 vs Random, charset toggles, length slider). β₂. +- Hotp counter UI. β₂ or later. +- Per-type form custom defaults (e.g. exposing `Totp.digits` / `Totp.period_seconds`). β₂ via the custom-fields editor. +- Document type. γ. +- Attachment upload, trash view, field-history view, device-management UI. γ. + +## File map + +### New +``` +crates/relicario-core/src/item_types/totp.rs # Steam alphabet output (modified) +extension/src/popup/components/fields.ts # row / concealed-row / signature-block helpers +extension/src/popup/components/types/login.ts # extracted from existing item-detail/form Login branches +extension/src/popup/components/types/secure-note.ts +extension/src/popup/components/types/identity.ts +extension/src/popup/components/types/card.ts +extension/src/popup/components/types/key.ts +extension/src/popup/components/types/totp.ts +extension/src/popup/components/__tests__/fields.test.ts +extension/src/popup/components/types/__tests__/save-shape.test.ts +``` + +### Modified +``` +extension/src/popup/components/item-detail.ts # dispatch on item.type → types/.renderDetail +extension/src/popup/components/item-form.ts # dispatch on item.type → types/.renderForm +extension/src/popup/components/item-list.ts # "+ New" button opens type picker +extension/src/popup/styles.css # signature-block + field-row classes +crates/relicario-core/src/item_types/totp.rs # see above +``` + +### Deleted +None. + +## Slice 1 — Rust Steam encoding + +**File**: `crates/relicario-core/src/item_types/totp.rs` + +Patch shape: + +```rust +const STEAM_ALPHABET: &[u8] = b"23456789BCDFGHJKMNPQRTVWXY"; + +pub fn compute_totp_code(config: &TotpConfig, now_unix_seconds: u64) -> Result { + let counter = match config.kind { + TotpKind::Totp => now_unix_seconds / config.period_seconds as u64, + TotpKind::Hotp { counter } => counter, + TotpKind::Steam => now_unix_seconds / config.period_seconds as u64, + }; + // ... existing HMAC + dynamic-truncation logic produces `truncated: u32` ... + + if matches!(config.kind, TotpKind::Steam) { + let mut t = truncated; + let mut out = String::with_capacity(5); + for _ in 0..5 { + out.push(STEAM_ALPHABET[(t % 26) as usize] as char); + t /= 26; + } + return Ok(out); + } + + let modulus = 10u32.pow(config.digits as u32); + Ok(format!("{:0width$}", truncated % modulus, width = config.digits as usize)) +} +``` + +`STEAM_ALPHABET` deliberately excludes `0`, `O`, `1`, `I`, `L`, `S`, `5`, `A`, `Z`. Same alphabet used by Steam Mobile Authenticator and WinAuth. + +### Tests (in the same file) + +- `steam_known_vector`: pin a `(secret, counter)` to its known Steam output. If a citeable third-party vector is available, prefer it; otherwise pin the value our impl computes today (regression test against accidental future change). +- `steam_alphabet_no_ambiguous_chars`: `assert!(!STEAM_ALPHABET.contains(&b'0' / &b'O' / &b'1' / &b'I' / &b'L' / &b'S' / &b'5' / &b'A' / &b'Z'))`. +- `steam_output_is_5_chars`: regardless of `config.digits`, Steam output is exactly 5 characters. +- `totp_kind_decimal_unaffected`: existing RFC 6238 vectors for `kind: 'totp'` still pass byte-for-byte. + +### WASM impact + +`totp_compute` in `crates/relicario-wasm/src/lib.rs` doesn't change — it forwards `kind` through serde. The TS `TotpKind` shape in `extension/src/shared/types.ts` is already correct. Only the Rust-side compute body changes. + +## Slice 2 — Shared field helpers + Login refactor + +### `extension/src/popup/components/fields.ts` + +Pure functions returning HTML strings + a small mount-time event-binding helper. No DOM ownership, no state. + +```ts +import { escapeHtml } from '../popup'; + +export interface RowOpts { + label: string; + value: string; + copyable?: boolean; + href?: string; // wraps value in + monospace?: boolean; + multiline?: boolean; // renders as
 instead of inline
+}
+export function renderRow(opts: RowOpts): string;
+
+export interface ConcealedRowOpts {
+  id: string;             // unique within the rendered detail view
+  label: string;
+  value: string;          // plaintext; rendered hidden until user reveals
+  monospace?: boolean;
+  multiline?: boolean;    // 
 when revealed; "•••• (N chars)" when hidden
+}
+export function renderConcealedRow(opts: ConcealedRowOpts): string;
+
+export interface SignatureBlockOpts {
+  accent?: 'blue' | 'green' | 'amber' | 'red';   // default 'blue'
+  children: string;        // HTML, caller's responsibility to escape
+}
+export function renderSignatureBlock(opts: SignatureBlockOpts): string;
+
+/// Wire reveal-toggle + copy handlers for all rows rendered above.
+/// Call once after the parent's innerHTML lands.
+export function wireFieldHandlers(scope: HTMLElement): void;
+```
+
+`wireFieldHandlers` looks for `data-field-action="reveal"` and `data-field-action="copy"` attributes inside `scope` and binds click handlers. Reveal toggles a `data-revealed` attribute on the row's value ``/`
`; copy uses `navigator.clipboard.writeText` and flashes a 1.5s "copied" badge.
+
+### CSS additions in `extension/src/popup/styles.css`
+
+```css
+.field-row {
+  display: grid;
+  grid-template-columns: 90px 1fr auto;
+  gap: 8px 10px;
+  align-items: baseline;
+  padding: 4px 0;
+  font-size: 12px;
+}
+.field-row__label   { color: #8b949e; }
+.field-row__value   { color: #c9d1d9; }
+.field-row__value.monospace { font-family: "SF Mono", "JetBrains Mono", monospace; }
+.field-row__value pre { margin: 0; white-space: pre-wrap; word-break: break-word; }
+.field-row__actions { display: flex; gap: 6px; font-size: 11px; color: #8b949e; }
+.field-row__actions button {
+  background: transparent; border: 0; color: inherit;
+  cursor: pointer; padding: 0; font: inherit;
+}
+.field-row__actions button:hover { color: #c9d1d9; }
+
+.sig-block {
+  background: #161b22;
+  border: 1px solid #30363d;
+  border-left: 3px solid #1f6feb;
+  border-radius: 5px;
+  padding: 14px;
+  margin-bottom: 10px;
+}
+.sig-block--blue  { border-left-color: #1f6feb; }
+.sig-block--green { border-left-color: #3fb950; }
+.sig-block--amber { border-left-color: #d29922; }
+.sig-block--red   { border-left-color: #f85149; }
+```
+
+### Login refactor (same slice)
+
+Extract `popup/components/types/login.ts` exporting `renderDetail(app, item)` / `renderForm(app, mode, existing)` / private `saveLogin(...)`. The bodies are the existing Login-branch code from `item-detail.ts` / `item-form.ts`, ported to use `renderRow` / `renderConcealedRow` / `renderSignatureBlock` instead of inline string concatenation.
+
+Net-line check: this slice should reduce total LOC slightly (helper consolidation) before adding any new types.
+
+### Helper unit tests (`fields.test.ts`)
+
+- `renderRow` produces expected HTML for plain / copyable / linked / monospace / multiline cases.
+- `renderConcealedRow` produces the hidden initial state, includes the unique id in `data-field-id`, has show + copy buttons, hides multiline value as `"•••• (N chars)"`.
+- `renderSignatureBlock` wraps children correctly with each accent class.
+- `wireFieldHandlers`: with a happy-dom `
` containing rendered rows, clicking the show button toggles `data-revealed`; clicking copy calls `navigator.clipboard.writeText` (mock). + +## Slices 3–5 — Per-type designs + +### SecureNote (Slice 3a) + +**Data**: `SecureNoteCore { body: Zeroizing }`. + +**Detail view**: title at top, then a single signature block (accent `green`) containing the body rendered as a concealed `
` block (multiline concealed row). Copy button copies the whole body verbatim. No other rows.
+
+**Form view**: a single `