v0.5.0 Plan B: extension UX polish + bug fixes #1

Closed
alee wants to merge 0 commits from feature/v0.5.0-plan-b-extension-ux into main
Owner

Summary

Implements Plan B for v0.5.0 polish + harden:

  • P4: Centralized ERROR_COPY map — every snake_case error code returned by the service worker router now maps to a friendly title/body/CTA. The popup's humanizeError regex chain and the fullscreen tab's raw state.error renders both replaced. A generated test discovers codes via live grep so the registry can't drift. (Closes B2.)
  • B1: Strength-meter regenerate desync fix — dispatch synthetic InputEvent('input', { bubbles: true }) after programmatic input.value assignment so the meter re-rates the new password.
  • P1: Password coloring — colorizePassword() pure utility splits passwords into character-class runs (digit/symbol/letter), with CSS custom-property defaults and a chrome.storage.sync-backed color scheme. Applied to field-history viewer, popup item-detail, fullscreen item-detail (shared code path), and generator preview. Boot-time applyColorScheme() wired to both popup and vault. Settings Display section with live swatch and reset.
  • P3: Form-layout envelope constraint (Approach A) — lower form sections (notes, custom-fields disclosure, attachments, form-actions) in the fullscreen login form now sit inside a .form-lower wrapper with the same max-width: 960px; margin: 0 auto envelope as .form-grid. Popup unchanged (gated on surface === 'fullscreen').
  • P2: Setup wizard → fullscreen vault tab handoff — finishSetup() opens vault.html in a new tab and best-effort closes the setup tab after successful device registration.

Spec: docs/superpowers/specs/2026-05-02-v0.5.0-polish-harden-design.md
Plan: docs/superpowers/plans/2026-05-02-v0.5.0-plan-b-extension-ux.md

Test plan

  • npx vitest run — 336 passed, 8 known pre-existing failures (all device-related)
  • npm run build — webpack compiled (pre-existing WASM module errors in service-worker, unrelated to this work)
  • cargo build -p relicario-wasm --target wasm32-unknown-unknown — clean
  • Manual viewport sweep at 1920×1080, 1440×900, 1024×768, 768×1024 (requires loaded extension in Chrome)
  • Manual setup-flow smoke — vault tab opens, setup closes (requires loaded extension in Chrome)
  • PM review

Viewport sweep notes

Manual viewport sweep requires loading the built extension in Chrome with a configured vault. Recommend PM/reviewer to verify P3 form layout visually.

P2 smoke note

Setup → vault tab handoff requires a Gitea instance for the full flow. Core logic verified by Vitest (chrome.tabs.create called with vault.html; tab-close failure is swallowed).

