Commit Graph

321 Commits

Author SHA1 Message Date
adlee-was-taken
288956c089 feat(core,wasm): filter_by_collections single-source + org_manifest_decrypt_filtered
Add OrgManifest::filter_by_collections(&[String]) as the single source of
grant-filter logic. Refactor filter_for_member to delegate to it (no
duplicated loop). Add org_manifest_decrypt_filtered WASM binding that
decrypts + filters in core so ungranted entries never cross to JS (phase-1
SOFT/UX filter; per-collection crypto isolation is phase-2). Declare the
binding in extension/src/wasm.d.ts. Vec<String> compiled cleanly for
wasm32-unknown-unknown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-26 22:19:55 -04:00
adlee-was-taken
0d56b3333b feat(ext/sw): persist device key at register, restore at unlock (+ migration)
- Add `persistDeviceKey(w, handle)` to vault.ts: calls
  `w.persist_device_key(handle)` and stores the ciphertext as base64
  in chrome.storage.local `device_key_enc`.
- Wire into `registerDeviceAndPersistConfig` (create + attach flows)
  so the key is persisted immediately after every `register_device`.
- Wire into `register_this_device` handler via `session.requireCurrent()`.
- Restore at unlock: if `device_key_enc` present, call
  `w.restore_device_key(handle, blob)`; failure is caught + logged but
  must not block unlock.
- Migration for pre-4.5b installs (absent `device_key_enc`): re-register,
  persist new key, addDevice to remote (best-effort, must not block).
  New device identity — org members must be re-granted to new pubkey.
- 4 new tests in device-key-persist.test.ts; existing 146 tests updated
  and kept green; router.test.ts register_this_device stub extended.
2026-06-26 22:13:50 -04:00
adlee-was-taken
be619f8ed0 feat(wasm): persist/restore device key under master key; org_unwrap_key reads DEVICE_STATE
- device.rs: add PersistedDeviceState (serde mirror), export_state_bytes
  (plaintext JSON in Zeroizing<Vec<u8>>, NOT a wasm_bindgen export),
  import_state_bytes (repopulates DEVICE_STATE with Zeroizing<String> keys),
  signing_seed (extracts ed25519 seed via core::device::extract_ed25519_seed)
- lib.rs: add persist_device_key (#[wasm_bindgen], encrypts state bytes under
  master key via relicario_core::crypto::encrypt; ciphertext only reaches JS),
  restore_device_key (#[wasm_bindgen], decrypts inside WASM then calls
  import_state_bytes; plaintext never leaves WASM)
- lib.rs: refactor org_unwrap_key to drop device_private_openssh param; reads
  seed from DEVICE_STATE via device::signing_seed()
- lib.rs: update org_unwrap_key_yields_a_session_that_decrypts_org_blobs to
  new signature (register_device first, use signing_pub for wrap_org_key)
- lib.rs: add device_persist_tests module (4 tests: round-trip, org-unwrap
  post-restore, ciphertext-not-plaintext, no-device-error)
- lib.rs: add DEVICE_TEST_LOCK static to serialize DEVICE_STATE-touching tests
- lib.rs: add #[derive(Debug)] to SessionHandle
- wasm.d.ts: update org_unwrap_key sig; declare persist_device_key +
  restore_device_key

