33e4b4b614
- 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>
458 lines
21 KiB
Markdown
458 lines
21 KiB
Markdown
# Password Manager — Architecture éclair
|
|
|
|
Desktop password vault Windows. Exe Delphi FMX embarquant WebView2 qui
|
|
charge un frontend HTML/JS via Indy HTTP loopback (127.0.0.1:8765).
|
|
100 % offline, multi-utilisateur, chiffrement client-side (AES-GCM,
|
|
clé dérivée PBKDF2-SHA256 600 k iter).
|
|
|
|
## Stack
|
|
|
|
- **Backend** : Delphi 12, FMX, TMS FNC WebBrowser (Edge WebView2)
|
|
- **HTTP** : Indy `TIdHTTPServer` bindé 127.0.0.1 only
|
|
- **DB** : SQLite via FireDAC (`vault.db`, gitignored)
|
|
- **Frontend** : vanilla JS + CSS, pas de framework (`js/app.js` monofile ~4000 lignes)
|
|
|
|
## Build pipeline (CRITIQUE)
|
|
|
|
Toute modif `index.html` / `js/` / `css/` nécessite :
|
|
|
|
1. `delphi-backend\assets\BuildAssets.cmd` → régénère `assets.res`
|
|
2. Build Delphi (F9 ou MSBuild `PMServer.dproj`)
|
|
|
|
Sans étape 1, l'exe embarque l'ancienne version des assets — le bug le
|
|
plus courant après modif frontend.
|
|
|
|
## Carte des fichiers
|
|
|
|
| Rôle | Path |
|
|
|---|---|
|
|
| Form principale, wire bridge, dispatch cmd:// | `delphi-backend/UMainForm.pas` |
|
|
| Bridge JS↔Delphi (clipboard, tray, hotkeys, autofill, focus, debug) | `delphi-backend/Source/PM.Bridge.pas` |
|
|
| HTTP server (ordre dispatch : router → embedded → static → 404) | `delphi-backend/Source/PM.HTTPServer.pas` |
|
|
| Routes API (regex match) | `delphi-backend/Source/PM.Router.pas` |
|
|
| Auth + sessions + CSRF | `delphi-backend/Source/PM.Session.pas` |
|
|
| Migrations DB idempotentes (`AddColumnIfMissing`) | `delphi-backend/Source/PM.Database.pas` |
|
|
| Static files disque (mort en prod, embedded gagne) | `delphi-backend/Source/PM.StaticFiles.pas` |
|
|
| Assets embedded dans .res | `delphi-backend/Source/PM.EmbeddedAssets.pas` |
|
|
| Quick unlock DPAPI (device-bound) | `delphi-backend/Source/PM.QuickUnlock.pas` |
|
|
| Prefs key/value DPAPI (`prefs.bin`) | `delphi-backend/Source/PM.UserPrefs.pas` |
|
|
| Single-instance mutex + broadcast | `delphi-backend/Source/PM.SingleInstance.pas` |
|
|
| Start with Windows (HKCU Run) | `delphi-backend/Source/PM.AutoStart.pas` |
|
|
| Handlers REST | `delphi-backend/Handlers/PM.Handler.*.pas` |
|
|
| Frontend complet | `js/app.js` |
|
|
| HTML racine | `index.html` |
|
|
| Styles | `css/style.css` |
|
|
|
|
## Bridge cmd:// (JS → Delphi)
|
|
|
|
JS navigue vers `cmd://action/sub?params` → intercepté par
|
|
`WebBrowserBeforeNavigate` → `HandleBridgeCommand`.
|
|
|
|
Commandes connues :
|
|
- `clipboard/copy`, `clipboard/clear`, `clipboard/read` (Paste custom menu)
|
|
- `quickunlock/{store,get,clear,status}`
|
|
- `prefs/{get,set}?key=...` (device-bound DPAPI key/value, voir plus bas)
|
|
- `autostart/{get,set}?enabled=1|0` (HKCU Run registry, "Start with Windows")
|
|
- `autofill/{configure,hotkeys,execute,cancel}`
|
|
- `app/focus` (ramène la fenêtre au premier plan, pour le picker)
|
|
- `app/ready` (page chargée → SetFocus WebBrowser + DOM focus auth input)
|
|
- `app/theme?mode=dark|light` (sync title bar + popup menus via `SetPreferredAppMode`)
|
|
- `audit` (POST log d'action user-visible)
|
|
- `entry/new-from-title` (Ctrl+Shift+A → modal pré-rempli)
|
|
|
|
Retour Delphi → JS via `WebBrowser.ExecuteJavaScript('Bridge.onX(...)')`.
|
|
|
|
## Hotkeys globaux Win32
|
|
|
|
- `Ctrl+Shift+L` : autofill complet (user + Tab + password)
|
|
- `Ctrl+Shift+P` : autofill password seul (forms step-2, unlock screens)
|
|
- `Ctrl+Shift+A` : new entry pré-rempli avec le titre de la fenêtre foreground
|
|
- `Ctrl+Shift+D` : toggle debug panel (uniquement si `config.txt` présent)
|
|
|
|
Combos autofill configurables depuis Settings (synced en DB
|
|
via `settings_json`).
|
|
|
|
## Debug mode
|
|
|
|
Fichier `config.txt` à côté de l'exe (tous champs optionnels) :
|
|
```
|
|
port=8765 # 0 ou absent = port aléatoire ephemeral (49152-65535)
|
|
debug=true # affiche PanelTop (Start/Stop, log memo)
|
|
require_token=false # désactive le token validation (curl/Postman direct)
|
|
require_process_check=false # désactive le PID-on-socket check
|
|
```
|
|
Sans `config.txt`, Ctrl+Shift+D ne fait rien (sécurité, panel jamais
|
|
exposé en prod).
|
|
|
|
## HTTP server lockdown (anti-browser-direct)
|
|
|
|
Le serveur Indy n'accepte une requête que si elle a :
|
|
- Query param `?pmt=<token>` (sur la 1ʳᵉ navigation) → Set-Cookie
|
|
- OU cookie `pm_token=<token>; HttpOnly; SameSite=Strict; Path=/`
|
|
|
|
Au start :
|
|
- Port aléatoire dans `[49152, 65535]` si pas configuré
|
|
- Token random 32-chars hex généré
|
|
- WebView2 navigate vers `http://127.0.0.1:{port}/index.html?pmt={token}`
|
|
- JS strip le `?pmt=...` du URL bar via `history.replaceState`
|
|
|
|
Requête sans token valide → `404` (pas 403 pour ne pas révéler que le
|
|
serveur existe). Override via `require_token=false`.
|
|
|
|
### Couche 2 : PID-on-socket process check
|
|
|
|
Sur chaque request, `PM.ProcessLockdown` :
|
|
- `GetExtendedTcpTable` (winapi `iphlpapi.dll`) lookup le PID qui possède
|
|
la connexion entrante (match sur Chrome's `localPort` + `remotePort = our port`)
|
|
- `IsDescendantOfCurrentProcess` walk parent tree via `Toolhelp32` →
|
|
notre exe est-il ancêtre ?
|
|
- Si non (curl externe, autre browser, etc.) → 404
|
|
|
|
Defense in depth : si quelqu'un connaît le token (leaked logs, mémoire),
|
|
il faut AUSSI exécuter le code dans notre lignée de process pour passer.
|
|
Override via `require_process_check=false`. Notre WebView2 (children
|
|
`msedgewebview2.exe`) passent naturellement.
|
|
|
|
### Token masqué dans les logs
|
|
|
|
`MaskAccessToken(url)` dans UMainForm remplace `?pmt=<32 chars>` par
|
|
`?pmt=***` avant `LogLine`. Évite le leak via copie/upload du log.
|
|
|
|
## Title bar + popup menus dark mode
|
|
|
|
`PM.Bridge.ApplyTitleBarTheme(ADark: Boolean)` fait 2 choses :
|
|
|
|
1. `DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE=20)` → title bar.
|
|
No-op sur Win < 19044.
|
|
2. `uxtheme!SetPreferredAppMode` (ordinal #135) + `FlushMenuThemes` (#136)
|
|
— API privée stable depuis Win10 1809 utilisée par Explorer/Edge/Office.
|
|
Themea automatiquement les popup menus (tray context menu inclus),
|
|
scrollbars, tooltips. Valeurs : `ForceDark=2`, `ForceLight=3`.
|
|
|
|
Appelé au FormCreate + via `cmd://app/theme?mode=dark|light` quand JS toggle.
|
|
|
|
## Single instance
|
|
|
|
Au démarrage, `PM.SingleInstance.AcquireOrSignal` :
|
|
- Crée un mutex nommé `Local\PMServer.SingleInstance.Mutex` (namespace
|
|
`Local\` = per-session, donc deux users Windows distincts peuvent
|
|
chacun lancer le leur)
|
|
- Si `ERROR_ALREADY_EXISTS` : `PostMessage(HWND_BROADCAST, WM_PMSHOW, 0, 0)`
|
|
où `WM_PMSHOW = RegisterWindowMessage('PMServer_ShowExisting')` est un
|
|
ID system-unique. Puis exit immédiat avant `Application.Initialize`.
|
|
|
|
L'instance existante reçoit `WM_PMSHOW` dans son message-only window
|
|
(`PM.Bridge.MsgWindowHandler`) → déclenche `FOnTrayRestore` →
|
|
`RestoreFromTray`. `AllocateHWnd` crée une fenêtre top-level (pas
|
|
`HWND_MESSAGE`) donc elle reçoit bien le broadcast.
|
|
|
|
## Tray icon
|
|
|
|
`Shell_NotifyIcon(NIM_ADD)` au **constructor** de `TPMBridge`, pas
|
|
au premier minimize → ic ône visible dès le démarrage, même si la
|
|
fenêtre est ouverte. `NIM_DELETE` uniquement au destructor.
|
|
|
|
`MinimizeToTray` ne touche plus à l'icône, juste à la visibilité de
|
|
la fenêtre + clipboard clear + balloon first-time.
|
|
|
|
Menu : Open / Lock vault / Quit (via `TrackPopupMenu`, themé par
|
|
`SetPreferredAppMode` ci-dessus).
|
|
|
|
## Start with Windows (`PM.AutoStart`)
|
|
|
|
- Toggle dans Settings → "Start with Windows" (visible seulement quand
|
|
`Bridge.active`)
|
|
- Mécanisme : `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`,
|
|
valeur `"PMServer"` = `"<exe-path>" -tray` (single dash — `--tray`
|
|
ne match pas `FindCmdLineSwitch`)
|
|
- Per-user, pas d'admin requis. Visible dans Task Manager → Startup tab
|
|
- Toggle "on" = registered AND la valeur pointe vers notre exe courant
|
|
(un old entry d'un exe déplacé lit comme "off" → user peut re-enable
|
|
pour rafraîchir le path)
|
|
- CLI flag `-tray` (`FindCmdLineSwitch('tray', True)`) dans FormCreate
|
|
→ `FBridge.MinimizeToTray` via `TThread.ForceQueue` (defer après la
|
|
show initiale FMX pour minimiser le flash)
|
|
|
|
## Device-bound prefs (`PM.UserPrefs`)
|
|
|
|
**Problème résolu** : le serveur HTTP bind un port éphémère aléatoire
|
|
(49152-65535) sur chaque start. `localStorage` est keyed par origin
|
|
(scheme+host+**port**) → reboot = nouvelle origine = `localStorage`
|
|
wipé. Pour les prefs qui doivent survivre (`rememberedUsername`,
|
|
etc.), on persiste via DPAPI.
|
|
|
|
- `%LOCALAPPDATA%\PMServer\prefs.bin` — DPAPI-encrypted UTF-8 JSON
|
|
`{"key":"value",...}`
|
|
- `PM.UserPrefs.GetPref(key) / SetPref(key, value)` Delphi-side
|
|
- Bridge JS : `await Bridge.getPref(key)` (Promise) / `Bridge.setPref(key, value)`
|
|
- Callback : `Bridge.onPrefResult(key, value)` posé par Delphi via
|
|
`ExecuteJavaScript`. Resolvers stockés dans `prefResolvers[key]`
|
|
avec timeout 2 s.
|
|
|
|
`LoadAll` retourne toujours un `TJSONObject` valide (jamais nil) —
|
|
défault = objet vide, remplacé seulement sur parse success.
|
|
|
|
## Quick unlock — interactions
|
|
|
|
- Au boot, `init()` call `bridgeQuickUnlockStatus()` et synchronise
|
|
`state.quickUnlockEnabled` + `localStorage` depuis la source of truth
|
|
DPAPI (corrige le décalage localStorage quand le port change).
|
|
- Sur Windows lock / sleep (`BridgeSystemLock` → `Bridge.onSystemLock`) :
|
|
- Si `state.quickUnlockEnabled` → **pas de lockVault**. Le blob DPAPI
|
|
gate déjà l'accès via le compte Windows, re-locker est redondant.
|
|
Toast informatif affiché.
|
|
- Sinon → comportement d'origine (`lockVault()`).
|
|
- L'auto-lock par inactivité (`autoLockTimer`) reste indépendant — il
|
|
ignore le flag QU (opt-in utilisateur explicite via Settings).
|
|
|
|
## Authenticator + TOTP tool
|
|
|
|
Sidebar Tools expose 2 items MFA :
|
|
|
|
- **Authenticator** (`state.view = 'authenticator'`) : grille de cards
|
|
(chaque entry avec TOTP secret) — code 6 chiffres en gros + barre
|
|
countdown. `renderAuthenticatorGrid()` décrypte tous les secrets
|
|
upfront puis tick 1 s. `authTickTimer` clear sur lock / view change.
|
|
Snap instantané à 100% sur reset de période (sinon CSS transition
|
|
anime le saut backward → effet "freeze").
|
|
- **TOTP generator** (`openTotpTool()`) : modal standalone. Paste
|
|
base32 ou `otpauth://` URI → code live. Bouton "Generate" génère un
|
|
secret base32 aléatoire 20 bytes (`randomBase32Secret()`). Rien
|
|
n'est sauvegardé.
|
|
|
|
Code TOTP retourne `{ code, period, secondsLeft }` — attention au nom,
|
|
**pas `remaining`** (utiliser `t.secondsLeft`).
|
|
|
|
## Sidebar sections collapsibles
|
|
|
|
Sections `Folders`, `Tags`, `Tools` ont chacune un `.section-toggle`
|
|
button (chevron + label + badge count) qui toggle `.is-collapsed` sur
|
|
la section parent. Body masqué via `display: none` sous `.is-collapsed`.
|
|
|
|
État persisté dans `state.sidebarCollapsed = { folders, tags, tools }`,
|
|
synced via `SYNCED_SETTING_KEYS` (settings_json). Badges (`#countFolders`,
|
|
`#countTags`) restent visibles quand replié.
|
|
|
|
## Hotkey verrouillé (UX)
|
|
|
|
`autofillHandleRequest` : si `state.locked` ou pas de cryptoKey →
|
|
`Bridge.cancelAutofill() + Bridge.focusApp()` puis focus le master
|
|
password input via DOM. Remplace l'ancien no-op silencieux qui laissait
|
|
l'utilisateur perplexe.
|
|
|
|
## WebView2 / TMS settings — pièges majeurs
|
|
|
|
`TTMSFNCWebBrowser` expose des props qui semblent settables au FormCreate
|
|
mais ne sont effectivement appliquées qu'après l'init async du WebView2
|
|
sous-jacent. **Set ces props dans `WebBrowser.OnInitialized`, pas FormCreate** :
|
|
|
|
- `EnableContextMenu := False` → kill le menu Edge natif (Inspecter, Importer mots de passe…)
|
|
- `EnableShowDebugConsole := False` → kill DevTools
|
|
- `EnableAcceleratorKeys := False` ← **NE PAS METTRE** : casse aussi Ctrl+C/V/X/Z/A/K dans les inputs
|
|
|
|
Le blocking F12 / Ctrl+Shift+I/J / Ctrl+U se fait côté JS (keydown
|
|
preventDefault) en complément.
|
|
|
|
## Custom right-click menu
|
|
|
|
Le menu natif est désactivé via TMS. On gère un mini-menu JS dédié sur
|
|
right-click dans les inputs : `installCustomContextMenu()` → Cut / Copy /
|
|
Paste / Select All. Paste utilise `Bridge.readClipboard()` (sans prompt
|
|
WebView2 contrairement à `navigator.clipboard.readText()`). `stopPropagation`
|
|
sur les click items sinon le slideover se ferme (chip retiré du DOM
|
|
avant que le doc click handler check `.closest('.slideover')`).
|
|
|
|
## Browser shortcuts bloqués (keydown JS)
|
|
|
|
Bloqués via `preventDefault` :
|
|
- `F12`, `Ctrl+Shift+I/J`, `Ctrl+U` — DevTools / View source
|
|
- `Ctrl+J` — Downloads overlay (le moins évident, déclenche un popup Edge)
|
|
- `Ctrl+H/S/P/T/N/R`, `F5` — History / Save / Print / New tab+win / Reload
|
|
- `Ctrl+Shift+N/W/Delete` — Incognito / Close window / Clear data
|
|
|
|
**Conservés** : `Ctrl+C/V/X/A/Z` (édition), `Ctrl+F` (find in page), `Ctrl+0/+/-`
|
|
(zoom accessibility), `Ctrl+K` (notre command palette), `Ctrl+Shift+L/P/A/D`
|
|
(nos hotkeys).
|
|
|
|
## Pagination
|
|
|
|
State : `pageSize` (10/25/50/100, default 25, **synced via settings_json**)
|
|
+ `currentPage` runtime-only. Active sur les 3 view modes (cards/list/
|
|
table) si > 10 items. Position **top** (au-dessus du grid/table).
|
|
|
|
CSS : `.pagination { grid-column: 1 / -1 }` pour span full width en cards
|
|
view (sinon ça occupe une slot de card).
|
|
|
|
Reset `currentPage = 1` sur : search, sort, view (folder/fav/tag),
|
|
viewMode change, pageSize change. Render via `renderPagination(total, totalPages)`
|
|
+ `computePageList(current, total)` avec ellipsis intelligente
|
|
(`1 … 4 5 6 … 12`).
|
|
|
|
## Flash animation (nouvelle entry)
|
|
|
|
`flashEntry(id)` appelée après `saveEntry` success + `duplicateEntry` :
|
|
`setTimeout(0)` → `querySelector('.entry-card[data-id],.entry-row[data-id]')`
|
|
→ `scrollIntoView({block:'center'})` + classe `is-flash` 2.5s
|
|
(`@keyframes entryFlash` pulse cyan).
|
|
|
|
Résout le cas "j'ai ajouté un mot de passe avec tri A-Z, où se loge-t-il ?"
|
|
— scroll + pulse trouvent la nouvelle entry dans la grille triée.
|
|
|
|
## Settings sync
|
|
|
|
Per-user blob JSON dans `users.settings_json`, exposé via `GET/PUT
|
|
/settings`. Synced : theme, autoLock, viewMode, sortBy/sortDir, hibp,
|
|
hotkeys autofill, etc. **Device-only** (localStorage seulement) :
|
|
`quickUnlockEnabled` (DPAPI lié au compte Windows), `autofillEnabled`
|
|
(toggle hotkey Win32), `rememberedUsername` (auth screen autofill local).
|
|
|
|
## Quick Unlock
|
|
|
|
DPAPI blob à `%LOCALAPPDATA%\PMServer\quickunlock.bin` (tied to Windows
|
|
user, **pas** à l'emplacement de l'exe — survit au déplacement).
|
|
|
|
**Cold-start restore** : `tryQuickUnlock` ne check pas `localStorage`
|
|
comme source of truth (WebView2 user-data folder est relatif à l'exe →
|
|
move = localStorage perdu). Le fichier DPAPI seul gouverne. Si présent :
|
|
- Décrypte blob → restore key
|
|
- **Re-login fresh** via `/login` avec `verifier = bytesToHex(rawKey)`
|
|
(le token stocké dans le blob peut être expiré ; on en récupère un
|
|
nouveau à chaque cold-start)
|
|
- Resync `localStorage` à `'1'` pour que Settings affiche "Disable"
|
|
|
|
## Recovery code (refactor 2026-05)
|
|
|
|
- **5 uses par code** (compteur `remaining_uses` dans `recovery_keys`)
|
|
- Décrémenté à chaque redeem. Si tombe à 0 → row deletée
|
|
- Row deletée aussi sur `successful change-master-password`
|
|
- Workflow attendu : recover → set new master pw (consomme le code) →
|
|
regenerate un nouveau code
|
|
|
|
**State `justRecovered`** : flag JS posé après redeem, cleared sur
|
|
- successful master pw change (le code est consumed)
|
|
- `lockVault` (lock = perte de contexte recovery)
|
|
- `doLogin` success (login normal avec pw = sortie du mode recovery)
|
|
|
|
En mode recovery, le modal "Change master password" masque le champ
|
|
"Current master password" ET retire son `required` (sinon HTML5 form
|
|
validation bloque silencieusement le submit). Le verifier "current" est
|
|
dérivé du `state.cryptoKey` en mémoire (qu'on vient de recover).
|
|
|
|
## Autofill pipeline
|
|
|
|
### Workflow utilisateur (important)
|
|
|
|
**L'utilisateur doit cliquer le champ cible AVANT de presser le hotkey.**
|
|
Le code ne fait plus de "click au centre de la fenêtre" pour deviner le
|
|
champ — ça détruisait le focus sur les forms qui n'étaient pas centrés
|
|
(Gitea login, etc.).
|
|
|
|
- `Ctrl+Shift+L` (full) : click sur **username field** → hotkey → user+Tab+pwd
|
|
- `Ctrl+Shift+P` (password only) : click sur **password field** → hotkey → pwd seul
|
|
|
|
### Pipeline Delphi
|
|
|
|
1. `RegisterHotKey` Ctrl+Shift+L/P sur message-only window
|
|
2. `WM_HOTKEY` → capture `GetForegroundWindow` + titre, fire callback
|
|
3. JS reçoit `Bridge.onAutofillRequest(title, kind)` → strip browser
|
|
suffix (`- Google Chrome`, etc.) → matching `state.entries.site`
|
|
4. Single match → `Bridge.executeAutofill(user, pwd)` direct
|
|
Multi-match → `Bridge.focusApp()` + picker modal → user pick → execute
|
|
Note : `closeAutofillPicker(false)` quand user pick (skip cancel
|
|
cmd qui clearrait `FAutofillTargetHWND`)
|
|
5. Delphi `ExecuteAutofill` :
|
|
- Si app foreground → `SW_MINIMIZE` (laisse la place au target)
|
|
- `ForceForegroundWindow(target)` avec `AttachThreadInput` + poll
|
|
jusqu'à `GetForegroundWindow == target` (max 600ms)
|
|
- `WaitForModifierRelease` (sinon Ctrl+Shift restent down → toutes
|
|
les frappes deviennent Ctrl+Shift+X)
|
|
- `Sleep(120)` settle
|
|
- **Pas de click parasite** — le user a déjà focusé le bon champ
|
|
- `Ctrl+A + Del` avant chaque champ (clear contenu existant)
|
|
- SendInput : `Ctrl+A+Del → username → 200ms → Tab → 200ms → Ctrl+A+Del → password`
|
|
|
|
### Matching titre → entry
|
|
|
|
- Strip suffixe browser via `BROWSER_SUFFIX_RE` (Chrome, Firefox, Edge,
|
|
Brave, Opera, Vivaldi, Safari, Tor, Arc) avant toute comparaison
|
|
- Score = max sur 3 champs : `entry.title` (display name, score 1500+),
|
|
`entry.site` hostname full (1000+), SLD (500+)
|
|
- Pour SLD courts (`x`, `qq`), match avec word boundaries
|
|
- `entry.title` priorisé → si utilisateur stocke `site = "git.example.com"`
|
|
(hostname), mettre aussi `title = "Gitea"` (brand) couvre le browser
|
|
title sans ambiguïté
|
|
|
|
### Contraintes connues autofill
|
|
|
|
- **UIPI** : SendInput vers process élevé bloqué. Notepad++ admin,
|
|
regedit → autofill silencieusement no-op. Tester avec apps non-élevées.
|
|
- **Chrome password manager** : si Chrome a des credentials sauvegardés
|
|
pour le domaine, il peut racer avec notre SendInput et écraser notre
|
|
username pendant le Tab. Workaround : user désactive Chrome PM par
|
|
site (clic-droit password field → "Never save passwords for this site")
|
|
- **Gitea-like forms** : tabindex explicite + `autocomplete="current-password"`
|
|
→ Chrome PM s'active. Désactiver pour ce domaine ou utiliser Ctrl+Shift+P
|
|
- **Title-based matching** : si l'utilisateur stocke `site = "git.ai-agents4you.com"`
|
|
(hostname) mais le titre browser n'a que la marque ("Gitea"), pas de
|
|
match. Convention : préférer `site = "Gitea"` (brand) pour les sites
|
|
brandés. URL hostname-only fonctionne pour les sites dont le titre
|
|
contient le hostname (rare).
|
|
|
|
## Contraintes / pièges connus
|
|
|
|
- **UIPI** : process non-élevé ne peut pas SendInput vers process élevé
|
|
(Notepad++ admin, regedit, …). Solution future : manifest
|
|
`uiAccess=true` + exe signé EV + installé dans `C:\Program Files\`.
|
|
- **Assets embedded > static** dans le dispatch HTTP : modifier JS sans
|
|
rebuild `assets.res` n'a aucun effet sur l'exe.
|
|
- **WebView2 mange tous les keydown** : ne pas utiliser `FormKeyDown`
|
|
pour des raccourcis globaux → toujours `RegisterHotKey`.
|
|
- **FMX `FormCloseQuery`** : minimize-to-tray sauf si `FQuitting=True`
|
|
(set par le tray menu "Quit" uniquement).
|
|
- **`SetForegroundWindow` Win10/11** : règles strictes anti-stealing →
|
|
nécessite `AttachThreadInput` pour cross-process.
|
|
- **`updated_at`** est bumpé à chaque PUT entry → sort par défaut =
|
|
`name` asc pour que modifier une entry ne change pas sa position.
|
|
- **TMS render-time vs DOM-attachment** : pattern courant — fonctions
|
|
qui rendent un widget (`soTagsField`, `soTotpField`) appellent un
|
|
helper (`renderSoChips`, `startTotpTick`) qui fait `$('#id')` sur un
|
|
élément pas encore dans le DOM. Solution : render inline OU
|
|
`setTimeout(fn, 0)` pour deferrer jusqu'au tick suivant.
|
|
- **HTML5 `required` + `display:none`** : un input `required` caché
|
|
bloque silencieusement le form submit. Toggle aussi `.required = false`
|
|
quand on cache (cf. modal recovery).
|
|
- **WebView2 user-data folder est relatif à l'exe** : déplacer l'exe =
|
|
nouveau folder = `localStorage`/`sessionStorage` perdu. Pour ce qui
|
|
doit survivre au move, stocker via Delphi (DPAPI, fichiers user).
|
|
- **Port éphémère = nouvelle origin = localStorage wipé chaque launch** :
|
|
cause distincte du point précédent (l'exe ne bouge pas, mais le port
|
|
change). Mêmes conséquences : pour tout pref qui doit survivre au
|
|
reboot, passer par `PM.UserPrefs` via `Bridge.getPref/setPref`. Le
|
|
fix port-fixe est possible (`port=N` dans `config.txt`) mais on perd
|
|
l'aléatoire qui complique le scan externe.
|
|
- **Modifier les `Enable*` props TMS en `OnInitialized`**, pas FormCreate.
|
|
|
|
## Conventions code (héritage sessions précédentes)
|
|
|
|
- **Pas** de commentaires multi-lignes pour chaque fix : préférer noms
|
|
de variables/fonctions descriptifs (`OwnFormHwnd`,
|
|
`EnsureTargetThreadHasKeyboardFocus`, `FocusSettleDelayMs`)
|
|
- Migrations DB : toujours via `AddColumnIfMissing` (idempotent)
|
|
- Nouveau setting UI : ajouter à `SYNCED_SETTING_KEYS` + handler dans
|
|
`loadServerSettings` switch case + listener change qui appelle
|
|
`saveServerSettings()`
|
|
- Tout new bridge cmd : handler dans `UMainForm.HandleBridgeCommand`,
|
|
log via `LogLine` pour traçabilité
|
|
|
|
## Audit / docs
|
|
|
|
- `security-issues.md` à la racine : audit sécu legacy (api.php)
|
|
- Ce CLAUDE.md = source de vérité pour l'architecture courante
|
|
|
|
## Git
|
|
|
|
- Pas de remote configuré (dev local)
|
|
- `vault.db`, `*.db-shm`, `*.db-wal`, `delphi-backend/Win32/`, `*.dcu`,
|
|
`assets.res` gitignored
|
|
- Messages commit : `type(scope): description` style conventional
|