Reveal URLs — Arhitektura

Ta dokument opisuje, kako je razširitev Reveal URLs zgrajena in kako deluje njena osrednja logika. Namenjen je sodelavcem in tehnično usmerjenim bralcem. Skozi besedilo navaja dejanske datoteke in simbole (v obliki path symbol), da je mogoče vsako trditev preveriti v izvorni kodi. Uporabniški pogled je v manual; koraki ročnega preverjanja so v manual test plan.

Pregled

Reveal URLs je ena sama MV3 WebExtension, zgrajena za več pogonov — Chrome, Edge, Opera, Firefox in Thunderbird — iz enega deljenega, čistega jedra kode. (Safari je za zdaj odložen: ima manifest in ga je mogoče zgraditi, če ga izrecno navedete, vendar ni del privzete gradnje niti CI.) Razširitev ob povezavi v izrisanem e-sporočilu pokaže ciljni URL in označi povezavo, katere vidno besedilo poimenuje drugo registrabilno domeno kot njen href — klasičen znak phishinga.

Repozitorij je pnpm/TypeScript monorepo (pnpm-workspace.yaml, package.json z "private": true). Pakiranje vodi en sam skript na osnovi esbuild, tooling/build.mjs, ki prevede skupne pakete in tanke vstopne točke posameznih pogonov v naložljiv imenik dist/<target>/. webextension-polyfill je vključen v vsak skript, namesto da bi se zanašali na globalni runtime objekt, zato isti izvorni kôd brez sprememb deluje v Chromiumu in Gecko.

Dva natívna dodatka za e-pošto razširjata isto jedro zaznavanja na površine, do katerih WebExtension ne more: Outlook Add-in (naloga Office.js, dosegljiva v Outlook on the web/Windows/Mac/iOS/Android in Outlook.com) in Gmail Add-on (Apps Script CardService za Gmail na spletu/Android/iOS). Oba ponovno uporabljata samo čisto analizo — nov modul packages/core/src/findings.ts (analyseAnchors/analyseHtml, platformno nevtralen model Finding[]) ponovno uporablja obstoječo logiko hostMismatch in izpeljavo registrabilnih domen prek tldts. Razlikujeta se samo prilagojevalnik gostitelja (kako se telo sporočila prebere in razčleni) in predstavitev. Outlookovo opravilo telo razčleni z lastnim DOMParser in teče na strani odjemalca; Gmailov dodatek ga razčleni z node-html-parser in teče na strežniški infrastrukturi Google Apps Script V8. Ker noben okvir ne more spreminjati izrisanega DOM-a prebranega sporočila (setAsync/prependAsync v Office delujeta le pri pisanju, CardService pa upodablja kartice, ne HTML sporočila), oba dodatka prikažeta panel/kartico ugotovitev namesto vstavljenih pripisov v samo besedilo. Obstoječa DOM-mutacijska pot iz linkProcessor.ts in REVEAL_URLS_CSS se zato v teh dodatkih ne uporabita. Relativni href se razreši le glede na zaupanja vreden <base href> znotraj e-sporočila; sicer se preskoči in nikoli ne razrešuje glede na izvor predala ali ponudnika.

Odgovornosti v kodi so jasno ločene:

Razporeditev repozitorija

packages/core — čisto jedro

Noben modul tukaj ne uvaža API-ja razširitve (browser.*/chrome.*/messenger.*) in ne uporablja DOM sinkov, ki sprejemajo niz kot markup (innerHTML/insertAdjacentHTML). Ta pogodba je mehansko uveljavljena v packages/core/test/purity.test.ts, ki pregleda vse src/**/*.ts, odstrani telesa komentarjev z lastnim stripComments in nato preveri, da v nobenem modulu ne ustrezata niti EXTENSION_API_PATTERN niti UNSAFE_DOM_PATTERN. Javni vmesnik se ponovno izvozi iz packages/core/src/index.ts. Edina runtime odvisnost je tldts, potreben za primerjavo domen z upoštevanjem public suffixov.

