Commit Graph

186 Commits

Author SHA1 Message Date
adlee-was-taken
8eff96da9d ext(affordances): tighten FillFromTabOpts.sendMessage return type 2026-05-01 17:54:57 -04:00
adlee-was-taken
4be0bcff83 ext(affordances): wireFillFromTab + .glyph-btn CSS 2026-05-01 17:07:01 -04:00
adlee-was-taken
918fdef519 ext(sw): expand active-tab URL filter; isolate chrome stub in tests
Expand get_active_tab_url protocol filter regex to include view-source:,
data:, devtools:, and other browser-internal/extension contexts that would
misbehave if autofilled. Add third regression test for view-source: URLs.

Wrap get_active_tab_url tests in dedicated describe block with beforeEach/
afterEach to snapshot/restore globalThis.chrome, preventing stub leakage
between tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 17:01:36 -04:00
adlee-was-taken
f872ab5183 ext(sw): add get_active_tab_url popup handler 2026-05-01 16:57:18 -04:00
adlee-was-taken
6eeb292fd0 ext(affordances): seed shared/form-affordances/ + barrel test 2026-05-01 16:53:58 -04:00
adlee-was-taken
ef7bd5b848 refactor(ext/popup): renderFormHeader takes options object
Whole-branch review recommendation: switch renderFormHeader's signature
from positional (titleText) to options ({ titleText }) so Phase 3 can
add 'dirty' (and any future hooks like a save-keybinding hint) without
touching all 7 call sites in lockstep with the unsaved-guard work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 14:33:29 -04:00
adlee-was-taken
1454cd8165 refactor(ext/popup): extract renderFormHeader + .form-header CSS
Code-review feedback on Task 8: the conditional empty
<div style="margin-bottom:16px;"> spacer was an inline-styled magic
number and the 6-line header pattern was duplicated across all 7 typed
forms.

Now:
- .form-header class owns the bottom margin in both stylesheets.
- :has(+ .form-subtitle) selector drops the margin when a subtitle
  follows, so spacing tokens stay in CSS instead of inline styles.
- renderFormHeader(titleText) shared helper collapses the 6-line
  duplication to a one-liner per form. item-form.ts (type-selection
  screen) is unaffected — it uses a different header structure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 14:26:16 -04:00
adlee-was-taken
381e8ed496 feat(ext): static 'esc to cancel' subtitle in fullscreen form headers
All seven type forms plus the type-selection screen now show a small
'esc to cancel' subtitle under the heading when rendered in the
fullscreen vault tab (isInTab() === true). The subtitle is suppressed
in the popup, where esc has the more general meaning of closing the
popup. .form-subtitle class is shared between popup and vault
stylesheets so future hooks can reuse it.

Dynamic dirty-state ('unsaved · esc to cancel') wiring is deferred to
Phase 3 (unsaved-changes guard).

Plan 2026-04-30 fullscreen UX phase 1 task 8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 14:17:59 -04:00
adlee-was-taken
38ba31768a refactor(ext/test): extract TYPED_FORMS shared list for it.each tests
Code-review feedback on Task 7: the same Array<[name, renderForm]> of
all 7 typed forms appeared in three test files (required-pill,
popout-button, popout-button-fullscreen). A new typed form would have
required updating all three.

Now defined once in __tests__/_typed-forms.ts. Future typed-form
additions get regression coverage automatically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 14:14:21 -04:00
adlee-was-taken
71ad91592d feat(ext/popup): hide popout-to-tab button in fullscreen forms
The ⤴ popout button is meaningless when the form is already in
vault.html — gate it on !isInTab(). Affects all seven type forms plus
the type-selection screen. Regression tests cover both popup (button
present) and fullscreen (button absent) contexts via it.each across
all 7 forms.

Plan 2026-04-30 fullscreen UX phase 1 task 7.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 21:01:47 -04:00
adlee-was-taken
05b1fae9f4 style(ext/popup): replace settings nav emoji with shared glyphs
▦ trash and ⌬ devices in the popup settings panel now match the
fullscreen sidebar's glyph language. Lowercased labels match the brand.

