diff --git a/docs/superpowers/specs/2026-04-12-idfoto-credential-capture-design.md b/docs/superpowers/specs/2026-04-12-idfoto-credential-capture-design.md new file mode 100644 index 0000000..123f809 --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-idfoto-credential-capture-design.md @@ -0,0 +1,180 @@ +# idfoto — Credential Capture Design + +Experimental feature that detects login form submissions and prompts the user to save or update credentials in the vault. Configurable prompt style (notification bar or toast). Off by default. + +## Scope + +- Content script: detect form submissions with password fields, capture credentials +- Prompt UI: injected notification bar or floating toast (user-configurable) +- Dedup: check manifest before prompting — skip if already saved, offer update if password changed +- Blacklist: "Never for this site" option, persisted in `chrome.storage.local` +- Settings: enable/disable capture, choose prompt style +- Popup: settings view accessible from unlock screen + +## Trigger + +The content script listens for two events on forms that contain a password field: + +1. `submit` event on the `
` element +2. `click` event on submit buttons (`button[type=submit]`, `input[type=submit]`, or buttons inside the form) + +When triggered: +1. Read the username value from the detected username field (same detection priority as `detector.ts`) +2. Read the password value from the password field +3. If either is empty, skip +4. Send `{ type: 'check_credential', url, username, password }` to the service worker + +## Service Worker: `check_credential` Message + +New message type added to the Request union: + +```typescript +{ type: 'check_credential'; url: string; username: string; password: string } +``` + +Response: + +```typescript +{ ok: true; data: { action: 'save' | 'update' | 'skip'; entryId?: string; entryName?: string } } +``` + +Logic: +1. If vault is locked, respond `skip` +2. Check `captureBlacklist` in `chrome.storage.local` — if the URL's hostname is blacklisted, respond `skip` +3. Check `captureEnabled` setting — if false, respond `skip` +4. Search manifest entries by hostname match (same as `findByUrl`) +5. If no match: respond `{ action: 'save' }` +6. If match with same username and same password: respond `{ action: 'skip' }` (already saved) +7. If match with same username but different password: respond `{ action: 'update', entryId, entryName }` (password changed) +8. If match with different username: respond `{ action: 'save' }` (new account on same site) + +To compare passwords in step 6/7, the service worker must decrypt the matched entry to read the stored password. This is acceptable because it only happens on form submission, not on every page load. + +## Prompt UI + +Two styles, user-configurable: + +### Bar Mode (default) + +A fixed-position bar at the top of the page, injected into the DOM: + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ idfoto: Save login for github.com? (alee) [Save] [Never] [✕] │ +└──────────────────────────────────────────────────────────────────┘ +``` + +- Background: #161b22, border-bottom: 1px solid #30363d +- Text: #c9d1d9, monospace font +- Slides down from top with CSS transition +- z-index: 2147483647 (max, above everything) +- Save button: #1f6feb, Never button: #21262d, Dismiss: ✕ icon +- For updates: "Update password for github.com? (alee)" with [Update] button + +### Toast Mode + +A floating element in the bottom-right corner: + +``` +┌─────────────────────────────────┐ +│ idfoto │ +│ Save login for github.com? │ +│ alee │ +│ [Save] [Never] [✕] │ +└─────────────────────────────────┘ +``` + +- Position: fixed, bottom: 16px, right: 16px +- Same color scheme as bar mode +- Border: 1px solid #30363d, border-radius: 4px +- Auto-dismiss after 15 seconds if not interacted with +- For updates: same layout with "Update password?" text + +### Prompt Behavior + +When user clicks: +- **Save:** Content script sends `add_entry` message to service worker with `{ name: hostname, url: full_url, username, password }`. On success, prompt shows brief "Saved" confirmation then disappears. +- **Update:** Content script sends `update_entry` message with the existing entry ID and new password. Brief "Updated" confirmation. +- **Never:** Content script sends `{ type: 'blacklist_site', hostname }` to service worker, which appends to `captureBlacklist` in `chrome.storage.local`. Prompt disappears. +- **Dismiss (✕):** Prompt disappears. No action taken. Will prompt again next time. + +## Settings + +Stored in `chrome.storage.local` under key `settings`: + +```typescript +interface IdfotoSettings { + captureEnabled: boolean; // default: false + captureStyle: 'bar' | 'toast'; // default: 'bar' +} +``` + +Plus a separate key `captureBlacklist: string[]` (array of hostnames). + +### Settings View in Popup + +Accessible from the unlock screen via a "settings" link (already exists as a button). New popup view `settings` that shows: + +``` +← back + +SETTINGS + +CREDENTIAL CAPTURE (experimental) +[toggle] Auto-detect logins +Style: [bar ▾] / [toast] + +BLACKLISTED SITES +github.com [✕] +netflix.com [✕] +``` + +The toggle and style selector write to `chrome.storage.local`. Blacklist entries can be removed individually. + +## New Message Types + +```typescript +// Request +| { type: 'check_credential'; url: string; username: string; password: string } +| { type: 'blacklist_site'; hostname: string } +| { type: 'get_settings' } +| { type: 'update_settings'; settings: Partial } +| { type: 'get_blacklist' } +| { type: 'remove_blacklist'; hostname: string } + +// Response for check_credential +{ ok: true; data: { action: 'save' | 'update' | 'skip'; entryId?: string; entryName?: string } } +``` + +## File Structure + +### New files + +``` +extension/src/content/capture.ts # Form submission listener + prompt injection +extension/src/popup/components/settings.ts # Settings view +``` + +### Modified files + +``` +extension/src/content/detector.ts # Import and init capture module +extension/src/service-worker/index.ts # Handle new message types +extension/src/shared/messages.ts # Add new Request/Response types +extension/src/shared/types.ts # Add IdfotoSettings interface +extension/src/popup/popup.ts # Add 'settings' view to state machine +extension/src/popup/components/unlock.ts # Wire up settings button +``` + +## Security + +- Credentials are captured from the DOM only on form submission — no keylogging, no continuous monitoring +- Captured credentials are sent to the service worker via `chrome.runtime.sendMessage` (same secure channel as autofill) +- The prompt UI is injected into the page DOM but styled with inline styles and high z-index to avoid CSS conflicts +- The "Never" blacklist prevents unwanted prompting but doesn't affect manual autofill + +## Non-Goals + +- Detecting password change forms (change old → new password flows) +- Capturing credentials from non-standard login flows (OAuth redirects, SSO) +- Syncing settings across devices