Moduli so:

packages/webext — ovoji brskalniškega API-ja

Moduli tukaj SMEJO uporabljati browser.* in so edino mesto, kjer se dotaknemo WebExtension API-jev. Javni vmesnik se ponovno izvozi iz packages/webext/src/index.ts. Moduli so: content.ts (življenjski cikel content skript), contentRegistration.ts (dinamična registracija), storage.ts (vztrajnost konfiguracije), background.ts (obnašanje ob namestitvi), toolbarAction.ts (preklop + badge), messageDisplay.ts in messageDisplayBackground.ts (Thunderbird) ter options/options.ts (krmilnik strani z nastavitvami).

extensions/<engine> — tanke vstopne točke in manifesti

Vsak pogon ima svoj manifest.json in icons/. Chrome, Firefox in Thunderbird imajo dodatno še src/ z vstopnimi točkami; Edge, Opera in Safari imajo le manifest in ikone in PONOVNO uporabljajo Chromov src/ (določeno v deskriptorju gradnje spodaj). Vstopne točke so namenoma zelo majhne: na primer extensions/chrome/src/content.ts vsebuje le void createContentController().bootstrap();, medtem ko extensions/chrome/src/background.ts kliče registerBackground, registerContentReconciliation in registerToolbarToggle. Firefoxova background vstopna točka je enaka Chromovi; Thunderbirdova (extensions/thunderbird/src/background.ts) namesto tega kliče registerBackground in registerMessageDisplay, ker content skript nima. extensions/_template je pripravljena Chromium osnova za kopiranje, ne build target.

tooling, features in smoke testi

tooling vsebuje build.mjs, version.mjs in icons.mjs. features vsebuje scenarije Gherkin, njihove step definitions in zveste test doubles v features/support/webext.ts in features/support/world.ts. Vsak pogon ima še extensions/<engine>/test/manifest.smoke.test.mjs (_template ima template.smoke.test.mjs) — ti testi predstavljajo avtoritativno pogodbo manifesta (glej spodaj razdelek »Gradnja po pogonu in pogodba manifesta«).

Potek razkrivanja

Pot vsebine se začne v vstopni točki pogona, na primer extensions/chrome/src/content.ts, ki kliče createContentController().bootstrap() iz packages/webext/src/content.ts.

Bootstrap

createContentController zažene bootstrap enkrat na okvir:

  1. Okvir si sinhrono prilasti še pred vsakim await prek claimBootstrap, ki na window tega okvirja prebere in nastavi zastavico BOOTSTRAP_MARKER. Statičen vnos v content_scripts in prekrivajoči se dinamični uporabniški skript lahko oba v isti okvir vbrizgata content.js, zato je skupni window v isolated world prava zaščita »samo enkrat«, zgodnja prilastitev pa zagotovi, da dve skoraj sočasni injekciji ne uspeta obe.
  2. Najprej naloži shranjeno konfiguracijo prek loadConfigWithRetry (BOOTSTRAP_CONFIG_RETRIES dodatnih poskusov ob prehodni napaki) in med ponovitvami drži marker, da se prekrivajoča injekcija, ki je že končala kot no-op, ne izgubi. Šele ko je branje uspešno, vstavi stylesheet (injectStyles, ki REVEAL_URLS_CSS dodeli prek textContent), tako da neuspešno branje ne doda nobenega <style> in ponovni poskus ne more podvojiti vnosa.
  3. Če nalaganje konfiguracije odpove pri vseh poskusih, releaseBootstrap marker vrne nazaj (samo tukaj, še preden se začne označevanje), da lahko poznejša reinjekcija poskusi znova; ko je start enkrat stekel, mora marker ostati, ker nov anotator ne bi posedoval vozlišč iz prejšnjega prehoda.
  4. Ko velja config.enabled, se pokliče start; vedno pa se registrira tudi poslušalec onConfigChanged, ki konfiguracijo znova uporabi in pokrije prehod disabled → enabled.

Določanje pravila aktivnega mesta

