Reveal URLs — Arhitectură
Acest document descrie cum este structurată extensia Reveal URLs și cum funcționează
logica sa de bază, pentru contribuitori și cititori tehnici. Citează fișiere și
simboluri reale peste tot (în forma path symbol), astfel încât orice afirmație să poată fi verificată
față de sursă. Pentru descrierea orientată către utilizator, consultați manualul; pentru
pașii de verificare manuală, consultați planul de testare manuală.
Prezentare generală
Reveal URLs este o singură extensie WebExtension MV3 construită pentru mai multe motoare — Chrome,
Edge, Opera, Firefox și Thunderbird — dintr-un singur nucleu de cod comun, pur. (Safari
este amânat: are un manifest și este compilabil când este numit explicit, dar este
exclus din build-ul implicit și din CI.) Extensia afișează URL-ul de destinație al fiecărui
link lângă link într-un e-mail redat și semnalează un link al cărui
text vizibil indică un domeniu înregistrabil diferit de href-ul său — indiciul clasic
de phishing.
Depozitul este un monorepo pnpm/TypeScript (pnpm-workspace.yaml,
package.json cu "private": true). Împachetarea este un singur script bazat pe esbuild,
tooling/build.mjs, care compilează pachetele comune și punctele de intrare subțiri ale fiecărui
motor într-un director încărcabil dist/<target>/.
webextension-polyfill este împachetat în fiecare script, în loc să fie folosit ca
global de runtime, astfel încât aceeași sursă rulează neschimbată în Chromium și Gecko.
Două suplimente native de e-mail extind același nucleu de detecție la suprafețe pe care
WebExtension nu le poate atinge: un supliment Outlook (panou de activități Office.js, care ajunge la
Outlook pe web/Windows/Mac/iOS/Android și Outlook.com) și un supliment Gmail
(CardService Apps Script, care ajunge la aplicațiile Gmail web/Android/iOS). Amândouă reutilizează doar
analiza PURĂ — un nou modul packages/core/src/findings.ts
(analyseAnchors/analyseHtml, un model Finding[] neutru față de platformă) care reutilizează
logica existentă hostMismatch și derivarea domeniului înregistrabil tldts. Doar
adaptorul de gazdă (cum este citit și analizat corpul) și prezentarea diferă per
suprafață: panoul de activități Outlook analizează corpul cu propriul DOMParser și rulează
pe partea de client; suplimentul Gmail analizează cu node-html-parser și rulează
pe partea de server, pe infrastructura Apps Script V8 a Google (găzduire gratuită, deoarece
Google deține deja e-mailul; implementat local-interactiv prin clasp). Deoarece
niciunul dintre cadre nu poate muta DOM-ul mesajului redat, în mod-citire (setAsync/prependAsync ale Office sunt doar pentru compunere;
CardService redă carduri, nu HTML de mesaj), ambele suplimente prezintă un panou/card de constatări, nu o adnotare în linie —
calea existentă de mutare a DOM din linkProcessor.ts și REVEAL_URLS_CSS NU sunt
reutilizate de niciunul dintre suplimente. Un href relativ este rezolvat doar față de un
<base href> de încredere din e-mail și altfel omis, niciodată față de originea căsuței/furnizorului
(care ar fabrica o destinație).
Baza de cod separă preocupările clar:
packages/coreconține logică PURĂ — fără API-uri de browser, fără stocare, fără sink-uri de marcaj. Este testabilă unitar în JavaScript simplu și este singura sursă de adevăr pentru adnotarea linkurilor, validarea configurației, potrivirea tiparelor, comparația gazdelor și colorarea conștientă de contrast.packages/webextînfășoară API-urile WebExtension în jurul acelui nucleu: ciclul de viață al conținutului, înregistrarea dinamică a scripturilor de conținut, interfața de opțiuni, cablajul de fundal, acțiunea din bara de instrumente și calea de afișare a mesajelor Thunderbird.extensions/<engine>conține punctele de intrare subțiri per motor plusmanifest.json-ul fiecărui motor.toolingconține scripturile de build, de ștampilare a versiunii și de generare a pictogramelor.featuresconține scenariile BDD Cucumber și dublurile lor de test.
Structura depozitului
packages/core — nucleul pur
Fiecare modul de aici nu importă niciun API de extensie (browser.*/chrome.*/messenger.*)
și nu folosește niciun sink DOM de la șir la marcaj (innerHTML/insertAdjacentHTML). Acel
contract este impus mecanic de packages/core/test/purity.test.ts, care
parcurge fiecare src/**/*.ts, elimină corpurile de comentarii prin propriul stripComments
(astfel încât docblock-urile care NUMESC legitim token-urile interzise să nu declanșeze
garda) și apoi afirmă că nici EXTENSION_API_PATTERN, nici
UNSAFE_DOM_PATTERN nu se potrivesc vreunui modul. Suprafața publică este reexportată din
packages/core/src/index.ts. Singura dependență de runtime este tldts (pentru
comparația domeniilor conștientă de sufixul public).
Modulele sunt:
packages/core/src/config.ts— schemaConfig/SiteRule, valorile implicite și singura pâlnie de validarenormaliseConfig.packages/core/src/matchPattern.ts— gramatica tiparelor de potrivire, potrivitorul, clasarea specificității și acoperirea originii acordate.packages/core/src/linkProcessor.ts— adnotatorul limitat la document.packages/core/src/hostMismatch.ts— verificarea nepotrivirii domeniului înregistrabil.packages/core/src/styles.tsșipackages/core/src/contrast.ts— foaia de stil injectabilă și colorarea conștientă de contrast.packages/core/src/safeColour.ts— predicatul de culoare sigurăisSafeColour.
packages/webext — înfășurările de API-uri de browser
Modulele de aici POT folosi browser.* și sunt singurul loc unde sunt atinse API-urile
WebExtension. Suprafața publică este reexportată din packages/webext/src/index.ts.
Modulele sunt: content.ts (ciclul de viață al conținutului), contentRegistration.ts
(înregistrare dinamică), storage.ts (persistența configurației), background.ts
(comportament la instalare), toolbarAction.ts (comutator + insignă), messageDisplay.ts
și messageDisplayBackground.ts (Thunderbird) și options/options.ts (controlerul
paginii de setări).
extensions/<engine> — puncte de intrare subțiri și manifeste
Fiecare motor poartă propriul manifest.json și icons/. Chrome, Firefox și
Thunderbird livrează suplimentar un src/ de puncte de intrare; Edge, Opera și Safari
poartă doar un manifest și pictograme și REUTILIZEAZĂ src/-ul Chrome (declarat de descriptorul
de build — vezi mai jos). Intrările sunt în mod deliberat minuscule: de exemplu
extensions/chrome/src/content.ts este doar
void createContentController().bootstrap();, iar
extensions/chrome/src/background.ts apelează registerBackground,
registerContentReconciliation și registerToolbarToggle. Intrarea de fundal a Firefox
este identică cu cea a Chrome; cea a Thunderbird
(extensions/thunderbird/src/background.ts) apelează în schimb registerBackground și
registerMessageDisplay (nu are scripturi de conținut). extensions/_template
este un schelet Chromium gata de copiat, nu o țintă de build.
tooling, features și testele de fum
tooling conține build.mjs, version.mjs și icons.mjs. features conține
scenariile Gherkin, definițiile lor de pași și dublurile de test fidele din
features/support/webext.ts și features/support/world.ts. Fiecare motor poartă și
un extensions/<engine>/test/manifest.smoke.test.mjs (cel al _template este
template.smoke.test.mjs) — acestea sunt contractul autoritar al manifestului (vezi
„Build-ul per motor și contractul manifestului" mai jos).
Conducta de afișare
Calea de conținut începe la o intrare de motor precum
extensions/chrome/src/content.ts, care apelează createContentController().bootstrap()
din packages/webext/src/content.ts.
Bootstrap
createContentController bootstrap rulează o singură dată per cadru:
- Revendică cadrul sincron, înainte de orice
await, princlaimBootstrap, care citește/setează flag-ulBOOTSTRAP_MARKERpewindow-ul cadrului. O intrarecontent_scriptsîncorporată statică și un script de utilizator dinamic suprapus pot injecta ambelecontent.jsîn același cadru;window-ul izolat comun face din aceasta garda corectă de o-singură-dată, iar revendicarea înainte de oriceawaitînseamnă că două injectări aproape simultane nu pot trece amândouă de ea. - Încarcă configurația persistată MAI ÎNTÂI prin
loadConfigWithRetry(BOOTSTRAP_CONFIG_RETRIESîncercări suplimentare la o respingere tranzitorie), păstrând marcatorul de-a lungul reîncercărilor, astfel încât o injectare suprapusă care a devenit deja no-op să nu rămână blocată. Doar după ce citirea reușește injectează foaia de stil (injectStyles, care atribuieREVEAL_URLS_CSSprintextContent), astfel încât o citire eșuată nu adaugă niciun<style>, iar o reîncercare nu-l poate duplica. - Dacă încărcarea configurației eșuează la fiecare încercare,
releaseBootstrapderulează marcatorul înapoi (doar aici, înainte ca adnotarea să înceapă), astfel încât o reinjectare ulterioară să poată reîncerca; odată cestarta rulat, marcatorul trebuie să persiste, deoarece un adnotator proaspăt nu ar deține nodurile trecerii anterioare. - Când
config.enabled, apeleazăstartși înregistrează întotdeauna un ascultătoronConfigChangedcare reaplică configurația (acoperind tranziția dezactivat→activat).
Rezolvarea regulii de site active
start (și applyConfig) apelează resolveSiteRule pentru a decide dacă — și unde
— să adnoteze. resolveSiteRule încearcă mai întâi PROPRIA locație a documentului care rulează
prin resolveForHref, care filtrează config.sites la regulile activate și
apelează selectMostSpecific al nucleului, astfel încât cea mai specifică potrivire să câștige (o
regulă încorporată specifică bate o metacaractere largă de utilizator). Când propria locație nu se potrivește cu nimic ȘI
originea proprie a documentului este opacă sau moștenită — controlat de hasOpaqueOrigin,
adevărat doar pentru about:blank/about:srcdoc (moștenită prin INHERITED_ORIGIN_URLS)
sau blob:/data: (OPAQUE_ORIGIN_SCHEMES) — revine, în ordine, la
cadrul de SUS (readTopHref, cazul iframe-ului de mesaj Proton încorporat), fereastra
OPENER (readOpenerHref, fereastra de citire pop-out Outlook deschisă prin
window.open("about:blank")) și, în final, document.referrer (readReferrer).
Fiecare citire externă este protejată împotriva accesului cross-origin (care aruncă o excepție) și
contribuie cu un candidat doar când este citibilă. Un document nepotrivit OBIȘNUIT este
autoritar și nu moștenește niciodată o regulă din contextul ambiant, lăsând
controlerul inert.
Limitarea la rădăcina de conținut și observarea mutațiilor
start înregistrează selectorul contentRoot al regulii potrivite, creează adnotatorul
prin createAnnotator(config) și procesează fiecare rădăcină potrivită din
selectRoots(doc, contentRoot) (un querySelectorAll înfășurat în try/catch, astfel încât un
selector invalid să eșueze în siguranță la []). Apoi observă doc.body (un container
stabil) cu OBSERVER_OPTIONS ({ childList: true, subtree: true }).
Observatorul alimentează processMutations, care controlează fiecare mutație pe rădăcina de
conținut: reprocesează un target de mutație doar când este un Element din interiorul unei
rădăcini (isWithinRoot, prin closest) și direcționează fiecare Element adăugat prin
processAddedNode, care adnotează un nod care este în interiorul unei rădăcini, ESTE o rădăcină sau
CONȚINE una (o montare SPA leneșă). Așadar, agitația din interiorul rădăcinii și un corp de mesaj
frate care se montează mai târziu sunt ambele prinse, în timp ce interfața aplicației din afara rădăcinilor este lăsată
neatinsă. Un adnotator nul (extensia este oprită) este un no-op.
applyConfig reconciliază o modificare de configurație live: demontează când nicio regulă nu se potrivește
sau flag-ul activat este oprit; repornește (revenire + reobservare) când contentRoot-ul
rezolvat s-a schimbat; și altfel refluează pe loc — annotator.setConfig
apoi annotator.process peste fiecare rădăcină potrivită — fără a reporni
observatorul.
Adnotarea unei singure ancore
Adnotatorul trăiește în packages/core/src/linkProcessor.ts. createAnnotator
returnează un Annotator peste o stare de închidere pe care pagina nu o poate citi: un Map puternic
indexat după identitatea ancorei, un token data-ru per adnotator din
generateToken (crypto.getRandomValues) și configurația curentă. Metoda sa
process(root):
- Elimină intrările de registru a căror ancoră nu mai este conectată.
- Colectează ancorele candidate cu
collectAnchors(descendenta[href]plus rădăcina însăși când este una[href], deduplificat). - Rezolvă destinația fiecărei ancore cu
resolveAnnotatableUrl, care returneazănull(omite) pentru un href gol/în aceeași pagină, un href neanalizabil, orice schemă, alta decâthttp:/https:, sau o gazdă egală cu sau sub o intrareignoreHosts; rezolvă href-urile relative față debaseURI, astfel încât rezultatul să fie absolut. - Ia calea rapidă idempotentă când
isAnnotationIntactconfirmă că amprenta de randare este neschimbată și fiecare nod creat este încă conectat și pe poziție; altfel revine la adnotarea învechită și re-randează. - Calculează nepotrivirea cu
hostMismatchpe textul curat al ancorei, respectăshowOnlyOnMismatch(lăsând linkurile cinstite neatinse) și apeleazăannotateAnchor.
annotateAnchor randează doar DOM-SIGUR. În modul "inline" prepune un
<span class="reveal-urls-url"> al cărui URL este setat prin textContent (prefixat cu
o săgeată și un spațiu neîntrerupt U+00A0, trunchiat de truncateUrl), urmat de
un <br>, lăsând copiii proprii ai linkului neatinși; span-ul conectat primește apoi un
fundal de contrast prin applyContrastBackdrop. În modul "title" salvează
title-ul original (TitleSave), îl suprascrie cu URL-ul — singura scriere pe
ancoră — și, la o nepotrivire accentuată, adaugă un frate
<span class="reveal-urls-warn"> insignă care poartă REVEAL_URLS_WARN_LABEL. Fiecare
nod creat este etichetat data-ru="<token>" (niciodată ancora), iar fiecare intrare
înregistrează configFingerprint-ul său, astfel încât o modificare de configurație de randare să forțeze un reflux. Culorile
și suprascrierile de font sunt aplicate prin proprietăți tipate element.style.*
(applyColour/applyFontOverrides), niciodată interpolate într-un șir CSS.
revertOwned elimină nodurile create prin referință și restabilește title-ul salvat;
revertAll face acest lucru pentru fiecare ancoră deținută și golește registrul.
truncateUrl operează peste PUNCTELE DE COD Unicode ([...displayHref]), astfel încât o tăietură
nu împarte niciodată o pereche surogat; păstrează originea schemă-și-gazdă și
scurtează restul cu un URL_ELLIPSIS final.
Nepotrivire de gazdă
packages/core/src/hostMismatch.ts hostMismatch compară textul vizibil al linkului
cu href-ul său după domeniul înregistrabil, nu după numele brut de gazdă, astfel încât un subdomeniu
cinstit să nu fie semnalat, în timp ce unul care imită este. extractHostCandidates împarte
textul și reduce fiecare token prin hostCandidate (care necesită un punct și un
sufix public ICANN recunoscut de tldts, respingând text cu puncte obișnuit precum
e.g). Atât gazda href, cât și fiecare candidat sunt reduse la domeniul lor înregistrabil
cu tldts.getDomain, iar ORICE candidat al cărui domeniu diferă de cel al href-ului
produce o nepotrivire — astfel încât numirea gazdei rău-intenționate reale alături de o gazdă-momeală
nu poate suprima avertizarea. Nu aruncă niciodată o excepție.
Stiluri și contrast
packages/core/src/styles.ts conține REVEAL_URLS_CSS injectabil (aspect, grosime,
formă fixe și un fundal de cip fix alb, complet opac — background: #ffffff,
opacity: 1; culorile NU sunt NICIODATĂ interpolate în el) și ajutoarele de runtime
applyColour/applyFontOverrides/applyContrastBackdrop.
applyContrastBackdrop citește culoarea rezolvată a elementului și primul fundal opac
găsit mergând de la elementul ÎNSUȘI în sus (propriul său fundal — albul fix implicit al
cipului sau o suprascriere mai specifică a paginii gazdă — înaintea oricărui
strămoș) prin getComputedStyle standard (provenit din
element.ownerDocument?.defaultView, deci este sigur pe cadre și substituibil) și,
când packages/core/src/contrast.ts needsWhiteBackdrop raportează că textul eșuează
WCAG AA (CONTRAST_THRESHOLD) față de acel fundal ȘI albul ajută cu adevărat,
setează style.backgroundColor = "white". contrast.ts oferă parseColour,
relativeLuminance și contrastRatio; analizează formele rgb()/rgba()/hex
pe care le returnează getComputedStyle și tratează o culoare complet transparentă ca „negăsită".
Gazde configurabile și limitarea conținutului
O SiteRule (packages/core/src/config.ts) spune UNDE rulează adnotarea (match,
allFrames) și CARE container o limitează (contentRoot), plus enabled și un
flag builtin. DEFAULT_SITES livrate acoperă Gmail, Proton (cu
allFrames) și ambele gazde Outlook. Config agregă comutatoarele globale,
culorile, suprascrierile de font și tabloul sites.
Motorul de tipare de potrivire
packages/core/src/matchPattern.ts conține o gramatică restrictivă în mod deliberat, la momentul adăugării,
MATCH_PATTERN (doar http/https, metacaractere opțional de gazdă *. la început,
cale glob; fără port, fără schemă *, fără <all_urls>). matchesPattern potrivește un
URL față de un tipar validat, delegând calea la pathGlobMatches (fiecare
* se potrivește cu orice secvență de caractere); eșuează în siguranță la false.
parsePatternParts împarte un tipar validat în host/path/scheme/
wildcardHost.
Regulile suprapuse se rezolvă prin „cea mai specifică câștigă":
compareSiteSpecificity clasează după (1) gazdă exactă peste gazdă cu metacaractere, (2) gazdă
literală mai lungă, (3) cale mai literală (literalPathLength), apoi (4) un
departajator ASCII determinist; selectMostSpecific returnează cea mai specifică
regulă activată care se potrivește unui URL.
Acoperirea originii acordate este un potrivitor SEPARAT, mai LARG în mod deliberat.
permissions.getAll() poate raporta acordări în gramatica WebExtension completă
(<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*) pe care
MATCH_PATTERN-ul restrictiv le-ar respinge. parseGrantedOrigin le analizează
în GrantedOriginParts, iar originCovers raportează dacă o origine acordată
acoperă match-ul unei reguli (de exemplu, o acordare largă https://*/* acoperă cu adevărat fiecare
regulă https). matchAllowsOriginFallback raportează dacă calea unei reguli este exact
ORIGIN_FALLBACK_PATH (/*) — intersecția sigură a constrângerilor Chromium și
Gecko/Firefox 128 asupra revenirii dinamice la originea opacă.
Înregistrare dinamică
packages/webext/src/contentRegistration.ts înregistrează un script de conținut dinamic
per regulă adăugată de utilizator. Cele încorporate sunt servite de intrările statice content_scripts
și nu sunt NICIODATĂ înregistrate dinamic. desiredContentScripts filtrează
config.sites la regulile neîncorporate, activate, a căror origine isOriginGranted
(delegând la originCovers), mapează fiecare la un RegisteredContentScript cu un
id stabil (contentScriptId, un hash FNV-1a al match-ului, astfel încât id-ul să folosească doar
setul de caractere sigur pentru API) și setează matchOriginAsFallback: true DOAR când
matchAllowsOriginFallback(rule.match) este valabil (o cale /*). Controlarea flag-ului în
acest fel previne ca o regulă cu cale neconformă să provoace respingerea întregii
înregistrări în lot; o astfel de regulă se înregistrează fără acoperire pop-out prin
proiectare.
reconcileContentScriptsOnce citește originile acordate, configurația stocată și
înregistrările live, apoi converge printr-o diferență CONȘTIENTĂ DE PROPRIETĂȚI:
sameRegistration proiectează atât descriptorul dorit, cât și citirea live
pe o formă normalizată (aplicând valoarea implicită reală WebExtension a fiecărui câmp —
allFrames/matchOriginAsFallback implicit false, persistAcrossSessions
implicit ADEVĂRAT, runAt implicit document_idle), astfel încât un script proaspăt înregistrat
să se compare EGAL cu descriptorul care l-a produs și să nu poată apărea nicio buclă de
reînregistrare. Deoarece nu există updateContentScripts, un script modificat este
dezînregistrat, apoi reînregistrat. reconcileContentScripts înfășoară acest lucru cu o
promisiune în curs (inFlight), astfel încât două declanșatoare aproape simultane (o modificare de configurație
ȘI permissions.onAdded declanșând ambele la adăugarea unui site) să fie serializate în loc să
concureze. registerContentReconciliation cablează declanșatoarele
(permissions.onAdded/onRemoved, onConfigChanged) și converge o dată la
pornire; este importat doar de intrările de fundal Chrome și Firefox.
Fluxul de permisiune opt-in
Manifestele statice declară optional_host_permissions (http://*/*,
https://*/*), astfel încât un utilizator să poată acorda o gazdă web suplimentară arbitrară la runtime.
packages/webext/src/options/options.ts addSite validează ÎNTREAGA regulă candidată
(match ȘI content-root) prin normaliseSiteRule ÎNAINTE de a solicita vreo
permisiune, derivă originea gazdei cu matchOrigin și apelează
browser.permissions.request din interiorul gestului de clic pe butonul Adaugă; doar la
acordare adaugă un rând builtin:false și persistă prin calea de scriere canonică.
Configurație, stocare și tiparul canonic
Singura pâlnie de validare
normaliseConfig din packages/core/src/config.ts este singura cale de citire canonică:
constrânge și limitează fiecare câmp, revenind la valoarea implicită la orice
invalid și nu aruncă niciodată o excepție. Este construit din validatori per câmp —
normaliseBoolean, normaliseRenderMode, normaliseMaxLength,
normaliseIgnoreHosts (reducând fiecare intrare la un nume de gazdă punycode simplu prin
bareHostname), normaliseMatchColour/normaliseMismatchColour (controlate de
safeColour.ts isSafeColour), normaliseCssSize, normaliseFontWeight,
normaliseContentRoot (limitată de CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH),
normaliseMatchPattern (controlată de MATCH_PATTERN) și normaliseSites.
normaliseSites elimină respinsele, deduplifică după match, FORȚEAZĂ builtin: true
pe orice regulă al cărei match este egal cu al unei încorporate (închizând
vectorul potrivire-exactă-eclipsează-încorporata / dublă-injectare), reseminează orice
încorporată lipsă (astfel încât un magazin falsificat să nu poată elimina un furnizor de bază)
și sortează alfanumeric după match pentru o ordine canonică. configFingerprint hashează
doar câmpurile care afectează randarea (excluzând enabled, ignoreHosts și sites,
care conduc pornirea/oprirea și omiterea în loc de reflux) prin fnv1a.
Stocare
packages/webext/src/storage.ts înfășoară API-ul de stocare WebExtension.
configArea folosește browser.storage.sync când este disponibil (astfel încât setările să se deplaseze) și
revine la browser.storage.local (de ex. pe Thunderbird); configAreaName
raportează care este activ. getConfig, setConfig și onConfigChanged toate
canalizează valoarea lor brută prin normaliseConfig, astfel încât un magazin malformat sau falsificat
să nu poată preda niciodată o Config invalidă unui apelant. onConfigChanged ignoră suplimentar
evenimentele din zona de stocare INACTIVĂ și modificările cheilor neînrudite.
Cele două suplimente reutilizează aceeași pâlnie citire -> normaliseConfig -> return peste propriile
magazine gazdă. extensions/outlook/src/roamingStorage.ts al panoului de activități Outlook
înfășoară Office.context.roamingSettings (get/set sincron, comis cu
saveAsync); packages/gmail/src/propertiesStorage.ts al suplimentului Gmail înfășoară
PropertiesService.getUserProperties() (per utilizator, deplasându-se pe dispozitivele acelui utilizator,
codificat JSON sub o singură cheie și fără a necesita NICIUN domeniu OAuth suplimentar — niciodată
ScriptProperties comune). Ambele expun getConfig/setConfig plus accesoarele relevante pentru card
getIgnoreHosts/getHighlightMismatch și protejează o suprafață gazdă indisponibilă
cu o eroare dedicată. Declanșatorul de pagină de pornire Gmail randează un card de
setări CardService din getConfig, iar gestionarul de trimitere a formularului onSaveSettings îmbină
ignoreHosts/highlightMismatch analizate pe configurația curentă și le persistă
prin setConfig; declanșatorul contextual introduce ignoreHosts în adaptor
și highlightMismatch în constructorul de card, citind defensiv, astfel încât o eroare de stocare
să revină la valorile implicite în loc să întrerupă analiza mesajelor.
Tiparul configurație-canonică + actualizare-țintită
Trei căi de scriere citesc configurația canonică, schimbă exact un singur lucru și o scriu înapoi — niciodată comițând editări de formular nesalvate sau re-randând formularul:
removeSite(options.ts): citeștegetConfig, persistăsetConfigpeste un tablousitescu doar regula eliminată filtrată (persistă MAI ÎNTÂI, apoirow.remove(), apoi unpermissions.removecondiționat decis din configurația următoare). O eșuare de persistare aruncăSiteRemovalSaveErrorși lasă DOM-ul neatins; o revocare eșuată aruncăSitePermissionRevokeErrordupă ce rândul este deja dispărut.addSite(options.ts): descris mai sus — validează, solicită permisiune, adaugă rândul, apoisaveOptions.toggleEnabled(options.ts): comutatorul instant principal Activați răstoarnă DOARenabledpe configurația stocată și o scrie înapoi, fără a re-randa (astfel încât o editare în curs să nu fie niciodată suprascrisă); la o scriere eșuată revine caseta de bifat la valoarea stocată.
Propriul toggleEnabled al acțiunii din bara de instrumente
(packages/webext/src/toolbarAction.ts) urmează același tipar din partea de
fundal.
Build-ul per motor și contractul manifestului
tooling/build.mjs conduce un singur build esbuild pentru fiecare țintă WebExtension.
Descriptorul TARGETS numește TARGET-ul SURSĂ al fiecărei ținte: Chrome, Firefox și
Thunderbird livrează propriul src/; Edge, Opera și Safari declară chrome ca
sursa lor; suplimentul Outlook (Office.js) este propria sa sursă. ACTIVE_TARGETS (Chrome,
Edge, Firefox, Opera și Thunderbird — Safari și Outlook sunt excluse) este ceea ce
compilează --all/--package; Safari și Outlook se compilează doar când sunt numite explicit,
iar suplimentul Gmail (Apps Script) este un pachet esbuild separat (make build-gmail)
în afara buclei --all. Acel pachet Gmail țintește runtime-ul V8 al Apps Script, care
nu are nici module ES, nici un URL nativ: este construit ca ESM și apoi are
declarația sa finală export {…} eliminată (astfel încât funcțiile de declanșare să rămână globale
de top-level pe care Apps Script le poate invoca) și împachetează un mic polyfill URL (instalat doar
când URL este absent), astfel încât new URL(...) al nucleului comun să rezolve linkurile pe partea de server.
buildTarget curăță dist/<target>/, copiază manifest.json per țintă și
icons/ verbatim, împachetează fiecare script sursă ca IIFE (content.ts/messageDisplay.ts
injectate INJECTED_SCRIPTS nu pot fi module ES, iar
fundalurile sunt și ele workeri/pagini de eveniment clasice) și pagina de opțiuni comună ca
ESM și copiază options.html/options.css din packages/webext/src/options/.
webextension-polyfill este împachetat în fiecare script.
tooling/version.mjs ștampilează versiunile: preia MAJOR.MINOR din package.json-ul
rădăcină și adaugă un număr de BUILD auto-incremental (citit din
cel mai mare al treilea component existent din manifestele țintă) în fiecare manifest
țintă, păstrându-le în pas. Majoritatea țintelor poartă un singur manifest.json; suplimentul
Outlook poartă două manifeste versionate (manifest.json și manifest.xml)
și ambele sunt ștampilate, iar scheletul _template este ștampilat în pas de asemenea. package.json-ul
rădăcină este păstrat la aceeași versiune, dar rămâne semver valid (o versiune din patru părți
este permisă într-un manifest, niciodată în package.json). tooling/icons.mjs
(rulat prin make icons) rasterizează unica sursă vectorială assets/icon.svg în
icons/icon48.png și icons/icon128.png al fiecărei ținte, încercând cairosvg, apoi
inkscape, apoi ImageMagick.
Contractul manifestului — câmpurile exacte necesare per motor — este singura
sursă de adevăr în fiecare extensions/<engine>/test/manifest.smoke.test.mjs (de ex.
assertManifestShape al extensions/chrome/test/manifest.smoke.test.mjs, care
construiește și ținta și confirmă că fiecare fișier referit se rezolvă în dist).
Conform convenției de deduplicare a proiectului, forma câmp cu câmp NU este
reenumerată aici; consultați acele teste de fum și nota „Contractul manifestului"
din planul de testare manuală.
Internaționalizare (i18n)
Reveal URLs este localizat în multe limbi în afară de engleză. Setul este
single-sourced în packages/core/src/locales.json (SUPPORTED_LOCALES, numele
native și valoarea implicită), pe care atât pachetele extensiei (prin importul JSON rezolvat)
cât și build-ul de website plain-Node îl citesc. Fiecare șir care nu este în engleză este
tradus automat și în așteptarea revizuirii umane; marcatorul de proveniență este per format
(cele din _locales folosesc description-ul fiecărui mesaj, dicționarele de interfață ale
website-ului îl înregistrează în site/i18n/README.md, iar sursele de documente traduse poartă un comentariu HTML pe prima linie).
Există trei suprafețe de localizare independente, în mod deliberat necablate încrucișat, și fiecare este împărțită de-a lungul unei linii build-time / runtime.
- Pagina de opțiuni (AD-1). Livrează marcaj static în engleză; fiecare nod traductibil
poartă o adnotare
data-i18n="<key>"(text) saudata-i18n-<attr>="<key>"(atribut). La runtimepackages/webext/src/options/locale.tseste stratul de aplicare: facefetchla propriul_locales/<code>/messages.jsonîmpachetat al paginii (prinruntime.getURL, fără a fi nevoie deweb_accessible_resources), rescrie fiecare nod adnotat DOAR printextContent(astfel încât indiciile de exemplu<code>ale formularului să rămână intacte) și setează<html lang>. Pagina este COMPLET localizată: alături de proza statică, etichetele de rând de site și butonul Eliminați construite prin JS poartă o cheiedata-i18n(cu un texttranslated inițial), astfel încât același strat de aplicare să relocalizeze rândurile randate fără o re-randare, iar șirurile cu adevărat dinamice — linia de stare, feedbackul de adăugare-site și linia de versiune — se randează printranslate(<key>)allocale.ts(currentMessages → revenire la engleză → cheie) la momentul apelului. Revenirea la engleză este catalogul_locales/en/messages.jsonIMPORTAT (împachetat) înoptions.jsla momentul build-ului, astfel încâttranslatesă se rezolve la engleză lizibilă SINCRON de la încărcarea modulului — un șir dinamic randat înainte ca comutatorul să se așeze, sau după ce un fetch de catalog de runtime eșuează, nu degradează niciodată la o cheie brută de mesaj.locale.tspublică harta de mesaje aplicată și declanșează ascultătorii săionLocaleChangedupă fiecare aplicare, astfel încâtoptions.tssă re-randeze starea/feedbackul/versiunea afișate curent și să repoziționeze linkul Manual online la fiecare comutare. Linia de versiune folosește un mesajoptionsVersional cărui placeholder{version}este înlocuit cu numărul de build (token tradus, număr verbatim). Linkul de manual urmează localizarea activă la varianta sa de document (AD-9): engleza păstreazămanual.htmlde top-level, fiecare altă localizare deschide numele său de fișier localizat sub<code>/(de ex.nl/handleiding.html, tradus prinlocalisedSitePageal nucleului — aceeași hartă comună sub care build-ul de website emite fișierul), păstrând ștampila de versiune?v=. Localizarea activă este alegerea salvată a utilizatorului sau, în lipsa ei, limba browserului rezolvată la o limbă de bază acceptată prinresolveLocaleal nucleului (AD-5); o localizare neacceptată sau eșuată lasă engleza pre-randată (iartranslatedegradează apoi la revenirea la engleză). Comutatorul manual Limba de afișare persistă alegerea sa sub o cheie dedicatăuiLocaleînstorage.local— niciodată înConfig-ul sincronizat, deci este per dispozitiv și nu se deplasează niciodată. - Interfața website-ului (AD-2). Generatorul de site static
tooling/site.mjsrandează pe server fiecare nod de interfață în engleză în timp ce îl adnoteazădata-i18nși păstrează<html lang="en-GB">(AD-4). Încărcătorul din partea browseruluisite/scripts/i18n.mjs(un modul ES fără dependențe) schimbă apoi interfața în limba vizitatorului prin precedență — o alegerelocalStoragepersistată (revealUrlsSiteLocale) → prima potrivire de bază acceptată dinnavigator.languages(AD-5) → engleză — preluândsite/i18n/<code>.json. Spre deosebire de stratul de aplicare al opțiunilor, schimbă prininnerHTML, deoarece valorile de interfață ale site-ului poartă marcaj inline de încredere (<code>,<a>); un test de paritate fixează secvența tag/atribut/URL a fiecărei valori care nu este în engleză identică octet cu octet cu engleza, astfel încât o traducere să nu poată elimina niciodată un link sau strica un tag. O cheie lipsă păstrează instantaneul de engleză pre-randat; un fetch eșuat lasă pagina engleză intactă (degradare grațioasă, niciodată goală). - Paginile de documentație (AD-8/AD-9). Paginile de document sunt randate PER LOCALIZARE la
momentul build-ului, nu schimbate la runtime. Documentele englezești rămân la căile de top-level; fiecare
altă localizare primește o copie sub un folder
<code>/, sub un nume de fișier TRADUS în localizare (nl/handleiding.html,de/datenschutz.html) din harta comunăpackages/core/src/site-page-names.json(pagina de pornire rămâneindex.html), dintr-undocs/<code>/<DOC>.mdtradus când există, altfel sursa engleză — o revenire per pagină pe carebuildSiteo ÎNREGISTREAZĂ în loc să o omită tacit. Deoarece modelul anterior publica acestea sub numele lor englezești, build-ul emite și un redirect subțirenoindexla fiecare cale veche redenumită (nl/manual.html→nl/handleiding.html), astfel încât un semn de carte vechi, un rezultat de căutare sau linkul de manual al unei extensii mai vechi deja instalate să se rezolve în continuare. Corpul documentului nu poartă niciundata-i18n(este markdown randat brut), astfel încât încărcătorul de interfață de runtime să nu-l atingă niciodată; doar nodurile de interfață comune se schimbă. Comutatorul de limbă al paginii de document NAVIGHEAZĂ la pagina localizării frate în loc să schimbe pe loc. - Localizarea linkurilor interne (Finding 2). Limba selectată se transferă între
navigarea documentelor. Fiecare ancoră de document INTERNĂ (cele cinci pagini
architecture.html,index.html,licence.html,manual.html,privacy.html) este etichetatădata-doc-link="<page>"de generator, iar pentru cazul fără JavaScript href-ul său se rezolvă deja la varianta de localizare proprie a paginii CURENTE, sub numele său de fișier tradus (navDocHref— fiecare pagină internă are acum o variantă per localizare; doar pagina de pornire păstreazăindex.htmlîn fiecare localizare).rewriteDocLinksal încărcătorului de runtime reorientează apoi fiecare ancoră de document internă — cele etichetate ȘI orice ancoră simplă injectată de catalog precummanual.html#installing— la varianta localizării ACTIVE, păstrând orice#hash/?queryși fără a atinge niciodată linkurile externe. Etichetadata-doc-linkpoartă întotdeauna numele de pagină englez CANONIC; href-ul rezolvat folosește numele de fișier TRADUS al localizării (AD-9,nl/handleiding.html), astfel încât încărcătorul mapează un nume de fișier localizat înapoi la pagina sa canonică pentru a rămâne idempotent. Pe o pagină de document, o alegere STOCATĂ care diferă de localizarea proprie a paginii NAVIGHEAZĂ la varianta corespunzătoare; garda de buclă se declanșează doar la o alegere stocată explicită (niciodată o preferință de limbă a navigatorului singură) și doar când ținta diferă de pagina deja afișată. Cataloagele însele își păstrează URL-urile interne englezești, astfel încât testul de paritate să rămână verde — localizarea este o trecere pură de runtime peste DOM.
Suprimarea „Site-urilor active" pentru clientul de e-mail (AD-3)
O țintă de client de e-mail (Thunderbird astăzi) vede deja fiecare mesaj redat, deci
editorul „Site-uri active" per gazdă este redundant acolo și este eliminat. Decizia
este purtată de un flag de descriptor documentat per țintă, mailClient: true, pe
TARGETS.thunderbird al tooling/build.mjs — niciodată un nume de țintă hardcodat în
logica de build sau de interfață — și este implementată în două jumătăți:
- La momentul build-ului.
copyAssetsse bazează pedescriptorFor(target).mailClient: pentru o țintă de client de e-mail rulează transformarea purăremoveSitesSection(html)(care elimină întregul editor de gazde<section class="sites">, inclusiv fiecare control de adăugare-site, și aruncăMissingSourceErrordacă secțiunea este absentă, astfel încât o modificare de marcaj să nu poată livra niciodată tacit editorul) și scrie rezultatul; țintele de browser primescoptions.htmloctet cu octet princpSync. - La runtime. Controlerul împachetat
packages/webext/src/options/options.tstolerează secțiunea absentă:renderSites/readSites/applyConfig/readFormverifică#sites-listși devin no-op în loc să arunceOptionsFieldMissingError, iarreadFormOMITE cheiasitescând secțiunea a dispărut. Deoarece omitereasitesar lăsanormaliseConfigsă reseminezе valorile implicite încorporate și să elimine site-urile utilizatorului,saveOptionsrestabileștesites-urile stocate înainte de normalizare ori de câte ori secțiunea este absentă — acesta este ceea ce face ca o salvare de client de e-mail să păstreze gazdele configurate. O gardă de runtime,guardMailClientSites, elimină secțiunea dacă este încă prezentă, detectând un client de e-mail după prezența API/funcție (messages/messageDisplay/scripting.messageDisplaypebrowser-ul injectat), niciodată după numele țintei; rulează prima îninitOptions, astfel încât cablajul ulterior de adăugare/listare să omită natural elementele absente. Într-un runtime de browser garda este un no-op.
Postura de securitate și confidențialitate
- Fără rețea sau exfiltrare. Niciun modul nu efectuează
fetch/XHRsau orice alt apel de rețea; extensia citește doar configurația stocată și DOM-ul paginii. - Doar DOM-sigur. Adnotarea și foaia de stil injectată folosesc
createElement/textContent/proprietăți tipatestyle.*exclusiv, niciodatăinnerHTML/insertAdjacentHTML/eval. Pentru nucleu, acest lucru este impus depackages/core/test/purity.test.ts. - Toate intrările externe/de configurație normalizate. Fiecare citire și scriere canalizează prin
normaliseConfig; culorile trecisSafeColour; tiparele de potrivire trecMATCH_PATTERN; selectoarele content-root sunt limitate deCONTENT_ROOT_PATTERNși doar predate luiquerySelectorAll/closest, niciodată interpolate în marcaj sau CSS. - Privilegiu minim. Originile de webmail încorporate sunt
host_permissionsfixe; gazdele suplimentare sunt opt-in prinoptional_host_permissionsși unpermissions.requestlegat de gest. Acțiunea din bara de instrumente nu declară niciun popup. - Colectare de date Gecko. Manifestele Firefox și Thunderbird declară
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Licență. Proiectul este AGPL-3.0-only (
LICENSE,package.json).
Testare
- Testele unitare Vitest acoperă ambele pachete:
packages/core/test/*(config, contrast, nepotrivire de gazdă, procesor de linkuri, stiluri și garda de puritate) șipackages/webext/test/*(conținut, stocare, opțiuni, înregistrare, bara de instrumente, afișarea mesajelor și un test end-to-end de site personalizat). Fiecare pachet ruleazăvitest run. - Scenariile Cucumber BDD trăiesc în
features/(cablate decucumber.json) și conduc modulele reale@reveal-urls/webextprin dubluri de test fidele înfeatures/support/webext.ts— în specialnormaliseReadBack, care oglindește motorul OMIȚÂND flag-urile cu valoare false la citirea înapoi, astfel încât un harness indulgent să nu poată ascunde o buclă de reînregistrare — peste un document jsdom configurat înfeatures/support/world.ts. - Testele de fum de manifest per țintă (
extensions/<engine>/test/manifest.smoke.test.mjs,node:testîncorporat al Node) afirmă forma exactă a fiecărui manifest și că dist-ul construit rezolvă fiecare fișier referit.tooling/test/adaugă teste de build, versiune și definiție-a-terminat. - CI (
.github/workflows/ci.yml) controlează fiecare push și pull request pemake test,make bddșimake lint(rulate direct pe runner, suprascriindRUN/IMAGE_DEPDocker ale Makefile-ului). Ștampilarea versiunii și împachetarea sunt excluse în mod deliberat; trăiesc în.github/workflows/release.ymlseparat, declanșat de tag.