## Summary Implements Plan B for v0.5.0 polish + harden: - **P4**: Centralized `ERROR_COPY` map — every snake_case error code returned by the service worker router now maps to a friendly title/body/CTA. The popup's `humanizeError` regex chain and the fullscreen tab's raw `state.error` renders both replaced. A generated test discovers codes via live grep so the registry can't drift. (Closes B2.) - **B1**: Strength-meter regenerate desync fix — dispatch synthetic `InputEvent('input', { bubbles: true })` after programmatic `input.value` assignment so the meter re-rates the new password. - **P1**: Password coloring — `colorizePassword()` pure utility splits passwords into character-class runs (digit/symbol/letter), with CSS custom-property defaults and a `chrome.storage.sync`-backed color scheme. Applied to field-history viewer, popup item-detail, fullscreen item-detail (shared code path), and generator preview. Boot-time `applyColorScheme()` wired to both popup and vault. Settings Display section with live swatch and reset. - **P3**: Form-layout envelope constraint (Approach A) — lower form sections (notes, custom-fields disclosure, attachments, form-actions) in the fullscreen login form now sit inside a `.form-lower` wrapper with the same `max-width: 960px; margin: 0 auto` envelope as `.form-grid`. Popup unchanged (gated on `surface === 'fullscreen'`). - **P2**: Setup wizard → fullscreen vault tab handoff — `finishSetup()` opens `vault.html` in a new tab and best-effort closes the setup tab after successful device registration. Spec: `docs/superpowers/specs/2026-05-02-v0.5.0-polish-harden-design.md` Plan: `docs/superpowers/plans/2026-05-02-v0.5.0-plan-b-extension-ux.md` ## Test plan - [x] `npx vitest run` — 336 passed, 8 known pre-existing failures (all device-related) - [x] `npm run build` — webpack compiled (pre-existing WASM module errors in service-worker, unrelated to this work) - [x] `cargo build -p relicario-wasm --target wasm32-unknown-unknown` — clean - [ ] Manual viewport sweep at 1920×1080, 1440×900, 1024×768, 768×1024 (requires loaded extension in Chrome) - [ ] Manual setup-flow smoke — vault tab opens, setup closes (requires loaded extension in Chrome) - [ ] PM review ### Viewport sweep notes Manual viewport sweep requires loading the built extension in Chrome with a configured vault. Recommend PM/reviewer to verify P3 form layout visually. ### P2 smoke note Setup → vault tab handoff requires a Gitea instance for the full flow. Core logic verified by Vitest (`chrome.tabs.create` called with `vault.html`; tab-close failure is swallowed).
alee added 57 commits 2026-05-02 23:28:04 +00:00
- 2026-04-18 initial audit verification (all fixed except H8)
- 2026-05-01 audit with 8 new findings (B1-B4, I1-I6)
- Plan 4: Security Blocker Fixes implementation plan

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Real device auth replacing the security-theater implementation:
- Signing keys (ed25519) for commit signatures
- Deploy keys managed via Gitea API
- Server-side pre-receive hook enforcement
- CLI and extension feature parity
- Instant revocation (signing + push access)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase A: 8 security fixes (B2-B4, I1-I6)
Phase B: 10 tasks for real device authentication
- ed25519 signing keys with git SSH signing
- Deploy keys managed via Gitea API
- Pre-receive hook for server-side enforcement
- WASM API that keeps private keys internal

Total: 18 tasks

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backup KDF was passing raw passphrase bytes to Argon2id without NFC
normalization, causing cross-platform restore failures for non-ASCII
passphrases (macOS NFD vs Linux NFC).

Now matches derive_master_key behavior from crypto.rs.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- AttachmentId now uses 16 bytes of SHA-256 (128 bits) instead of 8,
  requiring ~2^64 work for birthday collision instead of ~2^32.
- Added is_valid() to ItemId and AttachmentId for path traversal
  prevention during backup restore.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
HOTP requires incrementing and persisting the counter after each use.
Without vault-save machinery in compute_totp_code, HOTP would desync
immediately. Now returns HotpNotSupported error.

TOTP and Steam codes continue to work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
RELICARIO_TEST_PASSPHRASE and friends were checked in production code,
exposing the passphrase via /proc/<pid>/environ and shell history.

Now only compiled into debug binaries via cfg(debug_assertions) helper
functions. Release builds compile the helpers to return None, so the
env var names are absent from the release binary (verified via strings).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Crafted .relbak files with IDs like "../../.bashrc" could escape the
target directory. Now validates that item/attachment IDs are hex-only
via is_valid() before any fs::write.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Control characters (newlines, tabs) in item titles corrupted git log
output. Now strips control chars and truncates to 50 chars.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
per_vault_soft_cap_bytes and per_vault_hard_cap_bytes were defined in
VaultSettings but never checked. Now enforced in cmd_attach with
warning at soft cap, error at hard cap.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Clarifies what AEAD protects (tampering) vs. what it doesn't (deletion,
rollback). Documents that git history is the audit trail and device
authentication is the mitigation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
OpenSSH-format keypair generation, signing, and verification.
Foundation for device authentication.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Create, delete, and list deploy keys via Gitea REST API.
Foundation for device authentication.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- verify-commit command checks signature against devices.json
- generate-hook outputs installable pre-receive script
- Foundation for server-side enforcement

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create crates/relicario-cli/src/device.rs: local key storage under
  ~/.config/relicario/devices/<name>/, current-device tracking, and
  git signing config (gpg.format=ssh, user.signingkey, core.sshCommand)
- Add Device command to CLI with add/revoke/list subcommands
- cmd_device add: generates two ed25519 keypairs (signing + deploy),
  registers deploy key via Gitea API, stores keys at 0600, configures
  git SSH signing, updates .relicario/devices.json and commits
