diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 380ce6c..6898fe7 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -113,7 +113,8 @@ App data dir of the bundle id (`dev.rendy.subclave`; `pnpm tauri:dev` uses 3. **Lock**: manual, from the lock button, the status bar or the `vault.lock` shortcut (`useVaultStore.lock`, `vault_lock`, `lock_inner`); idle, when `spawn_vault_tick_thread` calls `VaultState::access` every 5 s past the deadline that `vault_touch` extends on activity (`drain_auto_lock`); minimize, in `on_main_window_event` when `lock_on_minimize` is set; tray, from the Lock item in `tray.rs`. Every path calls `Unlocked::wipe` and drops the key, then `emit_locked` closes the sync session (`session_closed`), drops every browser connection (`close_all`) and emits `subclave:vault-locked` with the `LockReason`. 4. **Sync pull and push**: `startSync` builds the scheduler (`createPolicy`). It pulls on unlock and on window focus (`onFocus`, at most once per `FOCUS_INTERVAL_MS`), and pushes `PUSH_DEBOUNCE_MS` after the first change of a burst (`markDirty`, on any `subclave:vault-changed` whose origin is not `sync`). Passes run one at a time (`serialize`) and re-read the switch each time (`readConfig`). Pull: `sync_pull` snapshots `locals_from_payload` and the etags, releases the lock, runs `pull` (list, fetch the objects whose etag moved, `open_envelope`, `merge`, prune this device's expired tombstones), lands the report with `apply_pull` (`merge_into_payload` per record) and `commit`s, then emits origin `sync`. Push: `sync_push` takes `take_dirty_envelopes`, `push` seals each one (`seal_envelope`) and writes it conditionally on its etag where the backend can, re-merging once on a conflict (`retry_merged`), then `finish_push` clears the marks that landed and `commit`s. 5. **Browser fill**: every request goes service worker, `createNativeTransport`, `subclave-proxy` (`read_frame`, `write_frame`), then the app's `accept_loop`, `handle_conn` and `dispatch` (status, locked check, `hello` and `auth` handshake, then the action). - - Inline: `startInline` and `createInline` draw an icon per field found by `scan`. Opening the picker sends `inline-logins`; the service worker answers it in `handleInline` for the sender's own URL at `scope: "host"` (`getState`, `get-logins`, `get_logins`, which returns no password). A pick passes `guard.check` (`createGuard`), then `inline-fill` runs `fillEntry` with `via: "inline"`: `get-credential` (`get_credential` re-runs `matching::matches` and requires `same_host`), then `subclave:fill` to the content script, where `fillTarget` refuses another origin and `fillCredential` fills. + - Inline: `startInline` and `createInline` draw an icon per field found by `scan`. Opening the picker sends `inline-logins`; the service worker answers it in `handleInline` at `scope: "host"`, and fills and the picker use the sender's own URL (`getState`, `get-logins`, `get_logins`, which returns no password). A pick passes `guard.check` (`createGuard`), then `inline-fill` runs `fillEntry` with `via: "inline"`: `get-credential` (`get_credential` re-runs `matching::matches` and requires `same_host`), then `subclave:fill` to the content script, where `fillTarget` refuses another origin and `fillCredential` fills. + - Save prompt: `pass` wires the form of each login field (`wireForm`). A trusted submit, or a click on its submit button, that follows a user gesture reads the pair with `submittedLogin` and sends `inline-submitted`; the service worker keeps it per tab in `chrome.storage.session` with the sender's URL and a fresh id. Each page load in that tab, and the submitting page itself once the password has left its form, sends `inline-pending-save`, which runs `check-login` with the asking page's URL as `pageUrl` (`check_login`: `other-site` off the submit's site by the Domain rule, else the submitted host's exact-host entries, no password returned); a new or changed login opens the picker as a prompt in the viewport's corner (`promptSave`). Add or Update pass `guard.check`, then `inline-save` carries the pair's id and sends `save-login` with `via: "inline"` (`save_login` requires `same_host` for an update, and the old password goes to history); Cancel sends `inline-save-cancel`, which drops the pair. A new submit closes a prompt that is showing. - Popup: `PopupApp` asks `get-state` for the active tab at `scope: "all"`, and a pick sends `fill-entry`, which runs `fillEntry` with `via: "popup"` (`ensureContentScript` injects `content.js` when it is missing). - Command: `fill-login` runs `runFillCommand`: `get-logins` for the active tab, then one match fills with `via: "command"`, and several open the popup where `chrome.action.openPopup` exists, or fill the most recently used one. - A released credential stamps `lastUsedAt` and emits `subclave:vault-changed` with origin `browser`. diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b87e8..787dec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,5 +13,5 @@ The latest release only. Every earlier version: - Entries and nested groups with tags, favourites, expiry dates, search, Trash and per-entry version history. - Copy with clipboard auto-clear, TOTP codes, and a password generator with a strength meter. - End-to-end encrypted sync to your own S3-compatible bucket or WebDAV share, with a merge that keeps both sides of a conflict. -- Browser extension for Chromium browsers and Firefox: pairing, an icon and picker inside login fields, popup fill, the fill command, and "Generate for this site". +- Browser extension for Chromium browsers and Firefox: pairing, an icon and picker inside login fields, popup fill, the fill command, "Generate for this site", and a save prompt that adds or updates a login typed on a page. - CSV import from KeePassXC, Bitwarden, Chrome and Firefox, CSV export, and encrypted `.subclave-backup` files. diff --git a/KNOWN-LIMITS.md b/KNOWN-LIMITS.md index 6c36f6f..e9b9ba6 100644 --- a/KNOWN-LIMITS.md +++ b/KNOWN-LIMITS.md @@ -28,10 +28,15 @@ lives, and what would change it. - **No fill in cross-origin iframes**: the content script runs in the top frame only and reaches same-origin iframes through `contentDocument`, which a cross-origin frame does not expose, so a login form inside one is neither detected nor filled. `extension/src/content/fill.ts` (`rootsOf`). Changes when: the content script runs in every frame with a per-frame origin check. - **The inline guards are JavaScript against JavaScript**: the six clickjacking guards run in the same page as the code they guard against, and Firefox has no `IntersectionObserver` v2, so the visibility guard (guard 6) runs on Chromium only. `extension/src/content/guard.ts` (`createGuard`). Changes when: browsers give extension UI a visibility guarantee. -- **No inline UI without the Popover API**: Firefox 115 to 124 (and Chrome before 114) get no icon in login fields; the popup and the fill command still work. `extension/src/content/inline.ts` (`startInline`). Changes when: the minimum Firefox version reaches 125. +- **No inline UI without the Popover API**: Firefox 115 to 124 (and Chrome before 114) get no icon in login fields and no save prompt; the popup and the fill command still work. The save prompt is part of the inline UI, so "Show in login fields" off turns it off too. `extension/src/content/inline.ts` (`startInline`). Changes when: the minimum Firefox version reaches 125. - **Very late login forms get no icon**: a page with no login field 5 seconds after it has loaded stops watching for good, so a single-page app that shows its login form later (or navigates to it without a page load) gets no icon. The popup and the fill command still fill it. `extension/src/content/inline.ts` (`SETTLE_AFTER_LOAD_MS`, `settle` in `createInline`). Changes when: detection keeps a cheap watch running on pages without a field. - **An unhinted first login step gets no icon**: a text field on its own (the first step of a two-step login) gets an icon only when it says `autocomplete="username"` or `"email"`, which keeps icons out of search boxes and newsletter fields. A first step without the hint (Yahoo and Zoom when last checked) gets no icon; the popup and the fill command still fill it. `extension/src/content/detect.ts` (`scan`). Changes when: detection gets a name or label heuristic that still leaves search and newsletter fields alone. - **"Show in login fields" applies from the next page load**: the content script reads the setting once when the page loads, so open tabs keep their current state. `extension/src/content/index.ts` (`askSettings`). Changes when: the service worker tells open tabs about the change. +- **No save prompt for password-only forms**: a submitted form is offered for saving only when it holds a username next to the filled password, so the second step of a two-step login, and a change-password form without a username field, get no prompt. An update from such a form would blank the stored username. `extension/src/content/inline.ts` (`submittedLogin`). Changes when: the first step's username is carried to the second. +- **No save prompt for formless or scripted sign-ins**: the submit is caught on the `