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>
This commit is contained in:
@@ -39,7 +39,8 @@ uses
|
||||
|
||||
const
|
||||
ICON_URL_TEMPLATE = 'https://icons.duckduckgo.com/ip3/%s.ico';
|
||||
MAX_ICON_BYTES = 65536; // 64 KB cap (matches handler's SetEntryIcon limit)
|
||||
MAX_ICON_BYTES = 262144; // 256 KB cap (DDG sometimes serves full-res
|
||||
// assets; matches handler + JS upload limits)
|
||||
HTTP_TIMEOUT_MS = 5000;
|
||||
// DDG returns a generic placeholder for unknown domains. Bigger threshold
|
||||
// than 100 to avoid treating its blank globe glyph as a real icon.
|
||||
@@ -153,48 +154,51 @@ begin
|
||||
Exit;
|
||||
end;
|
||||
|
||||
// Strategy: prefer DDG (privacy-centralising) but fall back to the
|
||||
// site's own /favicon.ico for domains DDG doesn't index (self-hosted
|
||||
// tools, niche services, fresh subdomains, etc.). The user already
|
||||
// opted into "fetch icons" so the DNS leak to one extra host they
|
||||
// already visit is an acceptable trade-off for actually getting an icon.
|
||||
// Strategy: prefer the SLD (brand domain) when the host has a subdomain,
|
||||
// because DDG often returns a generic placeholder for chat.X.com / app.X.com
|
||||
// / etc. (passes our byte threshold but looks wrong) while having the real
|
||||
// brand icon under X.com. For bare 2-label hosts we go straight to step 2.
|
||||
|
||||
LSld := ExtractSLD(LHost);
|
||||
LOk := False;
|
||||
|
||||
// 1) DDG full host.
|
||||
LUrl := Format(ICON_URL_TEMPLATE, [LHost]);
|
||||
if FetchOneIcon(LUrl, LBytes) then
|
||||
// 1) DDG SLD first when host has a subdomain (e.g. chat.deepseek.com →
|
||||
// try deepseek.com.ico first). Skipped for bare hosts.
|
||||
if LSld <> '' then
|
||||
begin
|
||||
if Length(LBytes) >= MIN_REAL_ICON_BYTES then
|
||||
LUrl := Format(ICON_URL_TEMPLATE, [LSld]);
|
||||
if FetchOneIcon(LUrl, LBytes) then
|
||||
begin
|
||||
LOk := True;
|
||||
Trace(Format('OK step1 DDG host: %s (%d bytes)', [LUrl, Length(LBytes)]));
|
||||
if Length(LBytes) >= MIN_REAL_ICON_BYTES then
|
||||
begin
|
||||
LOk := True;
|
||||
Trace(Format('OK step1 DDG sld: %s (%d bytes)', [LUrl, Length(LBytes)]));
|
||||
end
|
||||
else
|
||||
Trace(Format('skip step1 DDG sld: %s only %d bytes', [LUrl, Length(LBytes)]));
|
||||
end
|
||||
else
|
||||
Trace(Format('skip step1 DDG host: %s only %d bytes (< %d)',
|
||||
[LUrl, Length(LBytes), MIN_REAL_ICON_BYTES]));
|
||||
end
|
||||
else
|
||||
Trace('fail step1 DDG host: ' + LUrl);
|
||||
Trace('fail step1 DDG sld: ' + LUrl);
|
||||
end;
|
||||
|
||||
// 2) DDG SLD (e.g. "deepseek.com" when "chat.deepseek.com" 404s).
|
||||
if (not LOk) and (LSld <> '') then
|
||||
// 2) DDG full host as fallback (covers brands whose subdomain has its own
|
||||
// distinct icon, OR plain hosts like github.com that have no SLD step).
|
||||
if not LOk then
|
||||
begin
|
||||
var LTry: TBytes;
|
||||
LUrl := Format(ICON_URL_TEMPLATE, [LSld]);
|
||||
LUrl := Format(ICON_URL_TEMPLATE, [LHost]);
|
||||
if FetchOneIcon(LUrl, LTry) then
|
||||
begin
|
||||
if Length(LTry) >= MIN_REAL_ICON_BYTES then
|
||||
begin
|
||||
LBytes := LTry; LOk := True;
|
||||
Trace(Format('OK step2 DDG sld: %s (%d bytes)', [LUrl, Length(LTry)]));
|
||||
Trace(Format('OK step2 DDG host: %s (%d bytes)', [LUrl, Length(LTry)]));
|
||||
end
|
||||
else
|
||||
Trace(Format('skip step2 DDG sld: %s only %d bytes', [LUrl, Length(LTry)]));
|
||||
Trace(Format('skip step2 DDG host: %s only %d bytes', [LUrl, Length(LTry)]));
|
||||
end
|
||||
else
|
||||
Trace('fail step2 DDG sld: ' + LUrl);
|
||||
Trace('fail step2 DDG host: ' + LUrl);
|
||||
end;
|
||||
|
||||
if (not LOk) or (Length(LBytes) = 0) then
|
||||
|
||||
Reference in New Issue
Block a user