Reveal URLs — Architettura
Questo documento descrive come è strutturata l'estensione Reveal URLs e come
funziona la sua logica di base, per i contributori e i lettori tecnici. Cita file
e simboli reali in tutto il testo (nella forma path symbol) così che ogni
affermazione possa essere verificata rispetto al codice sorgente. Per la
descrizione rivolta all'utente consulta il manuale; per i passaggi di
verifica manuale consulta il piano di test manuale.
Panoramica
Reveal URLs è una singola WebExtension MV3 costruita per diversi motori — Chrome,
Edge, Opera, Firefox e Thunderbird — a partire da un unico core di codice puro e
condiviso. (Safari è rinviato: ha un manifest ed è compilabile quando nominato
esplicitamente, ma è escluso dalla build predefinita e dalla CI.) L'estensione
rivela l'URL di destinazione di ogni link accanto al link in un'email
visualizzata, e contrassegna un link il cui testo visibile indica un dominio
registrabile diverso dal suo href — il classico segnale di phishing.
Il repository è un monorepo pnpm/TypeScript (pnpm-workspace.yaml,
package.json con "private": true). Il bundling è un unico script guidato da
esbuild, tooling/build.mjs, che compila i pacchetti condivisi e i sottili punti
di ingresso di ciascun motore in una directory caricabile dist/<target>/.
webextension-polyfill è incluso in ogni script anziché essere usato come
variabile globale a runtime, così lo stesso codice sorgente gira immutato su
Chromium e Gecko.
Due add-on email nativi estendono lo stesso core di rilevamento a superfici che la
WebExtension non può raggiungere: un add-in per Outlook (riquadro attività
Office.js, che raggiunge Outlook su web/Windows/Mac/iOS/Android e Outlook.com) e
un add-on per Gmail (CardService di Apps Script, che raggiunge le app Gmail
web/Android/iOS). Entrambi riutilizzano solo l'analisi PURA — un nuovo modulo
packages/core/src/findings.ts (analyseAnchors/analyseHtml, un modello
Finding[] neutrale rispetto alla piattaforma) che riutilizza la logica
hostMismatch esistente e la derivazione del dominio registrabile di tldts.
Solo l'adattatore host (come il corpo viene letto e analizzato) e la presentazione
differiscono per superficie: il riquadro attività di Outlook analizza il corpo con
il proprio DOMParser e gira lato client; l'add-on per Gmail analizza con
node-html-parser e gira lato server sull'infrastruttura V8 di Apps Script di
Google (hosting gratuito, dato che Google detiene già la posta; distribuito in
modo locale-interattivo tramite clasp). Poiché nessuno dei due framework può
modificare il DOM del messaggio visualizzato in modalità lettura (i metodi
setAsync/prependAsync di Office sono solo per la composizione; CardService
rende delle schede, non l'HTML del messaggio), entrambi gli add-on presentano un
riquadro/scheda di risultati, non un'annotazione in linea — il percorso di
modifica del DOM linkProcessor.ts esistente e REVEAL_URLS_CSS NON sono
riutilizzati da nessuno dei due add-on. Un href relativo viene risolto solo
rispetto a un affidabile <base href> interno all'email e altrimenti saltato, mai
rispetto all'origine della casella/provider (che fabbricherebbe una destinazione).
Il codebase separa nettamente le responsabilità:
packages/corecontiene logica PURA — nessuna API del browser, nessun archivio, nessun sink di markup. È testabile a livello di unità in puro JavaScript ed è l'unica fonte di verità per l'annotazione dei link, la convalida della configurazione, la corrispondenza dei match-pattern, il confronto degli host e la colorazione consapevole del contrasto.packages/webextavvolge le API WebExtension attorno a quel core: il ciclo di vita del contenuto, la registrazione dinamica dei content script, l'interfaccia delle opzioni, il cablaggio del background, l'azione della barra degli strumenti e il percorso di visualizzazione dei messaggi di Thunderbird.extensions/<engine>contiene i sottili punti di ingresso per ciascun motore più ilmanifest.jsondi ogni motore.toolingcontiene gli script di build, di marcatura della versione e di generazione delle icone.featurescontiene gli scenari BDD Cucumber e i loro test double.
Struttura del repository
packages/core — il core puro
Ogni modulo qui non importa alcuna API di estensione
(browser.*/chrome.*/messenger.*) e non usa alcun sink DOM da stringa a
markup (innerHTML/insertAdjacentHTML). Quel contratto è applicato
meccanicamente da packages/core/test/purity.test.ts, che esegue il glob di ogni
src/**/*.ts, rimuove i corpi dei commenti tramite il proprio stripComments
(così che i docblock che legittimamente NOMINANO i token vietati non facciano
scattare la guardia) e poi verifica che né EXTENSION_API_PATTERN né
UNSAFE_DOM_PATTERN corrispondano ad alcun modulo. La superficie pubblica è
riesportata da packages/core/src/index.ts. L'unica dipendenza a runtime è
tldts (per il confronto dei domini consapevole dei suffissi pubblici).
I moduli sono:
packages/core/src/config.ts— lo schemaConfig/SiteRule, i valori predefiniti e l'unico imbuto di convalidanormaliseConfig.packages/core/src/matchPattern.ts— la grammatica dei match-pattern, il matcher, la classificazione per specificità e la copertura delle origini concesse.packages/core/src/linkProcessor.ts— l'annotatore con ambito al documento.packages/core/src/hostMismatch.ts— il controllo di discrepanza sul dominio registrabile.packages/core/src/styles.tsepackages/core/src/contrast.ts— il foglio di stile iniettabile e la colorazione consapevole del contrasto.packages/core/src/safeColour.ts— il predicato di colore sicuroisSafeColour.
packages/webext — i wrapper delle API del browser
I moduli qui POSSONO usare browser.* e sono l'unico luogo in cui si toccano le
API WebExtension. La superficie pubblica è riesportata da
packages/webext/src/index.ts. I moduli sono: content.ts (ciclo di vita del
contenuto), contentRegistration.ts (registrazione dinamica), storage.ts
(persistenza della configurazione), background.ts (comportamento
all'installazione), toolbarAction.ts (interruttore + badge), messageDisplay.ts
e messageDisplayBackground.ts (Thunderbird), e options/options.ts (il
controller della pagina delle impostazioni).
extensions/<engine> — punti di ingresso sottili e manifest
Ogni motore porta il proprio manifest.json e icons/. Chrome, Firefox e
Thunderbird forniscono inoltre un src/ di punti di ingresso; Edge, Opera e Safari
portano solo un manifest e le icone e RIUTILIZZANO il src/ di Chrome (dichiarato
dal descrittore di build — vedi sotto). I punti di ingresso sono deliberatamente
minuscoli: per esempio extensions/chrome/src/content.ts è solo
void createContentController().bootstrap();, e
extensions/chrome/src/background.ts chiama registerBackground,
registerContentReconciliation e registerToolbarToggle. Il punto di ingresso di
background di Firefox è identico a quello di Chrome; quello di Thunderbird
(extensions/thunderbird/src/background.ts) chiama invece registerBackground e
registerMessageDisplay (non ha content script). extensions/_template è uno
scaffold Chromium pronto da copiare, non un target di build.
tooling, features e gli smoke test
tooling contiene build.mjs, version.mjs e icons.mjs. features contiene
gli scenari Gherkin, le loro definizioni dei passaggi e i fedeli test double in
features/support/webext.ts e features/support/world.ts. Ogni motore porta
anche un extensions/<engine>/test/manifest.smoke.test.mjs (quello di _template
è template.smoke.test.mjs) — questi sono il contratto autorevole del manifest
(vedi «Build per motore e il contratto del manifest» sotto).
La pipeline di rivelazione
Il percorso del contenuto inizia da un punto di ingresso del motore come
extensions/chrome/src/content.ts, che chiama
createContentController().bootstrap() da packages/webext/src/content.ts.
Bootstrap
createContentController bootstrap viene eseguito una volta per frame:
- Rivendica il frame in modo sincrono, prima di qualsiasi
await, tramiteclaimBootstrap, che legge/imposta il flagBOOTSTRAP_MARKERsulwindowdel frame. Una voce statica integratacontent_scriptse uno script utente dinamico sovrapposto possono entrambi iniettarecontent.jsnello stesso frame; ilwindowcondiviso del mondo isolato rende questa la corretta guardia una-volta-sola, e rivendicare prima di qualsiasiawaitsignifica che due iniezioni quasi simultanee non possono superarla entrambe. - Carica PRIMA la configurazione persistita tramite
loadConfigWithRetry(BOOTSTRAP_CONFIG_RETRIEStentativi aggiuntivi su un rifiuto transitorio), mantenendo il marcatore durante i tentativi così che un'iniezione sovrapposta che ha già eseguito un no-op non rimanga bloccata. Solo una volta che la lettura riesce inietta il foglio di stile (injectStyles, che assegnaREVEAL_URLS_CSStramitetextContent), così una lettura fallita non aggiunge alcun<style>e un nuovo tentativo non può duplicarlo. - Se il caricamento della configurazione fallisce a ogni tentativo,
releaseBootstrapriporta indietro il marcatore (solo qui, prima che inizi l'annotazione) così che una successiva re-iniezione possa riprovare; una volta chestartè stato eseguito il marcatore deve persistere, perché un nuovo annotatore non possiederebbe i nodi del passaggio precedente. - Quando
config.enabledchiamastart, e registra sempre un listeneronConfigChangedche riapplica la configurazione (coprendo la transizione disabilitato→abilitato).
Risolvere la regola del sito attiva
start (e applyConfig) chiama resolveSiteRule per decidere se — e dove —
annotare. resolveSiteRule prova prima la posizione PROPRIA del documento in
esecuzione tramite resolveForHref, che filtra config.sites alle regole
abilitate e si affida a selectMostSpecific del core così che la corrispondenza
più specifica vinca (un built-in specifico batte un ampio wildcard utente). Quando
la posizione propria non corrisponde a nulla E l'origine propria del documento è
opaca o ereditata — controllato da hasOpaqueOrigin, vero solo per
about:blank/about:srcdoc (ereditate tramite INHERITED_ORIGIN_URLS) o
blob:/data: (OPAQUE_ORIGIN_SCHEMES) — ripiega, nell'ordine, sul frame
SUPERIORE (readTopHref, il caso integrato dell'iframe del messaggio di Proton),
sulla finestra che lo ha APERTO (readOpenerHref, la finestra di lettura a
comparsa di Outlook aperta tramite window.open("about:blank")) e infine su
document.referrer (readReferrer). Ogni lettura esterna è protetta contro
l'accesso cross-origin (che genera un'eccezione) e fornisce un candidato solo
quando leggibile. Un documento ORDINARIO senza corrispondenza è autorevole e non
eredita mai una regola dal contesto ambientale, lasciando il controller inerte.
Limitare l'ambito alla radice del contenuto e osservare le mutazioni
start registra il selettore contentRoot della regola corrispondente, crea
l'annotatore tramite createAnnotator(config), ed elabora ogni radice
corrispondente da selectRoots(doc, contentRoot) (un querySelectorAll racchiuso
in try/catch così che un selettore non valido fallisca in modo sicuro a []).
Osserva poi doc.body (un contenitore stabile) con OBSERVER_OPTIONS
({ childList: true, subtree: true }).
L'observer alimenta processMutations, che vincola ogni mutazione alla radice del
contenuto: rielabora un target di mutazione solo quando è un Element
all'interno di una radice (isWithinRoot, tramite closest), e instrada ogni
Element aggiunto attraverso processAddedNode, che annota un nodo che è dentro una
radice, È una radice, o ne CONTIENE una (un mount SPA pigro). Così sia il
rimescolamento all'interno della radice sia un corpo del messaggio fratello
montato più tardi vengono entrambi catturati, mentre l'interfaccia dell'app al di
fuori delle radici resta intatta. Un annotatore nullo (l'estensione è ferma) è un
no-op.
applyConfig riconcilia una modifica della configurazione in tempo reale: smonta
tutto quando nessuna regola corrisponde o il flag enabled è disattivato; riavvia
(revert + ri-osserva) quando il contentRoot risolto è cambiato; e altrimenti
riscorre sul posto — annotator.setConfig poi annotator.process su ogni radice
corrispondente — senza riavviare l'observer.
Annotare una singola ancora
L'annotatore risiede in packages/core/src/linkProcessor.ts. createAnnotator
restituisce un Annotator su uno stato di chiusura che la pagina non può leggere:
una Map forte indicizzata per identità dell'ancora, un token data-ru per
annotatore da generateToken (crypto.getRandomValues), e la configurazione
corrente. Il suo metodo process(root):
- Pota le voci del registro la cui ancora non è più connessa.
- Raccoglie le ancore candidate con
collectAnchors(i discendentia[href]più la radice stessa quando è una[href], deduplicati). - Risolve la destinazione di ogni ancora con
resolveAnnotatableUrl, che restituiscenull(salta) per un href vuoto/interno alla pagina, un href non analizzabile, qualsiasi schema diverso dahttp:/https:, o un host uguale o sottostante a una voce diignoreHosts; risolve gli href relativi rispetto abaseURIcosì che il risultato sia assoluto. - Prende il percorso veloce idempotente quando
isAnnotationIntactconferma che l'impronta di rendering è invariata e ogni nodo creato è ancora connesso e in posizione; altrimenti annulla l'annotazione obsoleta e ri-renderizza. - Calcola la discrepanza con
hostMismatchsul testo pulito dell'ancora, rispettashowOnlyOnMismatch(lasciando intatti i link onesti), e chiamaannotateAnchor.
annotateAnchor rende solo DOM-SICURO. In modalità "inline" antepone un
<span class="reveal-urls-url"> il cui URL è impostato tramite textContent
(preceduto da una freccia e uno spazio unificatore U+00A0, troncato da
truncateUrl) seguito da un <br>, lasciando intatti i figli propri del link; lo
span connesso ottiene poi uno sfondo di contrasto tramite applyContrastBackdrop.
In modalità "title" salva il title originale (TitleSave), lo sovrascrive con
l'URL — l'unica scrittura sull'ancora — e, su una discrepanza enfatizzata, aggiunge
un badge fratello <span class="reveal-urls-warn"> recante REVEAL_URLS_WARN_LABEL.
Ogni nodo creato è contrassegnato con data-ru="<token>" (mai l'ancora), e ogni
voce registra il proprio configFingerprint così che una modifica della
configurazione di rendering forzi una riscorsa. I colori e le sovrascritture dei
caratteri sono applicati tramite proprietà tipizzate element.style.*
(applyColour/applyFontOverrides), mai interpolati in una stringa CSS.
revertOwned rimuove i nodi creati per riferimento e ripristina il titolo salvato;
revertAll fa questo per ogni ancora posseduta e svuota il registro.
truncateUrl opera sui PUNTI DI CODICE Unicode ([...displayHref]), così che un
taglio non divida mai una coppia surrogata; preserva l'origine schema-e-host e
accorcia il resto con un URL_ELLIPSIS finale.
Discrepanza dell'host
packages/core/src/hostMismatch.ts hostMismatch confronta il testo visibile del
link con il suo href per dominio registrabile, non per hostname grezzo, così che
un sottodominio onesto non venga contrassegnato mentre un sosia sì.
extractHostCandidates divide il testo e riduce ogni token tramite hostCandidate
(che richiede un punto e un suffisso pubblico ICANN riconosciuto da tldts,
rifiutando testo puntato ordinario come e.g). Sia l'host dell'href sia ogni
candidato vengono ridotti al loro dominio registrabile con tldts.getDomain, e
QUALSIASI candidato il cui dominio differisce da quello dell'href produce una
discrepanza — così nominare il vero host malevolo accanto a un host esca non può
sopprimere l'avviso. Non genera mai eccezioni.
Stili e contrasto
packages/core/src/styles.ts contiene il REVEAL_URLS_CSS iniettabile (layout,
spessore, forma fissi e uno sfondo del chip bianco fisso e completamente opaco —
background: #ffffff, opacity: 1; i colori non vi sono MAI interpolati) e gli
helper a runtime applyColour/applyFontOverrides/applyContrastBackdrop.
applyContrastBackdrop legge il colore risolto dell'elemento e il primo sfondo
opaco trovato risalendo dall'elemento STESSO verso l'alto (il suo stesso sfondo —
il bianco predefinito fisso del chip o una sovrascrittura più specifica della
pagina host — prima di qualsiasi antenato) attraverso lo standard getComputedStyle
(ottenuto da element.ownerDocument?.defaultView, così è sicuro rispetto ai frame
e sostituibile con stub) e, quando packages/core/src/contrast.ts
needsWhiteBackdrop segnala che il testo non supera il WCAG AA
(CONTRAST_THRESHOLD) rispetto a quello sfondo E il bianco aiuta davvero, imposta
style.backgroundColor = "white". contrast.ts fornisce parseColour,
relativeLuminance e contrastRatio; analizza le forme rgb()/rgba()/esadecimali
che getComputedStyle restituisce e tratta un colore completamente trasparente
come «non trovato».
Host configurabili e ambito del contenuto
Una SiteRule (packages/core/src/config.ts) dice DOVE viene eseguita
l'annotazione (match, allFrames) e QUALE contenitore ne delimita l'ambito
(contentRoot), più enabled e un flag builtin. I DEFAULT_SITES forniti
coprono Gmail, Proton (con allFrames) ed entrambi gli host di Outlook. Il
Config aggrega gli interruttori globali, i colori, le sovrascritture dei
caratteri e l'array sites.
Il motore dei match-pattern
packages/core/src/matchPattern.ts contiene una grammatica MATCH_PATTERN
deliberatamente RESTRITTIVA al momento dell'aggiunta (solo http/https, wildcard
host iniziale *. opzionale, percorso glob; nessuna porta, nessuno schema *,
nessun <all_urls>). matchesPattern confronta un URL con un pattern convalidato,
delegando il percorso a pathGlobMatches (ogni * corrisponde a qualsiasi
sequenza di caratteri); fallisce in modo sicuro a false. parsePatternParts
divide un pattern convalidato in host/path/scheme/wildcardHost.
Le regole sovrapposte si risolvono con «vince la più specifica»:
compareSiteSpecificity classifica per (1) host esatto rispetto a host wildcard,
(2) host letterale più lungo, (3) percorso più letterale (literalPathLength), poi
(4) un tiebreak ASCII deterministico; selectMostSpecific restituisce la regola
abilitata più specifica che corrisponde a un URL.
La copertura delle origini concesse è un matcher SEPARATO, deliberatamente PIÙ
AMPIO. permissions.getAll() può segnalare concessioni nella grammatica
WebExtension completa (<all_urls>, *://*/*, https://*/*, *://*.host/*,
https://host/*) che il restrittivo MATCH_PATTERN rifiuterebbe.
parseGrantedOrigin le analizza in GrantedOriginParts, e originCovers segnala
se un'origine concessa copre il match di una regola (per esempio un'ampia
concessione https://*/* copre genuinamente ogni regola https).
matchAllowsOriginFallback segnala se il percorso di una regola è esattamente
ORIGIN_FALLBACK_PATH (/*) — l'intersezione sicura dei vincoli di Chromium e
Gecko/Firefox 128 sul fallback dinamico a origine opaca.
Registrazione dinamica
packages/webext/src/contentRegistration.ts registra un content script dinamico
per ogni regola aggiunta dall'utente. I built-in sono serviti dalle voci statiche
content_scripts e non sono MAI registrati dinamicamente. desiredContentScripts
filtra config.sites alle regole non-builtin e abilitate la cui origine
isOriginGranted (delegando a originCovers), mappa ciascuna a un
RegisteredContentScript con un id stabile (contentScriptId, un hash FNV-1a del
match così che l'id usi solo l'insieme di caratteri sicuri per l'API), e imposta
matchOriginAsFallback: true SOLO quando matchAllowsOriginFallback(rule.match) è
vero (un percorso /*). Vincolare il flag in questo modo impedisce che una regola
con percorso non conforme provochi il rifiuto dell'intera registrazione in batch;
una tale regola si registra senza copertura delle finestre a comparsa per scelta
progettuale.
reconcileContentScriptsOnce legge le origini concesse, la configurazione
memorizzata e le registrazioni attive, poi converge con un diff CONSAPEVOLE DELLE
PROPRIETÀ: sameRegistration proietta sia il descrittore desiderato sia la
rilettura attiva su una forma normalizzata (applicando il vero valore predefinito
WebExtension di ogni campo — allFrames/matchOriginAsFallback predefiniti a
false, persistAcrossSessions predefinito a TRUE, runAt predefinito a
document_idle) così che uno script appena registrato risulti UGUALE al
descrittore che lo ha prodotto e non possa sorgere alcun loop di ri-registrazione.
Poiché non esiste updateContentScripts, uno script modificato viene deregistrato
e poi ri-registrato. reconcileContentScripts lo avvolge con una promise in volo
(inFlight) così che due trigger quasi simultanei (una modifica della
configurazione E permissions.onAdded che scattano entrambi su un'aggiunta di
sito) siano serializzati anziché in concorrenza. registerContentReconciliation
collega i trigger (permissions.onAdded/onRemoved, onConfigChanged) e converge
una volta all'avvio; è importato solo dai punti di ingresso di background di Chrome
e Firefox.
Il flusso di consenso ai permessi
I manifest statici dichiarano optional_host_permissions (http://*/*,
https://*/*) così che un utente possa concedere un host web aggiuntivo arbitrario
a runtime. addSite di packages/webext/src/options/options.ts convalida l'INTERA
regola candidata (match E content-root) tramite normaliseSiteRule PRIMA di
richiedere qualsiasi permesso, deriva l'origine dell'host con matchOrigin, e
chiama browser.permissions.request all'interno del gesto di clic del pulsante
Aggiungi; solo alla concessione aggiunge una riga builtin:false e persiste
attraverso il percorso di scrittura canonico.
Configurazione, archiviazione e il pattern canonico
L'unico imbuto di convalida
normaliseConfig in packages/core/src/config.ts è l'unico percorso di lettura
canonico: coerce e delimita ogni campo, ripiegando sul valore predefinito per
qualsiasi cosa non valida, e non genera mai eccezioni. È costruito a partire da
validatori per campo — normaliseBoolean, normaliseRenderMode,
normaliseMaxLength, normaliseIgnoreHosts (riducendo ogni voce a un semplice
hostname punycode tramite bareHostname),
normaliseMatchColour/normaliseMismatchColour (controllati da safeColour.ts
isSafeColour), normaliseCssSize, normaliseFontWeight, normaliseContentRoot
(delimitato da CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH),
normaliseMatchPattern (controllato da MATCH_PATTERN) e normaliseSites.
normaliseSites scarta i rifiuti, deduplica per match, FORZA builtin: true su
qualsiasi regola il cui match è uguale a quello di un built-in (chiudendo il
vettore corrispondenza-esatta-oscura-built-in / doppia-iniezione), reinserisce ogni
built-in mancante (così che un archivio manomesso non possa eliminare un provider
di base) e ordina alfanumericamente per match per un ordine canonico.
configFingerprint calcola l'hash solo dei campi che influenzano il rendering
(escludendo enabled, ignoreHosts e sites, che guidano avvio/arresto e salto
anziché la riscorsa) tramite fnv1a.
Archiviazione
packages/webext/src/storage.ts avvolge l'API di archiviazione WebExtension.
configArea usa browser.storage.sync quando disponibile (così le impostazioni si
spostano tra dispositivi) e ripiega su browser.storage.local (per esempio su
Thunderbird); configAreaName segnala quale è attiva. getConfig, setConfig e
onConfigChanged incanalano tutti il loro valore grezzo attraverso
normaliseConfig, così che un archivio malformato o manomesso non possa mai
consegnare una Config non valida a un chiamante. onConfigChanged ignora inoltre
gli eventi provenienti dall'area di archiviazione INATTIVA e le modifiche a chiavi
non correlate.
I due add-on riutilizzano lo stesso imbuto lettura -> normaliseConfig ->
restituzione sui propri archivi host. extensions/outlook/src/roamingStorage.ts
del riquadro attività di Outlook avvolge Office.context.roamingSettings
(get/set sincroni, salvati con saveAsync);
packages/gmail/src/propertiesStorage.ts dell'add-on per Gmail avvolge
PropertiesService.getUserProperties() (per utente, in roaming tra i dispositivi
di quell'utente, codificato in JSON sotto un'unica chiave, e che NON richiede alcun
ambito OAuth aggiuntivo — mai le ScriptProperties condivise). Entrambi espongono
getConfig/setConfig più gli accessor rilevanti per la scheda
getIgnoreHosts/getHighlightMismatch e proteggono una superficie host non
disponibile con un errore dedicato. Il trigger della homepage di Gmail rende una
scheda di impostazioni CardService da getConfig, e il gestore di invio del modulo
onSaveSettings unisce ignoreHosts/highlightMismatch analizzati sulla
configurazione corrente e la persiste tramite setConfig; il trigger contestuale
infila ignoreHosts nell'adattatore e highlightMismatch nel costruttore della
scheda, leggendo in modo difensivo così che un guasto dell'archiviazione ripieghi
sui valori predefiniti anziché interrompere l'analisi del messaggio.
Il pattern configurazione-canonica + aggiornamento-mirato
Tre percorsi di scrittura leggono la configurazione canonica, modificano esattamente una cosa e la riscrivono — senza mai salvare modifiche del modulo non salvate né ri-renderizzare il modulo:
removeSite(options.ts): leggegetConfig, persistesetConfigsu un arraysitescon solo la regola rimossa filtrata via (persiste PRIMA, poirow.remove(), poi unpermissions.removecondizionale deciso dalla configurazione successiva). Un fallimento di persistenza generaSiteRemovalSaveErrore lascia il DOM intatto; una revoca fallita generaSitePermissionRevokeErrordopo che la riga è già scomparsa.addSite(options.ts): descritto sopra — convalida, richiede il permesso, aggiunge la riga, poisaveOptions.toggleEnabled(options.ts): l'interruttore principale Abilita istantaneo commuta SOLOenabledsulla configurazione memorizzata e la riscrive, senza ri-renderizzare (così una modifica in corso non viene mai sovrascritta); su una scrittura fallita riporta la casella di spunta al valore memorizzato.
Il toggleEnabled proprio dell'azione della barra degli strumenti
(packages/webext/src/toolbarAction.ts) segue lo stesso pattern dal lato del
background.
Build per motore e il contratto del manifest
tooling/build.mjs guida un'unica build esbuild per ogni target WebExtension. Il
descrittore TARGETS nomina il TARGET SORGENTE di ciascun target: Chrome, Firefox
e Thunderbird forniscono il proprio src/; Edge, Opera e Safari dichiarano
chrome come loro sorgente; l'add-in per Outlook (Office.js) è la sorgente di se
stesso. ACTIVE_TARGETS (Chrome, Edge, Firefox, Opera e Thunderbird — Safari e
Outlook sono esclusi) è ciò che --all/--package compilano; Safari e Outlook
compilano solo quando nominati esplicitamente, e l'add-on per Gmail (Apps Script) è
un bundle esbuild separato (make build-gmail) al di fuori del ciclo --all. Quel
bundle di Gmail ha come target il runtime V8 di Apps Script, che non ha né moduli
ES né un URL nativo: è compilato come ESM e poi gli viene rimossa l'istruzione
finale export {…} (così che le funzioni trigger rimangano variabili globali di
primo livello che Apps Script può invocare), e include un piccolo polyfill di URL
(installato solo quando URL è assente) così che il new URL(...) del core
condiviso risolva i link lato server. buildTarget pulisce dist/<target>/, copia
il manifest.json e icons/ per target alla lettera, raggruppa ogni script
sorgente come IIFE (gli INJECTED_SCRIPTS iniettati content.ts/messageDisplay.ts
non possono essere moduli ES, e anche i background sono classici worker/event page)
e la pagina delle opzioni condivisa come ESM, e copia options.html/options.css
da packages/webext/src/options/. webextension-polyfill è incluso in ogni script.
tooling/version.mjs marca le versioni: prende MAJOR.MINOR dal package.json
radice e aggiunge un numero di BUILD auto-incrementante (letto dal terzo componente
esistente più alto tra i manifest dei target) in ogni manifest di target,
mantenendoli allineati. La maggior parte dei target porta un singolo
manifest.json; l'add-in per Outlook porta due manifest versionati (manifest.json
e manifest.xml) ed entrambi vengono marcati, e anche lo scaffold _template
viene marcato in modo allineato. Il package.json radice è mantenuto alla stessa
versione, ma resta semver valido (una versione a quattro parti è consentita in un
manifest, mai in package.json). tooling/icons.mjs (eseguito tramite
make icons) rasterizza l'unica sorgente vettoriale assets/icon.svg negli
icons/icon48.png e icons/icon128.png di ciascun target, provando cairosvg, poi
inkscape, poi ImageMagick.
Il contratto del manifest — gli esatti campi richiesti per motore — è l'unica fonte
di verità in ogni extensions/<engine>/test/manifest.smoke.test.mjs (per esempio
assertManifestShape di extensions/chrome/test/manifest.smoke.test.mjs, che
compila anche il target e conferma che ogni file referenziato si risolva nella
dist). Secondo la convenzione di de-duplicazione del progetto, la forma campo per
campo NON è rienumerata qui; fai riferimento a quegli smoke test e alla nota
«Manifest contract» nel piano di test manuale.
Internazionalizzazione (i18n)
Reveal URLs è localizzato in molte lingue oltre all'inglese.
L'insieme ha un'unica fonte in packages/core/src/locales.json
(SUPPORTED_LOCALES, i nomi nativi e il valore predefinito), che leggono sia i
bundle dell'estensione (tramite l'import JSON risolto) sia la build del sito web in
puro Node. Ogni stringa non inglese è tradotta automaticamente e in attesa di
revisione umana; il marcatore di provenienza è per formato (i _locales usano la
description di ogni messaggio, i dizionari del chrome del sito web lo registrano
in site/i18n/README.md, e le sorgenti dei documenti tradotti portano un commento
HTML in prima riga).
Ci sono tre superfici di localizzazione indipendenti, deliberatamente non interconnesse, e ciascuna è divisa lungo una linea build-time / runtime.
- La pagina delle opzioni (AD-1). Fornisce markup inglese STATICO; ogni nodo
traducibile porta un'annotazione
data-i18n="<key>"(testo) odata-i18n-<attr>="<key>"(attributo). A runtimepackages/webext/src/options/locale.tsè il livello di applicazione: esegue ilfetchdel_locales/<code>/messages.jsonimpacchettato della pagina stessa (tramiteruntime.getURL, senza bisogno diweb_accessible_resources), riscrive ogni nodo annotato SOLO tramitetextContent(così che i suggerimenti di esempio<code>del modulo restino intatti), e imposta<html lang>. La pagina è COMPLETAMENTE localizzata: accanto alla prosa statica, le etichette delle righe dei siti e il pulsante Rimuovi costruiti in JS portano una chiavedata-i18n(con un testo iniziale tradotto contranslate) così che lo stesso livello di applicazione rilocalizzi le righe renderizzate senza ri-renderizzare, e le stringhe VERAMENTE dinamiche — la riga di stato, il feedback di aggiunta del sito e la riga della versione — vengono renderizzate tramitetranslate(<key>)dilocale.ts(currentMessages → ripiego inglese → chiave) al momento della chiamata. Il ripiego inglese è il catalogo_locales/en/messages.jsonIMPORTATO (incluso) inoptions.jsal momento della build, così chetranslatesi risolva in inglese leggibile in modo SINCRONO dal caricamento del modulo — una stringa dinamica renderizzata prima che il selettore si assesti, o dopo che un fetch del catalogo a runtime fallisce, non degrada mai a una chiave di messaggio grezza.locale.tspubblica la mappa dei messaggi applicata e attiva i suoi listeneronLocaleChangedopo ogni applicazione, così cheoptions.tsri-renderizzi lo stato/feedback/versione attualmente mostrati e ri-punti il link Manuale online a ogni cambio. La riga della versione usa un messaggiooptionsVersionil cui segnaposto{version}viene sostituito con il numero di build (token tradotto, numero alla lettera). Il link al manuale segue la locale attiva alla sua variante di documento (AD-9): l'inglese mantiene ilmanual.htmldi primo livello, ogni altra locale aprenl/handleiding.html, preservando il marcatore di versione?v=. La locale attiva è la scelta salvata dall'utente o, in mancanza, la lingua del browser risolta a una lingua di base supportata daresolveLocaledel core (AD-5); una locale non supportata o fallita lascia l'inglese pre-renderizzato (etranslatedegrada allora al ripiego inglese). Il selettore Lingua di visualizzazione del manuale persiste la sua scelta sotto una chiave dedicatauiLocaleinstorage.local— mai nellaConfigsincronizzata, così è per dispositivo e non si sposta mai tra dispositivi. - Il chrome del sito web (AD-2). Il generatore di sito statico
tooling/site.mjsrenderizza lato server ogni nodo del chrome in inglese annotandolo condata-i18n, e mantiene<html lang="en-GB">(AD-4). Il loader lato browsersite/scripts/i18n.mjs(un modulo ES privo di dipendenze) scambia poi il chrome nella lingua del visitatore per precedenza — una sceltalocalStoragepersistita (revealUrlsSiteLocale) → la prima corrispondenza di base supportata innavigator.languages(AD-5) → inglese — eseguendo il fetch disite/i18n/<code>.json. A differenza del livello di applicazione delle opzioni, scambia tramiteinnerHTML, perché i valori del chrome del sito portano markup inline affidabile (<code>,<a>); un test di parità fissa la sequenza tag/attributo/URL di ogni valore non inglese come identica byte per byte all'inglese così che una traduzione non possa mai eliminare un link o rompere un tag. Una chiave mancante mantiene lo snapshot inglese pre-renderizzato; un fetch fallito lascia la pagina inglese intatta (degradazione elegante, mai vuota). - Le pagine di documentazione (AD-8/AD-9). Le pagine di documentazione sono
renderizzate PER LOCALE al momento della build, non scambiate a runtime. I
documenti inglesi restano ai percorsi di primo livello; ogni altra locale ottiene
una copia sotto una cartella
<code>/, da undocs/<code>/<DOC>.mdtradotto quando presente, altrimenti la sorgente inglese — un ripiego per pagina chebuildSiteREGISTRA NEI LOG anziché omettere silenziosamente. Il CORPO del documento non porta alcundata-i18n(è markdown renderizzato grezzo), così il loader del chrome a runtime non lo tocca mai; solo i nodi del chrome condiviso vengono scambiati. Il selettore di lingua della pagina di documentazione NAVIGA alla pagina della locale corrispondente anziché scambiare sul posto. - Localizzazione dei link interni (Finding 2). La lingua selezionata viene
mantenuta nella navigazione tra documenti. Ogni ancora di documento INTERNA (le
cinque pagine
architecture.html,index.html,licence.html,manual.html,privacy.html) è contrassegnata condata-doc-link="<page>"dal generatore, e nel caso senza JavaScript il suo href si risolve già alla variante di locale propria della pagina CORRENTE (navDocHref— le tre pagine di documentazione per locale si localizzano; le pagine solo inglesiindex.html/licence.htmlrestano di primo livello). IlrewriteDocLinksdel loader a runtime ri-punta poi ogni ancora di documento interna — quelle contrassegnate E qualsiasi ancora semplice iniettata dal catalogo comemanual.html#installing— alla variante della locale ATTIVA, preservando qualsiasi#hash/?querye senza mai toccare i link esterni. Su una pagina di documentazione, una scelta MEMORIZZATA che differisce dalla locale propria della pagina NAVIGA alla variante corrispondente; la guardia del loop scatta solo su una scelta memorizzata esplicita (mai una preferenza di lingua del navigatore da sola) e solo quando il target differisce dalla pagina già mostrata. I cataloghi stessi mantengono i loro URL interni inglesi, così il test di parità resta verde — la localizzazione è un puro passaggio a runtime sul DOM.
Soppressione dei «Siti attivi» nel client di posta (AD-3)
Un target client di posta (oggi Thunderbird) vede già ogni messaggio renderizzato,
quindi l'editor «Siti attivi» per host è ridondante lì e viene rimosso. La
decisione è veicolata da un flag documentato del descrittore per target,
mailClient: true, su TARGETS.thunderbird di tooling/build.mjs — mai un nome di
target hardcoded nella logica di build o dell'interfaccia — ed è implementata in
due metà:
- Build-time.
copyAssetssi basa sudescriptorFor(target).mailClient: per un target client di posta esegue la trasformazione puraremoveSitesSection(html)(che rimuove l'intero editor host<section class="sites">, compreso ogni controllo di aggiunta sito, e generaMissingSourceErrorse la sezione è assente così che una modifica del markup non possa mai distribuire silenziosamente l'editor) e scrive il risultato; i target browser ricevonooptions.htmlbyte per byte tramitecpSync. - Runtime. Il controller incluso
packages/webext/src/options/options.tstollera la sezione assente:renderSites/readSites/applyConfig/readFormsondano#sites-listed eseguono un no-op anziché generareOptionsFieldMissingError, ereadFormOMETTE la chiavesitesquando la sezione è scomparsa. Poiché ometteresiteslascerebbe anormaliseConfigreinserire i valori predefiniti integrati ed eliminare i siti dell'utente,saveOptionsripristina isitesmemorizzati prima della normalizzazione ogni volta che la sezione è assente — è questo che fa sì che un salvataggio in un client di posta preservi gli host configurati. Una guardia a runtime,guardMailClientSites, rimuove la sezione se è ancora presente, rilevando un client di posta dalla presenza di API/funzionalità (messages/messageDisplay/scripting.messageDisplaysulbrowseriniettato), mai dal nome del target; viene eseguita per prima ininitOptionscosì che il successivo cablaggio di aggiunta/elenco salti naturalmente gli elementi assenti. In un runtime browser la guardia è un no-op.
Postura di sicurezza e privacy
- Nessuna rete o esfiltrazione. Nessun modulo esegue
fetch/XHRo qualsiasi altra chiamata di rete; l'estensione legge solo la configurazione memorizzata e il DOM della pagina. - Solo DOM sicuro. L'annotazione e il foglio di stile iniettato usano
esclusivamente
createElement/textContent/proprietà tipizzatestyle.*, maiinnerHTML/insertAdjacentHTML/eval. Per il core questo è applicato dapackages/core/test/purity.test.ts. - Tutto l'input esterno/di configurazione normalizzato. Ogni lettura e
scrittura passa per
normaliseConfig; i colori superanoisSafeColour; i match pattern superanoMATCH_PATTERN; i selettori della radice del contenuto sono delimitati daCONTENT_ROOT_PATTERNe vengono passati solo aquerySelectorAll/closest, mai interpolati in markup o CSS. - Privilegio minimo. Le origini webmail integrate sono
host_permissionsfisse; gli host aggiuntivi sono a consenso esplicito tramiteoptional_host_permissionse unpermissions.requestvincolato a un gesto. L'azione della barra degli strumenti non dichiara alcun popup. - Raccolta dati Gecko. I manifest di Firefox e Thunderbird dichiarano
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Licenza. Il progetto è AGPL-3.0-only (
LICENSE,package.json).
Test
- I test unitari Vitest coprono entrambi i pacchetti:
packages/core/test/*(configurazione, contrasto, discrepanza dell'host, link processor, stili e la guardia di purezza) epackages/webext/test/*(contenuto, archiviazione, opzioni, registrazione, barra degli strumenti, visualizzazione dei messaggi e un test end-to-end di sito personalizzato). Ogni pacchetto eseguevitest run. - Gli scenari BDD Cucumber risiedono in
features/(collegati dacucumber.json) e guidano i moduli reali@reveal-urls/webextattraverso fedeli test double infeatures/support/webext.ts— in particolarenormaliseReadBack, che rispecchia il motore che OMETTE i flag dal valore falso in rilettura così che un'imbracatura indulgente non possa nascondere un loop di ri-registrazione — su un documento jsdom configurato infeatures/support/world.ts. - Gli smoke test del manifest per target
(
extensions/<engine>/test/manifest.smoke.test.mjs, ilnode:testintegrato di Node) verificano la forma esatta di ogni manifest e che la dist compilata risolva ogni file referenziato.tooling/test/aggiunge test di build, versione e definition-of-done. - La CI (
.github/workflows/ci.yml) vincola ogni push e pull request amake test,make bddemake lint(eseguiti direttamente sul runner, sovrascrivendoRUN/IMAGE_DEPDocker del Makefile). La marcatura della versione e l'impacchettamento sono deliberatamente esclusi; risiedono nel separato.github/workflows/release.ymlattivato dai tag.