Files
relicario/docs/superpowers/specs/2026-04-12-relicario-credential-capture-design.md
adlee-was-taken 39ae2ecbf3 style: capitalize "Relicario" in prose / UI / CLI help
Brand name uses capital R in user-facing text — extension UI strings,
CLI clap help / descriptions / error prose, markdown docs. Lowercase
preserved for the binary command, crate names, npm package, file
paths, env vars, and code identifiers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 17:29:10 -04:00

181 lines
7.0 KiB
Markdown

# Relicario — 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 `<form>` 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:
```
┌──────────────────────────────────────────────────────────────────┐
│ Relicario: 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:
```
┌─────────────────────────────────┐
│ Relicario │
│ 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 RelicarioSettings {
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<RelicarioSettings> }
| { 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 RelicarioSettings 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