docs: Plan 1C-β₁ (typed-item forms) design spec
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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/<x>.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/<x>.renderDetail
|
||||
extension/src/popup/components/item-form.ts # dispatch on item.type → types/<x>.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<String> {
|
||||
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 <a target="_blank" rel="noopener">
|
||||
monospace?: boolean;
|
||||
multiline?: boolean; // renders as <pre> 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; // <pre> 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 `<span>`/`<pre>`; 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 `<div>` 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<String> }`.
|
||||
|
||||
**Detail view**: title at top, then a single signature block (accent `green`) containing the body rendered as a concealed `<pre>` block (multiline concealed row). Copy button copies the whole body verbatim. No other rows.
|
||||
|
||||
**Form view**: a single `<textarea>` (10-row default) for the body. Title at the top (always required on the Item envelope, not on the body field). No signature-block visual on the form — the textarea is the content.
|
||||
|
||||
### Identity (Slice 3b)
|
||||
|
||||
**Data**: `IdentityCore { full_name?, address? (multiline), phone?, email?, date_of_birth? }`.
|
||||
|
||||
**Detail view**: title at top; signature block (accent `amber`) with a monogram "avatar" (initials extracted from `full_name`, or `?`) + the name in larger type. Below the block, plain rows in this order: phone, email, address (multiline), date_of_birth (formatted as the user's locale via `toLocaleDateString`). Email and phone are copyable.
|
||||
|
||||
**Form view**: plain rows:
|
||||
- `full_name`: `<input type="text">`
|
||||
- `address`: `<textarea>` (3 rows)
|
||||
- `phone`: `<input type="tel">`
|
||||
- `email`: `<input type="email">` (browser-native validation surfaces on submit)
|
||||
- `date_of_birth`: `<input type="date">` — wire format matches Rust `NaiveDate`'s `"YYYY-MM-DD"` serialization
|
||||
|
||||
Empty strings → `undefined` per the established convention.
|
||||
|
||||
### Card (Slice 4a)
|
||||
|
||||
**Data**: `CardCore { number?, holder?, expiry?: MonthYear, cvv?, pin?, kind: CardKind }`. `MonthYear = { month, year }`. `CardKind = 'credit' | 'debit' | 'gift' | 'loyalty' | 'other'`.
|
||||
|
||||
**Detail view**: title at top; signature block (accent `blue`) matching the v2 mockup:
|
||||
- Top label band: `"<BRAND> · <KIND>"` uppercased (brand derived from card BIN; see below)
|
||||
- Masked card number with reveal toggle, monospace, letter-spaced
|
||||
- Footer: HOLDER (left) and EXPIRES (right)
|
||||
|
||||
Below the signature block: concealed rows for `cvv` and `pin`.
|
||||
|
||||
Brand derivation (display-only, not stored):
|
||||
```ts
|
||||
function brandFromNumber(num: string): string {
|
||||
if (/^3[47]/.test(num)) return 'AMEX';
|
||||
if (/^4/.test(num)) return 'VISA';
|
||||
if (/^5[1-5]/.test(num)) return 'MASTERCARD';
|
||||
if (/^6/.test(num)) return 'DISCOVER';
|
||||
return '';
|
||||
}
|
||||
```
|
||||
|
||||
**Form view**: plain rows:
|
||||
- `number`: `<input type="text" inputmode="numeric">`, no formatting on the form (paste-friendly)
|
||||
- `holder`: `<input type="text">`
|
||||
- `expiry`: two side-by-side `<select>`s — month (`01`–`12`) + year (current ± 25). Saves as `{ month: number, year: number }`. Empty selection → `undefined` for the whole `expiry`.
|
||||
- `cvv`: `<input type="password" inputmode="numeric" maxlength="4">`
|
||||
- `pin`: `<input type="password" inputmode="numeric" maxlength="8">`
|
||||
- `kind`: `<select>` with the 5 enum values, default `credit`
|
||||
|
||||
### Key (Slice 4b)
|
||||
|
||||
**Data**: `KeyCore { key_material: Zeroizing<String>, label?, public_key?, algorithm? }`. `key_material` is required.
|
||||
|
||||
**Detail view**: title at top; signature block (accent `green`) showing the `key_material` as a concealed monospace `<pre>` block. Below: plain rows for `label`, `algorithm` (free-form text), `public_key` (multiline monospace, **not concealed** — public keys are public).
|
||||
|
||||
**Form view**: plain rows:
|
||||
- `key_material`: `<textarea>` (8 rows, monospace) with a sibling `[show]` toggle button (since `<textarea>` doesn't honor `type="password"`). Default state: a CSS rule sets `-webkit-text-security: disc` to mask characters; clicking the toggle removes the rule.
|
||||
- `label`: `<input type="text">`
|
||||
- `public_key`: `<textarea>` (4 rows, monospace, no masking)
|
||||
- `algorithm`: `<input type="text">` placeholder `"ed25519"`
|
||||
|
||||
### Totp (Slice 5)
|
||||
|
||||
**Data**: `TotpCore { config: TotpConfig, issuer?, label? }`. `TotpConfig = { secret: number[], algorithm: 'sha1'|'sha256'|'sha512', digits: number, period_seconds: number, kind: TotpKind }`. β₁ supports `kind: 'totp'` and `kind: 'steam'`.
|
||||
|
||||
**Detail view**: title at top (uses `issuer / label` to construct a default if title is empty: `"<issuer>: <label>"`). Signature block (accent `blue`) shows:
|
||||
- Large monospace rotating code (centered, 28pt)
|
||||
- Thin SVG countdown ring at the right side, sized 32×32
|
||||
|
||||
Below the block: plain rows for `issuer`, `label`, and a concealed row for `secret` (rendered as base32 via `shared/base32.ts` `base32Encode`).
|
||||
|
||||
The ring re-tick interval is 1000ms; on each tick it calls `chrome.runtime.sendMessage({ type: 'get_totp', id })` (the existing α handler in `router/popup-only.ts` — no new message type). The countdown value is `(expires_at - now)` per the existing `TotpResponse`.
|
||||
|
||||
**Form view**: a kind toggle at the top, then plain rows:
|
||||
|
||||
```
|
||||
┌─ kind ──────────────────────────┐
|
||||
│ [● TOTP] [○ Steam Guard] │
|
||||
└─────────────────────────────────┘
|
||||
secret (base32): [_______________]
|
||||
issuer: [_______________]
|
||||
label: [_______________]
|
||||
```
|
||||
|
||||
Toggle is a two-button group; click switches `state.kind` and re-renders the small subtitle below ("Standard time-based codes" vs "Steam Mobile Authenticator (5-char alphanumeric)"). For both kinds, `digits` / `period_seconds` / `algorithm` are written with their defaults (`6`/`30`/`sha1` for TOTP; `5`/`30`/`sha1` for Steam — Steam's compute uses the alphabet, ignoring the digits field). Power users who need non-default values use the CLI; β₂ may add a `[more options ▾]` disclosure on the Totp form if this turns out to bite real users.
|
||||
|
||||
`secret` parsed via `base32Decode` from `shared/base32.ts` (already exists). Empty string is rejected with a friendly error from the popup's `humanizeError` path.
|
||||
|
||||
### Dispatcher updates
|
||||
|
||||
`item-detail.ts` after β₁:
|
||||
|
||||
```ts
|
||||
import * as login from './types/login';
|
||||
import * as secureNote from './types/secure-note';
|
||||
import * as identity from './types/identity';
|
||||
import * as card from './types/card';
|
||||
import * as key from './types/key';
|
||||
import * as totp from './types/totp';
|
||||
|
||||
export async function renderItemDetail(app: HTMLElement): Promise<void> {
|
||||
const item = getState().selectedItem;
|
||||
if (!item) { navigate('list'); return; }
|
||||
switch (item.type) {
|
||||
case 'login': return login.renderDetail(app, item);
|
||||
case 'secure_note': return secureNote.renderDetail(app, item);
|
||||
case 'identity': return identity.renderDetail(app, item);
|
||||
case 'card': return card.renderDetail(app, item);
|
||||
case 'key': return key.renderDetail(app, item);
|
||||
case 'totp': return totp.renderDetail(app, item);
|
||||
case 'document': return renderComingSoonPlaceholder(app, item.type);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`item-form.ts` follows the same shape with `renderForm(app, mode, existing)`.
|
||||
|
||||
### "New…" picker
|
||||
|
||||
`item-list.ts`'s `+ New` button opens a small picker (popover anchored to the button):
|
||||
|
||||
```
|
||||
new item
|
||||
🔑 login
|
||||
📝 secure note
|
||||
🪪 identity
|
||||
💳 card
|
||||
🗝 key
|
||||
⏱ totp
|
||||
📄 document ← greyed; tooltip "coming in γ — needs attachment upload"
|
||||
```
|
||||
|
||||
Selecting a type stores `state.newType` (transient — added to PopupState with `'login' | 'secure_note' | …`) and navigates to `'add'`. The form dispatcher reads `state.newType` for add-mode and `state.selectedItem.type` for edit-mode.
|
||||
|
||||
The popover lives in the popup's own DOM (no closed Shadow DOM needed — the popup is its own origin and not subject to page-injection threats). Standard `<div>` with `position: absolute` anchored to the button.
|
||||
|
||||
## Testing
|
||||
|
||||
### Rust
|
||||
|
||||
`cargo test --workspace` stays green. New tests in `crates/relicario-core/src/item_types/totp.rs` listed in §Slice 1.
|
||||
|
||||
### Vitest
|
||||
|
||||
Existing 55 tests stay green. New:
|
||||
|
||||
- `extension/src/popup/components/__tests__/fields.test.ts` (helper unit tests, ~12 cases).
|
||||
- `extension/src/popup/components/types/__tests__/save-shape.test.ts` (per-type form→Item transform, ~5 cases × ~3 sub-assertions = ~15 cases).
|
||||
|
||||
The save-shape tests use happy-dom to render each form's HTML, populate inputs, fire the save handler, and intercept the `add_item` message via a `vi.fn()` shim of `chrome.runtime.sendMessage`. Asserts cover:
|
||||
|
||||
- SecureNote: `core.body === '<input value>'`, `core.type === 'secure_note'`.
|
||||
- Identity: each present field in JS shape matches the wire format; absent fields are `undefined` (not empty string).
|
||||
- Card: `expiry === { month: 8, year: 2029 }`; concealed fields (`number`/`cvv`/`pin`) round-trip through the form values; `kind` matches the select.
|
||||
- Key: `key_material` always present; `algorithm` free-form.
|
||||
- Totp: `config.secret === Array.from(base32Decode('JBSWY3DPEHPK3PXP'))`; `config.kind === 'totp'` or `'steam'` depending on toggle; for Steam, `config.digits === 5`.
|
||||
|
||||
### Manual matrix
|
||||
|
||||
Re-run the α matrix's 11 steps (§5.4 of α spec) plus, per type:
|
||||
|
||||
1. Add a new item of the type → it appears in the list with the right icon.
|
||||
2. Open the item → detail view renders correctly (signature block + rows; no console errors).
|
||||
3. For types with concealed fields: click reveal → value appears; click copy → clipboard contains the value.
|
||||
4. Edit → save → list updates with new modified time; detail reflects changes.
|
||||
5. Trash → moves out of the live list; CLI `relicario list --trashed` shows it.
|
||||
6. For Totp: code rotates every 30s; Steam Guard kind produces 5-char alphanumeric; TOTP kind produces 6-digit decimal; switching kinds in the edit form re-renders the detail view's compute output correctly after save.
|
||||
|
||||
### Acceptance
|
||||
|
||||
- `cargo test --workspace` green.
|
||||
- `bun run test` green.
|
||||
- `bun run build:all` green for both Chrome and Firefox.
|
||||
- `git grep -n 'coming-soon\|Coming in' extension/src/popup/components/` returns hits ONLY for `'document'`.
|
||||
- All 5 type matrices pass on Chrome and Firefox.
|
||||
- No new lint regressions; `git grep -n '@ts-nocheck' extension/src/` returns zero.
|
||||
|
||||
## Open questions deferred to the plan
|
||||
|
||||
- Exact CSS sizing of the Totp signature block's countdown ring (32px or 40px). Picked at implementation time.
|
||||
- Whether the Card brand-from-BIN is comprehensive enough (currently 4 brands). Likely fine for α/β₁ — extending the table is a one-line change.
|
||||
- For Steam toggle UX: a two-button group or a dropdown. Brainstorming locked in two-button; implementation may push back if it's awkward at popup width.
|
||||
- Whether to expose `Totp.algorithm` / `digits` / `period_seconds` to power users via a `[more options ▾]` disclosure on the form. β₁ defaults them; β₂ revisits if the CLI workaround friction is real.
|
||||
Reference in New Issue
Block a user