Plan 2026-04-30 fullscreen UX phase 1 task 6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:57:00 -04:00
adlee-was-taken
e2260e9df4 style(ext/vault): replace sidebar emoji nav with monochrome glyphs
▦ trash · ⌬ devices · ⚙ settings · ⏻ lock — all imported from the new
shared/glyphs module so popup and fullscreen stay in sync. Regression
test scans the source for the old escape-coded emoji to prevent
backsliding.

Plan 2026-04-30 fullscreen UX phase 1 task 5.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:53:50 -04:00
adlee-was-taken
a634b6c745 refactor(ext): broaden required-pill test + drop dead .label .req CSS
Code-review feedback on Task 4:
- Test expanded from login-only to it.each across all 7 type forms
  (14 assertions total). A future revert to <span class="req">*</span>
  in any form now fails CI.
- .label .req rule removed from popup/styles.css and vault/vault.css —
  zero consumers after the REQUIRED_PILL_HTML migration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:52:26 -04:00
adlee-was-taken
e2381ed2ec refactor(ext/popup): migrate required-field markers to REQUIRED_PILL_HTML
Replaces ten <span class="req">*</span> sites across all seven type
forms with the shared REQUIRED_PILL_HTML snippet ('required' badge).
Adds a regression test pinning the new HTML in the login form.

Plan 2026-04-30 fullscreen UX phase 1 task 4.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:46:07 -04:00
adlee-was-taken
6e720554fa style(ext/vault): migrate .btn:focus to :focus-visible + var(--focus-ring)
Code-review feedback on Task 3: vault button focus was the last
hardcoded #d2ab43 + bare :focus rule not yet migrated. Brings vault
button focus into parity with popup (which Task 2 already migrated)
and removes the last raw accent literal from the focus-related rules.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:42:24 -04:00
adlee-was-taken
f0d8758a80 style(ext/vault): mirror color tokens, focus ring, required-pill class
Same :root block and .req-pill rule as popup/styles.css so the two
stylesheets share visual tokens. Vault input focus migrated to
:focus-visible + box-shadow ring.

Plan 2026-04-30 fullscreen UX phase 1 task 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:39:46 -04:00
adlee-was-taken
e5875249bf style(ext/popup): add color tokens, focus ring, required-pill class
Establishes :root CSS custom properties (accent, surfaces, status, focus
ring) and applies the focus ring to inputs/buttons via :focus-visible.
Adds .req-pill class used by Task 4 to replace the bare-asterisk required
marker. Existing .label .req kept for backward compatibility during the
migration window.

Plan 2026-04-30 fullscreen UX phase 1 task 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:36:26 -04:00
adlee-was-taken
506ad9711d refactor(ext/shared): rename REQUIRED_PILL → REQUIRED_PILL_HTML
Code-review feedback on Task 1: the _HTML suffix makes the 'this is raw
HTML, do not escape' contract obvious at every call site. Cheap to do
now (zero consumers); would be 8 diffs once Tasks 4-6 wire the constant
into the type forms.

Plan updated in lockstep so Task 4 references the new name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:29:49 -04:00
adlee-was-taken
33b3f0b019 feat(ext/shared): glyph constants module for unified icon language
Centralizes the unicode glyphs used by sidebar nav and form action buttons
so popup and fullscreen surfaces stay in sync. Includes the REQUIRED_PILL
snippet used to replace the trailing-asterisk required-field marker.

Plan 2026-04-30 fullscreen UX phase 1 task 1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:25:12 -04:00
adlee-was-taken
31672b714d fix(ext/vault): renderPane preserves in-memory newType when hash lacks /type
In the fullscreen UX, clicking '+ new item' set the hash to '#/add'
(no type) and called renderPane. The user then clicks a type button;
its handler calls setState({ newType: type }), which in vault.ts
triggers renderPane again. renderPane was unconditionally re-deriving
state.newType from the URL hash — clobbering the just-selected type
back to null. Result: the type-selection screen kept re-rendering and
no item could be created.