start (in applyConfig) kliče resolveSiteRule, da odloči, ali in kje se bo označevalo. resolveSiteRule najprej poskusi z lastno lokacijo dokumenta prek resolveForHref, ki filtrira config.sites na omogočena pravila in delegira na selectMostSpecific, tako da zmaga najnatančnejše ujemanje. Ko lastna lokacija ne zadene ničesar, izvor dokumenta pa je neprozoren ali podedovan — kar določa hasOpaqueOrigin, true le za about:blank/about:srcdoc (podedovano prek INHERITED_ORIGIN_URLS) ali blob:/data: (OPAQUE_ORIGIN_SCHEMES) — se po vrsti preizkusijo TOP okvir (readTopHref), okno odpiralca (readOpenerHref) in nazadnje document.referrer (readReferrer). Vsako zunanje branje je zaščiteno pred cross-origin dostopom in kandidata prispeva le, če je branje dejansko mogoče. Običajen dokument brez ujemanja ostane avtoritativen in nikoli ne podeduje pravila iz okoliščin.

Omejitev na koren vsebine in opazovanje sprememb

start shrani contentRoot iz izbranega pravila, ustvari anotator prek createAnnotator(config) in obdela vsak koren iz selectRoots(doc, contentRoot). Nato opazuje doc.body s OBSERVER_OPTIONS ({ childList: true, subtree: true }).

Observer mutacije pošilja v processMutations, ki vsako mutacijo najprej omeji na content root: cilj ponovno obdela le, če je Element znotraj root-a, vsak dodani element pa posreduje v processAddedNode, ki anotira vozlišče, če je znotraj root-a, če je samo root ali če root vsebuje. Tako se ujame dogajanje znotraj telesa sporočila in tudi poznejša montaža novega telesa v SPA, medtem ko chrome aplikacije zunaj root-ov ostane nedotaknjen. Null anotator nima stranskih učinkov.

applyConfig uskladi živo spremembo konfiguracije: če ni več ujemajočega pravila ali če je razširitev izklopljena, se vse podre; če se spremeni contentRoot, se izvede ponovni zagon (revert + novo opazovanje); sicer pa se konfiguracija preteče na mestu z annotator.setConfig in annotator.process brez ponovnega zagona observerja.

Označevanje posamezne povezave

Anotator živi v packages/core/src/linkProcessor.ts. createAnnotator vrne Annotator, katerega stanje živi v closure in ni vidno strani: močna Map, ključa po identiteti anchorja, token data-ru, ustvarjen z generateToken (crypto.getRandomValues), in trenutna konfiguracija. Metoda process(root):

  1. Odstrani vnose za anchore, ki niso več povezani.
  2. Zbere kandidatne anchore prek collectAnchors.
  3. Razreši cilj vsakega anchorja prek resolveAnnotatableUrl, ki vrne null za prazen ali notranji href, neparsabilen href, sheme razen http:/https: in gostitelje z ignoreHosts.
  4. Uporabi idempotentno hitro pot, kadar isAnnotationIntact potrdi, da se render fingerprint ni spremenil in da so ustvarjena vozlišča še vedno povezana in na pravem mestu; sicer star pripis odstrani in ga izriše znova.
  5. Izračuna neujemanje prek hostMismatch na očiščenem besedilu anchorja, upošteva showOnlyOnMismatch in pokliče annotateAnchor.

annotateAnchor ustvarja samo varen DOM. V načinu "inline" na začetek vstavi <span class="reveal-urls-url"> z URL-jem prek textContent (s puščico in U+00A0, krajšano s truncateUrl) ter za njim <br>, pri čemer otroci same povezave ostanejo nedotaknjeni. Povezani span nato prek applyContrastBackdrop dobi kontrastno ozadje. V načinu "title" shrani izvirni title (TitleSave), ga prepiše z URL-jem in pri poudarjenem neujemanju doda sosednji badge <span class="reveal-urls-warn"> z besedilom REVEAL_URLS_WARN_LABEL. Vsako ustvarjeno vozlišče je označeno z data-ru="<token>", vsak vnos pa nosi configFingerprint, da sprememba render konfiguracije prisili nov izris. Barve in tipografske prilagoditve se uporabijo prek typed element.style.* properties (applyColour/applyFontOverrides), nikoli z interpolacijo v CSS niz. revertOwned odstrani ustvarjena vozlišča po referenci in obnovi shranjeni title; revertAll to stori za vse anchorje in počisti register.