- Gitea config read from flags or RELICARIO_GITEA_{URL,TOKEN,OWNER,REPO}
- --no-gitea flag skips API registration for non-Gitea remotes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove device from devices.json
- Append to revoked.json with timestamp and revoked_by
- Delete Gitea deploy key (best-effort, warns if env vars missing)
- Always commit both devices.json and revoked.json together
- Print revoked signing public key for audit confirmation
- Guard against revoking the current device (would lose push access)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New WASM bindings that keep private keys internal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add createDeployKey/deleteDeployKey to GiteaHost
- Add RevokedEntry interface and readRevoked() to devices.ts
- Update revokeDevice() to write revoked.json alongside devices.json
- Update router to use new register_device WASM API (private keys internal)
- Pass revokedBy device name when revoking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- register_device() generates signing + deploy keypairs via core device
  module, stores them in DEVICE_STATE (once_cell Lazy<Mutex>), and
  returns only public keys to JS
- sign_for_git() signs data using the internal signing key
- get_device_info() returns name and public keys; returns null if not
  registered
- clear_device() zeroes and drops device state (logout / re-registration)
- Removed generate_device_keypair() which exposed raw private key bytes

Fixes audit I5: private key material no longer crosses the WASM boundary.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Show revoked devices in collapsible section with strikethrough styling
- Fetch revoked.json via new list_revoked message + router case
- Registration flow uses register_device WASM API (private keys internal)
- Display revoked_by and timestamp for each revoked entry
- Update setup wizard to use new register_device API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-column CSS Grid for login forms, sticky save bar, and dirty-state
header subtitle. Other item types stay single-column with the polish
applied. Stacks to single column at <=720px viewport.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Chrome manifest was already updated; the Firefox manifest still
showed lowercase 'relicario' as the extension name and was pinned at
0.1.0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bundles patina palette shift, logo update (translucent gradient gem),
glass-card vocabulary across login/setup/fullscreen, and the original
two-column form layout. Updates relicario-logo.svg and -16.svg to the
patina palette.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
13-task plan to land patina palette, polish vocabulary (.surface-backdrop,
.glass, .btn-primary/secondary, ▸ arrow glyph), restructured login popup,
setup wizard polish, two-column login form, sticky save bar, and dirty-
state header subtitle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces bright amber #d2ab43 with patina gold #a88a4a as the new base.
Keeps --accent as alias for backwards compatibility. Adds --bg-card
and --border-soft for upcoming glass card class.
Mirrors popup/styles.css token block so the two surfaces share a
consistent color vocabulary.
Subtle radial top-glow + 18px grid texture. Used as the backdrop for
the login popup, setup wizard, and fullscreen vault shell.
Translucent fill, soft border, inner highlight, drop shadow. Used for
the unlock card, setup step cards, and form section panels.
Two-tier button hierarchy. .btn-primary uses patina gold fill; .btn-secondary
is a ghost button with muted border. Existing .btn class kept for
backwards compatibility.
Replaces the ASCII rightwards arrow → with U+25B8 ▸ in settings-vault
buttons. Matches the existing ▾/▸ disclosure-glyph family.
Restructures the unlock screen so the form sits in a glass card with
a primary 'unlock vault' button. Logo, brand, and tagline are grouped
as a lockup. Open-vault and settings are demoted to secondary buttons.
Body gets the .surface-backdrop wrapper.
- Wraps setup content in .surface-backdrop
- Each wizard step gets a .glass card
- Mode-picker cards become glass cards
- 'next' / 'continue' buttons get the ▸ glyph
- Migrate from .btn .btn-primary to the new .btn-primary class
Subtle radial top-glow + grid texture behind the existing vault shell.
No layout changes — existing panes sit above the backdrop's ::before.
renderForm() takes an optional { surface: 'popup' | 'fullscreen' }
parameter. When 'fullscreen', the Identity and Credentials field
groups render as glass cards inside a .form-grid (two columns,
stacks at <=720px). Popup keeps its single-column layout.
The form pane gets a flex column layout: scrollable content above,
sticky save bar at bottom. Bar uses translucent fill with backdrop-blur
and a 24px gradient fade so content scrolls under it. Save / cancel
buttons reuse the form's existing handlers via externalActions flag.
Title left ('new login' / 'edit login'), subtitle below cycles between
'no changes' and 'unsaved · esc to cancel' on input events. Right side
shows the platform-aware save hint ('⌘+S to save' / 'Ctrl+S to save').
The actual ⌘+S keymap arrives in Phase 3 — this is a visual hint only.
Anchors on a HIGH-severity auth bypass in the relicario-server
pre-receive hook (revocation + registered-device checks both
unimplemented), bundles two hardening follow-ups, two confirmed
bugs, and four UX improvements. Splits into Plan A (Rust + docs)
and Plan B (extension UX) for independent merge cadence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plan A (Rust + docs): S1 pre-receive hook fix, S2 tar
path-traversal hardening, S3 RELICARIO_* env-var audit, C1
stale branch cleanup. ~9 tasks, ~50 steps.