All gates: cargo test -p relicario-wasm (15/15), cargo test -p relicario-core
(no regression), cargo build --target wasm32-unknown-unknown (clean), clippy
-D warnings (clean).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-26 21:53:24 -04:00
adlee-was-taken
ce141480b9 merge: origin/main (Dev-D keyfile, d649203) into org-foundation
Additive conflict in extension/src/wasm.d.ts resolved by keeping BOTH the
org_* declarations (org_unwrap_key/org_manifest_decrypt/org_manifest_encrypt)
and the keyfile_* declarations (unlock_with_secret/keyfile_encode/keyfile_decode).
Rust lib.rs (core + wasm) auto-merged cleanly with both feature sets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-26 21:35:56 -04:00
adlee-was-taken
f5336e352f feat(ext/sw): org config storage + org_list_configs message
Introduces OrgConfig/OrgConfigSummary types, chrome.storage.local backing
via org-config.ts, and the first org SW message (org_list_configs) wired in
all three required places: PopupMessage union, POPUP_ONLY_TYPES set, and a
case arm in popup-only.ts. Handler projects to OrgConfigSummary — apiToken,
hostUrl, repoPath, memberId never leave the SW. 3 new TDD tests (RED→GREEN).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 23:40:44 -04:00
adlee-was-taken
2ecd98208d feat(wasm,ext): org_manifest_decrypt/encrypt over WASM + faithful OrgManifest TS mirror
- Add `org_manifest_decrypt` + `org_manifest_encrypt` to relicario-wasm/src/lib.rs,
  placed after `org_unwrap_key`; mirror the existing manifest_decrypt/manifest_encrypt
  pattern exactly (need_key guard, session::with, js_value_for). Core import path:
  relicario_core::decrypt_org_manifest / relicario_core::encrypt_org_manifest (crate root).
- Add round-trip test `org_manifest_round_trip_via_handle` in org_tests: encrypt via
  WASM wrapper, decrypt via core directly (avoids serde_wasm_bindgen off-wasm).
  Asserts collection/title/tags/schema_version survive the round-trip; asserts nonce
  uniqueness. TDD: RED (compile error) → GREEN confirmed.
- Declare org_manifest_decrypt/org_manifest_encrypt in extension/src/wasm.d.ts.
- Add faithful/lean OrgManifestEntry + OrgManifest TS interfaces to
  extension/src/shared/types.ts: {id, type, title, tags[], modified, trashed_at?,
  collection} — no personal-only fields; collection required. Mirrors org.rs:184-202.
- Plan-doc: rewrite Task 2 body (re-scope rationale + checked steps), fix File
  Structure + Architecture lines, amend Hand-off contract (org_list_items ->
  OrgManifestEntry[]; org items remain personal Item).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 23:33:29 -04:00
adlee-was-taken
f85dc6729e fix(ext/sw): best-effort clearAll + setOrg frees displaced handle
clearAll is the security-zero path: each .free() in its own try/catch so a
single throwing handle can't leave the others un-zeroed; state is fully reset
regardless. setOrg frees any prior handle for the same org before overwriting
so a re-unlock can't orphan an un-zeroed WASM key. Tests updated to the
best-effort contract (session.test.ts) plus independent-mock, throwing-free,
and re-unlock cases (org-session.test.ts).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 21:40:37 -04:00
adlee-was-taken
6bdb7cf262 feat(ext/sw): multi-context session (personal + orgs), clearAll zeroes all
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 21:29:08 -04:00
adlee-was-taken
6cb12990ec feat(wasm): unlock_with_secret + keyfile encode/decode bindings + equivalence proof
Adds three WASM bindings for the pluggable second-factor work:
- unlock_with_secret: mirrors unlock() but accepts a raw 32-byte secret
  instead of a carrier image, skipping imgsecret::extract
- keyfile_encode: armors a 32-byte secret into relicario-keyfile-v1 format
- keyfile_decode: decodes keyfile armor back to the raw 32-byte secret

Adds native equivalence test proving that unlock_with_secret and unlock
derive an identical Argon2id master key when given the same secret,
passphrase, salt, and KDF params. Comparison is direct (master-key bytes),
not cross-decrypt, so the test is both native-safe and unambiguous.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
2026-06-25 21:24:10 -04:00
adlee-was-taken
d55861ffd8 feat(wasm): org_unwrap_key — ECIES unwrap into a session handle
Expose org_unwrap_key(keys_blob, device_private_openssh) to let the
extension SW unwrap a member's ECIES-wrapped org master key into an
ordinary Zeroizing session handle. The returned handle is key-agnostic
and works with the existing item_decrypt/manifest_decrypt AEAD.

Key-form finding: device private keys are OpenSSH PEM blobs produced by
relicario_core::device::generate_keypair(), not base64 raw bytes. The
plan's "device_private_key_base64" parameter name was misleading; all
code and the .d.ts declaration use "device_private_openssh" to match
the spec and reality.