Fix: prefer route.type when present (deep-link case); otherwise keep
the in-memory state.newType. Same field order, same one-line touch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:22:06 -04:00
adlee-was-taken
f1ae5841bc fix(ext): generate_device_keypair returns object not JSON string
The wasm-bindgen binding for generate_device_keypair uses
serde-wasm-bindgen and returns a plain JsValue (object), not a JSON
string. Two consumers were calling JSON.parse on it, causing the
runtime error 'SyntaxError: "[object Object]" is not valid JSON' which
broke device registration end-to-end.

Fixes:
- wasm.d.ts: return type now { public_key_hex; private_key_base64 }
  matching the rate_passphrase pattern (also a JsValue-returning
  binding).
- popup-only.ts (register_this_device handler) and setup.ts (initial
  device wire-up): drop JSON.parse, use the object directly.
- router.test.ts: pin the contract — mock generate_device_keypair as a
  function returning an object (matching real binding behavior) and
  assert register_this_device returns ok and forwards the public key.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:21:47 -04:00
adlee-was-taken
ab5a885f10 test(ext/vault): vitest for the Import panel
Mocks sendMessage. Covers: file-picker fires
parse_lastpass_csv, preview text matches the parsed counts,
confirm fires import_lastpass_commit with the parsed items,
warnings render after import, cancel clears the preview.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 18:45:23 -04:00
adlee-was-taken
66981588e7 feat(ext/vault): Import panel — LastPass CSV
New vault.html#import panel with a file picker, parse-preview
("N logins, M notes, K skipped — proceed?"), confirm/cancel
buttons, inline progress, and a post-import warnings list. The
popup's settings-vault view links to it via a new
"LastPass CSV →" button next to "Backup & restore →".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 18:43:35 -04:00
adlee-was-taken
da6f08fa35 test(ext/router): sender matrix for LastPass import messages
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:33:52 -04:00
adlee-was-taken
ecb137a120 test(ext/sw): unit tests for parse + commit handlers
Mocks the WASM bridge and vault helpers. Covers:
- parse_lastpass_csv pass-through + error surface
- commit happy path: 3 items → 3 encryptAndWriteItem +
  1 encryptAndWriteManifest call
- vault_locked + empty-items rejections
- IDs re-minted by SW so manifest keys match the new IDs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:33:16 -04:00
adlee-was-taken
b29a138411 feat(ext/sw): parse + commit handlers for LastPass import
parse_lastpass_csv is a pure pass-through to the WASM bridge.
import_lastpass_commit re-mints each item's ID via
state.wasm.new_item_id() (same pattern as add_item), encrypts
and writes per-item via git.writeFile, then writes the manifest
last. Per-item commits + a final manifest commit — extension
GitHost has no atomic-batch API, so the single-commit semantics
the CLI provides aren't replicable here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:30:26 -04:00
adlee-was-taken
fbd029e4cb feat(ext/shared): message types for LastPass import
Adds parse_lastpass_csv (preview) and import_lastpass_commit
(write) to the popup-only message set, plus typed response
helpers. SW handlers + UI follow in Tasks 12-14.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 23:30:18 -04:00
adlee-was-taken
218ccb8efa test(ext/sw): export/restore handler unit tests 2026-04-28 22:20:07 -04:00
adlee-was-taken
c1f48ecb71 test(ext): vault-tab Backup & Restore panel 2026-04-28 22:17:09 -04:00
adlee-was-taken
419408bbad feat(ext): vault-tab Backup & Restore panel
Two cards — Export (passphrase + include-image checkbox → download)
and Restore (file picker + passphrase + new-remote form). Deep-linked
from settings-vault > 'Backup & restore →'.
2026-04-28 22:11:51 -04:00
adlee-was-taken
06913a0aed test(ext/sw): router accepts/rejects backup messages per sender 2026-04-28 22:03:02 -04:00
adlee-was-taken
9ec5e9b4e1 fix(ext/sw): atomic chrome.storage update in restore_backup
Single set({vaultConfig, imageBase64?}) instead of two sequential sets,
so a partial-write window can't leave vaultConfig pointing to the new
remote while imageBase64 still references the old vault.
2026-04-28 22:01:56 -04:00
adlee-was-taken
2e825a9d33 feat(ext/sw): restore_backup handler
Unpacks .relbak via WASM, writes every vault artifact to the
user-specified fresh remote via writeFileCreateOnly (refuses to
clobber), and updates chrome.storage.local so subsequent unlocks
hit the restored vault. The reference image — when bundled — is
restored to imageBase64; otherwise the user keeps using their
existing reference.jpg.
2026-04-28 21:58:14 -04:00
adlee-was-taken
5d9ea37b7f feat(ext/sw): export_backup handler
Reads vault state via GitHost, calls pack_backup_json in WASM, returns
the .relbak bytes back to the panel for chrome.downloads.download.
Reference image inclusion comes from chrome.storage.local.imageBase64.
Git history is never bundled from the extension (CLI is the source of
full backups).
2026-04-28 20:16:52 -04:00
adlee-was-taken
f32c14f939 feat(ext/sw): export_backup / restore_backup message types 2026-04-28 20:12:07 -04:00
adlee-was-taken
c66fd520f8 docs(arch): per-codebase ARCHITECTURE.md + cross-codebase overview
Strategic-depth architecture documentation, the kind that's hard to
recover by reading code: invariants, multi-file flows, design rationale,
gotchas. Goal is to cut the token cost for future Claude sessions.