truncateUrl dela po Unicode code points ([...displayHref]), zato nikoli ne pretrga surrogate para; ohrani shemo in gostitelja, preostanek pa skrajša z URL_ELLIPSIS.

Neujemanje gostitelja

packages/core/src/hostMismatch.ts hostMismatch primerja vidno besedilo povezave z njenim href po registrabilni domeni, ne po surovem hostname. Poštena poddomena zato ni označena, medtem ko je posnemovalna. extractHostCandidates razbije besedilo in vsak token zmanjša prek hostCandidate, ki zahteva piko in prek tldts prepoznan ICANN public suffix. Gostitelj iz href-a in kandidati se zmanjšajo na registrabilno domeno prek tldts.getDomain; do neujemanja pride, če se kateri koli kandidat razlikuje. Funkcija nikoli ne meče izjem.

Slogi in kontrast

packages/core/src/styles.ts vsebuje vstavljivi REVEAL_URLS_CSS (fiksna postavitev, teža, oblika in povsem neprosojno belo ozadje čipa) ter runtime helperje applyColour, applyFontOverrides in applyContrastBackdrop. applyContrastBackdrop prek getComputedStyle prebere izračunano barvo besedila in prvo neprosojno ozadje pri hoji od samega elementa navzgor; ko packages/core/src/contrast.ts needsWhiteBackdrop ugotovi, da besedilo ne dosega WCAG AA (CONTRAST_THRESHOLD) in da bela res pomaga, nastavi style.backgroundColor = "white". contrast.ts ponuja parseColour, relativeLuminance in contrastRatio; zna razčleniti oblike rgb()/rgba()/hex, ki jih vrača getComputedStyle, popolnoma prozorno barvo pa obravnava kot »ni najdeno«.

Nastavljivi gostitelji in omejevanje vsebine

SiteRule (packages/core/src/config.ts) določa, KJE pripisovanje teče (match, allFrames) in KATERI vsebinski vsebnik ga omejuje (contentRoot), skupaj z enabled in oznako builtin. Vgrajeni DEFAULT_SITES pokrivajo Gmail, Proton (z allFrames) in oba Outlook gostitelja. Config združuje globalna stikala, barve, tipografske prilagoditve in polje sites.

Mehanizem vzorcev ujemanja

packages/core/src/matchPattern.ts vsebuje namenoma omejeno slovnico MATCH_PATTERN (samo http/https, opcijski začetni *. wildcard za gostitelja, glob za pot; brez porta, brez sheme *, brez <all_urls>). matchesPattern primerja URL z validiranim vzorcem in pot prepusti pathGlobMatches. parsePatternParts razdeli vzorec na host, path, scheme in wildcardHost.

Pri prekrivajočih se pravilih zmaga »najbolj specifično«: compareSiteSpecificity razvršča po natančnem gostitelju pred wildcard gostiteljem, po daljšem dobesednem gostitelju, po bolj dobesedni poti in nato po determinističnem ASCII razreševalcu izenačenj; selectMostSpecific vrne najbolj specifično omogočeno pravilo, ki se ujema z URL-jem.