Adds ssh-key dep to relicario-wasm for PEM parsing in decode_device_seed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
2026-06-25 21:02:54 -04:00
adlee-was-taken
5a5cf04b8f release: v0.8.2 — extension vault-creation fix (binary transport)
Extension-only patch: fixes vault create/attach in the browser extension, where
chrome.runtime.sendMessage silently dropped ArrayBuffer payloads (carrier JPEG →
0 bytes → opaque "no SOF marker found in JPEG"). See CHANGELOG for the full set.

- Version bumps: extension 0.7.0→0.8.2 (it had no changes in 0.8.0/0.8.1);
  relicario-core/-cli/-wasm 0.8.1→0.8.2 for unified-tag consistency. The Rust
  core/CLI are functionally unchanged from v0.8.1. relicario-server stays 0.1.1
  (independent version line; no functional change).
- CHANGELOG.md: v0.8.2 section. STATUS.md / ROADMAP.md: v0.8.2 landed, v0.9.0
  flagged as ready to kick off.

Verify: cargo build/test/clippy -D warnings all green (35 test binaries);
extension 435/435 vitest, build:all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
2026-06-25 20:26:36 -04:00
adlee-was-taken
20a25cebd9 fix(extension): drawer + lock-screen layout (center list, full-width lock)
- Closed vault drawer now negates its 440px via margin-right so it reserves no
  layout space — the list pane and its empty state center in the full
  post-sidebar width instead of being shifted left by the off-screen drawer.
- .vault-lock-screen gets flex:1 so the locked vault tab grows to full viewport
  width instead of collapsing to content width and pinning to the left.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
2026-06-25 18:38:37 -04:00
adlee-was-taken
56adb68935 fix(extension): binary-safe message transport + carrier-image guard
chrome.runtime.sendMessage serializes with a JSON-like algorithm that drops
ArrayBuffer/TypedArray payloads — they arrive as empty objects, so a carrier
JPEG reached the service worker as 0 bytes and failed with the opaque WASM
error "no SOF marker found in JPEG". This broke extension vault creation.

- shared/message-binary.ts: encodeBinary/decodeBinary deep-walk a message tree
  and base64-envelope every binary buffer so it survives the channel (chunked
  base64 to avoid stack overflow on multi-MB buffers); plain number[] untouched.
- Wire encode/decode at all four message boundaries: popup, service-worker
  onMessage listener (decode request, encode response), setup swSend, vault
  postToServiceWorker.
- handleCreateVault: magic-byte guard classifies empty / non-JPEG carriers into
  actionable errors (carrier_image_empty / carrier_image_not_jpeg) instead of
  the opaque WASM error; error-copy.ts adds user-facing text; setup errors now
  route through lookupErrorCopy.
- router SETUP_ALLOWED: add create_vault / attach_vault / generate_recovery_qr —
  the Phase-3 setup wizard sends these from the setup page and they were being
  rejected as unauthorized_sender.
- Tests: message-binary round-trip suite (7) + carrier-guard (2) + setup
  allowlist (3). 435/435 vitest, build:all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
2026-06-25 18:38:37 -04:00
adlee-was-taken
7c7efa7c43 release: v0.7.0 — extension restructure complete (Plan C Phases 3/4/6)
Completes the extension restructure begun in v0.6.0. Phases 3 (setup
wizard SW migration + step registry), 4 (vault.ts split + vault_locked
lift), and 6 (get_vault_status + sidebar status indicator) all merged to
main (9df2fee, 3b8368d, 397cc78) via three parallel worktree streams.

This commit is the release-prep wrap-up:
- Version bump to v0.7.0 across the three relicario crates + Cargo.lock,
  extension/package.json, and both extension manifests (the manifests had
  lagged at 0.5.0 — corrected here).
- CHANGELOG.md v0.7.0 entry.
- STATUS.md: extension restructure moved to shipped; Phases 3/4/6 landing
  section added.
- ROADMAP.md: v0.7.0 row added; Up-next now command palette.
- extension/ARCHITECTURE.md: all three phases integrated (new vault-*
  modules, setup-steps.ts, get_vault_status protocol + status indicator,
  vault_locked lift, git-host sync cache).
