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

7.0 KiB

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:

{ type: 'check_credential'; url: string; username: string; password: string }

Response:

{ 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:

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

// 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