Pokritje odobrenih izvorov je ločen in namerno širši matcher. permissions.getAll() lahko vrne odobritve v polni webextension slovnici (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*), ki jih omejeni MATCH_PATTERN ne sprejme. parseGrantedOrigin jih razčleni v GrantedOriginParts, originCovers pa pove, ali odobren izvor pokriva pravilo match. matchAllowsOriginFallback pove, ali je pot pravila točno ORIGIN_FALLBACK_PATH (/*), torej varen presek omejitev Chromiuma in Gecko/Firefox 128 pri fallbacku neprozornih izvorov.

Dinamična registracija

packages/webext/src/contentRegistration.ts registrira en dinamičen content script za vsako uporabniško dodano pravilo. Vgrajena pravila obdelajo statični content_scripts in se nikoli ne registrirajo dinamično. desiredContentScripts filtrira config.sites na nevgrajena, omogočena pravila z odobrenim izvorom, vsako preslika v RegisteredContentScript s stabilnim id (contentScriptId, FNV-1a hash match vzorca) in nastavi matchOriginAsFallback: true le tedaj, ko velja matchAllowsOriginFallback(rule.match). Tako pravilo z neustrezno potjo ne more podreti celotne serije registracij; v takem primeru se skript registrira brez pokritja pop-out oken.

reconcileContentScriptsOnce prebere odobrene izvore, shranjeno konfiguracijo in žive registracije ter jih uskladi prek diff-a po lastnostih: sameRegistration želeni deskriptor in stanje, vrnjeno iz API-ja, normalizira na isto obliko, vključno z dejanskimi privzetimi vrednostmi. Ker ne obstaja updateContentScripts, se spremenjen skript odregistrira in registrira znova. reconcileContentScripts to ovije z in-flight promise (inFlight), da se dve skoraj sočasni sprožitvi ne stepeta. registerContentReconciliation priključi triggery permissions.onAdded/onRemoved in onConfigChanged ter ob zagonu izvede eno uskladitev; uvožen je samo v Chrome in Firefox background entry.

Tok izrecnega dovoljenja

Statični manifesti deklarirajo optional_host_permissions (http://*/*, https://*/*), da lahko uporabnik med delovanjem odobri poljuben dodatni spletni gostitelj. packages/webext/src/options/options.ts addSite validira CELOTNO kandidatno pravilo (match in content root) prek normaliseSiteRule, še preden zahteva dovoljenje, izpelje origin gostitelja prek matchOrigin in pokliče browser.permissions.request znotraj klika na gumb Add. Šele po odobritvi doda vrstico builtin:false in jo shrani po kanonični poti zapisovanja.

Konfiguracija, shramba in kanonični vzorec

Enotni validacijski lijak

normaliseConfig v packages/core/src/config.ts je enotna kanonična pot branja: vsako polje pretvori, omeji in ob neveljavni vrednosti vrne privzeto stanje; nikoli ne meče izjem. Gradi na validacijah normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts, normaliseMatchColour, normaliseMismatchColour, normaliseCssSize, normaliseFontWeight, normaliseContentRoot, normaliseMatchPattern in normaliseSites. normaliseSites zavrže neveljavna pravila, deduplicira po match, vsili builtin: true pravilom, ki se ujemajo z vgrajenimi, ponovno doda manjkajoča vgrajena pravila in vse razvrsti alfanumerično po match. configFingerprint zgoščuje samo polja, ki vplivajo na izris.

Shramba

packages/webext/src/storage.ts ovija WebExtension storage API. configArea uporablja browser.storage.sync, kjer je na voljo, sicer pa pade nazaj na browser.storage.local. configAreaName pove, katero območje je aktivno. getConfig, setConfig in onConfigChanged vedno vodijo surovo vrednost skozi normaliseConfig, zato niti poškodovana shramba ne vrne neveljavnega Config. onConfigChanged dodatno ignorira dogodke iz neaktivnega storage območja in spremembe nepovezanih ključev.

Oba dodatka ponovno uporabita isti vzorec read → normaliseConfig → return nad lastnima gostiteljskima shrambama. Outlook task pane uporablja extensions/outlook/src/roamingStorage.ts, Gmailov dodatek pa packages/gmail/src/propertiesStorage.ts. Obe plasti izpostavljata getConfig in setConfig ter dodatne accessorje za kartice, manjkajoč host surface pa obravnavata z namensko napako. Gmail homepage trigger iz getConfig sestavi CardService settings card, onSaveSettings pa prebrane ignoreHosts/highlightMismatch zlije v trenutno konfiguracijo in jih shrani prek setConfig; contextual trigger potem ta polja obrambno prebere z vrnitvijo na privzete vrednosti.

Vzorec kanonične konfiguracije in ciljnih posodobitev

Tri poti zapisovanja najprej preberejo kanonično konfiguracijo, spremenijo točno eno stvar in jo zapišejo nazaj — nikoli ne shranijo nezaključenih urejanj obrazca in nikoli po nepotrebnem ne izrišejo celotnega obrazca:

Enak vzorec z background strani uporablja tudi toolbar action toggleEnabled (packages/webext/src/toolbarAction.ts).

Gradnja po pogonu in pogodba manifesta

tooling/build.mjs vodi enoten esbuild build za vse cilje WebExtension. Deskriptor TARGETS določa izvorni target za vsak cilj: Chrome, Firefox in Thunderbird imajo lasten src/; Edge, Opera in Safari kot vir uporabijo chrome; Outlook add-in je ločen vir. ACTIVE_TARGETS (Chrome, Edge, Firefox, Opera in Thunderbird — brez Safari in Outlooka) so cili, ki jih gradi --all/--package. Safari in Outlook se zgradita le ob izrecni navedbi, Gmail add-on pa se pakira ločeno z make build-gmail. Gmailov bundle cilja Apps Script V8 runtime, ki nima ES modulov niti native URL: zgrajen je kot ESM, odstrani se njegov zaključni export {…}, priloži pa se majhen polyfill URL, da lahko skupno jedro strežniško razrešuje povezave.

buildTarget počisti dist/<target>/, dobesedno prekopira ciljni manifest.json in icons/, vsak izvorni skript zgradi kot IIFE, skupno stran options pa kot ESM ter prekopira options.html/options.css iz packages/webext/src/options/. webextension-polyfill je vključen v vsak skript.

tooling/version.mjs pečati različice: iz root package.json vzame MAJOR.MINOR in doda samodejno povečevano BUILD številko v vse target manifeste. Večina targetov ima en manifest.json; Outlook add-in ima dva manifestna zapisa (manifest.json in manifest.xml) in oba se pečatita, enako velja za scaffold _template. Root package.json ostane na isti različici, vendar v veljavnem semver zapisu. tooling/icons.mjs (zagnan z make icons) rasterizira edini vektorski vir assets/icon.svg v icons/icon48.png in icons/icon128.png za vsak target.

Pogodba manifesta — torej natančno zahtevana polja po posameznem pogonu — je en sam vir resnice v extensions/<engine>/test/manifest.smoke.test.mjs. Na primer extensions/chrome/test/manifest.smoke.test.mjs prek assertManifestShape potrjuje obliko manifesta in da vsak referenciran objekt v izhodnem buildu res obstaja. Zaradi pravila deduplikacije se zato tukaj točen seznam polj ne našteva znova; glejte smoke teste in opombo »Manifest contract« v manual test plan.

Internacionalizacija (i18n)

Reveal URLs je lokaliziran v številne jezike poleg angleščine. Nabor je single-source v packages/core/src/locales.json (SUPPORTED_LOCALES, native names in default), ki ga berejo tako paketi razširitve kot tudi plain-Node build spletnega mesta. Vsako neangleško besedilo je strojno prevedeno in čaka na človeški pregled; oznaka izvora se razlikuje po formatu.

Obstajajo tri neodvisne površine lokalizacije, namenoma brez križnega povezovanja, vsaka pa je razdeljena na build-time in runtime plast.

Zatiranje »Aktivnih mest« v poštnem odjemalcu (AD-3)

Mail-client target (danes Thunderbird) že vidi vsako izrisano sporočilo, zato je urejevalnik »Active sites« odveč in se odstrani. Odločitev nosi zastavica mailClient: true pri TARGETS.thunderbird v tooling/build.mjs, izvedena pa je v dveh plasteh:

Varnostni in zasebnostni profil

Testiranje