Commit Graph

27 Commits

Author SHA1 Message Date
r-zakarya 5e88ad33d1 feat(crypto): adopt Argon2id (argon2id-v2) on register + master-pw change
Phase 2 of CODE_AUDIT §1.2 — live adoption of the Argon2id foundation.
Verified at runtime: a rotated account shows hash_algo=argon2id-v2 with
argon2_m=19456,t=2,p=1 in vault.db.

Server (never runs Argon2 — zero-knowledge, only stores/echoes params):
- DB: users.argon2_m/t/p columns (default 0 = PBKDF2).
- PM.Handler.Auth: HASH_ALGO_ARGON2 + param bounds, ReadArgon2Params /
  AppendArgon2Params helpers. /register and /change-master-password accept
  hashAlgo='argon2id-v2' + argon2:{m,t,p} and persist them; /login/challenge
  echoes them. Verify path (VerifierToStoredHash/CheckVerifier) is
  KDF-agnostic — the 64-hex verifier is SHA256-wrapped as for any -v2 scheme.

Client (app.js):
- state.argon2Params, cached from the challenge and persisted to
  sessionStorage + the quick-unlock / PIN cold-start blobs (so a cold-started
  session can still derive-from-password for reauth/rotation).
- Register + master-pw rotation derive with argon2id-v2 + ARGON2_DEFAULT_PARAMS
  (OWASP m=19MiB,t=2,p=1) and send the params. Rotation re-encrypts the whole
  vault under the new Argon2 key (natural migration point). Existing accounts
  stay PBKDF2 until they rotate.
- Params threaded through every derive-from-password site (login, reauth,
  recovery setup, change-pw current verifier). Cold-start verifier-from-raw-key
  paths need no params (isDecoupledVerifierAlgo handles the -v2 wrap).

Tests: +2 param-contract tests (register<->login determinism, param
sensitivity). 42/42. Assets rebuilt to embed js/argon2.js.

Docs: CLAUDE.md auth-hash section rewritten (4 markers); CODE_AUDIT §1.2 +
table + plan marked done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:52:11 +01:00
r-zakarya 2bd0fcfbf8 feat(crypto): Argon2id KDF foundation (vendored, not yet adopted)
Phase 1 of CODE_AUDIT §1.2 — additive, no live account uses Argon2id yet.

- Vendor @noble/hashes@2.2.0 argon2id as js/argon2.js (esbuild IIFE exposing
  globalThis.NobleArgon2). Pure-JS, not WASM: CSP is script-src 'self' with no
  wasm-unsafe-eval, so WASM would require weakening it. Verified against the
  RFC 9106 §5.3 test vector. Server needs zero Argon2 (zero-knowledge: it only
  ever SHA256-wraps the client verifier).
- app.js: deriveKeyBytes(pwd, salt, algo, iters, argonParams) branches Argon2id
  vs PBKDF2; deriveKeyAndVerifier refactored around it. New markers
  HASH_ALGO_ARGON2='argon2id-v2' + ARGON2_DEFAULT_PARAMS (OWASP m=19MiB,t=2,p=1,
  ~0.65s/unlock). isDecoupledVerifierAlgo() generalises the decoupled-verifier
  rule to any '-v2' scheme so argon2id-v2 inherits it. AES key is still ALWAYS
  the raw KDF output → entries decryptable, legacy accounts untouched.
- index.html loads js/argon2.js before app.js; added to BuildAssets whitelist;
  test harness loads it into the sandbox first.
- Tests: +5 (RFC 9106 vector via vendored bundle, argon2 branch derives Argon2
  key not PBKDF2, decoupled verifier, AES round-trip under Argon2 key). 40/40.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:13:36 +01:00
r-zakarya d9397881dc test: add frontend unit suite + fix mixed local/UTC timestamps
Two CODE_AUDIT items in one session.

§3.2 — Frontend regression net (js/tests/, 35 tests, node:test, zero deps):
- harness.js loads app.js (monofile, no exports) into a node:vm with browser
  globals stubbed, surfacing internals via an export epilogue.
- crypto: deriveKeyAndVerifier (AES key == raw PBKDF2, cross-checked vs Node
  pbkdf2Sync), legacy-vs-v2 verifier decoupling, encrypt/decrypt round-trip,
  IV uniqueness, AEAD tamper/wrong-key.
- csv: parseCSV tokenizer, findColumn heuristics, Bitwarden/KeePass mapping.
- merge: applyRemoteSnapshot add/update/skip (LWW), tombstone delete,
  resurrection arbitration (both NaN branches), local-tombstone veto,
  additive folder merge. Only api() is stubbed; loadEntries/encryptImportEntry
  run for real.
- Wired as a build gate in BuildAssets.ps1 (after node --check, bypass
  PM_SKIP_TESTS=1).

§2.2 — Unify timestamps on UTC:
- Entry created_at/updated_at were written via Delphi FormatDateTime(Now)
  = LOCAL, while deleted_at/tombstones use SQLite CURRENT_TIMESTAMP = UTC.
  The tombstone-resurrection arbitration compared the two zones, skewing by
  the machine's UTC offset even single-device.
- Add NowUTC/NowUTCStr to PM.Database, swap in at every entry/attachment
  write site (Entries create/update/bulk, Attachments POST echo).
- No JS change needed: arbitration now compares same-zone values.
- Existing rows self-heal on next edit (no destructive migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:17:33 +01:00
r-zakarya ac909f4f09 feat: sync ETag concurrency + fix chunk-transfer hang + sync overlay + auto-VACUUM
Sync optimistic concurrency (ETag/If-Match)
- webdav GET captures the response ETag; PUT sends it back as If-Match so
  the server rejects (412) our write when another device changed the file
  between our pull and push. A 412 re-runs the whole pull→merge→push
  (bounded to 3) so the other device's changes are folded in instead of
  clobbered. Servers without ETags → empty etag → no If-Match → falls
  back to last-write-wins (no regression). onWebdavResult gained a 4th
  etag arg.

Chunked webdav PUT (big vaults no longer black-screen on sync)
- The whole encrypted snapshot base64'd into a single cmd://webdav/put URL
  blew past WebView2's cap → black screen once the vault grew (20MB of
  attachments). PUT bodies now stream through the file/chunk transport and
  commit via a new webdav/put-commit (reads the accumulated buffer).

Chunk-transfer hang fix (root cause of the stuck "Preparing…" sync)
- All chunked transfers (saveFile/writeFile/webdav PUT) share one
  reqId-keyed resolver. A resolved chunk's stale 30s timeout would later
  delete the CURRENT chunk's resolver and fire the wrong res(), leaving
  that chunk's await pending forever. Extracted a single _streamChunks()
  helper whose ack CLEARS the pending timeout, so resolvers stay strictly
  one-at-a-time. Also fixed _webdavCall referencing the Bridge-local cmd()
  from module scope (latent ReferenceError).

