vault_entries uses SQLite's space-separated UTC format everywhere, and both
sorting and sync last-write-wins compare the strings lexically — so a foreign
JSON import carrying strict-ISO 'T'/millis/Z/offset timestamps would slot in
with a different format and subtly break ordering and merge arbitration.
normalizeImportTimestamp converts any ISO-ish variant to 'YYYY-MM-DD
HH:MM:SS' UTC (bare strings treated as UTC, garbage -> '' = server stamps
now). +1 unit test (69 total).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
soSave had no re-entrancy guard: a second click while the first run awaited
encryption/POST ran the whole save again -> two POSTs, two entries. Wrapped
in a soSaving latch (same class as the sync-button guard); body moved to
soSaveInner so every early validation return releases the latch via finally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same Esc fall-through class: the menu's Esc handler was bubble-phase and
didn't stop the keystroke, so the slideover capture handler fired first and
popped the discard prompt while the menu also hid. Capture + stopPropagation,
gated on the menu being visible; registered before the slideover handler
(installCustomContextMenu runs at the top of init) so ordering is guaranteed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hunted the classes behind the recent quick-search fixes across the codebase:
- Esc priority: the command palette is .cmd-palette (not .modal), so the
slideover's capture-phase Esc handler didn't see it and popped the
discard-confirm UNDER the open palette. Capture handler now also yields
to the palette.
- Esc fall-through: the fallback Esc branch closed palette + slideover +
entry modal + generator ALL on one keystroke. Now closes exactly one
surface per keystroke, topmost first.
- Modifier chord: the real-VK Tab between username and password becomes
Shift+Tab if the user holds Shift mid-fill -> focus moves backward and
the password lands in the username field. ForceReleaseModifiers before
SendVKey(VK_TAB).
- Stale placement: RestoreFromTray replayed the MinimizeToTray snapshot for
a merely taskbar-minimised window, teleporting it to the last tray-hide
position. Snapshot now replays only on a genuine tray return (captured
before Show flips Visible); iconic windows use Windows' own placement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The qsInput Esc handler didn't stopPropagation, so the SAME keystroke fell
through to the document-level Esc handlers which, seeing the modal now
closed, also closed (or discard-prompted) the dirty slideover behind it.
- autofill/cancel now hands the foreground back to the saved target HWND:
Esc after a fill-mode hotkey returns the user to the window they came
from instead of leaving our app focused. Locked-vault path still ends
focused on us (cancelAutofill runs before focusApp).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wasHidden counted IsIconic too, so a window merely minimised to the taskbar
("visible" to the user) was treated as tray-origin: Esc/pick sent it to the
TRAY, vanishing from the taskbar it came from. wasHidden is now strictly
"came from the tray" (Visible=false); an iconic window gets restored by the
hotkey and simply stays open when the modal closes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Opening a fresh new entry while a dirty unsaved one was open skipped the
discard prompt: both soState.id and id are null, so soState.id !== id was
false and "switching" never triggered. Existing->anything worked (ids
differ). OR in the null/null case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "clears in 30s" suffix was hardcoded in ~14 toasts. Single helper
clipClearSuffix() reads clipboardClearSeconds (''=Never); prefixes concatenated
to it. Cheatsheet text made generic "(auto-clears)".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single choke point: copySecure overrides any positive clearAfterMs with the
clipboardClearSeconds setting (0 = user disabled). The 15 call sites keep
passing 30000 unchanged — positive just means "auto-clear this secret";
explicit 0 (username copies) still never clears. Synced setting + a select in
Settings > Security (Clipboard privacy). Win+V history exclusion is
deliberately NOT exposed — a password manager must not offer to leak into
history / cloud clipboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copy a password (30s auto-clear armed), then copy something else from
another app before the timer fires: the tick emptied the clipboard anyway,
destroying the user's newer content. Guard with the Win32 clipboard sequence
number: SetText snapshots GetClipboardSequenceNumber, ClearIfOurs only
empties when it hasn't moved. Applied to the auto-clear timer AND the
clear-on-minimize path (same bug class); the explicit JS clipboard/clear
command stays unconditional (user-initiated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Residual new-entry trigger (1 in 6): password chars go out as
KEYEVENTF_UNICODE (VK_PACKET, can't match a hotkey) — the real chord risk is
the Ctrl+A clear-field, which sends a real VK_A. If the user re-presses
Ctrl+Shift mid-sequence, that VK_A becomes physical Ctrl+Shift+A = our own
new-entry hotkey. ForceReleaseModifiers now runs inside
SendSelectAllAndDelete, at the risky instant, not just once up front.
Balloon: was gated by "Show tray notifications" (OFF for this user) — now
gated by its own synced setting "Tray alert when autofill is blocked"
(autofillFailBalloon, Settings > Autofill, default ON), carried as notify=0
on cmd://autofill/execute. ShowBalloon no longer gates internally; each
caller applies its own setting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of "Ctrl+Shift+P opened the new-entry modal": if the user still
holds Ctrl+Shift when WaitForModifierRelease times out (1s), every password
letter is typed as a Ctrl+Shift+<letter> chord — garbage in the field AND it
fires our own global hotkeys (a password containing 'a' triggers Ctrl+Shift+A
= new entry). ForceReleaseModifiers now injects KEYUP for any still-held
modifier before typing.
Also: when the fill is blocked (elevated target) while the window is hidden
in the tray, the in-app toast is invisible — show a tray balloon instead.
ShowFirstTimeBalloon generalized into ShowBalloon(title, text, warning),
gated by the existing "Show tray notifications" setting.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Ctrl+A + Del sent before each field misbehaves on targets where Ctrl+A
isn't select-all (terminals, some remote desktops). New synced setting
(autofillClearField, Settings > Autofill) gates it: JS appends clear=0 to
cmd://autofill/execute when off; ExecuteAutofill wraps the three
SendSelectAllAndDelete calls behind AClearFirst. Absent param = ON, so
existing behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
autofillFillEntry (Ctrl+Shift+L/P path) still toasted "Password filled:"
optimistically alongside the honest UIPI failure toast. Route it through
autofillPendingToast / Bridge.onAutofillResult like the quick-search path,
and label with entryDisplayName (site is often empty -> "filled:" + nothing).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bug 1: a maximized window trayed via the quick-search fill flow came back
"normal" on the next restore. ExecuteAutofill minimizes the window BEFORE
MinimizeToTray snapshots the placement, so the snapshot said SHOWMINIMIZED
and the never-restore-minimized guard forced SHOWNORMAL. Now honours
WPF_RESTORETOMAXIMIZED (Windows keeps the pre-minimize state in flags).
Bug 2: filling into an elevated app (admin Notepad) showed "password sent"
while UIPI silently discarded the keystrokes (SendInput even reports
success). ExecuteAutofill is now a function: it checks the target process
elevation up front (can't-open counts as elevated) and returns False without
typing. UMainForm feeds the result to JS via Bridge.onAutofillResult; the
quick-search success toast is deferred until Delphi confirms, and a failure
shows "Autofill blocked - the target window runs as administrator".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The minimize-then-SW_SHOWNOACTIVATE approach flickered and sometimes lost the
restore race (window stayed minimized). Root fix: when the window was open
before the hotkey (ARestoreAfter), skip the minimize entirely — being the
foreground process is exactly what allows handing focus to the target, so the
window simply stays in place beside it. Tray-origin flow keeps the old
minimize (the window is a temporary overlay, trayed after the fill anyway).
Safety: if the target refuses the foreground (elevated / UIPI) and our window
is still foreground, bail before typing — otherwise the password would be
typed into the vault's own visible UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bugs when Ctrl+Shift+Q fires while the app window is open beside the
target app:
- RestoreFromTray re-applied FSavedPlacement (captured at the LAST
MinimizeToTray) to an already-visible window -> it jumped to a stale
position. Now: visible and not iconic -> just SetForegroundWindow.
- ExecuteAutofill minimizes our window when it is foreground (the user just
clicked the entry) and never brought it back when hide_after was false.
New ARestoreAfter param (= not HideAfter): restore with SW_SHOWNOACTIVATE
after the fill, so the window returns to its position without stealing
focus from the freshly-filled target.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migration verified (0 unmigrated cleartext templates across both accounts)
before contracting:
- GET/POST/PUT/bulk no longer read or write the cleartext template column
(only template_enc/iv); the PUT partial-update gate stays keyed on the
'template' JSON key presence.
- Removed AddColumnIfMissing for template AND tags/title — those two had
silently re-added the dropped columns as empty ghosts at every start.
- CREATE TABLE: removed site/username NOT NULL cleartext columns — a FRESH
database rejected the very first INSERT (which no longer ships them).
User can now DROP COLUMN template (and re-drop the ghost tags/title).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app.js 9363 -> 7962 lines. Three new classic-script modules:
- app.attachments.js (250): blob crypto + upload/download UI, pure
declarations, loads before app.js
- app.autofill.js (276): Win32 combos, title->entry matching, picker,
pure declarations, loads before app.js
- app.unlock.js (907): Quick Unlock + PIN + recovery code grouped (same
"enter without master pw" theme); assigns Bridge.onPinResult /
onQuickUnlockResult at top level so it loads AFTER app.js, like app.sync.js
Audit viewer stays in app.js (only 65 lines, not worth a file). Clipboard
bridge helpers stay too (were interleaved in the quick-unlock section but
unrelated). Registered in BuildAssets whitelist + index.html + APP_PARTS.
Verified in-app: quick unlock cold-start, attachment upload/download.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
assets.res/.rc/.inc are regenerated by BuildAssets before every Delphi build
and linked into the exe — tracking them just churned a binary each session.
Untrack + gitignore (matches the intent already stated in CLAUDE.md). A fresh
clone runs BuildAssets first anyway (documented build step 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
applyRemoteSnapshot swallowed attachment + folder restore errors in silent
catch blocks. Count them (attFailed/folderFailed) and warn in a toast after
the sync summary. These don't abort the push (the entry synced, only its
attachment/folder didn't) unlike a failed entry import.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unit suite concatenates + parses every embedded app.*.js (plus argon2)
in a vm, so a syntax error already fails it. Run the ~10 cold `node --check`
spawns (~3-4s) only when tests are bypassed (PM_SKIP_TESTS) or absent.
Cuts the pre-compile freeze from ~7s to ~4s.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add --disable-renderer-backgrounding / -backgrounding-occluded-windows /
-background-timer-throttling to the WebView2 args. Without them Chromium
freezes a hidden renderer, so the first autofill hotkey after a tray-only
start (Start with Windows) waited 3-5s for onAutofillRequest to run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-backup no longer wipes the stored password when disabled, so
re-enabling reuses it silently. A dedicated "Set/Change backup password"
button (mirrors sync) owns the password, with a warning status when
unset. Corrected the stale hint that claimed the backup pwd was derived
from the master password. Added icons to each settings tab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extracted the autofill toggle + hotkey combos out of the Security section into
their own .slideover-field labelled "Autofill", added an Autofill tab + map
entry. 5 tabs now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Side-mode settings panel (420px) clipped its content (Clear cache button) now
that the vertical tab column eats ~100px. Bumped #settingsPanel to 520px (id
beats .slideover's 420; entry #slideover unchanged). Nudged centered height
620→660.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Centered settings modal gets a fixed height (min(88vh,620px)) so switching
between short/long tabs no longer resizes + re-centers it. Entry #slideover
stays content-sized (edit/new unaffected).
- PIN unlock / Quick unlock / Recovery key moved from Sécurité to the Account
tab (unlock methods live with the account). Autofill stays in Sécurité.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrapped the tab bar + body in a .settings-main flex row; tabs now stack
vertically on the left with a right border, body scrolls on the right. JS
unchanged (toggles .is-tab-hidden on sections regardless of layout).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long settings panel → 4 tabs (Général / Sécurité / Account / Sync & Backup).
Each section (.slideover-field) is keyed by its label text to a tab via
SETTINGS_TAB_OF; applySettingsTab toggles .is-tab-hidden on the rest. No HTML
restructure (sections were already .slideover-field siblings), no dep. The
existing settings search composes: a live query suspends the tab filter so
cross-tab matches show, clearing it restores the active tab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pressing Esc during a sync/backup closed Settings under the busy overlay.
The Esc handler already bails for open modals — added the same bail when
#busyOverlay is visible.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The full-screen #busyOverlay (shown during sync/backup) sits outside
#settingsPanel, so a mousedown on it fired the click-outside handler and
closed Settings mid-sync. Exempted #busyOverlay like .modal/.toast already are.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking Sync twice started a second concurrent runSyncNow, and the click
event was passed as runSyncNow's `_attempt` retry counter (so the "Syncing…"
toast and 412-retry bound were both broken). Wrapped the handler: disable the
button while a sync runs, and call runSyncNow() with no arg. Internal retries
are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The entry modal + register already show a strength bar; the slideover
password field didn't. Added one in soPasswordField reusing the existing
.strength-bar/--strength CSS and computeStrength (no zxcvbn). Vault health
already scores weakness via the same function.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>