Four new docs (2091 lines total):

- crates/relicario-core/ARCHITECTURE.md (514 lines) — bytes-in/bytes-out
  boundary, 24 verified invariants (VERSION_BYTE=0x02, length-prefixed
  KDF input, NFC normalization, content-addressed AttachmentId, history-
  tracked field kinds, 60% imgsecret confidence floor, MAX_DIMENSION=
  10000, etc.), 7 multi-module flows, 16 non-obvious gotchas (QUANT_STEP=
  50, central-70%-embed, BIP39-128bit-then-truncate, Steam alphabet
  rationale).

- crates/relicario-cli/ARCHITECTURE.md (539 lines) — module map for the
  three source files; the cmd_add/cmd_edit per-type helper pattern (post-
  2026-04-27 refactor); the hardened-git invariant (Command::new("git")
  is gated to helpers.rs:46); the five history synthetic keys; the env-
  var escape-hatch policy; cmd_generate's two-mode design (no-unlock
  outside vault, unlock-and-read-defaults inside).

- extension/ARCHITECTURE.md (831 lines) — five-bundle structure (popup,
  vault, setup, content, service-worker); SW-as-crypto-fortress model;
  capability-set-or-silent-rejection contract; vault-tab-as-popup-class
  router parity (commit a7dbf35); origin TOFU flow; setup state machine;
  test-vs-build gap.

- docs/architecture/overview.md (207 lines) — cross-codebase entry point.
  How the three codebases fit together, the four versioned wire formats
  between them (core→WASM ABI, SW chrome.runtime protocol, vault on-disk
  layout, GitHost API), per-codebase secret residency table, build
  matrix, conventions that span all three.

Specs in docs/superpowers/specs/ remain as historical decision artifacts
("why we chose this") — the new arch docs are the source of truth for
"what is" current invariants and flows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:41:26 -04:00
adlee-was-taken
f79a67bb15 refactor(ext/setup): extract pure helpers to setup-helpers.ts
The setup wizard was 1205 lines in a single file. Extract the
state-independent helpers (escapeHtml, ratePassphrase, scheduleRate,
entropyText, STRENGTH_LABELS, the Strength interface) into a sibling
setup-helpers.ts. updateStrengthUi stays in setup.ts since it walks the
live wizard state object and would force every caller to thread that
state through.

setup.ts: 1205 → 1137 lines. Pure mechanical extraction; no behavior
change. Existing tests are the safety net (24 vitest files, all pass).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:13:13 -04:00
adlee-was-taken
a7dbf35126 feat(ext): sync now button + device register from popup; vault tab parity
Closes three audit gaps in one pass:

1. Sync now button in the popup settings view (📤). Triggers the existing
   { type: 'sync' } SW message and surfaces success / failure inline. The
   SW message was already wired but had no UI entry point.