Plan B (extension UX): P4 error-copy centralization (subsumes
B2), B1 strength-meter regenerate fix, P1 password coloring
(inlined), P3 form-layout envelope, P2 setup → fullscreen tab.
~15 tasks, ~85 steps.

Doc audit: 14 findings, 6 fixed inline (README, ARCHITECTURE,
overview), 8 proposed for v0.5.0 release prep.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Apply trivial-fix findings from the 2026-05-02 doc audit:
- README: items/ vs entries/, settings.enc + attachments/ +
  revoked.json in vault layout, full crate tree (relicario-wasm
  + relicario-server + typed-items modules), 16-char hex IDs,
  roadmap reflects shipped trains
- ARCHITECTURE.md: git-server box reflects items/ + 16-char IDs;
  relicario-core inner box lists typed-items modules
- architecture/overview.md: ID width / 128-bit AttachmentId

8 deeper findings still proposed for v0.5.0 release prep.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three-terminal coordination paradigm: a PM session reviews and
integrates while two senior-dev sessions work parallel feature
branches in their own worktrees, dispatching subagents per
task. Prompts encode roles, boundaries, status/directive/question
block formats for user-relayed cross-terminal coordination, and
pre-tag checklists.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the popup's regex-chain humanizeError with a total lookup over
every error code returned by extension/src/service-worker/router/. A
generated test discovers codes via grep so the registry can't drift.
The popup keeps its small set of regex translators for Rust/serde error
phrasing that doesn't go through the router's error vocabulary.

Subsumes B2 — fullscreen consumer lands in the next commit.
Replaces raw escapeHtml(state.error) renders with lookupErrorCopy()-driven
title/body/CTA blocks. vault_locked specifically gets an 'Unlock vault'
CTA that refocuses the passphrase input. Other CTAs route to setup.html
or chrome.runtime.reload().

Closes B2; concludes P4.
Programmatic input.value = newPassword does not fire input events, so
the strength-meter listener at shared/form-affordances/password-tools.ts:65
never re-rates the new value — meter stays stuck on the prior reading.

Extract applyGeneratedPassword(input, value) helper that sets value, type,
then dispatches new InputEvent('input', { bubbles: true }). Vitest covers
the dispatch + a sanity check that bubbling listeners fire.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Import colorizePassword and post-process .revealed value cells after
innerHTML render, replacing escaped-HTML text with colored spans via
the valueStore plaintext lookup.
Add data-field-kind attribute to renderConcealedRow so wireFieldHandlers
can distinguish password fields from other concealed rows (TOTP secrets,
CVV, PIN, private keys). Apply colorizePassword() on reveal when kind is
"password"; plain textContent otherwise. Pass kind through renderSections
for custom-section password fields.
Import applyColorScheme in popup.ts and vault.ts, await it at boot,
and register a chrome.storage.onChanged listener so live color-picker
changes take effect without a reload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notes, custom-fields disclosure, attachments disclosure, and form-actions
in fullscreen logins now sit inside a .form-lower wrapper with the same
max-width: 960px; margin: 0 auto envelope as .form-grid above. Removes
the visual rhythm break at the 2-col -> full-width transition.

Popup keeps its current single-column behavior (gated on surface flag).
After successful device registration (state.configPushed = true), the
wizard now opens vault.html in a new tab and closes the setup tab.
Both create-new and attach-existing flows funnel through the same
finishSetup() handler. Closing the setup tab is best-effort --
chrome.tabs.remove failures don't block the vault open.

Add src/__stubs__/relicario_wasm.stub.ts + vitest.config alias so
setup.ts can be imported in unit tests without the runtime WASM file.
Exclude the stubs dir from the webpack/tsc build in tsconfig.json.
alee closed this pull request 2026-05-03 21:06:48 +00:00

Pull request closed

Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: alee/relicario#1