\a s'etait change en BEL dans E:\a pmanager. Le commit precedent
affirmait qu'il ne restait plus rien sans l'avoir mesure apres coup —
c'est ce qui l'a laisse passer. Mesure cette fois : zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
\t et \r s'etaient changes en tabulation et en retour chariot dans les
chemins du piege TMS. Verifie apres coup : plus aucun caractere de
controle parasite dans le fichier.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
L'ecran d'activation ne consultait qu'un branchement : le rapiecer
ouvrait tout, l'API ne demandant rien. Chaque route de PM.Router passe
maintenant par PM.LicenceGate.ApiLicenceOk avant son handler.
Aucune fonction de decision partagee avec PM.Licence — sinon un seul
rapiecage ouvrirait les deux couches. La verification est refaite de
bout en bout, appels CNG redeclares compris ; seules la cle publique et
le nom de la pref sont partages, parce que ce sont des donnees.
Branchee dans DispatchRequest : les fichiers de l'ecran d'activation
passent par TryServeEmbedded, non garde, sinon l'ecran ne se chargerait
plus. Un oui est retenu pour le processus, un non jamais.
Le risque etait qu'elle refuse un client licencie. Auto-test sur un
vecteur lie a une empreinte fixe, sens accepte en premier ; et accord
verifie en conditions reelles sur ce poste : IsLicensed = TRUE,
ApiLicenceOk = TRUE.
Documente aussi le piege du jour, sans lien avec ce code : des .dcu TMS
x64 deposes a cote des sources cassaient le build Win32, et la sortie
redirigee vers E: faute de place sur Z:.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
origin pointe sur le Gitea auto-heberge. CLAUDE.md affirmait "pas de
remote configure (dev local)", ce qui a cesse d'etre vrai sans que le
document suive — 427 commits d'avance au moment du constat.
Rien n'est pousse automatiquement, et c'est note : lire "il y a un
remote" ne doit pas se comprendre comme "c'est sauvegarde ailleurs".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Differe volontairement : l'APK ne part chez personne aujourd'hui. Le
jour venu, l'instantane publie par le poste portera la licence et le
telephone exigera d'y en trouver une — il le lit deja, donc aucun
nouveau format, aucune empreinte Android, aucune emission de plus.
Les trois raisons de ne pas refaire un second dispositif sont notees
pour ne pas les re-deriver : Java attend du DER la ou CNG accepte r||s
brut, ANDROID_ID est remis a zero par une reinitialisation d'usine (donc
des clients legitimes refuses), et un APK se repaquette en dix minutes.
Et l'APK n'aura PAS a verifier la signature, seulement la presence :
payer le DER et le JNI pour durcir un controle que le repaquetage retire
serait le mauvais echange.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LICENCE-TESTS.md : cinq series. B eprouve ce qui doit tomber (chaine
quelconque, licence tronquee, charge utile trafiquee, autre poste, autre
paire de cles), C le risque symetrique et plus couteux — refuser un
client qui a paye (espaces colles, retour a la ligne, redemarrage
machine). D est dediee a la stabilite de l'empreinte, le defaut deja
rencontre. E delimite la portee de l'activation.
Le plan citait cmd://licence/clear sans moyen de l'appeler : un bouton
"Deactivate (debug)" au bas de Settings -> Licence, revele seulement
quand config.txt existe. Il donne au passage son unique appelant a
ClearLicence, qui etait du code mort.
Le document dit aussi ce qu'aucun essai ne prouve : rapiecer l'exe reste
possible, il n'y a pas de revocation, et le brouillage n'est pas du
chiffrement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La premiere privee a ete supprimee sans copie : plus aucune licence
nouvelle n'etait emettable. Regeneration + reemission, indolore
aujourd'hui parce qu'une seule licence circulait.
licence.ps1 ecrit desormais la cle dans %USERPROFILE%\Documents, hors du
depot. Elle vivait dans tools\, ou elle n'attendait qu'un menage : le
.gitignore la protegeait du depot, rien ne la protegeait de son
proprietaire.
Verifie : la licence de la nouvelle paire passe, celle de la paire
perdue tombe — la regeneration a bien remplace, pas ajoute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le frontend etait lisible dans l'onglet Image de Process Explorer, avec
les commentaires. BuildAssets.ps1 brouille chaque fichier avant brcc32,
PM.EmbeddedAssets le debrouille au moment de servir.
C'est de l'opacite, pas du secret : la cle est dans l'exe. Ca supprime
strings et l'onglet Image ; la memoire, un debogueur et la reponse HTTP
rendent toujours le texte. D'ou XOR plutot qu'AES — a securite egale
(nulle contre qui a l'exe), dix lignes, et aucune force affichee qu'on
n'a pas.
Une magie PMA1 prefixe le clair : sans elle, deux constantes qui derivent
serviraient du binaire au navigateur, ecran blanc sans trace.
Verifie en comparant les flux des deux cotes bloc par bloc, puis par
aller-retour sur app.js, index.html et style.css. Le frontend ne se
trouve plus dans le binaire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Porteur, courriel, date d'emission et Installation ID, lus dans la charge
utile deja verifiee. Rempli une fois au demarrage sur les deux chemins
(deja licencie, activation reussie) : rien n'y change pendant une
session. Section masquee hors navigateur hote.
Les champs sont echappes avant interpolation dans ExecuteJavaScript bien
qu'ils viennent d'une charge utile signee — signee ne veut pas dire
inoffensive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BoardUUID s'appuyait sur un COM initialise par quelqu'un d'autre. Appele
apres WebView2 il lisait le vrai UUID de carte mere, appele avant il
echouait et retombait sur MachineGuid : deux empreintes pour une seule
machine, et une licence liee a l'une que l'autre refuse. Il suffisait que
licence/status parte avant l'init du navigateur pour verrouiller
l'utilisateur hors de son coffre.
CoInitializeEx est desormais appele sur place. Pas de CoUninitialize
symetrique : essaye, il rend une violation d'acces — les mandataires WMI
et le cache de type-info de ComObj se liberent plus tard, dans un
appartement qu'on vient de fermer.
Verifie avec un harnais qui n'initialise PAS COM : il rend maintenant la
meme empreinte que l'application.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Une cle flottante etait sortie. Un schema hors ligne n'a pas de
revocation — rien ne peut aller dire a un exe installe qu'une cle ne vaut
plus — donc la seule prise est de cesser d'accepter toute la classe.
MachineMatches exige desormais une empreinte, et -Floating disparait du
script : il ne peut plus fabriquer une cle que l'exe rejetterait.
Effet de bord assume : plus de licence de site, et un poste sans
empreinte exploitable n'est plus licenciable du tout — mieux qu'une
empreinte degeneree qui ouvrirait partout.
Verifie : liee a ce poste acceptee, liee a un autre refusee, les deux
flottantes deja emises refusees.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
C'etait la decision prise, et je l'avais mal lue : "pour le moment 1"
repondait a la question 1, pas "une seule licence".
PM.MachineId melange l'UUID de la carte mere (WMI) et la serie du volume
systeme, puis n'en publie que 16 hexa : l'empreinte se recopie dans un
courriel, l'UUID brut n'a pas a circuler. Un UUID nul ou tout a FF est
rejete — VM et images clonees l'annoncent, et deux postes tomberaient
alors sur la meme empreinte.
Le champ m est compare APRES la signature. m absent = licence flottante,
acceptee : seule la cle privee peut en emettre, et le champ ne peut pas
etre retire sans casser la signature. Cote script -Machine est
obligatoire sauf -Floating explicite.
L'ecran d'activation affiche l'Installation ID : une licence liee ne peut
plus etre emise avant l'installation.
Verifie sur ce poste : liee a lui acceptee, liee a un autre refusee,
flottante acceptee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ensureLicence() en tete d'init(), avant tout le reste : un ecran
d'authentification qui s'affiche puis se fait recouvrir donnerait
l'impression que le coffre etait accessible.
L'ecran reprend les classes de l'ecran d'authentification — zero style
nouveau hormis un z-index. Le statut est demande deux fois avant de le
montrer : un demarrage a froid lent manquerait la fenetre de reponse, et
le silence se lirait comme un refus.
Une cle refusee ne dit pas pourquoi, et le journal note le verdict sans
jamais la cle.
ATTENTION : tant que LicencePublicKeyBlob est a zero, l'ecran est
infranchissable. Poser la cle publique avant de deployer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PM.Licence verifie une chaine charge-utile.signature contre la cle
publique de l'editeur, via BCryptVerifySignature. Aucun ASN.1 : la
signature du script est deja en r||s brut et les coordonnees se rangent
telles quelles dans un BCRYPT_ECCPUBLIC_BLOB.
La cle publique est livree A ZERO — tant que la sortie de licence.ps1
-Init n'y est pas collee, aucune licence n'est valide. C'est le defaut
sur : un blob par defaut qui accepterait quelque chose serait pire.
IsLicensed REVERIFIE la licence rangee plutot que de lire un drapeau.
La signature est verifiee AVANT que la charge utile soit analysee.
SelfTestLicence a l'initialization rejoue un vecteur fige signe par une
paire de test : valable accepte, un octet retourne refuse. Verifie en
executant l'unite, pas seulement en la compilant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
localStorage meurt avec l'origine a chaque lancement, donc un autofill
desactive revenait sur ON. Miroite en pref DPAPI comme unlockMode et
rememberedUsername ; la copie DPAPI fait autorite, relue dans enterApp
juste avant la poussee des combos vers Delphi.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le lien n'etait garde par rien : un port pris levait, et l'application
s'ouvrait sur une boite d'erreur. Trois essais desormais - le premier
honore le port de config.txt, que l'utilisateur a peut-etre choisi
expres, les suivants prennent un ephemere.
SE LIER EST LE TEST. Sonder le port avant serait plus faible - un service
qui ecoute sans repondre passerait inapercu - et sujet a une course : le
port peut etre pris entre le sondage et le lien. Chaque deplacement est
journalise, un serveur qui change de port en silence etant pire qu'un
serveur qui refuse de demarrer.
Trouve en chemin : Randomize n'est appele NULLE PART, donc le port
"aleatoire" de port=0 etait le meme a chaque lancement - l'aleatoire cense
compliquer un balayage exterieur n'existait pas. Il vient maintenant de
RandomBytes, donc de BCryptGenRandom, deja utilise pour le jeton d'acces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le premier nomme la chose, le second decrivait l'utilisateur. Le signe
qui a tranche : l'ancien libelle avait besoin d'une parenthese pour se
faire comprendre - "Lazy mode (fill with the mouse)" - et c'etait elle
qui portait le sens.
Seul le libelle change. Les identifiants internes gardent "lazy" -
FLazyWnd, LAZY_*, et surtout les prefs lazyOn / lazyPos / lazyAlphaPct :
renommer ces cles perdrait les reglages deja poses sur la machine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pref DPAPI lazyOn, reprise par RestoreLazyBar depuis FormCreate - apres
le theme, parce que la barrette se peint elle-meme, et apres le cablage
de OnTrayRestore, dont depend son bouton Open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quand la barrette est allumee, elle est souvent la seule chose de
l'application a l'ecran : revenir au coffre ne doit pas demander de
retrouver l'icone du tray.
Le menu d'opacite passait derriere : le battement qui repose la barrette
en tete de la bande TOPMOST cinq fois par seconde recouvrait le menu que
la barrette venait d'ouvrir - lui aussi dans cette bande. TrackPopupMenu
est modal, donc le minuteur battait pendant tout ce temps. Il s'arrete
maintenant le temps du menu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TOPMOST n'est pas un rang, c'est une bande. La barre des taches y est
aussi, et cliquer son fond vide la remonte en tete de cette bande : une
barrette posee dessus passe DERRIERE et semble avoir disparu. Le meme
battement de 200 ms la repose.
Traine depuis un bouton : les boutons sont subclasses, et DragDetect
tranche entre clic et glissement en respectant le seuil regle par
l'utilisateur. Sur un clic, on envoie BN_CLICKED a la place du bouton -
DragDetect a avale le WM_LBUTTONUP, lui rendre la main le laisserait
enfonce pour toujours.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pref DPAPI lazyPos, ecrite sur WM_EXITSIZEMOVE - une ecriture par
deplacement, pas une par pixel parcouru.
La position n'est reprise que si MonitorFromRect la trouve encore sur un
ecran. La barrette n'a pas d'entree dans la barre des taches : une
position heritee d'un ecran debranche, ou d'une resolution changee, la
laisserait hors du bureau et definitivement irrecuperable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Clic droit dessus -> 100/75/50/25 %, persiste en pref DPAPI. Pas dans
settings_json : la barrette peut etre allumee coffre ferme, ou aucune
session n'existe pour lire les reglages du serveur.
Le survol est sonde toutes les 200 ms plutot que suivi par
TrackMouseEvent : avec quatre fenetres enfants, passer de la barrette a
un bouton envoie deja un WM_MOUSELEAVE au parent, et il faudrait suivre
la souris sur les cinq fenetres pour recoller les morceaux.
Le menu appartient a la fenetre message-only : TrackPopupMenu reclame un
SetForegroundWindow pour se refermer seul, et le faire sur la barrette
detruirait le NOACTIVATE qui la fait fonctionner.
Transparente n'est pas traversante - les clics sont toujours avales.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un quatrieme bouton X. Il POSTE WM_LAZY_CLOSE a la fenetre message-only
plutot que de detruire sur place : liberer une fenetre depuis son propre
gestionnaire libererait, en plein envoi, l'instance qui lui sert de
window proc.
Au passage, deux libelles sortaient en mojibake - "Quick search__|" dans
le menu du tray, un tiret cadratin dans l'infobulle. Le fichier est en
UTF-8 SANS BOM, que Delphi lit en ANSI, un octet par caractere : les
litteraux restent donc en ASCII.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SetPreferredAppMode(ForceDark) ne themise que ce que le systeme dessine
lui-meme - menus, ascenseurs, infobulles. Un bouton Win32 standard reste
donc gris clair sur fond sombre, ce que la capture montrait.
BS_OWNERDRAW + WM_DRAWITEM : le systeme garde le suivi de la souris et
l'etat enfonce, nous ne reprenons que les couleurs, recopiees des
variables de css/style.css. Le cadre passe dans WM_ERASEBKGND plutot que
par WS_BORDER, qui se sert dans les couleurs systeme.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Menu du tray -> "Lazy mode". Deux boutons Full / Password qui flottent
au-dessus de tout et font ce que font Ctrl+Shift+L / Ctrl+Shift+P.
WS_EX_NOACTIVATE est toute l'astuce : une fenetre ordinaire prendrait le
premier plan au clic, le champ que l'utilisateur vient de cliquer perdrait
son curseur, et GetForegroundWindow nous designerait nous-memes comme cible.
Le clic ne deplace donc jamais le premier plan.
Le corps du handler WM_HOTKEY est extrait dans FireAutofillRequest : hotkey
et barrette partagent la capture de la cible, le matching de titre et le
picker. Un seul chemin, deux gestes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A backup that silently drops the trash is not a backup: export, empty the vault, import, and the trashed entries were gone for good with nothing said. Both containers now carry state.trashed with deleted/deleted_at, and deleted_at is preserved so a restore resumes the auto-purge clock instead of restarting it - hence its addition to the GET echo.
The sync snapshot deliberately does NOT: the trash is device-local, and re-publishing deleted rows is what produced the duplicate-uuid bug. The two identical export loops were folded into buildBackupEntry first, so the trash handling exists once.
Same workflow as the password-only hotkey: click the 2FA field, press the
combo, the current 6-digit code is typed in.
Reuses the whole autofill pipeline — no new native typing path. The code is
sent in the password slot with an empty username, so Delphi's existing
password-only branch types just the code.
- Bridge: akTotp kind, hotkey id 47, registered/torn down alongside the other
two autofill combos; an unconfigured combo (vk=0) doesn't count as a
registration failure.
- Matching filters on totp_secret BEFORE scoring, so the multi-match picker
never offers an entry that has no code.
- Configurable + synced like the others (autofillHotkeyTotp), collision-checked
against the other four slots, covered by Reset to defaults and the cheatsheet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A pinned entry came back unpinned after export→import. Wider than pinned:
`favorite` was WRITTEN into the export/backup/sync containers but nothing
ever read it back — not parseEntriesFromJSON, not encryptImportEntry, not
the server INSERTs. So it was a write-only field, and a restore silently
dropped both flags. `pinned` wasn't in any container at all.
Wired the full round-trip:
- containers: export + auto-backup now carry `pinned` (favorite was there).
- parseEntriesFromJSON reads both back (absent → 0, e.g. foreign CSV).
- encryptImportEntry passes them through to the body.
- server create + bulk-import INSERTs write favorite/pinned
(GetValue<Boolean>(..., False) → 0 default). PUT still ignores them, so a
sync update can't clobber local flags.
Sync stays device-local as decided 2026-07-11: buildSyncSnapshot now OMITS
favorite explicitly (it was dead weight before; with the passthrough live,
leaving it in would have started propagating it). Commented so the intent
isn't re-broken.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up to 18fe13a: the custom-icon flag now survives round-trips instead
of resetting to fetched on a restore.
- Containers write icon_custom: manual export (doExport), auto-backup, sync
snapshot; parseEntriesFromJSON carries it back (default 0 when absent, e.g.
foreign CSV).
- encryptImportEntry ships it in the body (shared by bulk-import + sync
create/update).
- Server create + bulk-import INSERTs read icon_custom
(GetValue<Boolean>('icon_custom', False) → 0 default). An empty icon always
forces icon_custom=0.
Now a custom icon stays custom after export→import and across sync — it won't
be silently reclassified as cache and clobbered by Re-fetch-all on the other
device.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-fetched favicons and user uploads shared one icon_b64 column, so
"Clear cache" wiped custom uploads and "Re-fetch all" overwrote them — data
loss the user can't undo (a custom icon exists precisely because DDG can't
fetch that site).
New icon_custom flag (0=fetched cache, 1=user upload). Invariant: an
automatic action never touches a custom icon; only the user changes/removes
it per entry.
- Schema: icon_custom INTEGER on vault_entries.
- POST /entries/{id}/icon takes {custom}; GET /entries echoes icon_custom;
DELETE /entries/icons/all filters COALESCE(icon_custom,0)=0.
- saveEntryIcon(id, uri, isCustom): upload→true, fetch/remove→false.
- backfillFavicons skips icon_custom even on force (Re-fetch all).
- clearAllFavicons clears only non-custom locally + refreshes the open editor
(the icon preview didn't update on clear before).
- duplicate re-stamps a copied custom icon; import-overwrite carries the flag.
Not propagated across bulk-import / sync (the bulk INSERT doesn't read it) —
a restored icon counts as fetched. Documented; custom icons are typically
self-hosted sites DDG won't re-fetch anyway.
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>
Extends the username-at-rest scheme to site, title and tags — the last
searchable metadata still stored cleartext. Same design: dedicated
<f>_enc/<f>_iv columns (AES-GCM under the vault key), decrypted at load into
e.<f>, so client-side search/sort/render/favicon/autofill-match are unchanged.
Full-strength random-IV AES-GCM (no searchable encryption) because search is
client-side.
Generalized the helpers over ENCRYPTED_META_FIELDS = [username, site, title,
tags]:
- withEncryptedUsername → withEncryptedMeta (encrypts all four, blanks
cleartext) — wraps every POST/PUT body.
- decryptEntryUsernames → decryptEntryMeta (decrypts all four at load).
- migrateUsernamesAtRest → migrateMetadataAtRest (sweeps any field still
cleartext, live + trash).
- doChangeMasterPassword re-encrypts all four under the new key.
Server (Entries + Auth + Database):
- Columns site_enc/iv, title_enc/iv, tags_enc/iv; GET emits them (new
AddNullableField helper); POST/PUT/bulk read+persist (BindNullable helper);
rotation UPDATE re-encrypts them.
- Removed the server "Site required" validation (site='' when encrypted — the
client enforces it) at POST/PUT/bulk.
- ?q= server search neutralized (site+username ciphertext → LIKE useless; the
frontend never sends ?search=).
Tests: merge assertions updated to decrypt site (encrypted on import). 65/65.
username was runtime-validated earlier; site/title/tags NOT yet compiled/
runtime-tested (Delphi) — large multi-handler change. Rebuild BuildAssets +
PMServer, then create/edit/dup/move/tag/import/rotate and verify the DB shows
no cleartext site/title/tags (and the app still renders/searches).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
username is no longer stored cleartext. New columns username_enc/username_iv
(AES-GCM under the vault key, same as encrypted_password). Search/sort/render
stay client-side, so the field is decrypted at loadEntries into e.username in
memory — everything downstream is unchanged. Full-strength random-IV AES-GCM
(no searchable/deterministic encryption) precisely because search is
client-side.
Server (PM.Handler.Entries / .Auth / PM.Database):
- Schema: vault_entries.username_enc, username_iv.
- GET returns them; POST/PUT/bulk-import read + persist them; master-pw
rotation re-encrypts them under the new key (UPDATE + loop).
- ?q= server search drops `username LIKE` (ciphertext won't match; frontend
searches client-side anyway).
Client (app.js / app.import.js):
- loadEntries/loadTrash decrypt username_enc → e.username (fallback to
cleartext for un-migrated rows).
- withEncryptedUsername(obj): write choke point — encrypts obj.username into
username_enc/username_iv and blanks the cleartext. Wraps every POST/PUT
body: saveEntry, soSave, duplicateEntry, moveEntryToFolder, addTagToEntry,
batchMove/AddTag, encryptImportEntry (import + sync-apply).
- doChangeMasterPassword re-encrypts username under the new key.
- migrateUsernamesAtRest(): one-time sweep at enterApp, PUT-re-ships rows that
still carry cleartext username so the DB gets scrubbed (bumps updated_at
once; plaintext unchanged so devices converge).
site/title/tags stay cleartext (same pattern later — see memory note). +1
merge test (username encrypted on import). 65/65.
NOT compiled/tested at runtime (Delphi) — large multi-handler change; rebuild
BuildAssets + PMServer and test create/edit/rotate/import/sync + verify the
DB shows no cleartext username.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The profile picture (users.avatar_b64, cosmetic/unencrypted) travelled only
in the manual export. Now it's also in buildSyncSnapshot and the auto-backup
container, so a new device / a restore picks it up.
- Restore is ADDITIVE (mirrors the import path): applyRemoteSnapshot adopts
remote.avatar_b64 only when the local device has no avatar — never clobbers
a locally-set picture. No per-avatar timestamp to arbitrate, so changing an
existing avatar doesn't propagate (cosmetic, accepted).
- +2 merge tests (adopt-when-empty, don't-clobber-when-set). 65/65 green.
- Server /avatar endpoint unchanged (already accepts {avatar_b64}).
Closes the avatar item of CODE_AUDIT §4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eighth slice. Quick search wasn't contiguous — its functions sat on both
sides of the cheatsheet and password-history modals (lines 725-1104). Rather
than a fiddly non-contiguous cut, the whole overlay cluster is extracted as
one byte-identical block: js/app.overlays.js (quick search + cheatsheet +
password history). Pure declarations, no top-level side effects → loads
before app.js; all state/api/Bridge/render/decryptPwd refs resolve via shared
global scope at call time.
- Byte-for-byte identical; syntax OK on all nine app parts; 62/62 tests green.
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
app.js: 11936 → 9170 lines (8 modules extracted, ~2770 lines). Load order:
argon2 → crypto → totp → favicon → import → backup → health → overlays →
app → sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seventh slice of the app.js split. Moves the vault-health dashboard
(computeHealthCache, healthScoreBand, renderHealthDashboard/Section,
openEntryForFix, entryAgeDays + scoring consts) to js/app.health.js. Pure
declarations, no top-level side effects → loads before app.js. Uses
computeStrength/decryptPwd/state/api via shared global scope at call time.
- Byte-for-byte identical extraction; syntax OK on all eight app parts.
- auditCache/auditFilter sit in this var block but drive the separate
Audit-log viewer in app.js — they ride along and resolve cross-file via
shared scope (documented).
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
- 62/62 tests green.
app.js: 11936 → 9545 lines (7 modules extracted).
NOTE: quick search is NOT contiguous (interleaved with cheatsheet +
history-modal code, lines 886-1063), so a clean byte-identical extraction
isn't trivial — deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per user request, swap the quick-search click/key mapping so the common case
(fill just the password) is the plain left-click / Enter:
left click / Enter → password only (was: full user+Tab+pwd)
right click / Shift+Enter → full user+pwd (was: username only)
Ctrl+click / Ctrl+Enter → username only (was: password only)
Keyboard mirrors the mouse. Copy-mode (tray/palette, no HWND target) shares
the same `mode`, so it shifts too: click/Enter copies password, Ctrl+click/
Ctrl+Enter copies username (right-click's 'full' has no copy meaning → pwd).
Updated the dynamic hint, the static index.html hint, and CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sixth slice of the app.js split. Moves the favicon fetch/cache section
(faviconHost, saveEntryIcon, ensureEntryFavicon, backfillFavicons,
clearAllFavicons) to js/app.favicon.js. Pure declarations, no top-level
side effects → loads before app.js.
- Code moved byte-for-byte; no duplicate const; syntax OK on all 7 app parts.
- NEW: js/tests/favicon.test.js — 7 tests for faviconHost, the pure
URL→validated-hostname function that decides which domain is sent to the
DuckDuckGo proxy (a bug there leaks the wrong host). Covers scheme/www/
path/port stripping, non-hostname rejection, malformed dotting, unsafe
chars, and the 253-char DNS cap.
- Fixed an inaccurate source comment surfaced by the tests: it claimed raw
IPs "stay valid", but the TLD rule /\.[a-z]{2,}$/ rejects a numeric final
label, so IPs get no favicon lookup (fine). Test pins the real behaviour.
- Suite: 55 → 62 tests, all green. Assets regenerated (8 ordered JS files).
app.js: 11936 → 9790 lines (6 modules extracted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fifth slice of the app.js split. Moves the scheduled encrypted-backup
feature (config, retention, runAutoBackupNow/runAutoBackupIfDue) to
js/app.backup.js. Pure declarations + two consts, no top-level side effects
→ loads before app.js; uses encryptExportPayload (app.import.js), Bridge,
api, state via shared global scope at call time.
- Byte-for-byte identical extraction; no duplicate const; no top-level
backup reference left in app.js; syntax OK on all six app parts.
- index.html + BuildAssets whitelist + harness APP_PARTS updated; assets
regenerated (manifest embeds all 7 ordered JS files).
- 55/55 tests green.
app.js: 11936 → 9900 lines — now under 10k. Five modules extracted
(~2000 lines): argon2 → crypto → totp → import → backup → app → sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth slice of the app.js split. Moves TOTP (base32Decode, generateTOTP,
parseOtpAuthUri) plus the TOTP-secret and custom-field AES-GCM wrappers to
js/app.totp.js. Loads before app.js (pure declarations), after app.crypto.js
(uses encryptPwd/decryptPwd). Also called by app.import.js and app.sync.js
via shared global scope.
- Byte-for-byte identical extraction; no duplicate const; syntax OK on all
five app parts.
- NEW: js/tests/totp.test.js — 13 tests including the 5 RFC 6238 Appendix B
reference vectors (generateTOTP reads Date.now(), so each case stubs the
sandbox clock to the vector's fixed time), base32 decode edge cases, and
parseOtpAuthUri. Extraction AND new coverage in one slice.
- Suite: 42 → 55 tests, all green.
- Assets regenerated (manifest now embeds all 6 ordered JS files:
argon2 → crypto → totp → import → app → sync); also fixes the previous
import commit's not-yet-rebuilt manifest.
- Delphi build artifacts (*.vrc, *.$manifest) gitignored.
app.js: 11936 → 10138 lines (4 modules extracted, ~1800 lines).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third slice of the app.js split. Moves the encrypted export container,
CSV/JSON import parsing (parseCSV, findColumn, parseEntriesFromCSV,
parseEntriesFromJSON), and the doImport/doExport/doExportCSV flows to
js/app.import.js. encryptImportEntry moves here too (also called by
app.sync.js — resolved via shared global scope at call time).
- Byte-for-byte identical to the extracted block; no duplicate const;
no top-level import ref left in app.js.
- Load order: BEFORE app.js (pure declarations, no top-level side effects),
alongside app.crypto.js. Full order: argon2 → crypto → import → app → sync.
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
- Safety net: the 14 CSV tests exercise parseCSV/parseEntriesFromCSV from
the extracted file and stay green (42/42).
app.js: 11936 → 10253 lines (crypto + sync + import now separate, ~1700
lines moved into 3 modules).
NOTE: assets.res not regenerated here (needs brcc32/Delphi) — run
BuildAssets before the next Delphi build to embed js/app.import.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second slice of the app.js split (after crypto). Moves the WebDAV sync
section to js/app.sync.js: transport (_webdavCall), buildSyncSnapshot,
applyRemoteSnapshot (merge + tombstone arbitration), runSyncNow, and the
sync settings UI.
- Byte-for-byte identical to the extracted block (verified before removal);
no duplicate const; no top-level sync reference left in app.js.
- Load order: AFTER app.js (unlike crypto, which loads before) because this
module has a top-level side effect — `Bridge.onWebdavResult = …` — that
needs Bridge/state/api already declared. Rule documented in CLAUDE.md.
- index.html + BuildAssets whitelist + harness APP_PARTS updated; assets
rebuilt to embed the new file.
- Safety net: the existing merge tests exercise applyRemoteSnapshot /
buildSyncSnapshot from the extracted file and stay green (42/42).
app.js: 11936 → 11256 lines (crypto + sync now separate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both UPDATEs mutated a synced column without touching updated_at, so the
change rode in the sync snapshot but other devices skipped it (last-write-
wins saw "not newer"). Now both SET updated_at = datetime('now') (UTC).
- POST /entries/{id}/icon (PM.Handler.Entries)
- folder delete → entries reassigned to 'All' (PM.Handler.Folders)
accessed_at stays exempt (read timestamp, not synced); bulk "clear all
icons" stays exempt (device-local favicon cache purge). Invariant documented
in CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of the app.js split. Approach: ordered classic-script files
loaded via separate <script> tags (argon2.js → app.crypto.js → app.js),
NOT ES modules / a bundler. Classic scripts share one global lexical
environment, so consts/functions cross-reference across files exactly as
in the monofile — zero call-site rewrites, near-zero risk. Chosen over the
audit's esbuild/ES-module suggestion because the code is written entirely
in global scope (functions call each other by bare name everywhere).
- js/app.crypto.js: KDF (PBKDF2 + Argon2id), verifier, AES-GCM encrypt/
decrypt, key persist/restore. Verified byte-for-byte identical to the
original block before removal; no duplicate const across the two scripts.
- index.html + BuildAssets whitelist + test harness updated for the load
order. Harness CONCATENATES app.crypto.js + app.js (node:vm doesn't share
top-level const across separate runInContext calls the way browsers share
it across <script> tags); argon2.js stays a separate IIFE.
- Runtime-validated: rebuilt exe unlocks via quick-unlock and loads/decrypts
entries — the extracted crypto (restoreCryptoKey, verifierFromKeyHex,
decryptPwd) works from the separate file. 42/42 tests green.
- Docs: CLAUDE.md "Découpage frontend" (pattern + rules), file map, tests
README, CODE_AUDIT §3.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>