2. Device registration from the popup. The "Register this device" button
   on the devices view used to error out with a "not yet implemented"
   message; it now opens an inline name input (default = browser+OS), and
   on confirm sends a new register_this_device SW message that generates
   an ed25519 keypair via WASM, persists private_key + name to
   chrome.storage.local, and writes the public key to the remote
   devices.json. No setup-wizard detour.

3. Vault tab is now an authorized sender for popup-only SW messages. The
   router accepts vault.html alongside popup.html, so the fullscreen tab
   can drive the same flows. Test covers acceptance from the vault tab.

New SW message: register_this_device { name }. Added to PopupMessage and
POPUP_ONLY_TYPES, handled in router/popup-only.ts.

Tests: 5 new vitest cases (3 in settings.test.ts, 2 in devices.test.ts)
+ 1 router test for vault-tab acceptance. All 194 extension tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 21:13:05 -04:00
adlee-was-taken
beff092818 fix(ext/setup): lock verified handle on Step 5 error + early-return paths
Mirrors Step 3b's discipline. Previously, if save_setup failed or addDevice
threw, state.verifiedHandle (the WASM session from Step 3b) would remain
in linear memory until tab close. Now lock+null on every exit path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 19:12:22 -04:00
adlee-was-taken
aa1ad99e6e chore: bump version to 0.2.0 + add CHANGELOG
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 19:02:35 -04:00
adlee-was-taken
2756033bf9 feat(ext/setup): unified device registration in Step 5; fixes silent dropped pubkey
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:34:35 -04:00
adlee-was-taken
e79e80b000 feat(ext/setup): Step 3b attach flow with decrypt verification
Replace placeholder renderStep3Attach/attachStep3Attach with the real
attach flow: file-picker for reference JPEG, passphrase input with
visibility toggle, then fetch salt+params+manifest.enc, call
unlock()+manifest_decrypt() to AEAD-verify credentials before
advancing to Step 4. Wrong passphrase/image shows a clear error;
partial handles are locked on failure to avoid key-material leaks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:32:27 -04:00
adlee-was-taken
214f8da673 fix(ext/setup): wizard writes settings.enc to match CLI init
Add default_vault_settings_json() to the hand-written wasm.d.ts
declarations, then use it in attachStep3New to encrypt and push
settings.enc after manifest.enc during new-vault creation. Wizard-
created vaults now have all four files the SW expects (salt,
params.json, manifest.enc, settings.enc), preventing the
get_vault_settings 404 on first unlock.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:29:10 -04:00
adlee-was-taken
399a276fdd feat(ext/setup): refuse to overwrite existing vault files (Step 3a clobber guard)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:24:16 -04:00
adlee-was-taken
f44aedfa76 feat(ext/setup): vault-presence probe + mode-mismatch banners on Step 2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:22:45 -04:00
adlee-was-taken
a182c1ac5a feat(ext/setup): Step 0 mode picker (new vs attach) + Step 1 back button
Replace the placeholder Step 0 with two clickable mode-card buttons (create
new vault / attach this device). Picking a card highlights it and enables
the next button; the back button on Step 1 returns to Step 0 without losing
state. Add .mode-card CSS using the existing dark palette (#30363d, #58a6ff).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:20:24 -04:00
adlee-was-taken
7fa1f2990f refactor(ext/setup): wizard state shape for mode-aware flow
Expand WizardState with mode/vaultProbe/referenceImageBytesAttach/
verifiedHandle/attaching fields; start wizard at step 0; grow progress
bar to 6 segments; rename renderStep3/attachStep3 to *New variants;
add placeholder renderStep0/attachStep0/renderStep3Attach/attachStep3Attach.
No behaviour change for the existing new-vault flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:14:42 -04:00
adlee-was-taken
8e72ed8714 feat(ext/setup): vault-presence probe helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:12:04 -04:00
adlee-was-taken
19bb5b5293 test(ext/sw): assert PUT method on GitHub writeFileCreateOnly create path
Mirrors the POST assertion already present in the Gitea "creates" test —
catches accidental method drift.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 18:10:32 -04:00
adlee-was-taken
86b5941875 feat(ext/sw): GitHost.writeFileCreateOnly() refuses to overwrite
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 18:06:48 -04:00