Replaces the ad-hoc char-class passphraseStrength() with a 5-segment
bar backed by a SW round-trip to rate_passphrase (zxcvbn). Input
handler debounces 150ms so we don't hammer the worker per keystroke.
The create-vault button is disabled unless the last score is ≥ 3
(zxcvbn's "safely unguessable" threshold), and the handler re-rates
synchronously on click as defence-in-depth. Label flips between "Too
weak" (red) and "Strong enough" (green).
Also:
- rewrites the vault-creation path to use the typed-item unlock +
manifest_encrypt APIs (derive_master_key/encrypt_manifest are gone);
the new initial manifest is { schema_version: 2, items: {} }.
- wasm.d.ts is now a pure `declare module 'relicario-wasm'` block;
tsconfig's stale `paths` alias is removed.
- @ts-nocheck removed from setup.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites item-form.ts for the typed-item Item shape. Login is the only
editable type in Slice 6; other types fall through to coming-soon.
Form fields: title (required) + url + username + password (with gen
button backed by DEFAULT_PASSWORD_REQUEST) + totp (base32) + group +
notes. TOTP base32 is decoded via shared/base32 and wrapped as a
number[] into FieldValue-shape TotpConfig { secret, algorithm: sha1,
digits: 6, period_seconds: 30, kind: 'totp' }. Decode failure sets
state.error and aborts.
Save constructs a full Item envelope (id, title, type, tags, favorite,
group, notes, created, modified, trashed_at, core, sections,
attachments, field_history). On edit we preserve the existing item's
metadata but EXPLICITLY set trashed_at: undefined — carry-forward
from Slice 5 review M3, so an edit cannot accidentally preserve stale
trash state.
@ts-nocheck removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites item-detail.ts to dispatch on item.type: login gets the full
detail view (url, username, masked password + copy, TOTP with 30s
countdown, notes, group, autofill/edit/trash/back buttons). Non-login
types get a coming-soon placeholder; those grow full UIs in later slices.
Fixes Slice 4 review I1: the old autofill path sent a malformed
fill_credentials payload ({ username, password } — no id/capturedTab).
The new handler uses the (capturedTabId, capturedUrl) pair snapshotted
at popup-open and calls fill_credentials with { id, capturedTabId,
capturedUrl }, matching the SW's handler signature that enforces the
M5 + TOCTOU checks.
TOTP poll now calls get_totp on a 1s timer and renders the 30s countdown
bar against expires_at. @ts-nocheck removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites item-list.ts to render the typed-item ManifestEntry v2
surface: title + type-icon emoji (🔑/📝/🪪/💳/🗝/📄/⏱) + icon_hint
as the meta line. Toolbar now has +new, sync, settings, lock. Keyboard
nav unchanged (/, +, arrows, Enter).
Clicking a row fires list_items → get_item (the new typed-item
messages) and stores the full Item in state.selectedItem before
navigating to 'detail'.
Also updates popup.ts PopupState:
- entries now typed Array<[ItemId, ManifestEntry]>
- selectedEntry → selectedItem (Item)
- init() uses list_items not list_entries
Trashed items (trashed_at set) are filtered out of the visible list.
@ts-nocheck removed from item-list.ts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Git-moves the three popup components so history survives the content
rewrite that follows in Tasks 22–24:
- entry-list.ts → item-list.ts
- entry-detail.ts → item-detail.ts
- entry-form.ts → item-form.ts
Also renames the exported render functions (renderEntryList →
renderItemList, etc.) and updates popup.ts imports + render switch.
The files still wear @ts-nocheck and reference the old Entry type;
content rewriting happens in Tasks 22–24.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After Slice 4's router split, the capture prompt's Save button was
silently failing on every site: content/capture.ts called four handlers
(get_settings, get_item, update_item, add_item) that are all in
POPUP_ONLY_TYPES, so the router rejected each with unauthorized_sender.
Fix in two parts:
Part A — get_settings: content scripts already have storage permission
via the manifest, so read relicarioSettings directly from
chrome.storage.local instead of round-tripping through the SW.
Part B — new content-callable 'capture_save_login' message that
consolidates what was previously three separate popup-only calls
(get_item + update_item or add_item) into one SW-side operation.
Content scripts no longer need to distinguish add vs update — the SW
does that itself from the manifest.
Security model (all enforced SW-side, never trusting content):
- Origin is derived from sender.tab.url by the router. The payload
contains only username + password; there is no way for content to
influence which host the new/updated item binds to.
- Update path re-verifies the existing item's core.url hostname
matches senderHost before mutating. If the manifest icon_hint ever
drifts from core.url, we return origin_mismatch rather than
silently binding a password to the wrong origin.
- Update mutates ONLY the password field + modified timestamp —
never title, url, or any other core field.
- Add path creates a new Login item whose title is senderHost and
whose url is the sender's origin.
Five new router tests cover: content-accept, popup-reject, update
path rotates only the password, add path creates bound item, and
origin_mismatch when the stored item's host disagrees with senderHost.
Tests: 47 -> 52.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new describe blocks cover the gaps flagged during Slice 4 review:
1. fill_credentials captured-tab verification — three cases:
- tab_navigated: chrome.tabs.get returns a tab whose hostname differs
from capturedUrl → handler must return { ok: false, tab_navigated }
and not call chrome.tabs.sendMessage.
- origin_mismatch: tab matches capturedUrl but the item's
LoginCore.url hostname differs → same refusal, no delivery.
- happy path: verify the forwarded message is exactly
{ type: 'fill_credentials', username, password, expectedHost }.
2. save_setup exception scope: the setup tab gets a narrow exception
to POST save_setup, but nothing else. Prove fill_credentials from
the setup tab is rejected with unauthorized_sender.
3. isContent sender.id guard: a content-shaped sender with a bogus
sender.id (≠ chrome.runtime.id) must be rejected.
Vault/session modules are partial-mocked via vi.mock + importOriginal so
the existing tests continue to exercise real listItems/findByHostname.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extend PopupState with {capturedTabId, capturedUrl} populated via
chrome.tabs.query({active: true, currentWindow: true}) in init().
These are later passed with fill_credentials so the SW can verify
the captured tab's hostname hasn't changed out from under the user
before forwarding credentials. Combined with expectedHost in the
forwarded payload + content-side re-check in fill.ts, this closes
the TOCTOU window on the popup → SW → content fill path.
popup.ts stays under @ts-nocheck (Slice 6 removes it alongside the
item-* rewrites).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two security fixes bundled together because they all live on the
icon-click/fill path:
1. Icon + picker + TOFU hint now render inside closed-mode Shadow DOM
(via shadow.createShadowHost). Page scripts can no longer find our
overlay via document.querySelector or rewrite buttons.
2. Icon's get_autofill_candidates call drops the `url` field — router
derives origin from sender.tab.url. Similarly get_credentials.
3. Icon's get_credentials response handling was buggy: the response is a
discriminated union { requires_ack, hostname } | { username, password }
and the old code always read .username (→ undefined when requires_ack).
New code dispatches on the `requires_ack` marker and either shows an
in-page TOFU hint or fills directly.
4. fill_credentials is popup-only in the router — the icon click cannot
(and MUST NOT) issue it from content. The new flow calls fillFields()
directly after get_credentials returns the plaintext: the content
script IS the origin, so no SW round-trip is needed for the typing.
5. TOCTOU on the popup → SW → content fill path: the SW verified the
captured tab's hostname matched capturedUrl, then forwarded blindly.
Between that check and chrome.tabs.sendMessage delivery, the tab can
navigate; chrome.tabs.sendMessage delivers to whatever content-script
principal is loaded at send-time. Closed by:
- Router forwards { expectedHost: currentHost } in the payload.
- fill.ts re-checks location.href.hostname === expectedHost before
typing anything; on mismatch replies { ok: false, error: 'origin_changed' }
and types nothing.
6. Remove @ts-nocheck from icon.ts, fill.ts, and detector.ts — all three
now type-check clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the capture prompt was a normal <div> appended to document.body
with innerHTML assembly. Any page script could find it via
document.querySelector('#relicario-capture-prompt') and either scrape
values or rewrite the buttons — and the innerHTML pattern meant hostname
interpolation was a latent XSS path (escapeForHtml helped but one mistake
would break it).
- Add content/shadow.ts — createShadowHost() with mode: 'closed', host.style.all = 'initial'.
- Rewrite capture.ts to mount inside the shadow root, build DOM via
createElement + textContent only, never innerHTML.
- Drop the `url` field from check_credential / blacklist_site — the router
now derives origin from sender.tab.url (Slice 3 contract).
- Update add_entry / update_entry calls to add_item / update_item with the
new typed Item + LoginCore shape.
- Swap RelicarioSettings → DeviceSettings.
- Remove @ts-nocheck — the file type-checks clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The popup is too constrained for multi-step setup (Chrome closes it when
focus shifts to a file picker). Previously the popup rendered a pass-through
setup-wizard component that itself opened setup.html in a tab. Cut the
middleman: if not configured, directly chrome.tabs.create the setup page
and window.close() the popup.
- Remove 'setup' from the View union and the setup case from render().
- Delete setup-wizard component entirely — setup.html is the canonical flow.
- Drop renderSetupWizard import.
The @ts-nocheck stays on popup.ts until Slice 6 (item-* rewrites).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setup.html is opened via chrome.tabs.create using a chrome-extension:// URL
which doesn't require WAR. WASM is bundled into service-worker.js/setup.js
and never fetched from a web page origin. Leaving them in WAR would expose
their URLs to any origin for probing/fingerprinting; shipping an empty WAR
array closes the surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slice 4 spec review caught: router.test.ts's narrow chrome.* shim
triggered 4 TS errors in webpack's ts-loader pass during production
builds (partial mocks don't match the full chrome.* type surface).
Plan's verbatim test body assumes tests aren't part of the build
compile. Add src/**/__tests__/** to tsconfig exclude — tests still
compile under Vitest's independent ts pipeline (42/42 passing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Non-functional tightening flagged in the slice-3 code review:
- session.ts: document future multi-vault refactor (β+) so the module-
scope singleton is explicitly "deliberately simple," not an oversight.
- vault.ts: move findByHostname doc comment above the function; note
α's intentionally-coarse hostname match (no www-stripping, no
public-suffix matching) and that tighter matching is a β/γ concern.
- index.ts: expand the passphrase scope-clearing comment to make
the theatre explicit rather than leaving it looking like real defense.
- index.ts: TODO(slice-4) marker on delete_item's non-atomic two-write
path — consider manifest-first ordering or retry/rollback at router-
split time.
- index.ts: cross-reference comment on itemToManifestEntry pointing at
the Rust-side ManifestEntry::from_item derivation it must mirror.
No behavior change; build still compiles with 2 bundle-size warnings.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add initSync named export (Chrome MV3 service worker path — can't use
dynamic import()), and correct TotpCode.expires_at from number to bigint
to match the u64 wasm-bindgen output.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Verified against the Plan 1A Rust sources:
- ItemType / ItemCore use snake_case with tag="type" internal tagging
(not the external tagging I initially wrote)
- TotpKind is default-externally-tagged (no tag attr), so it serializes
as bare "totp"/"steam" for unit variants and { hotp: { counter } }
- GeneratorRequest uses tag="kind" internal tagging
- FieldValue / TrashRetention / HistoryRetention / SymbolCharset use
adjacent tagging { tag: "kind", content: "value" }
- Fix Login form TOTP parse example and "gen" button payload
No scope change — this is a bookkeeping correction so the plan
author references the correct wire shapes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation slice of the browser-extension migration onto the typed-item
core from Plans 1A+1B. Scope: WASM artifact rebuild, typed-item shared
types, SessionHandle-based service worker, split router with sender
checks, full security architecture (origin-bound autofill, TOFU ack,
closed Shadow DOM, popup captured-tab verification), zxcvbn setup gate,
Login-parity popup. Other 6 item types land in 1C-β; attachments/trash/
history/device UI in 1C-γ.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A Task 6 implementer subagent ran `relicario init` inside the worktree
root during manual testing and committed the resulting vault skeleton
(.relicario/, manifest.enc, settings.enc) plus overwrote .gitignore.
None of these should be in the source repo.
Restores the original .gitignore (adds reference.jpg and ref.jpg to it)
and checks in the Plan 1B design doc that describes the work just merged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds RELICARIO_TEST_ITEM_SECRET env hatch for rpassword calls in
cmd_add / cmd_edit so piped-stdin tests can exercise the password
prompt paths without a TTY.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses assert_cmd + tempfile to spin up a fresh vault per test.
Covers init layout, add/list/get mask semantics, rm/restore/purge cycle,
and generate smoke. Adds RELICARIO_TEST_PASSPHRASE env-var hatch in
unlock_interactive and cmd_init so tests don't need a TTY.
Also fixes read_params in session.rs to correctly parse the nested
params.json format (kdf sub-object) rather than trying to deserialize
the whole file as KdfParams.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Maps serialize as JS objects, not Maps — what the extension popup
expects. Also ships hand-written TS declarations for the bridge
(consumed by Plan 1C).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Also ports TOTP RFC 6238 compute to relicario-core::item_types::totp
so native + CLI + WASM share one implementation (audit H5: CSPRNG
via core's Uniform-sampling generator).
Adds hmac = "0.12" and sha1 = "0.10" to relicario-core deps to support
HOTP/TOTP HMAC with Sha1/Sha256/Sha512. RFC 6238 test vector (t=59,
SHA-1, 8 digits) passes: "94287082".
Adds six #[wasm_bindgen] functions (manifest_encrypt/decrypt,
item_encrypt/decrypt, settings_encrypt/decrypt) plus a native
round-trip test that verifies encrypt→core_decrypt and nonce
uniqueness without calling js-sys (serde_wasm_bindgen::from_value
is wasm32-only; documented in test comment).
Master key never leaves WASM linear memory. Held in Zeroizing<[u8;32]>
inside a thread_local HashMap keyed by u32. lock() removes + zeroizes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLI no longer has its own charset-sampling path — uses the CSPRNG
generate_password / generate_passphrase in relicario-core, which use
rand::distributions::Uniform internally.
Extracted purge_item helper so cmd_trash_empty loops over it without
re-prompting for passphrase per item. Single git commit per trash empty
summarizing the count. Caught in Task 12 review.
Soft-delete sets trashed_at via Item::soft_delete; restore clears it.
Purge deletes item + attachment dir and removes manifest entry.
Trash empty scans for items past settings.trash_retention.
Title/group/tags always optional. Per-type prompts for core secret
fields (Login.password, Card.number, Key.material, SecureNote.body)
push the old value to field_history via a synthetic core:<key>
FieldId so rotation is audit-traceable.