From a7ad81c708ef57ac818597872887810c9fae6b57 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sun, 5 Jul 2026 16:58:16 +0100 Subject: [PATCH] =?UTF-8?q?refactor(js):=20extract=20favicon=20module=20+?= =?UTF-8?q?=20add=20faviconHost=20tests=20(=C2=A73.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 2 + CODE_AUDIT.md | 11 ++- delphi-backend/assets/BuildAssets.ps1 | 1 + delphi-backend/assets/assets.inc | 3 +- delphi-backend/assets/assets.rc | 1 + delphi-backend/assets/assets.res | Bin 709768 -> 710736 bytes index.html | 1 + js/app.favicon.js | 125 ++++++++++++++++++++++++++ js/app.js | 114 +---------------------- js/tests/favicon.test.js | 62 +++++++++++++ js/tests/harness.js | 4 +- 11 files changed, 206 insertions(+), 118 deletions(-) create mode 100644 js/app.favicon.js create mode 100644 js/tests/favicon.test.js diff --git a/CLAUDE.md b/CLAUDE.md index cf02427..c83a35b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -34,6 +34,7 @@ visibles des suivants exactement comme dans le monofichier. Ordre actuel : js/argon2.js (IIFE, globalThis.NobleArgon2) js/app.crypto.js (KDF, verifier, encrypt/decrypt — extrait §3.1) js/app.totp.js (TOTP RFC 6238 + TOTP/custom-field crypto — extrait §3.1) +js/app.favicon.js (favicon fetch/cache + faviconHost — extrait §3.1) js/app.import.js (export container + import CSV/JSON — extrait §3.1) js/app.backup.js (auto-backup planifié — extrait §3.1) js/app.js (le reste : state, Bridge, api, UI…) @@ -103,6 +104,7 @@ seule `api()` est stubbée). | Frontend principal (en cours de découpage §3.1) | `js/app.js` | | Crypto frontend (KDF, verifier, AES-GCM) — extrait §3.1 | `js/app.crypto.js` | | TOTP (RFC 6238) + TOTP/custom-field crypto — extrait §3.1 | `js/app.totp.js` | +| Favicons (fetch/cache, `faviconHost`) — extrait §3.1 | `js/app.favicon.js` | | Import/export frontend (CSV/JSON parse, export container) — extrait §3.1 | `js/app.import.js` | | Auto-backup frontend (planifié, chiffré) — extrait §3.1 | `js/app.backup.js` | | Sync frontend (WebDAV, snapshot, merge) — extrait §3.1 | `js/app.sync.js` | diff --git a/CODE_AUDIT.md b/CODE_AUDIT.md index fbff5eb..fa30fac 100644 --- a/CODE_AUDIT.md +++ b/CODE_AUDIT.md @@ -238,11 +238,14 @@ réécriture des call-sites, risque quasi nul vs conversion en modules ES). RFC 6238)** ajoutés en même temps → extraction *et* nouvelle couverture. - ✅ `js/app.backup.js` extrait (auto-backup planifié) — byte-for-byte identique, chargé AVANT app.js (pures déclarations). -- `app.js` : 11 936 → **9 900 lignes** (crypto + totp + import + backup + sync - sortis — 5 modules). -- Suite de tests : 42 → **55 tests**. +- ✅ `js/app.favicon.js` extrait (fetch/cache favicons) — chargé AVANT app.js. + **+7 tests `faviconHost`** (extraction hostname → décide quel domaine part + vers DDG). A révélé un commentaire faux (IPs « valides » alors qu'elles + sont rejetées par la règle TLD `[a-z]{2,}`) — corrigé. +- `app.js` : 11 936 → **9 790 lignes** (6 modules sortis). +- Suite de tests : 42 → **62 tests**. - Reste à extraire (grosses sections cohésives) : slideover, settings, - quicksearch, autofill, favicons, vault-health… + quicksearch, autofill, vault-health… - ✅ `node --check` en pré-étape de `BuildAssets.ps1` : **déjà fait** (cf. §3.2). ### 3.2 🟡 Aucun test automatisé — **partiellement adressé (2026-07-04)** diff --git a/delphi-backend/assets/BuildAssets.ps1 b/delphi-backend/assets/BuildAssets.ps1 index e9a955b..6011684 100644 --- a/delphi-backend/assets/BuildAssets.ps1 +++ b/delphi-backend/assets/BuildAssets.ps1 @@ -54,6 +54,7 @@ $patterns = @( 'js\argon2.js', 'js\app.crypto.js', 'js\app.totp.js', + 'js\app.favicon.js', 'js\app.import.js', 'js\app.backup.js', 'js\app.js', diff --git a/delphi-backend/assets/assets.inc b/delphi-backend/assets/assets.inc index 13111a1..4d934ed 100644 --- a/delphi-backend/assets/assets.inc +++ b/delphi-backend/assets/assets.inc @@ -1,11 +1,12 @@ // Auto-generated by BuildAssets.ps1 - do not edit by hand. const - EMBEDDED_ASSET_COUNT = 9; + EMBEDDED_ASSET_COUNT = 10; EMBEDDED_ASSETS: array[0..EMBEDDED_ASSET_COUNT-1] of TEmbeddedAsset = ( (UrlPath: '/index.html'; ResName: 'INDEX_HTML'), (UrlPath: '/js/argon2.js'; ResName: 'JS_ARGON2_JS'), (UrlPath: '/js/app.crypto.js'; ResName: 'JS_APP_CRYPTO_JS'), (UrlPath: '/js/app.totp.js'; ResName: 'JS_APP_TOTP_JS'), + (UrlPath: '/js/app.favicon.js'; ResName: 'JS_APP_FAVICON_JS'), (UrlPath: '/js/app.import.js'; ResName: 'JS_APP_IMPORT_JS'), (UrlPath: '/js/app.backup.js'; ResName: 'JS_APP_BACKUP_JS'), (UrlPath: '/js/app.js'; ResName: 'JS_APP_JS'), diff --git a/delphi-backend/assets/assets.rc b/delphi-backend/assets/assets.rc index bd8fde6..f62f816 100644 --- a/delphi-backend/assets/assets.rc +++ b/delphi-backend/assets/assets.rc @@ -5,6 +5,7 @@ INDEX_HTML RCDATA "Z:\\password-manager\\index.html" JS_ARGON2_JS RCDATA "Z:\\password-manager\\js\\argon2.js" JS_APP_CRYPTO_JS RCDATA "Z:\\password-manager\\js\\app.crypto.js" JS_APP_TOTP_JS RCDATA "Z:\\password-manager\\js\\app.totp.js" +JS_APP_FAVICON_JS RCDATA "Z:\\password-manager\\js\\app.favicon.js" JS_APP_IMPORT_JS RCDATA "Z:\\password-manager\\js\\app.import.js" JS_APP_BACKUP_JS RCDATA "Z:\\password-manager\\js\\app.backup.js" JS_APP_JS RCDATA "Z:\\password-manager\\js\\app.js" diff --git a/delphi-backend/assets/assets.res b/delphi-backend/assets/assets.res index 337a8057c83ac8d0a92d9f41078bfdf574969659..a0bef46c343565befa1686741256b582e3b95e35 100644 GIT binary patch delta 577 zcmYL`&ubGw6vtB7UOo?r5(s~dplii6sx|t0-n^gSK zDvI^sK|~m#f_e}ULF+*lD~NxBpk5UUf`=Y@>D@_eaNv6w=KDVHz3<$AJ@R#Z2}PY`(fXLe<3V z)|l|G$W)|WRfiG9meW@?7A+3zf3#S6s#cqttI9K3J-tG>0|8lrQbmBru24uHwMMOD z)>t|Y>Zqv<-fq%>+0bB|c;Jyb_0-zT6TN}Yll-*{*GJ8pP%EE;pzYCKs@(5K0}Vd_ z(x=tCyOk;?EV;~fA*K>jTq@sX^~2GRtbXTUbYlY_`K^6(qPNeqk9XyAMo-26|CxNA o(GzlgY~ai4+PJQ?M`^FpKBZW+Ha>9n7e=dFiT%-5BL4LAUyG~4Bme*a delta 206 zcmcbxLAzt6_5=k+i$=w*ii}spr^`z)PT#&mg7L$`?G5J{4W6 + diff --git a/js/app.favicon.js b/js/app.favicon.js new file mode 100644 index 0000000..8db0cbd --- /dev/null +++ b/js/app.favicon.js @@ -0,0 +1,125 @@ +// ============================================================ +// app.favicon.js — FAVICONS module (extracted from app.js, §3.1) +// ============================================================ +// +// Website favicon fetch/cache (opt-in, via the Delphi DuckDuckGo proxy). +// faviconHost() is a pure URL→validated-hostname function (unit-tested); +// the rest are api/Bridge/render calls. Pure declarations, no top-level +// side effects → loads BEFORE app.js. Uses state, api, Bridge, render via +// shared global scope at call time. +// +// ============================================================ +// FAVICONS (opt-in, cached server-side as base64 data URI) +// ============================================================ + +// Extract a usable host from entry.site (we accept anything user-typed). +// Returns '' for values that don't look like real hostnames — common case +// is users storing a brand label ("Gitea", "Work GitHub") to help the +// autofill matcher. Sending those to DDG would leak meaningless tokens +// without ever producing an icon. +function faviconHost(siteRaw) { + if (!siteRaw) return ''; + let s = String(siteRaw).trim().toLowerCase(); + s = s.replace(/^https?:\/\//, '').replace(/^www\./, ''); + s = s.split('/')[0].split(':')[0]; + // Validate: dot-separated labels, only hostname-safe chars, TLD ≥ 2 + // LETTERS. Rejects "Gitea", "my work pwd", and raw IPs like "1.2.3.4" + // (numeric final label fails the [a-z]{2,} TLD rule — no favicon for a + // bare IP, which is fine). 253-char overall cap mirrors the DNS spec. + if (!s || s.length > 253) return ''; + if (!/^[a-z0-9.-]+$/.test(s)) return ''; + if (s.indexOf('.') < 1) return ''; + if (!/\.[a-z]{2,}$/.test(s)) return ''; + if (s.startsWith('.') || s.endsWith('.')) return ''; + if (s.indexOf('..') >= 0) return ''; + return s; +} + +// Save the icon for one entry via the dedicated endpoint (no full PUT, +// no re-encryption). Fire-and-forget: failures are silent so a flaky +// network doesn't break the user's flow. +async function saveEntryIcon(entryId, dataUri) { + try { + const r = await fetch(API + '/entries/' + entryId + '/icon', { + method: 'POST', + headers: authHeaders({ 'Content-Type': 'application/json' }), + body: JSON.stringify({ icon_b64: dataUri || '' }), + }); + if (!r.ok) { + // Surface the server's reason so silent persistence failures + // (size cap, auth) stop being invisible bugs. + let msg = 'HTTP ' + r.status; + try { const b = await r.json(); if (b && b.error) msg = b.error; } + catch (_) {} + toast('Icon NOT saved: ' + msg, 'error'); + } + } catch (e) { + toast('Icon save failed: ' + (e && e.message || e), 'error'); + } +} + +// Fetch + save the favicon for one entry. Updates state.entries in-place +// so the next render() picks it up. No-op if the entry already has one. +// opts: { force: bypass "already has icon" skip, manual: bypass the global +// faviconsEnabled toggle (for explicit user actions like the Refresh button) } +async function ensureEntryFavicon(entry, opts) { + opts = opts || {}; + if (!Bridge.active) return; + if (!opts.manual && !state.faviconsEnabled) return; + if (!opts.force && entry.icon_b64) return; + const host = faviconHost(entry.site); + if (!host) return; + const dataUri = await Bridge.fetchFavicon(host); + if (!dataUri) return; + entry.icon_b64 = dataUri; + await saveEntryIcon(entry.id, dataUri); + // Full render() — patching the avatar in place is fragile because + // the avatar also contains the checkbox overlay. + render(); +} + +// Backfill: walk state.entries, fetch missing icons one at a time so we +// don't hammer the upstream. Used by the "Refresh icons" button. +async function backfillFavicons(force) { + if (!Bridge.active) return; + const all = state.entries; + const eligible = all.filter(e => faviconHost(e.site)); + const skipped = all.length - eligible.length; + const targets = eligible.filter(e => force || !e.icon_b64); + if (targets.length === 0) { + if (skipped > 0) { + toast('No icons to fetch — ' + skipped + + ' entries have a non-domain site (e.g. "Gitea")', 'warning'); + } else { + toast('No icons to fetch'); + } + return; + } + toast('Fetching ' + targets.length + ' icon' + (targets.length === 1 ? '' : 's') + '…'); + let ok = 0; + for (const e of targets) { + // Explicit user action — bypass the global toggle so the buttons + // work even when "Fetch website icons" is OFF (the toggle only + // gates auto-fetch on save). + await ensureEntryFavicon(e, { force: !!force, manual: true }); + if (e.icon_b64) ok++; + } + toast('Fetched ' + ok + ' / ' + targets.length + ' icons'); + render(); +} + +async function clearAllFavicons() { + try { + await fetch(API + '/entries/icons/all', { + method: 'DELETE', + headers: authHeaders(), + }); + } catch (e) { + toast('Failed to clear icons', 'error'); + return; + } + state.entries.forEach(e => { e.icon_b64 = null; }); + render(); + toast('Cached icons cleared'); +} + diff --git a/js/app.js b/js/app.js index ab8617d..5300cd1 100644 --- a/js/app.js +++ b/js/app.js @@ -718,120 +718,10 @@ async function api(path, opts) { // ============================================================ // ============================================================ -// FAVICONS (opt-in, cached server-side as base64 data URI) +// FAVICONS — extracted to js/app.favicon.js (§3.1), loaded as a +// separate