Sync busy overlay
- syncStatus() now drives the global busy overlay too, so a running sync
  blocks stray clicks (e.g. the auto-backup "Choose…" picker) and reads
  like the manual backup. The account-mismatch confirm hideBusy()s first
  so it's visible above the overlay.

Auto-VACUUM (reclaim space after deleting large attachments)
- SQLite never shrinks the file on DELETE, so deleting big attachments
  left vault.db bloated (35MB for 11 tiny entries). DB.CompactIfBloated
  VACUUMs when >20% of pages are free AND >~2MB is reclaimable — called on
  startup and after each attachment delete. A healthy small vault pays
  nothing. (Verified: 35MB → 695KB after the deletes.)

Rebuild: BuildAssets + F9 (UMainForm + PM.Database + PM.Handler.Attachments).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-04 00:02:25 +01:00
r-zakarya b367d031b5 style: define missing --bg-elev-3 / --accent-fg CSS variables
--bg-elev-3 was referenced (spinner ring, search-clear hover) but never
defined in either theme, so var(--bg-elev-3) with no fallback produced an
invalid declaration — that's why the busy spinner ring was invisible
until it was switched to --border. Define it as a real elevation step
above --bg-elev-2 in dark (#34343f) and light (#e3e3dd).

Also define --accent-fg (#fff) explicitly instead of relying on the
inline var(--accent-fg, #fff) fallback.

Audited used-vs-defined custom properties: the only remaining "undefined"
one is --strength, which is set at runtime by JS on the password-strength
bar (has a 0% fallback) — intentional, not a bug.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 18:01:57 +01:00
r-zakarya 48bb06c029 feat: rotation progress spinner + quick-unlock re-wrap on master-pw change
- doChangeMasterPassword shows the busy overlay while it re-encrypts the
  vault: "Re-encrypting vault…" → "Re-encrypting entries… N/total" →
  "Re-encrypting attachments… N/total", cleared in finally. A rotation on
  a big vault took tens of seconds with no feedback before.
- Quick-unlock now SURVIVES a master-pw change instead of being wiped.
  The blob stores the raw key (DPAPI-wrapped, no user secret), so it's
  re-wrapped in place with the new key/salt/iters/algo (state already
  holds the new values at that point). Cold-start then re-logs in with a
  verifier derived from the new key. Falls back to clearing if the
  re-wrap throws, so a stale old-key blob is never left behind.
- PIN blob still cleared (wrapped by PBKDF2(pin) — can't re-wrap without
  the PIN). A setTimeout(0) separates the quickunlock/store and pin/clear
  navigations so the back-to-back window.location.href assignments don't
  coalesce and drop the re-wrap.
- Fixed a `failed` counter declaration accidentally dropped from the
  attachment re-encryption loop while adding progress (ReferenceError at
  runtime; node --check wouldn't catch it).
- CLAUDE.md updated for the re-wrap vs clear distinction.

Rebuild: BuildAssets + F9 (JS only this commit; F9 to re-embed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 17:54:51 +01:00
r-zakarya 22bec64348 fix: chunk auto-backup writes + spinner on manual "Backup now"
Bridge.writeFile had the same URL-length trap saveFile did: a large
auto-backup base64'd into a single cmd://file/write URL blew past
WebView2's ~2MB navigation cap, so backups of big vaults failed
silently (or blanked the page) — for BOTH the manual "Backup now"
button and the silent scheduled run.

- writeFile now streams payloads over ~1MB in chunks (reusing the same
  file/chunk transport as saveFile), committed via a new
  file/write-commit. Delphi shares the decode+write logic through a new
  WriteDecodedFile helper and the existing FFileSaveChunks buffer.
- runAutoBackupNow(silent): the manual run shows the busy overlay
  ("Reading vault… N/total" → "Encrypting backup…" → "Writing file… N%")
  since a 20MB backup takes ~30s; the scheduled on-unlock run passes
  silent=true (no overlay, but still chunked so it no longer fails on
  large vaults).
- Fixed the "Backup now" click handler passing the click Event as the
  silent arg (truthy → would have suppressed the spinner and swallowed
  errors); wrapped in () => runAutoBackupNow().
- Pre-sync backup benefits from the chunked writeFile automatically.

Rebuild: BuildAssets + F9 (UMainForm.pas changed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 17:38:10 +01:00
r-zakarya c5fe26ce94 feat: chunked file save + export progress spinner + password-reveal + avatar size
Large file save (fixes black screen on big attachment download / export)
- Bridge.saveFile streams anything over ~1MB base64 in chunks through the
  cmd:// channel instead of stuffing the whole payload in one URL — a
  multi-MB base64 URL blew past WebView2's ~2MB navigation cap and blanked
  the document (black screen). Small payloads keep the single-shot path.
- Chunks are sent sequentially (each acked via Bridge.onFileChunkAck
  before the next) so repeated location.href assignments don't coalesce.
- Delphi accumulates chunks per reqId in a TStringBuilder
  (FFileSaveChunks), commits on file/save-commit, and shares the
  decode+dialog+write logic with the single-shot path via SaveDecodedFile.
- Chunk size 1MB → far fewer round-trips (a 20MB export dropped from ~67
  to ~27 hops).

Busy overlay + progress
- Global spinner overlay (showBusy/updateBusy/hideBusy). doExport shows it
  immediately on click — BEFORE the entry-decrypt + attachment-fetch loop
  that is the real cost — with a 0ms yield so it paints before the thread
  blocks (was appearing 3-5s late). Phases: "Reading vault… N/total" →
  "Encrypting export…" → "Preparing file… N%" (real chunk progress).
  Attachment download shows the same for files > 512KB.
- Spinner ring used an undefined --bg-elev-3 (invalid border → invisible);
  switched to --border. Fixed a second stale --bg-elev-3 use on the
  settings-search clear button hover.

Password reveal
- promptDialog gets an eye toggle in password mode, so every encrypted
  prompt (export, import, backup password, recovery code, sync password)
  can show/hide the typed value.

Avatar
- Top-right chip avatar enlarged 22px → 30px with the chip padding
  rebalanced.

Rebuild: BuildAssets + F9 (UMainForm.pas changed for the chunk handlers).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 16:56:47 +01:00
r-zakarya 3f8ecde571 feat(security): decouple the login verifier from the AES vault key
The zero-knowledge verifier sent to /login used to be the raw PBKDF2
output in hex — i.e. the exact bytes of the AES key that encrypts every
entry. Intercepting a /login body (loopback, but still) handed over the
vault key. This introduces a decoupled scheme where the transmitted
verifier is a one-way function of the key.

New auth-hash scheme
- users.hash_algo 'pbkdf2-sha256-v2': the client sends
  verifier = SHA256(keyHex + "pmserver/auth-verifier/v2") instead of
  keyHex. Stored form is still SHA256(verifier) (identical server wrap
  to 'pbkdf2-sha256'), so only the algo LABEL differs — it tells the
  client which verifier formula to use. Verification needs no new server
  branch (VerifierToStoredHash already SHA256-wraps any non-legacy
  verifier).
- The AES key (cryptoKey) stays hex(PBKDF2) for EVERY algo, so entries
  remain decryptable and switching schemes never re-encrypts data.

Adoption: new-registration + master-pw-change only
- Register and change-master-password write v2. Existing accounts keep
  their algo until they rotate — the login/reauth migration signal now
  fires only for LEGACY 'pbkdf2' (was: anything != CURRENT), so
  sha256/v2 accounts are never force-migrated (which would have
  downgraded v2 → sha256 via migrate-kdf).

Client (js/app.js): algo-aware everywhere
- verifierFromKeyHex(keyHex, algo) central helper; deriveKeyAndVerifier
  / computeVerifier take an algo arg. state.hashAlgo caches the account
  scheme, set from /login/challenge, register, change-master, the
  quick-unlock / PIN cold-start blobs, and the /recovery-key/redeem
  response. All ~12 verifier sites updated (login, register, reauth ×4,
  change-master current+new, migrate-kdf, quick-unlock + PIN cold-start,
  recovery-mode current verifier).

Safety invariant: unknown/empty hashAlgo → key hex → byte-identical to
the old behaviour, so every pre-decoupling account (and every existing
quick-unlock / PIN blob without the new field) keeps working unchanged.
Verified: existing account + pre-change quick-unlock still unlocks; a
master-pw change now writes 'pbkdf2-sha256-v2' in vault.db.

Server: recovery redeem returns hashAlgo; register + change-master store
the decoupled algo; login + reauth migration signal narrowed to legacy.

Also: BuildAssets.ps1 pipes $null into node --check so the JS syntax
gate can't block on stdin in the Delphi pre-build environment.

Addresses CODE_AUDIT.md section 1.1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 12:38:20 +01:00
r-zakarya 3076fec710 feat: quick-search fill modes + editable custom-field combobox + JS build gate
Quick search (Ctrl+Shift+Q fill mode)
- Enter / left-click → full autofill (username + Tab + password), like
  Ctrl+Shift+L.
- Shift+Enter / right-click → username only (new Delphi username-only
  SendInput path via field=user; ExecuteAutofill AUsernameOnly param).
- Ctrl+Enter / Ctrl+click → password only.
- Copy mode (tray / palette) unchanged: Enter/left = password,
  Shift+Enter/right = username.
- Clipboard fix: copy-then-minimise no longer wipes the just-copied
  password — MinimizeToTray takes an AClearClipboard flag (False on the
  quick-search copy path, driven by app/minimize?keepclip=1). The 30s
  auto-clear still guards it.
- Right-click on a result row suppresses the native/custom context menu
  (preventDefault + stopPropagation).

Editable custom-field combobox
- Option-backed custom fields (card brand, expiry year/month, etc.) now
  render a custom editable combobox instead of a locked <select>: an
  arrow drops a menu of ALL options (a native <datalist> filtered to the
  typed text, which confused users), while the input stays freely
  typeable for values not in the list. Storage shape unchanged.
- Outside-click closes the menu via the existing slideover mousedown
  handler; item mousedown + preventDefault so blur doesn't race the pick.

Build safety
- BuildAssets.ps1 runs `node --check` on every embedded .js before
  generating assets.res. A syntax error now aborts the asset build
  (exit 1, file + line logged) instead of shipping a dead bundle that
  only surfaces after a full Delphi rebuild. Node is optional: absent →
  warn and continue.

Docs
- CODE_AUDIT.md: full static-analysis report (security, latent bugs,
  maintainability, future features, prioritized action plan).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-03 08:13:25 +01:00
r-zakarya 7440d07793 feat: profile avatar + tombstone-restore fix + WebView2 nav race + sync summary
Profile picture / avatar
- users.avatar_b64 column (nullable, cosmetic, not encrypted) + GET/POST
  /avatar endpoints mirroring the settings handler pattern.
- Top-right chip + Settings→Account show a round avatar: custom picture
  if set, otherwise the username's initial on a deterministic
  hash-picked colour (stable across renders).
- Upload downscales + center-crops to a 128px JPEG via FileReader →
  data: URI (NOT blob:, which the CSP's `img-src 'self' data:` blocks)
  before POSTing. Remove button clears it.
- Carried in the encrypted JSON export; restored on import only when the
  current account has no picture (never clobbers a local one).

Tombstone restore-then-sync fix
- POST /entries and POST /entries/bulk-import now DELETE any tombstone
  matching an inserted uuid (same transaction) so a restored backup
  isn't re-killed on the next sync by its own stale tombstone.
- applyRemoteSnapshot arbitrates remote tombstones by timestamp: a
  tombstone is skipped when the local entry with that uuid is newer than
  deleted_at (resurrection wins). Ties / unparseable timestamps favour
  KEEP. loadEntries() up front so updated_at reflects the live rows.

WebView2 navigation race
- Black-window-on-cold-start fix: the 1.5s nav timer no longer consumes
  FPendingURL when WebView2 isn't initialised yet (it re-arms, bounded
  to ~10 retries). FBrowserInitialized flag set in OnInitialized; after
  the retry budget we Navigate best-effort rather than loop forever.

Sync UX
- Bidirectional toast: "pulled X new · Y updated · Z deleted · pushed N
  entries" so a 0/0/0 pull still shows the vault was uploaded.
- FolderPOST/PUT: pre-declare ftString on color/icon params (fixes the
  earlier [SQLite]-335 on NULL bind, already in play for CSV import).

Docs
- CLAUDE.md sync section documents tombstone purge-on-insert +
  resurrection arbitration.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-02 23:36:36 +01:00
r-zakarya 0a3372c151 feat: sync guards + import restore mode + inline progress + uncategorised polish
Sync engine
- Cross-account guard: refuse to merge a remote snapshot whose
  username differs from the currently-signed-in one (confirm dialog,
  Cancel by default) so a shared WebDAV URL / same sync password
  between accounts stops silently mixing vaults.
- Local tombstones veto: skip any remote entry whose uuid is already
  in the local entry_tombstones table — otherwise a perm-delete on
  this device was getting undone on the next pull.
- "deleted" counter fixed: report only tombstones that actually
  removed a live local entry this round, not the accumulated history
  the toast used to inflate ("73 deleted" for 48 real deletes).
- Push failure surfaces syncStatus reset + toast so state doesn't
  get stuck on a stale phase label.

Sync progress feedback
- Inline #syncStatus label next to the Sync button reports each
  phase: Local backup… → Pulling… → Merging… → Preparing… N/total
  (per-entry counter during the slow buildSyncSnapshot decrypt loop)
  → Encrypting… → Pushing…, then clears.
- Sync now / Test connection buttons are disabled while any run is
  in flight so double-clicks can't kick off a concurrent sync.

Import (JSON only — CSV out of scope)
- Uuid-aware dedup: split parsed rows into fresh (new uuid) vs
  overlaps (uuid already present locally).
- Overlaps prompt: confirm dialog offers Overwrite (restore/roll
  back) or Skip. Overwrite PUTs the file's payload over each match;
  Skip drops them and only imports fresh. Prevents the "re-import
  doubles everything" regression while still allowing restore.
- Bulk-import call is skipped entirely when there's nothing fresh to
  send (avoids a POST with an empty entries array).
- Template notes with empty body but populated custom_fields
  (credit-card, ssh-key, etc.) no longer skipped as "note body
  required" — kept as long as at least one custom field has a value.

Sidebar / uncategorised view
- "(no folder)" pseudo-entry stays visible whenever the vault has
  any real folder, so it always works as a drag target for
  uncategorising — and doesn't vanish mid-action when the user is
  currently viewing it.
- Header title reads "(no folder)" for state.view === 'folder:All'
  instead of the ambiguous "All".
- Dedicated empty-state copy for the uncategorised view.

Rebuild assets required.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-01 20:24:35 +01:00
r-zakarya fbfc24a4d5 feat: sticky slideover Save + uncategorised-view polish + regression plan
- Slideover Save action row is now sticky at the bottom of the scrolling
  body (background + top border) so it stays reachable on entries with
  many custom fields or attachments — was previously buried below the
  fold.
- Uncategorised view (state.view = 'folder:All'):
  - Header title reads "(no folder)" instead of the ambiguous "All".
  - Sidebar pseudo-entry stays visible whenever the vault has at least
    one real folder, so it can be used as a drag drop-target to move
    entries out of a folder even when its own count is 0.
  - Empty state gets its own copy ("No uncategorised entries" + hint
    to drop entries here to uncategorise) instead of the generic
    "Folder is empty".
- TEST_REGRESSION.md checked in — 16-section list scoped to what the
  recent Esc / drag / sync / import work touched, so post-commit
  regressions can be walked through methodically instead of poking
  the app ad-hoc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-01 13:04:45 +01:00
r-zakarya 4cb45ec63a fix: Esc priority across slideover / settings / modal handlers
Pressing Esc after editing a card silently failed: requestCloseSlideOver
opened the discard-confirm modal, then the global Esc-fallback handler
fired on the same keystroke and either closed the just-opened modal
(when Esc came on a clean slideover via the modal path) or, when
Settings was layered on top of a dirty slideover, opened the discard
prompt in the background while Settings closed.

Both panel-level Esc handlers now run in capture phase and
stopPropagation:
- Slideover handler stops only when it actually acts (slideover open,
  no modal up, Settings not on top) so a single Esc opens the discard
  confirm without the global fallback racing to close it.
- Settings handler stops when Settings is the active panel so the
  global fallback's requestCloseSlideOver branch can't fire underneath
  and pop a discard confirm on the slideover the user left dirty.

Priority order is now: open modal > Settings > slideover. First Esc on
"card dirty + Settings open" closes Settings; second Esc shows the
discard confirm. Clean slideover + Esc still closes directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 23:45:17 +01:00
r-zakarya bd14831449 feat: Bitwarden import bundle + settings search + quick-search hotkey + UX fixes
- Bitwarden CSV import: folders auto-created server-side; notes column on
  login rows surfaces as a "Notes" custom field instead of polluting tags;
  type=card / type=identity rows now mapped to kind=note with the
  credit-card / identity template + card_* / identity_* columns
  pulled into custom_fields; `fields` column parsed (Bitwarden's
  "label: value\nlabel: value" lines + our own JSON shape).
- Settings panel search: live filter at top of the panel, matches each
  .setting-row individually, hides whole section when no row matches,
  shows a "No matches" banner. Esc clears query (without closing
  Settings); Esc with empty query closes the panel.
- Quick-search hotkey customizable: SetQuickSearchHotkey added to
  PM.Bridge; cmd://autofill/hotkeys extended with qs_mods/qs_vk
  (independent of the autofill enabled flag — quick-search stays
  armed even when autofill is off); state.quickSearchHotkey synced
  via settings_json; new "Quick search picker" row in Settings.
- FireDAC SQLite folder POST/PUT: pre-declare ftString on color/icon
  params so .Clear (NULL) doesn't trip "[FireDAC][Phys][SQLite]-335
  type unknown" at Prepare — was crashing the CSV-import folder
  auto-creation path.
- Edge form-data autocomplete suppressed on slideover inputs (title,
  site, username, password, TOTP, note body, custom fields):
  autocomplete=off (new-password on secrets) + spellcheck=false. Fixes
  the "Informations enregistrées" dropdown popping over data after a
  field was edited.
- closeSlideOver blurs any focused descendant before removing .is-open
  so an invisible focused field can't react to arrow-down / backspace
  after dismissal.
- Slideover Esc handler upgraded to capture phase so it fires before
  the input's own keydown or browser-level Esc swallow on the active
  autocomplete popup.
- Settings panel Esc closes the panel when search input is empty;
  search keeps the keystroke when it has a query to clear.
- Discard-fantome on note open: customFields working copy and
  originalCustomJson now share the SAME normalized array — comparing
  raw plainCustom against the .map()'d working copy made notes look
  dirty on open.
- Delete / Backspace global shortcut: batch-trash on normal views,
  batch perm-delete on trash view, gated on selection + no input
  focused + no modal up.
- Toggle thumb vertical centering via top:50% + translateY(-50%);
  state checked uses translate(16px, -50%) to keep the centring.
- Batch bar disappears after per-card restore/perm-delete/trash:
  state.checked.delete(id) before render for the relevant flows;
  state.checked.clear() before render in emptyTrash and the new
  moveEntriesToFolder helper.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 23:25:50 +01:00
r-zakarya 6869b7c692 feat: WebDAV sync + batch DnD + clean shutdown + center-modal UX bundle
- Sync (WebDAV, auto-merge): UUID + tombstones foundations (server +
  JS), THTTPClient bridge cmds (get/put/test), runSyncNow engine with
  pull/merge/push flow, Settings UI, pre-sync backup option. Test
  connection now treats 404 as OK (snapshot not yet created) and 401/
  403 as auth failure with dedicated toast.
- Batch drag-drop: cards + table rows carry checked-set ids (CSV) when
  dragged from an active selection; folder + trash drop handlers parse
  and apply in batch via new moveEntriesToFolder helper that preserves
  TOTP / custom_fields / kind in the full PUT payload.
- Clean shutdown: WM_QUERYENDSESSION / WM_ENDSESSION captured in the
  bridge message-only window; FormCloseQuery bypasses the tray-minimize
  intercept on system shutdown / restart / logoff so FireDAC closes the
  SQLite WAL cleanly instead of leaving -shm / -wal residue after a
  force-kill.
- Center-mode modal: blur+dim backdrop via body::before pseudo-element
  in editor-position=center, swallows clicks below the panel so the
  existing outside-click handlers reliably dismiss the slideover /
  settings panel.
- Batch bar state fixes: state.checked cleared before render in
  moveEntriesToFolder, emptyTrash, and per-card restoreEntry /
  permanentDelete / deleteEntry so the action bar disappears once the
  selection is fully processed.
- Save-then-discard duplicate fix: soState reset to null before
  openSlideOver re-opens the freshly saved entry, otherwise the dirty
  check fired on the soState.id=null → newId switch and a Cancel left
  the form in new-entry mode (second Save → POST duplicate).
- TEST_SYNC.md: end-to-end checklist for validating the WebDAV sync
  with 2 real instances.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-30 00:32:12 +01:00
r-zakarya b00da43ab0 feat: PIN unlock + table column picker + edit-position chooser + UX
- PIN unlock: device-local 4-12 digit shortcut, DPAPI-wrapped vault
  key. Three modes (state.unlockMode): pw / pin / pw+pin. PIN
  derives a wrap key via PBKDF2(pin, salt, 100k) and unwraps the
  stored vault key (mirrors the Quick Unlock blob shape).
  Anti-brute-force: 5 wrong attempts wipes the blob. Setup gated by
  master-pw reauth so an unattended unlocked laptop can't be
  backdoored. Master pw rotation clears the PIN blob (key drift).
  loadServerSettings post-sync demotes pin/both -> pw when the local
  blob is missing, so a wiped device re-syncs the correct mode up.
  New unit PM.PinUnlock.pas + cmd://pin/{store,get,clear,status}.
- Table column picker: ⚙ in topbar (table view only), checkbox menu
  for Site/Username/Folder/Updated. Site also drives showSiteOnCards
  so the existing "Show site / URL" toggle in Settings stays in
  sync. NAME column auto-widths (180px min, content max, +32px
  right padding) so column hugs the next one without truncating.
- Editor position chooser (Appearance setting): Slide-over right /
  left / Centered modal. Scoped to #slideover + #settingsPanel so
  the click-outside / pointer-events logic doesn't accidentally
  trap the modal-style empty viewport.
- Confirm before discarding unsaved edits: state.confirmOnUnsaved
  setting (default ON), prompts on X / Esc / click-outside / switch-
  to-other-entry. Also gates Lock vault / Sign out actions when the
  editor is dirty; auto-lock and system-lock paths bypass to avoid
  blocking on an unattended machine.
- Open-in-browser button added to the actions cell of the table
  view (was card-only).
- Entry templates pass folder customization + template id through
  duplicate / export / import / auto-backup roundtrips.
- Folder color + icon now persisted across export/import: payload.
  folders carries name/color/icon; import creates missing folders
  additively (existing local customisation kept).
- Bulk move-to-folder, batch add-tag, single add-tag now re-ship
  the full entry payload so partial PUTs don't silently wipe
  TOTP / custom_fields / kind / template.
- FireDAC: switched ftString -> ftMemo for icon_b64 / custom_fields
  / TOTP / template params and replaced .AsString with .Value so a
  large (~200 KB) DeepSeek favicon no longer gets truncated at the
  default ANSI 4000-char cap.
- Unicode filenames: attachment INSERT now uses ftWideString +
  .AsWideString so non-ANSI filenames round-trip instead of being
  mangled to "?".
- HandleSetEntryIcon cap raised 256 KB -> 512 KB chars to accept
  base64 data URIs produced by max-raw favicon fetches.
- promptDialog + askReauth support inline `error` line + retry-
  with-count loops on doExport reauth and auto-backup password
  setup (5 attempts cap before bailing).
- Recently used moved from Tools to Vault section in the sidebar.
- Auth screen passkey button hidden (Delphi backend stubs WebAuthn).
- Sensitive cmd://favicon/refresh-style buttons in Settings now
  stopPropagation so the document-level "close panel" handler
  doesn't dismiss Settings mid-async during DOM reparenting.
- TEST_PLAN.md: +PIN unlock section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-29 04:41:39 +01:00
r-zakarya e23a78dda7 feat: entry templates + tag autocomplete + slideover push + robustness bundle
- Entry templates: new vault_entries.template column drives a typed
  sub-kind ('credit-card', 'ssh-key', 'server', 'recovery-codes'). Card
  + table label off the template, badge reads "credit card" instead of
  "note". Templates seed kind=note (no site/password required), use
  custom_fields with optional dropdown options (brand, month/year,
  protocol). Round-tripped across export/import/duplicate/master-pw
  rotation, preserved by partial PUTs via a HasTemplate flag.
- Custom fields: support per-field `options[]` rendering as <select>
  (card brand, expiry MM/YYYY, SSH/server protocol).
- Tags: existing-tag autocomplete dropdown under the chip input,
  filtered against what's already selected.
- Search history: per-query X for individual delete + 1s debounced
  commit (no Enter required).
- Slideover: clicking outside closes again (drag-selection respected
  via mousedown origin tracker), Esc closes, X closes. App shell is
  pushed left by 420px when the panel is open so the table / pagination
  / sort / search stay visible and interactive.
- Export/import: JSON now round-trips custom_fields, attachments
  (decrypted to base64, re-encrypted under current key on restore),
  icon_b64, and template. CSV warning lists what's not included.
- Auto-backup: same payload shape as user-driven export.
- Notes: import (JSON + CSV) accepts kind=note with empty site,
  preserves title/template/custom_fields. CSV parser detects kind/
  template columns.
- Bulk-import response returns `ids[]` parallel to input so the
  client can map back to new entry IDs (drives attachment restore).
- Move-to-folder bugs fixed: moveEntryToFolder, batchMoveToFolder,
  addTag, batchAddTag were all silently wiping TOTP / custom_fields
  / kind / template via partial PUT. Now re-ship full payload.
- Master-pw rotation: server mints a fresh session token + csrf so
  the very next request after rotation no longer ESessionRejects.
  Client adopts the new pair. Attachments are re-encrypted client-side
  during rotation (GET old → decrypt with old key → encrypt with new
  → PUT). New endpoints: GET /attachments/all, PUT /attachments/:id.
- Duplicate: carries icon_b64 + template + attachments to the copy.
- HandleCreateEntry: accepts icon_b64.
- FireDAC param fix: all blob/icon/custom_fields params use ftMemo +
  .Value assignment so SQLite TEXT no longer truncates to 4000 chars
  (deepseek's 200+ KB favicon was being wiped on lock/unlock).
- HandleSetEntryIcon cap: 262144 → 524288 chars (base64 of a 256 KB
  raw fetch overflows the old cap, fails silently in saveEntryIcon).
- Native save dialog: surfaces server errors instead of swallowing.
- Modals: reauth (export) + backup-password prompt support inline
  error display, retry up to 5 attempts, then hard-stop.
- Keyboard cursor (j/k): bootstraps to current page, auto-paginates
  when the cursor crosses a page boundary, Enter opens slideover.
- Slideover focuses Title on edit-open so j/k → Enter → type Just
  Works.
- TOTP tool: Esc closes the modal.
- App version + launch mode (auto/manual): exposed via bridge,
  surfaced in Settings → Account. Autostart launches suppress the
  first-time tray balloon.
- Passkey button hidden (Delphi backend stubs WebAuthn at 501).
- TEST_PLAN.md captured for regression coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 21:20:07 +01:00
Zaki fa7ea191be feat: native save + auto-backup + folder customization + attachments + UX bundle
- File: native Save As dialog via Bridge.saveFile (replaces WebView2
  browser download popup) for encrypted JSON + CSV exports.
- Auto-backup: silent periodic encrypted JSON to a chosen folder,
  user-set interval + retention, separate DPAPI-stored password, runs
  5s after unlock if due. New file/* bridge cmds (folder/pick,
  file/write, file/listMatch, file/delete).
- Folders: per-folder color + icon (8-swatch palette, 8 icon presets),
  drag-reorder via HTML5 DnD with insert-line indicators, edit pencil
  on hover. New POST /folders/reorder + PUT /folders/{name}. Folder
  chip on cards inherits custom icon + color.
- Recently used: vault_entries.accessed_at + POST /entries/{id}/touch
  (debounced 2s), sidebar Tools entry showing top-10 by accessed_at.
- Encrypted attachments: per-entry file storage (5MB cap), AES-GCM
  with vault key, native Save As download, paperclip upload in
  slideover. New entry_attachments table + PM.Handler.Attachments.
- Password expiry: vault_entries.password_changed_at (conditional bump
  via SQL CASE only when ciphertext differs), passwordExpiryDays
  setting, "Aged" badge on cards + matching Filters chip.
- Recovery: Print button on generated code modal (A4 printable sheet
  via @media print, code in 32px monospace + instructions).
- Audit log viewer (sidebar Tools, GET /audit with pagination cursor).
- Plaintext CSV export + Filters dropdown with 9 predicates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 23:12:06 +01:00
Zaki 63fac5b3b7 feat: secure notes + password history + custom fields + quick-win bundle
Big feature trio
- Secure notes (kind='login'|'note') reusing the encrypted_password+iv
  pipeline for the body. New sidebar entry, slideover variant (title +
  multiline body), distinct card / table-view rendering, badge in name
  column, copy-content button replacing the password copy on note rows.
- Password history: entries_password_history table keeps up to 20 prior
  ciphertexts per entry. HandleUpdateEntry pushes the pre-update
  encrypted_password into history ONLY when it actually differs from
  the incoming one (JS reuses originalEncrypted bit-for-bit when the
  plaintext is unchanged — avoids spamming history on title/folder edits).
  GET /entries/{id}/history endpoint. Slideover modal lists versions
  with mask/reveal/copy/revert. Master-pw rotation wipes history (old
  ciphertext can't be decrypted with the new key).
- Custom fields: per-entry encrypted JSON array of {label, value,
  is_secret}. Same crypto pipeline as the password. Slideover row UI
  with label/value inputs, secret toggle (eye), copy, delete. Re-
  encryption flows through bulk-import, change-master-password, and
  duplicate.

Quick wins
- Cheatsheet overlay (press '?' or topbar button or Ctrl+K). Lists all
  hotkeys + global / tray / card actions. SVG icons inline so the
  cheatsheet matches the actual app glyphs (no emoji mismatch).
- Open URL button on entry cards: ShellExecute via cmd://app/open-url,
  http(s) only, validates entry.site looks like a real hostname.
- Trash auto-purge: setting "Empty trash after N days" (never/7/30/90).
  DELETE /entries/trash/old?days=N called at every unlock.

Favicon strategy
- Subdomains (chat.deepseek.com, app.X.com…) now try the SLD first
  (deepseek.com.ico) before the full host. DDG often returns a generic
  placeholder for subdomains that passes the byte threshold; the SLD-first
  switch surfaces the real brand icon.
- Cap bumped 64 KB → 256 KB on all three sides (Delphi fetch, server
  endpoint, JS upload). DDG sometimes serves the full-res asset.

UX polish
- Click-outside-slideover: stopPropagation everywhere it bites. Custom
  fields buttons (add / delete / secret toggle / copy / eye) all stop
  the click bubble so the document-level "close on outside click" handler
  doesn't fire when rerender() detaches the target from the DOM.
- Native search-cancel button restyled: cyan accent X via mask-image,
  cursor: pointer, breathing room before the Ctrl+K kbd chip.
- Password history modal: scrollable body, multiline wrapped passwords,
  hover border highlight.
- Cheatsheet panel widened (560 → 720 px) so the descriptions no longer
  ellipsis-clip.
- "+ New" topbar splits into a small dropdown: New login / New note.
- Notes show a "note" badge in table-view name column, italic
  "Encrypted note" placeholder in the username column.

Internals
- duplicateEntry copies kind + custom_fields too (one-line forgotten
  earlier).
- entries_password_history dropped on master-pw rotation — the old
  ciphertexts are unrecoverable with the new key.
- bulk-import re-encryption path includes custom_fields.

CLAUDE.md
- "Entry payload — call sites à toucher ensemble" lists the 6 spots
  to update when adding a new (en)crypted field. Notes the historical
  miss of kind in duplicateEntry and custom_fields in the rotation +
  duplicate.

Repo hygiene
- .gitattributes forces CRLF on Delphi sources (RAD Studio refuses LF).
  text=auto for web frontend / docs, binary for .res / .exe / images.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-14 20:17:19 +01:00
Zaki 39406d712e feat: Ctrl+Shift+Q quick-search-fill + Edge browser directive
Ctrl+Shift+Q quick-search + autofill
- New global hotkey: capture the foreground HWND, restore the window
  if hidden, pop the quick-search modal in "fill mode". On pick, the
  password is SendInput'd into the saved HWND — no clipboard touch.
- hide_after flag added to cmd://autofill/execute: when set (tray-mode
  hotkey), Delphi MinimizeToTray's *after* SendInput completes. Hiding
  before SendInput would trip Win10/11 anti-focus-stealing rules and
  block focus handoff to the target.
- Quick-search modal hint text adapts to fill vs copy mode.
- Esc / close in fill mode sends cmd://autofill/cancel so a stale
  HWND doesn't get reused by an unrelated Ctrl+Shift+L later.

Compile-time browser engine switch
- {.$DEFINE USE_EDGE_BROWSER} in UMainForm.pas selects between
  TTMSFNCWebBrowser (default, cross-platform abstraction) and
  TTMSFNCEdgeWebBrowser (Windows-only WebView2 wrapper). Both
  inherit from TTMSFNCCustomWebBrowser so the bridge cmd:// glue is
  unchanged; the field type is a conditional alias TWebBrowserClass.
- WebBrowser is created dynamically in FormCreate so neither variant
  needs a second .fmx. Events are wired BEFORE Parent assignment so
  OnInitialized doesn't race the WebView2 async init on fast/pre-warmed
  Edge installs (was silently missing the disable-context-menu /
  disable-accelerator-keys calls).
- Native context menu disabled by assigning an empty PopupMenu1 (works
  for both backends, unlike OnGetContextMenu which is publish-gated
  via {$IFNDEF FNCLIB} on TTMSFNCWebBrowser).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 14:31:12 +01:00
Zaki f047fba9a3 feat: tray quick-search + privacy hardening + race fixes
Quick search from tray
- New "Quick search…" entry in the tray context menu (between Open
  and Lock vault).
- Compact modal with live-filtered top-8 entries, arrow keys / Enter
  to copy the password (Shift+Enter copies the username instead),
  Esc to dismiss. Each row shows the favicon when cached.
- Locked vault → focus the master password input instead of opening
  the modal (same pattern as the locked-autofill-hotkey path).
- Window-state restore: Delphi remembers whether the window was
  hidden before the menu was opened and tells JS via the
  Bridge.openQuickSearch(wasHidden) arg. After the copy (or cancel)
  we hide back to the tray so the previously-foreground app comes
  back and Ctrl+V drops the password in.

Tray notifications toggle
- New Settings → Security "Show tray notifications" toggle. Gates
  Shell_NotifyIcon NIF_INFO balloons (currently only the "still
  running in the tray" first-time popup). Default ON, synced via
  settings_json so it follows the user across devices.
- PM.Bridge.ShowNotifications exposed as a public property; JS
  pushes the value on every settings sync.

Privacy: WebView2 phone-home killed
- WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS set in the unit
  initialization section (before the TMS WebBrowser instantiates
  its CoreWebView2Environment). Disables: background networking,
  sync, component updates, breakpad/crashpad, domain reliability,
  client-side phishing detection, experiments, UMA upload,
  MediaRouter, OptimizationHints, SafeBrowsing enhanced, autofill
  server, privacy sandbox APIs. Verified via Resource Monitor: only
  127.0.0.1 connections remain (plus DDG when favicons are on).

Fixes
- Blank-window-on-launch race: the 1.5 s navigation timer assumes
  WebView2 finishes init in time, but on slow machines Edge
  Chromium needs 2-3 s and the Navigate() call is silently
  dropped. WebBrowserInitialized now also navigates if a URL is
  still pending — first to run wins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 19:35:37 +01:00
Zaki 2ef636ce30 feat: unified entry slideover + custom icons + UX fixes
Unified create/edit slideover
- openSlideOver(id) now accepts null for new entries. Same UI
  (icon, name, site, user, password, TOTP, folder, tags) for both
  create and edit. Drops the separate entry modal — no more "save
  first, then add TOTP" two-step.
- "+ New" button, Ctrl+K → New entry, and Ctrl+Shift+A all route
  through the slideover. Ctrl+Shift+A pre-fills the title with the
  foreground window's name.
- Save button visible from the start in new mode (no dirty wait).
- Title shows mode unambiguously: cyan "+ New entry" vs
  "Edit · <name>".

Custom icon upload (soIconField)
- 56×56 preview at the top of every slideover + Upload icon /
  Remove buttons. Same POST /entries/{id}/icon endpoint as the
  auto-fetch path. Validates type / size (64 KB cap matching server).
- Solves the case where DDG doesn't index a domain (self-hosted
  apps, private sites): the user pastes any image and it sticks.

Favicon: privacy-first, DDG only
- Removed the direct-fetch fallback steps (3-5). Privacy stance:
  zero DNS leak outside icons.duckduckgo.com. Domains DDG doesn't
  cover stay icon-less until the user uploads a custom one.
- PM.Favicon.FetchFaviconDataUri takes an optional TFaviconLog
  callback so UMainForm can stream per-step trace into LogLine for
  diagnostics.

Fixes
- Slideover z-index 30 → 50. The topbar's backdrop-filter creates a
  stacking context at z-index 40 which was clipping the slideover
  header (title + close button hidden behind topbar).
- RestoreFromTray no longer un-maximises a maximised window when
  called outside a tray-restore context (Ctrl+Shift+A, Ctrl+Shift+L
  picker, app/focus cmd). SW_RESTORE on a maximised window reverts
  to normal — now we only SW_RESTORE if IsIconic.
- "Show all"/"Show less" per-category state survives renderGrid
  re-renders (healthExpanded map).
- "+ New" and dashboard "Fix" buttons stopPropagation so the
  document-level click-outside handler doesn't close the slideover
  they just opened.
- soDirtyCheck keeps Save visible while in new mode regardless of
  diff.
- openSlideover → openSlideOver typo fix across all call sites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 00:15:20 +01:00
Zaki ad5fb21a18 feat: website favicons + vault health dashboard
Favicons
- PM.Favicon (new): THTTPClient/WinHTTP proxy to icons.duckduckgo.com.
  Native Windows TLS — no OpenSSL DLLs to ship (Indy would fail
  silently without them). 5 s timeout, max 3 redirects, 64 KB cap,
  magic-byte MIME sniffing.
- DB: vault_entries.icon_b64 TEXT (idempotent migration).
- Endpoints: POST /entries/{id}/icon stores a cached data URI without
  forcing a full PUT (which would re-encrypt the password). DELETE
  /entries/icons/all purges the cache.
- Bridge cmd://favicon/fetch?host=X&reqId=Y runs in an anonymous thread
  so the up-to-5 s HTTP GET doesn't block the main thread; result
  shipped back via Bridge.onFaviconResult(reqId, host, dataUri).
- Hostname validated on both sides (JS faviconHost + Delphi
  NormalizeHost) so brand labels like "Gitea" never leak upstream.
- Settings: opt-in "Fetch website icons" toggle (synced), three explicit
  actions (Fetch missing / Re-fetch all / Clear cache) that bypass the
  toggle — manual user actions always work.
- Entry card avatar shows <img> when cached, falls back to initials.
  onerror handler recovers silently from a corrupt data URI.

Vault health
- New sidebar Tools → "Vault health" view. Four category cards:
  Weak (strength < 50), Reused (same plaintext on ≥ 2 entries), Old
  (updated_at > 365d), Pwned (HIBP cache).
- Score 0-100 with colour band (Good/Fair/At risk/Critical).
- One-shot computation cached per session (healthCache), invalidated
  on lockVault, entry save, and the explicit "Recompute" button.
- "Fix" button on each item opens the slideover for the affected
  entry, unmasks the password, focuses it, and pulses the dice button
  — full context preserved, user decides how to fix.
- Click handler stopPropagation prevents the document-level
  "click outside slideover" listener from closing the panel that
  we just opened in the same click event.

Fixes
- openSlideover typo (lowercase O) → openSlideOver across all call
  sites. Was silently breaking the Authenticator card click and the
  Vault health Fix button.
- W1050 WideChar warning in PM.Favicon — replaced set-membership
  with explicit Ord-style range comparisons.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 00:05:54 +01:00
Zaki 33e4b4b614 feat(autostart): "Start with Windows" toggle
- PM.AutoStart wraps HKCU\Software\Microsoft\Windows\CurrentVersion\Run.
  Value "PMServer" = "<exe>" -tray. Per-user, no admin required, shows
  up in Task Manager → Startup so the user can override from there.

- UMainForm honours the -tray CLI flag (set by the registry entry):
  after the server starts, MinimizeToTray via TThread.ForceQueue so the
  app comes up directly in the tray with no visible window flash.

- Bridge cmd://autostart/{get,set} + Bridge.getAutoStart() /
  setAutoStart() / onAutoStartStatus(). Settings exposes a toggle in
  the Security section, visible only when Bridge.active (the PHP
  frontend can't touch the registry).

- Toggle reads "on" only when the registered command matches the
  current exe path, so a stale entry from a moved exe lets the user
  re-enable to refresh.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 21:39:37 +01:00
Zaki 40b3154a34 feat: MFA tools, single-instance, tray polish, prefs persistence
Session highlights:

- feat(prefs): DPAPI-backed key/value store (PM.UserPrefs) — fixes
  rememberedUsername being lost across reboots due to the random
  ephemeral HTTP port changing the localStorage origin every launch.
  Bridge cmd://prefs/{get,set} round-trips through Delphi.

- feat(tray): icon visible from startup (NIM_ADD at constructor, not
  at first minimize). Tray context menu themed via uxtheme!135
  SetPreferredAppMode so it follows the app's dark/light setting.

- feat(single-instance): named mutex + RegisterWindowMessage broadcast.
  Second launch posts WM_PMSHOW to HWND_BROADCAST and exits; the
  running bridge restores the window from tray. Mutex lives in Local\
  namespace so distinct Windows users can still each run one.

- feat(mfa): Authenticator sidebar view (live TOTP codes for every
  entry with a secret) + standalone TOTP generator modal (paste
  base32 / otpauth:// URI, or generate a random 20-byte secret).

- feat(sidebar): Folders / Tags / Tools sections collapsible with
  chevron toggle. Badge counts stay visible when collapsed. State
  persisted in settings_json (synced across devices).

- feat(autofill): hotkey when vault is locked now restores the app
  and focuses the master password input instead of no-op'ing
  silently. Cleaner UX for the common "I hit Ctrl+Shift+L but the
  vault was locked" path.

- feat(quick-unlock): when enabled, skip lockVault on Windows lock /
  sleep. Rationale: the DPAPI blob already gates access via the
  Windows account, so re-locking on top of the OS lock is redundant.
  Idle auto-lock still fires (separate opt-in).

- fix(quick-unlock): re-sync state.quickUnlockEnabled from DPAPI
  source-of-truth at boot, instead of trusting (now-volatile)
  localStorage.

- docs: CLAUDE.md updated with all new modules, bridge commands,
  and the port-ephemeral pitfall.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 21:31:39 +01:00
Zaki 506aee7e6f feat: Delphi backend + JS↔Delphi bridge (clipboard, tray, auto-lock)
Introduces the Delphi 12 FMX backend (PMServer) that hosts the embedded
WebView2 vault on 127.0.0.1, and a native bridge between JS and Delphi
that wires three privacy-focused features:

1. Secure clipboard
   Copying a password registers the Win32 "ExcludeClipboardContentFromMonitorProcessing"
   format alongside CF_UNICODETEXT, so Win+V clipboard history never sees
   the value. Auto-clears after 30s via TTimer. Bridge.copySecure() in
   app.js routes all password/username/secret copy paths through the
   native layer when running inside the Delphi WebView2 (falls back to
   navigator.clipboard for the PHP standalone).

2. Tray icon (X-to-tray when server running)
   Closing the dev panel hides both the form HWND and the TFMAppClass
   per-process proxy window that owns the FMX taskbar entry — the form's
   HWND alone is not the taskbar-visible one in FMX (took some iteration
   to discover). Tray menu: Open, Lock vault, Quit. Clipboard is force-
   cleared on minimize as extra safety. First-time minimize fires a
   balloon notification so the user knows the app is still running.

3. Auto-lock on Windows session lock (Win+L)
   wtsapi32.dll!WTSRegisterSessionNotification on a dedicated message-only
   window. On WM_WTSSESSION_CHANGE / WTS_SESSION_LOCK, the bridge calls
   ExecuteJavaScript('lockVault()'). Same path used by the tray "Lock vault"
   menu item.

Bridge architecture:
 - JS → Delphi via cmd:// URLs intercepted in OnBeforeNavigate
   (pattern lifted from DeskInsight Monaco). Currently exposes
   cmd://clipboard/copy?text=...&clear=... and cmd://clipboard/clear.
 - Delphi → JS via TTMSFNCWebBrowser.ExecuteJavaScript with guarded
   calls (typeof check) so the bridge degrades cleanly if app.js isn't
   loaded yet.

Files:
 - Source/PM.Bridge.pas (new) — TSecureClipboard + TPMBridge
 - UMainForm.pas/.fmx — bridge wiring, FormCloseQuery intercept, tray
   callbacks (BridgeTrayRestore / BridgeLockRequest / BridgeQuit)
 - js/app.js — Bridge object, 5 navigator.clipboard sites migrated to
   Bridge.copySecure with PHP-compatible fallback, Bridge.onTrayRestore
   handler that resets the auto-lock timer

.gitignore extended with Delphi build artifacts (*.dcu, Win32/, Win64/,
__history/, __recovery/, *.identcache, *.dsk, *.local, etc.) so source
checkouts stay clean.
2026-05-22 23:47:57 +01:00