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>
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>
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>
The popup is too constrained for multi-step setup (file pickers
close it, fields duplicate the init wizard). Now it just shows
a single button that opens the full-page setup wizard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chrome closes popups when file pickers steal focus. Instead, check
chrome.storage.local for an existing image (pushed by init wizard),
and redirect to the full-page setup.html if no image is found.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Calling setState() after FileReader.onload triggered a full popup
re-render which could crash or close the popup with large images.
Update DOM elements in place instead, and add error handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chrome MV3 service workers do not support dynamic import().
Switch to static import of the wasm-pack JS glue and use
initSync() with fetch() to load the WASM binary at runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix .idfoto/ prefix for salt and params.json in vault.ts
- Cache TOTP secrets by entry ID to avoid re-fetching every second
- Fix keyboard navigation to use filtered entries, not unfiltered
- Add window.close() on Escape from entry list
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Login form detector using password field + username heuristics,
native value setter fill for React/Vue compatibility, inline "id" icon
injection with autofill candidate picker, and MutationObserver for SPA support.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
View router (setup/locked/list/detail/add/edit), unlock screen with
passphrase input, entry list with search/group tabs/keyboard nav,
entry detail with TOTP countdown and copy shortcuts, add/edit form
with password generation, and 3-step setup wizard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Monospace font stack, #0d1117 background, blue accents, TOTP green,
entry list with keyboard selection, confirm overlay, wizard progress bar,
and custom 4px scrollbar.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Main entry point that loads WASM via dynamic import, manages vault state
(master key, manifest, git host), and handles all message types from
popup and content scripts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bridges WASM crypto with git host API for encrypt/decrypt of entries
and manifest, plus search, group filtering, and URL-based lookup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitHost interface for reading/writing vault files via REST API.
Gitea and GitHub implementations handle base64 content encoding,
SHA-based updates, and directory listing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entry, Manifest, VaultConfig types mirroring the Rust data model, plus
a discriminated-union Request type for all popup/content-to-service-worker messages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Manifest, package.json, tsconfig, webpack config, popup HTML shell,
WASM type declarations, and .gitignore entries for the Chrome MV3 extension.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>