fix(ext): content-callable capture_save_login closes critical router gap

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>
This commit is contained in:
adlee-was-taken
2026-04-20 20:57:38 -04:00
parent 1d5ad5e59e
commit 856ceb2d93
4 changed files with 257 additions and 55 deletions

View File

@@ -35,7 +35,8 @@ export type ContentMessage =
| { type: 'get_autofill_candidates' }
| { type: 'get_credentials'; id: ItemId }
| { type: 'check_credential'; username: string; password: string }
| { type: 'blacklist_site' };
| { type: 'blacklist_site' }
| { type: 'capture_save_login'; username: string; password: string };
// --- Union for chrome.runtime.sendMessage call sites ---
@@ -99,4 +100,5 @@ export const POPUP_ONLY_TYPES: ReadonlySet<PopupMessage['type']> = new Set([
export const CONTENT_CALLABLE_TYPES: ReadonlySet<ContentMessage['type']> = new Set([
'get_autofill_candidates', 'get_credentials', 'check_credential', 'blacklist_site',
'capture_save_login',
] as ContentMessage['type'][]);