- Plan completion checkboxes ticked.

Task 7.1 verification: done-criteria sweep all green; 423/423 vitest;
build:all clean (only the pre-existing 4MB WASM size warning).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:50:17 -04:00
adlee-was-taken
675452a9ef fix(ext/sw): null gitHost on explicit lock (Plan C Phase 6)
The explicit lock message handler nulled state.manifest but left
state.gitHost (now carrying the cached lastSyncAt) intact, so a lock then
re-unlock within one service-worker lifetime surfaced a stale sync time.
Null gitHost here too — symmetric with the session-expiry path (index.ts)
and completing Plan C Phase 5's don't-leak-git-host-across-a-lock intent;
unlock rebuilds it on demand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:11:02 -04:00
adlee-was-taken
f4b4cf3db7 refactor(ext): simplify Phase 6 — alias VaultStatus + reuse listItems
Two simplify-pass cleanups:
- vault-status.ts: VaultStatus is now an alias of GetVaultStatusResponse['data']
  instead of a re-declared 4-field interface, so the renderer's input shape is
  single-sourced from the message contract and can't drift from the SW handler.
- service-worker/vault.ts: handleGetVaultStatus counts active items via the
  existing listItems() helper rather than re-implementing the trashed_at filter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:54:42 -04:00
adlee-was-taken
c662db2875 feat(ext/vault): wire vault-status into sidebar footer (Plan C Phase 6)
Renders the status indicator into #vault-status-slot on sidebar mount and on
a manual ↻ button. No timer polling — get_vault_status returns cached state
and sync is user-initiated. Closes the relicario status CLI/extension parity
gap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:33:21 -04:00
adlee-was-taken
5efc3a5491 test(ext/vault): handler→renderer status integration + indicator CSS (Plan C Phase 6)
Pins the 6.1↔6.2 contract: handleGetVaultStatus output feeds straight into
renderStatusIndicator. Adds minimal self-contained .vault-status CSS. Stays
out of vault-sidebar.ts — the footer wiring (Task 6.3) is Dev-B's boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:26:48 -04:00
adlee-was-taken
61275574d4 feat(ext/sw): get_vault_status handler + cached sync state (Plan C Phase 6)
Returns cached ahead/behind/lastSyncAt from the GitHost plus a live count of
active (non-trashed) manifest items. No network call — sync is user-initiated;
the sync handler records lastSyncAt (unix seconds). ahead/behind stay 0 in the
extension (writes go straight to the host, no local commit graph) and exist
for parity with relicario status.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:26:48 -04:00
adlee-was-taken
3121431a7e feat(ext/vault): vault-status indicator renderer (Plan C Phase 6)
Renders sidebar-footer indicator with ahead/behind/pending state. Pure
DOM; reuses shared/glyphs (four new status glyphs) and shared/relative-time.
Status fetch happens in the wiring layer (Task 6.3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:15:50 -04:00
adlee-was-taken
0c722b3a9d refactor(ext/state): lift vault_locked intercept into shared/state.ts (Plan C Phase 4)
The session-lost intercept lived in vault.ts's local sendMessage; both surfaces
now consume it through the shared sendMessage() wrapper. On a vault_locked
response to any non-bypassed request, the wrapper calls host.navigate('locked').
The vault host's navigate gains a 'locked' branch (it shows its lock screen off
state.unlocked); the popup's navigate already handles 'locked'. vault.ts routes
ctx.sendMessage through the shared wrapper and registers a plain transport as
host.sendMessage, so internal RPCs keep the intercept without recursion.
grep -c vault_locked vault.ts == 0. New state-vault-locked.test.ts (TDD, 6 cases).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:26:25 -04:00
adlee-was-taken
31913b8648 refactor(ext/vault): extract vault-router.ts; trim vault.ts to entry point (Plan C Phase 4)
Moves the routing core — parseHash/setHash, the renderPane pane-dispatch +
teardownPaneComponents, loadManifest, and selectItem — out of vault.ts into
vault-router.ts (carrying the popup-component imports with it). vault.ts is now
just the entry point: state singleton, the VaultController assembly, the
StateHost registration, and the DOMContentLoaded bootstrap (1037 -> 203 LOC).
No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:26:25 -04:00
adlee-was-taken
fecf58e54a refactor(ext/vault): extract vault-form-wrapper.ts (Plan C Phase 4)
Moves renderFormWrapped (sticky save bar + header + dirty-state wiring), the
SAVE_HINT/isMac consts, and the __test__ export out of vault.ts into
vault-form-wrapper.ts, taking the VaultController ctx. Repoints the source-text
form-wrapper test to read the new module. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:26:25 -04:00
adlee-was-taken
7f076b49ac refactor(ext/vault): extract vault-drawer.ts + ensureDrawerClosedForRoute (Plan C Phase 4)
Moves the drawer (open/close/render + getDrawerCoreFields + selectItemForDrawer)
out of vault.ts into vault-drawer.ts, taking the VaultController ctx. Adds
ensureDrawerClosedForRoute(state, route) — called in renderPane before the view
switch — so drawer state cannot leak across navigation to non-list/detail
routes (P2 safety net). New drawer-state.test.ts covers it (TDD).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:26:25 -04:00
adlee-was-taken
68cada5593 refactor(ext/vault): extract vault-list.ts (Plan C Phase 4)
Moves the list-pane rendering (renderListPane: row markup, empty state, and
row-click → selectItemForDrawer) out of vault.ts into vault-list.ts, taking
the VaultController ctx. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:26:25 -04:00
adlee-was-taken
9049512e0d refactor(ext/vault): extract vault-sidebar.ts with debounced search (Plan C Phase 4)
Moves the sidebar column out of vault.ts/vault-shell.ts into vault-sidebar.ts:
its markup (now incl. an empty #vault-status-slot footer for Phase 6), the
category nav rendering, nav-button wiring, and search. The search input gains
an 80ms trailing-edge debounce (P2 fix — it re-filtered on every keystroke).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:26:25 -04:00
adlee-was-taken
51255b3583 refactor(ext/vault): extract vault-shell.ts + introduce VaultController ctx (Plan C Phase 4)
Introduces vault-context.ts (VaultView/HashRoute/VaultState types, the
VaultController contract, and the pure helpers escapeHtml/typeIcon/typeLabel/
getFilteredEntries). Extracts the shell concerns — render entry, lock screen,
3-column shell scaffolding, type picker panel, color-scheme apply, and the
session_expired listener — into vault-shell.ts. vault.ts now assembles the
ctx object and delegates shell rendering through it. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 20:26:25 -04:00
adlee-was-taken
eed48e2bbb fix(ext/sw): type-correct session.setCurrent + simplify create/attach handlers
Fixes a TS2345 that npx tsc --noEmit missed (it cannot resolve the generated
wasm/relicario_wasm types, degrading SessionHandle) but the webpack build
catches with real types: session.setCurrent(handle) was passed a
SessionHandle|null. Capture the unlock result in a non-null `const h:
SessionHandle` for the in-scope ops; `handle` remains the ownership tracker
the finally block cleans up.

Simplify pass: extract the shared register_device + addDevice + persist-config
tail into registerDeviceAndPersistConfig (both handlers ended identically),
hoist the Argon2 params literal to DEFAULT_PARAMS_JSON, and fan out the two
independent read-only GETs in the attach path via Promise.all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 19:39:48 -04:00
adlee-was-taken
8044310fba test(ext/setup): cover SetupStep registry shape + clearWizardState (Plan C Phase 3)
Asserts STEPS has the six steps in canonical order, each renders non-empty
HTML and returns a teardown from attach, and clearWizardState zero-fills the
reachable Uint8Array fields before resetting state. Keeps the existing
finishSetup tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:11:53 -04:00
adlee-was-taken
d300d62c60 polish(ext/setup): honest vault-step button labels + drop needless export
Both vault-step buttons now read "continue" -- they collect input and advance
to the device step, where the SW actually performs create_vault/attach_vault
(with its own busy spinner). The old "create vault" / "verify and attach"
labels implied the action happened on that click, which is no longer true.
Drops the unused export on vaultConfig().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 18:09:10 -04:00
adlee-was-taken
bceb44f8af refactor(ext/setup): split step registry into setup-steps.ts; restore copy-config escape hatch
Hits the Task 7.1 <=500 LOC gate for setup.ts by extracting the SetupStep
registry, the WizardState singleton, clearWizardState and finishSetup into a
sibling setup-steps.ts; setup.ts is now a thin shell (progress track + render
loop + boot + re-exports). The import is one-directional (setup -> setup-steps),
no cycle. Also restores the non-extension copy-vault-config-JSON escape hatch on
the done step (per product decision) while keeping the redundant register-device
button dropped (the SW handler registers the device).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:25:22 -04:00
adlee-was-taken
9fd5e33cd4 refactor(ext/setup): SW migration + step registry + clearWizardState (Plan C Phase 3)
setup.ts is now UI-only: deletes all direct WASM orchestration (loadWasm,
the wasm binding, verifiedHandle, the SessionHandle import). Vault creation
and attach go through sendMessage({type:'create_vault'|'attach_vault'}) fired
from the device step (where the device name is known); the SW owns the entire
crypto+remote+device flow. The six renderStepN/attachStepN pairs collapse into
the SetupStep registry (mode/host/connection/vault/device/done). The done step
drops the now-redundant register-device + copy-JSON paths, keeping reference
download + recovery QR (off the SW session) + open-vault. clearWizardState
zero-fills sensitive Uint8Array fields on beforeunload and on goto('mode').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:08:23 -04:00
adlee-was-taken
0befd4e629 feat(ext/sw): attach_vault handler (Plan C Phase 3)
Same shape as create_vault: the SW owns the attach flow end to end -- fetch
salt/params/manifest from the remote, unlock with the user's reference image,
manifest_decrypt to verify the passphrase+image, register this device, persist
config + reference image, and transition the SW to the unlocked state. On
failure the handle is locked then freed; ownership transfers to the session
only on success.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:34:16 -04:00
adlee-was-taken
0e1e1a722d feat(ext/sw): create_vault handler (Plan C Phase 3)
Lifts the full create-vault flow out of setup.ts into the SW: embed image
secret, unlock, encrypt empty manifest + default settings, push the vault
layout (create-only), register this device + write devices.json, persist
config + reference image locally, and transition the SW to the unlocked
state (handle becomes SW-owned, enabling recoveryQrAvailable). On failure
the handle is locked then freed per Plan A's .free() policy; ownership only
transfers to the session on success.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:23:52 -04:00
adlee-was-taken
2cf74968e0 feat(ext/messages): add create_vault, attach_vault, get_vault_status (Plan C Phase 3 prep)
Adds the request shapes + response interfaces. POPUP_ONLY_TYPES set grows
by three. SW handlers in service-worker/vault.ts land in the next tasks.

The new union members would make popup-only.ts's exhaustive handle() switch
non-total (TS2366), so a default case is added returning an explicit
"unhandled popup message" error. create_vault/attach_vault get real cases
in Tasks 3.2-3.3; get_vault_status in Dev-C's Phase 6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 11:36:18 -04:00
adlee-was-taken
0496dfe533 Merge phase-c-5-p2-cluster: Plan C Phase 5 (P2 cluster — 5 small fixes)
5 commits landing 5 independent P2 fixes:
- ba5d218 inactivity timer resets on all non-passive messages (READ_ONLY_CONTENT_CALLABLE exclusion set in session-timer.ts; index.ts inverts the gate)
- 35444e0 state.gitHost cleared on session expiry (alongside state.manifest)
- e43f121 teardownSettingsCommon extracted; both settings.ts + settings-vault.ts call it (parameterized over each file's own activeKeyHandler module variable)
- 39fac68 Promise.allSettled with per-slot fallback in devices.ts (list_devices+list_revoked + sshFingerprint map). trash.ts is a no-op on this branch — it doesn't have a Promise.all to migrate (single list_trashed call); plan was written against a different snapshot.
- fce1962 MutationObserver scan() debounced to 200ms in content/detector.ts (no test harness on this branch — manual verification per plan note)

377/377 vitest tests pass (baseline 371 + 6 new tests in session-timer + devices). Zero regressions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:49:17 -04:00
adlee-was-taken
fce1962315 perf(ext/content): debounce MutationObserver scan() to 200ms (Plan C Phase 5)
DEV-C P2: SPA churn was re-running the full scan many times per second.
Trailing-edge debounce coalesces bursts so scan() runs at most once per
quiet 200ms window.

No test harness exists for content/detector.ts on this branch; relies
on manual verification on a real SPA page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:48:08 -04:00
adlee-was-taken
c3f8e3541c Merge phase-c-1-statehost: Plan C Phase 1 (typed StateHost + __resetHostForTests)
P1.6 closed: shared/state.ts is type-checked end to end. StateHost
interface defines every field; double-registration throws; vitest gets
__resetHostForTests to break inter-test leakage. View + PopupState moved
to shared/popup-state.ts (broke the popup→shared circular-dep blocker).

PopupState widened to absorb VaultState's vault-tab-only fields (unlocked,
drawerOpen, typePanelOpen) with optional + commented justification, so the
two surfaces share one typed contract.

378/378 vitest tests pass (baseline 371 + 7 new state.test.ts cases).

Phase 5 still running in parallel. Cross-stream note from Phase 1 subagent:
settings.ts was not touched here, so Phase 5's teardownSettingsCommon
extraction rebases cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:48:07 -04:00
adlee-was-taken
39fac68fc1 fix(ext/popup): defensive Promise.allSettled in devices (Plan C Phase 5)
DEV-C P2: Promise.all meant one rejected RPC failed the whole render.
allSettled + per-slot fallback keeps the active-devices surface usable
when the revoked-list feed (or one bad ssh fingerprint) is down.

Two call sites converted in devices.ts:
  1. list_devices + list_revoked pair — revoked failures now render an
     inline "couldn't load" slot instead of failing the page.
  2. sshFingerprint map — one bad public key falls back to '(unknown)'
     instead of killing the whole device list.

trash.ts only has a single sendMessage in its load path on this branch,
so it has no Promise.all to migrate. Plan was written against a slightly
different snapshot; documented divergence in report.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:47:15 -04:00
adlee-was-taken
31ed5c0384 test(ext/shared): cover StateHost registration + reset (Plan C Phase 1)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:47:05 -04:00
adlee-was-taken
3f2e43753d refactor(ext): sweep View/PopupState imports to shared/popup-state (Plan C Phase 1)
Removes the re-export shim from popup/popup.ts now that all callers point
at the canonical shared/popup-state. No external callers were depending on
the popup.ts re-export, so this drop is a strict tightening.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:46:20 -04:00
adlee-was-taken
547f2d4089 refactor(ext/shared): typed StateHost + sweep as-any casts (Plan C Phase 1)
Replaces the previously any-typed StateHost contract with a typed interface.
Adds double-registration guard and __resetHostForTests for vitest.
sendMessage wrapper is currently a pass-through; Phase 4 will fill its body
with the vault_locked intercept lifted from vault.ts.

Widens PopupState/View on shared/popup-state.ts to cover vault-tab-only
views (history, backup, import) and vault-tab-only fields (unlocked,
drawerOpen, typePanelOpen) so VaultState satisfies StateHost.getState()
without a cast. The popup surface ignores the new optional fields.

Drops the `any` annotations on vault.ts's registerHost callbacks now that
the typed StateHost contract infers them from PopupState.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:45:20 -04:00
adlee-was-taken
e43f121dfb refactor(ext/popup): extract teardownSettingsCommon (Plan C Phase 5)
DEV-C P2: settings.ts:56-65 and settings-vault.ts:15-22 had near-
identical cleanup paths. Single source for closeGeneratorPanel +
activeKeyHandler removal. Helper takes the handler as a parameter and
returns null so each caller still owns its own module-scoped handler
state.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:44:54 -04:00
adlee-was-taken
20f074af20 refactor(ext/sw): extract storage.ts + move itemToManifestEntry (Plan C Phase 2)
P1.9: loadDeviceSettings / loadBlacklist / saveBlacklist / saveDeviceSettings
+ itemToManifestEntry were duplicated across popup-only.ts and
content-callable.ts. Lifts the four storage helpers into service-worker/
storage.ts and itemToManifestEntry into service-worker/vault.ts.

Both router files now import from one home each. Adds storage.test.ts
covering round-trips and defaults.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:44:10 -04:00
adlee-was-taken
35444e02be fix(ext/sw): clear state.gitHost on session expiry (Plan C Phase 5)
DEV-C P2: expiry cleared manifest but left the cached git-host client.
The initializer rebuilds gitHost on demand, so clearing here is safe.

No new test: index.ts has top-level chrome.* side effects that make it
expensive to import in a unit test, and the change is a one-liner state
mutation in an inline callback. Manually verified by tracing call sites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:43:50 -04:00
adlee-was-taken
ba5d218841 fix(ext/sw): inactivity timer resets on all non-passive messages (Plan C Phase 5)
DEV-C P2: an active autofiller never opens the popup, so under the old
rule it got force-locked despite continuous use. Inverts the rule:
reset on all messages except a documented exclusion set (only
get_autofill_candidates today).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:42:44 -04:00
adlee-was-taken
f1621df3e2 refactor(ext/shared): move View + PopupState to shared/popup-state.ts
Foundation for Plan C Phase 1: shared/state.ts (next task) needs to import
PopupState without creating a popup->shared circular dep. popup.ts now
re-exports from the new location so existing callers don't break in this
task. Task 1.4 will sweep them onto the canonical import path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:41:50 -04:00
adlee-was-taken
d2d11a4c9f chore: release v0.6.0
Rolls up four weeks of post-v0.5.0 work into one tag:

- Phase 2B polish foundation + form layout (2026-05-02, 5da1e52)
- v0.5.1 Stream A — 3-column vault layout + toast + bottom sheet (2026-05-03, c16adc4)
- v0.5.1 Stream B — left-nav settings (2026-05-03, bd6a301)
- v0.5.1 Stream C — Recovery QR + setup wizard Style C (2026-05-03, 934dfe0)
- 1C-γ — Document item type + attachments + device registration + trash + history
- Plan B refactor (Cycles 1+2) — commands/ split, prompt_or_flag, core/WASM seam
- Vault-tab management surfaces revamp (2026-05-24..30) — settings split, devices fingerprint, trash countdown, history polish
- Doc-structure redesign (2026-05-30) — DESIGN/CRYPTO/docs/FORMATS rename + scope headers + Next: footers
- Lock-screen logo for parity with popup unlock
- 17 stale tests updated to match post-Stream-B / post-revamp components

Versions: relicario-{core,cli,wasm} → 0.6.0; extension/package.json → 0.6.0.
relicario-server stays at 0.1.0 (separate cadence).

Suite status at tag time: 371/371 extension + 281 Rust tests green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:17:54 -04:00
adlee-was-taken
361f3b4368 fix(ext/tests): router register_this_device test references current API
The router migrated from generate_device_keypair → register_device
(returns signing_public_key + deploy_public_key with private keys
staying internal to WASM). Test still mocked the old function under
the old return shape (public_key_hex / private_key_base64), so the
router's state.wasm.register_device() call failed with
"is not a function".

Updates the mock function name, response shape, and assertion to the
current contract. Test intent (treat the WASM return as a JS object,
not a JSON string) is preserved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:16:14 -04:00
adlee-was-taken
c9802ef392 fix(ext/tests): update settings.test.ts for left-nav settings + revamp
Tests were written against the pre-Stream B flat settings page. After
the left-nav restructure (bd6a301) and the management-surfaces revamp,
the Display section's IDs are only in the DOM once the user navigates
there, and renderSettings makes additional sendMessage calls (is_unlocked,
per-section data) that the original mocks didn't cover.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 21:14:36 -04:00