Skip to content

feat(extension): save prompt for logins typed on a page - #12

Merged
rendyuwu merged 1 commit into
devfrom
feat/save-login-prompt
Oct 2, 2026
Merged

rendyuwu merged 1 commit into
devfrom
feat/save-login-prompt

Conversation

@rendyuwu

@rendyuwu rendyuwu commented Oct 2, 2026

Copy link
Copy Markdown
Owner

What

The extension now offers to save a login typed on a page. After a sign-in
submit, a small prompt in the page's top-right corner offers Add,
Update <title> (or "Update an existing login" for a list of the host's
entries) and Cancel. It never takes focus and is not modal.

Why

Closes #9.

Until now the extension only wrote to the vault through "Generate for this
site", so a login typed and submitted by hand never reached the vault.

How

  • Capture (extension/src/content/inline.ts, wireForm,
    submittedLogin): each detected login field's form listens for submit and
    for clicks on its submit buttons. A capture needs isTrusted and an active
    user gesture (navigator.userActivation), because requestSubmit() from page
    script also fires a trusted submit. It needs a username next to the
    password too: a password-only form would blank the stored username on Update.
    A change-password form gives its new password. Card codes and one-time codes
    (cc-*, one-time-code) are skipped, and a password field toggled to text
    before the submit still counts.
  • Pending pair (extension/src/background.ts): inline-submitted stores
    { id, url, username, password } per tab in chrome.storage.session, with
    url taken from the browser-stamped sender.url, never from the page. The
    pair survives a service worker restart. It is dropped once answered, when the
    tab closes, after 3 page loads, or on the first page load after 5 minutes.
    Every read-then-write of these pairs goes through one promise chain, because
    storage has no transactions.
  • Check (src-tauri/src/modules/browser/actions.rs, check_login): the new
    read-only check-login action compares the pair with the submitted host's
    exact-host entries (the inline rule) and answers new, changed or
    unchanged, plus those entries' id, title and username. It never returns a
    password. It answers other-site when the page asking to show the prompt is
    not on the submit's site by the vault's Domain rule. So login.example.com
    to www.example.com still prompts, and an unrelated site never shows another
    site's logins. get_logins' URL loop moved into page_match, which both
    actions share; no behaviour change.
  • Prompt (promptSave, showPrompt): the existing picker is opened with no
    anchor, so it reuses the closed shadow root, rendering and keyboard handling.
    Add and Update are guarded rows and pass all six clickjacking guards. "Update
    an existing login" and Cancel write nothing and need a trusted event only.
    Add and Update send the pair's id with inline-save, and the worker refuses
    on a mismatch, so a click saves only the pair the prompt shows. A new submit
    closes a prompt that is showing. The save itself is the existing save-login
    with via: "inline", so an update still requires same_host in Rust and the
    old password lands in history.
  • No prompt when the pair is already stored, when the vault is locked, or when
    the app is not running; the last two keep the pair for a later page load. A
    page load with nothing pending never connects to the app.
  • Docs: the threat model's "Web pages" section in SECURITY.md (new trust
    boundary and residual risks), five new entries in KNOWN-LIMITS.md,
    ARCHITECTURE.md flow 5 and the extension rules in SUBCLAVE.md.

Testing

  • cargo test --workspace --locked (325 passed), cargo clippy --workspace --all-targets --locked -- -D warnings, cargo fmt --all -- --check.

  • pnpm run verify (17/17), pnpm run format:check,
    pnpm --filter subclave-extension typecheck, the chrome and firefox builds
    (content.js is 18,096 bytes, under the 30 KB cap).

  • pnpm --filter subclave-extension test: 83 passed. New save.spec.ts (17
    tests) covers Add, Update, picking the entry from a list, a stored login
    getting no prompt, the locked vault, Cancel, the 3-load limit, a
    change-password form that stays on the page, an untrusted submit, a submit
    without a user gesture, a show-password toggle click, a service worker
    restart, a stale prompt after a newer submit, a new submit closing the
    prompt, another site, a checkout form's card code, a password shown as text,
    and the prompt staying in view when the window narrows. guard.spec.ts gains
    one guard test each for the Add and Update rows.

  • check_login_states in Rust covers all states, Trash, Domain-only entries,
    a sibling host of the same site, another site, and a missing parameter.

  • Each rule above was checked by breaking it in the production code: every
    such change turned its test red, and the suite went green again once it was
    restored.

  • By hand on Windows with pnpm tauri:dev and the built extension: the prompt
    on Firefox, a sign-in that posts and redirects, and one that stays on the
    page, each after the service worker had gone idle for more than 30 s. All
    worked.

  • pnpm exec tsc --noEmit clean

  • Manual smoke-test of the affected feature

  • (If you touched src-tauri/) cargo check clean

  • (If UI) tested in pnpm tauri:dev (isolated dev data dir)

Screenshots / GIFs

None attached.

Notes for reviewer

  • Residual risk, also in SECURITY.md: after a user gesture, a page can plant
    values in its own form and submit them, which tells it whether that pair is
    stored for its own exact host (whether the prompt shows). And another host of
    the same site shows the prompt with the submitted host's entry titles and
    usernames; its Add and Update can only save the pair the user typed to the
    submitted host.
  • Not bound by a test: the 5-minute wall clock, the 1024-character cap on
    captured values, skipping the prompt while another top-layer element is
    open, the one-time-code skip, and the guard delay restart when the corner
    prompt moves.
  • save.spec.ts runs on Chromium only; Firefox was checked by hand.

After a trusted sign-in submit that follows a user gesture, the content
script reads the form's username and password and the service worker keeps
the pair per tab in chrome.storage.session, stamped with the submit's
sender.url and a random id. The next page load in that tab, or the same page
once the password has left its form, asks for the prompt: the new
check-login action compares the pair with the submitted host's exact-host
entries (new, changed, unchanged) and answers other-site when the asking
page is not on the submit's site by the Domain rule. It never returns a
password.

The prompt is the inline picker in the viewport's top-right corner, never
focused, with Add, Update <title> (or a list of the host's entries) and
Cancel. Add and Update pass the six guards and send save-login with
via: "inline" and the pair's id, so they save only the pair the prompt
shows; a new submit closes a prompt that is showing. No prompt when the pair
is already stored, the vault is locked or the app is not running.

A pending pair is dropped once answered, when its tab closes, after 3 page
loads, or on the first page load after 5 minutes. Card codes and one-time
codes in password fields are not captured; a password field toggled to text
before the submit still counts.

Docs: SECURITY.md (web pages), KNOWN-LIMITS.md, ARCHITECTURE.md flow 5,
SUBCLAVE.md extension rules, CHANGELOG.md.

Closes #9.
@rendyuwu
rendyuwu merged commit 66f3a8e into dev Oct 2, 2026
7 checks passed
@rendyuwu
rendyuwu deleted the feat/save-login-prompt branch October 2, 2026 18:57
@rendyuwu rendyuwu mentioned this pull request Oct 2, 2026
2 of 4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The extension never offers to save a login typed on a page

1 participant