Reveal URLs — Architektúra

Tento dokument opisuje, ako je rozšírenie Reveal URLs usporiadané a ako funguje jeho základná logika. Je určený prispievateľom a technicky zameraným čitateľom. V celom texte odkazuje na skutočné súbory a symboly (v tvare path symbol), aby sa každé tvrdenie dalo overiť v zdrojovom kóde. Používateľský pohľad nájdete v manual; kroky ručného overenia sú v manual test plan.

Prehľad

Reveal URLs je jedna MV3 WebExtension budovaná pre viacero enginov — Chrome, Edge, Opera, Firefox a Thunderbird — z jedného zdieľaného, čistého jadra kódu. (Safari je zatiaľ odložené: má manifest a dá sa zostaviť, ak ho pomenujete výslovne, ale nie je súčasťou predvoleného buildu ani CI.) Rozšírenie zobrazuje cieľový URL každej poštovej väzby vedľa odkazu v vykreslenom e-maile a označí odkaz, ktorého viditeľný text pomenúva inú registrovateľnú doménu než jeho href — klasický príznak phishingu.

Repozitár je monorepo pnpm/TypeScript (pnpm-workspace.yaml, package.json s "private": true). Balenie riadi jediný skript postavený na esbuild, tooling/build.mjs, ktorý kompiluje zdieľané balíčky aj tenké vstupné body jednotlivých enginov do načítateľného adresára dist/<target>/. webextension-polyfill sa balí priamo do každého skriptu namiesto spoliehania sa na globálny runtime objekt, takže rovnaký zdroj beží bez úprav v Chromium aj Gecko.

Dva natívne poštové doplnky rozširujú rovnaké detekčné jadro na povrchy, kam sa WebExtension nedostane: Outlook Add-in (panel Office.js, dostupný v Outlook on the web/Windows/Mac/iOS/Android a Outlook.com) a Gmail Add-on (Apps Script CardService, pre Gmail na webe/Android/iOS). Oba znovu používajú iba čistú analýzu — nový modul packages/core/src/findings.ts (analyseAnchors/analyseHtml, platformovo neutrálny model Finding[]) stavia na existujúcej logike hostMismatch a odvodzovaní registrovateľných domén cez tldts. Líši sa iba hostiteľský adaptér (ako sa číta a parsuje telo správy) a prezentácia. Outlook panel parsuje telo vlastným DOMParser a beží na strane klienta; Gmail doplnok parsuje cez node-html-parser a beží na serverovej infraštruktúre Google Apps Script V8. Keďže ani jeden framework nedokáže meniť vykreslený DOM správy v režime čítania (setAsync/prependAsync v Office fungujú len pri kompozícii a CardService rendruje karty, nie HTML správy), oba doplnky zobrazujú panel/kartu zistení namiesto vkladania anotácií priamo do textu. Existujúca DOM-mutačná cesta z linkProcessor.ts ani REVEAL_URLS_CSS sa preto v týchto doplnkoch nepoužívajú. Relatívny href sa vyhodnocuje iba voči dôveryhodnému <base href> v e-maile; inak sa preskočí a nikdy sa nevyhodnocuje voči pôvodu schránky alebo poskytovateľa.

Kódová základňa má zodpovednosti ostro oddelené:

Rozloženie repozitára

packages/core — čisté jadro

Žiadny modul tu neimportuje API rozšírenia (browser.*/chrome.*/messenger.*) a nepoužíva DOM sinky, ktoré prijímajú reťazce ako markup (innerHTML/insertAdjacentHTML). Tento kontrakt mechanicky vynucuje packages/core/test/purity.test.ts, ktorý prejde všetky src/**/*.ts, odstráni telá komentárov vlastným stripComments (aby docblocky, ktoré tieto tokeny len pomenúvajú, nespôsobili falošný poplach) a potom overí, že sa v žiadnom module nezhodujú ani EXTENSION_API_PATTERN, ani UNSAFE_DOM_PATTERN. Verejná plocha sa re-exportuje z packages/core/src/index.ts. Jedinou runtime závislosťou je tldts, potrebný na porovnávanie domén s ohľadom na public suffix.

Moduly sú:

packages/webext — obaly browser-API

Moduly tu už MÔŽU používať browser.* a sú jediným miestom, kde sa dotýkame WebExtension API. Verejná plocha sa re-exportuje z packages/webext/src/index.ts. Moduly sú: content.ts (životný cyklus content skriptov), contentRegistration.ts (dynamická registrácia), storage.ts (perzistencia konfigurácie), background.ts (správanie pri inštalácii), toolbarAction.ts (prepínač + badge), messageDisplay.ts a messageDisplayBackground.ts (Thunderbird) a options/options.ts (radič stránky nastavení).

extensions/<engine> — tenké vstupné body a manifesty

Každý engine má vlastný manifest.json a icons/. Chrome, Firefox a Thunderbird navyše obsahujú src/ so vstupnými bodmi; Edge, Opera a Safari majú len manifest a ikony a ZNOVU používajú src/ z Chrome (deklarované v build deskriptore nižšie). Vstupné body sú zámerne malé: napríklad extensions/chrome/src/content.ts je iba void createContentController().bootstrap(); a extensions/chrome/src/background.ts volá registerBackground, registerContentReconciliation a registerToolbarToggle. Firefox background entry je zhodný s Chrome; Thunderbird (extensions/thunderbird/src/background.ts) namiesto toho volá registerBackground a registerMessageDisplay, pretože content skripty nemá. extensions/_template je kopírovateľná chromium kostra, nie build target.

tooling, features a smoke testy

tooling obsahuje build.mjs, version.mjs a icons.mjs. features obsahuje Gherkin scenáre, step definitions a verné test doubles v features/support/webext.ts a features/support/world.ts. Každý engine má aj extensions/<engine>/test/manifest.smoke.test.mjs (_templatetemplate.smoke.test.mjs) — tie predstavujú autoritatívny kontrakt manifestu (pozri nižšie časť „Build pre jednotlivé enginy a kontrakt manifestu“).

Pipeline odhaľovania

Cesta obsahu sa začína v engine entry, napríklad extensions/chrome/src/content.ts, ktoré volá createContentController().bootstrap() z packages/webext/src/content.ts.

Bootstrap

createContentController spustí bootstrap raz pre každý frame:

  1. Frame si nárokuje synchronne, ešte pred akýmkoľvek await, cez claimBootstrap, ktorý číta a nastavuje príznak BOOTSTRAP_MARKER na window daného frame. Statický vstup v content_scripts aj prekrývajúci sa dynamický používateľský skript môžu do toho istého frame injektovať content.js; zdieľané window v isolated world je preto správna ochrana „iba raz“ a skoré nárokovanie zaručí, že dve takmer súčasné injekcie neprejdú obe.
  2. Najprv načíta perzistovanú konfiguráciu cez loadConfigWithRetry (BOOTSTRAP_CONFIG_RETRIES dodatočných pokusov pri dočasnom zlyhaní), pričom marker drží počas opakovaní, aby sa prekrývajúca injekcia, ktorá už skončila ako no-op, neocitla v nekonzistentnom stave. Až po úspešnom čítaní injektuje stylesheet (injectStyles, ktorý priraďuje REVEAL_URLS_CSS cez textContent), takže zlyhané čítanie nepridá žiadny <style> a retry ho nemôže zdvojiť.
  3. Ak načítanie konfigurácie zlyhá pri všetkých pokusoch, releaseBootstrap marker vráti späť (iba tu, pred začatím anotácie), aby neskoršia reinjekcia mohla skúsiť znova; keď už start prebehne, marker musí zostať, lebo nový anotátor by nevlastnil uzly z predchádzajúceho behu.
  4. Keď config.enabled platí, zavolá start, a vždy zaregistruje listener onConfigChanged, ktorý znovu aplikuje konfiguráciu a pokrýva tak prechod disabled → enabled.

Určenie pravidla aktívnej stránky

start (aj applyConfig) volá resolveSiteRule, aby sa rozhodlo, či a kde sa má anotovať. resolveSiteRule najprv skúsi vlastnú lokaciou bežiaceho dokumentu cez resolveForHref, ktorý filtruje config.sites na povolené pravidlá a deleguje na selectMostSpecific, takže vyhrá najšpecifickejšia zhoda. Keď vlastná lokácia nič nezhoduje a pôvod dokumentu je nepriehľadný alebo zdedený — riadené hasOpaqueOrigin, ktoré vracia true iba pre about:blank/about:srcdoc (zdedené cez INHERITED_ORIGIN_URLS) alebo blob:/data: (OPAQUE_ORIGIN_SCHEMES) — pristupuje sa postupne k TOP frame (readTopHref), otvárajúcemu oknu (readOpenerHref) a napokon k document.referrer (readReferrer). Každé externé čítanie je chránené proti cross-origin prístupu a pridá kandidáta iba vtedy, keď je čitateľné. Bežný nezhodný dokument je autoritatívny a nikdy nepreberá pravidlo z okolitého kontextu.

Obmedzenie na koreň obsahu a sledovanie zmien

start si uloží contentRoot z vyhovujúceho pravidla, vytvorí anotátor cez createAnnotator(config) a spracuje každý koreň zhodný s selectRoots(doc, contentRoot). Potom pozoruje doc.body so OBSERVER_OPTIONS ({ childList: true, subtree: true }).

Observer posiela mutácie do processMutations, ktoré každú mutáciu filtruje cez content root: cieľ znovu spracuje len vtedy, keď je Element vnútri rootu, a každý pridaný element pošle do processAddedNode, ktoré anotuje uzol vtedy, keď je v roote, je sám rootom alebo root obsahuje. Vďaka tomu sa zachytí churn v tele správy aj neskoršie pripojenie súrodeneckého tela správy v SPA, zatiaľ čo aplikačné chrome mimo rootov zostane nedotknuté. Null anotátor je bez vedľajších účinkov.

applyConfig zosúlaďuje živú zmenu konfigurácie: keď už pravidlo neplatí alebo je rozšírenie vypnuté, ukončí sa; keď sa zmení contentRoot, vykoná reštart (revert + nové observe); inak iba pretečie konfiguráciu na mieste — annotator.setConfig a následné annotator.process nad každým rootom bez reštartu observera.

Označenie jedného odkazu

Anotátor žije v packages/core/src/linkProcessor.ts. createAnnotator vracia Annotator, ktorého stav žije v closure a stránka ho nevidí: silná Map kľúčovaná identitou anchoru, token data-ru generovaný cez generateToken (crypto.getRandomValues) a aktuálna konfigurácia. Metóda process(root):

  1. Odstráni záznamy pre anchory, ktoré už nie sú pripojené.
  2. Nazbiera kandidátne anchory cez collectAnchors.
  3. Vyrieši cieľ každého anchoru cez resolveAnnotatableUrl, ktorý vracia null pre prázdny alebo vnútrostránkový href, neparsovateľný href, schému inú než http:/https: a hostiteľov z ignoreHosts.
  4. Vezme idempotentnú rýchlu cestu, keď isAnnotationIntact potvrdí nezmenený fingerprint renderu a všetky vytvorené uzly sú stále pripojené a na správnom mieste; inak starú anotáciu revertuje a vyrenderuje znovu.
  5. Vypočíta nesúlad cez hostMismatch na očistenom texte anchoru, rešpektuje showOnlyOnMismatch a volá annotateAnchor.

annotateAnchor vytvára iba bezpečný DOM. V režime "inline" vloží na začiatok <span class="reveal-urls-url"> s URL nastaveným cez textContent (s predponou šípky a U+00A0, skracovaný cez truncateUrl) a zaň <br>, pričom deti samotného odkazu nechá nedotknuté. Pripojený span potom dostane kontrastné pozadie cez applyContrastBackdrop. V režime "title" uloží pôvodný title (TitleSave), prepíše ho URL a pri zvýraznenom nesúlade pridá súrodenecký badge <span class="reveal-urls-warn"> s textom REVEAL_URLS_WARN_LABEL. Každý vytvorený uzol je označený data-ru="<token>" a každý záznam nesie configFingerprint, aby zmena render-konfigurácie vynútila nové rozloženie. Farby a typografické override sa aplikujú cez typed element.style.* properties (applyColour/applyFontOverrides), nikdy nie interpoláciou do CSS reťazca. revertOwned odstráni vytvorené uzly podľa referencie a obnoví uložený title; revertAll to vykoná pre všetky vlastnené anchory a vyčistí register.

truncateUrl pracuje s Unicode code points ([...displayHref]), takže nikdy neprereže surrogate pair; zachová schému a hostiteľa a zvyšok skráti pridaním URL_ELLIPSIS.

Nesúlad hostiteľa

packages/core/src/hostMismatch.ts hostMismatch porovnáva viditeľný text odkazu s jeho href podľa registrovateľnej domény, nie podľa surového hostname. Čestná subdoména sa teda neoznačí, zatiaľ čo napodobnenina áno. extractHostCandidates rozdelí text a každý token zredukuje cez hostCandidate, ktoré vyžaduje bodku aj ICANN public suffix rozpoznaný cez tldts. Hostiteľ z href aj kandidáti sa zredukujú na registrovateľnú doménu cez tldts.getDomain a nesúlad vznikne pri ktoromkoľvek kandidátovi s inou doménou. Funkcia nikdy nevyhadzuje výnimku.

Štýly a kontrast

packages/core/src/styles.ts obsahuje injektovateľný REVEAL_URLS_CSS (pevné rozloženie, váhu, tvar a plne nepriehľadné biele pozadie čipu) aj runtime helpery applyColour, applyFontOverrides a applyContrastBackdrop. applyContrastBackdrop číta vypočítanú farbu textu a prvé nepriehľadné pozadie pri prechode od samotného elementu smerom nahor cez getComputedStyle; keď packages/core/src/contrast.ts needsWhiteBackdrop vyhodnotí, že text nespĺňa WCAG AA (CONTRAST_THRESHOLD) a biele pozadie skutočne pomôže, nastaví style.backgroundColor = "white". contrast.ts poskytuje parseColour, relativeLuminance a contrastRatio; parsuje tvary rgb()/rgba()/hex, ktoré vracia getComputedStyle, a úplne transparentnú farbu chápe ako „nenájdenú“.

Konfigurovateľní hostitelia a rozsah obsahu

SiteRule v packages/core/src/config.ts určuje KDE anotácia beží (match, allFrames) a KTORÝ kontajner ju obmedzuje (contentRoot), spolu s enabled a príznakom builtin. Dodané DEFAULT_SITES pokrývajú Gmail, Proton (allFrames) a oba Outlook hosty. Config združuje globálne prepínače, farby, typografické override a pole sites.

Engine match patternov

packages/core/src/matchPattern.ts obsahuje zámerne reštriktívnu gramatiku MATCH_PATTERN (iba http/https, voliteľný leading *. wildcard hosta, glob path; bez portu, bez schémy *, bez <all_urls>). matchesPattern porovná URL s validovaným patternom a cestu deleguje na pathGlobMatches. parsePatternParts rozkladá pattern na host, path, scheme a wildcardHost.

Pri prekrývajúcich sa pravidlách vyhráva najšpecifickejšie: compareSiteSpecificity hodnotí presný host nad wildcard hostom, dlhší doslovný host, doslovnejšiu cestu a nakoniec deterministický ASCII tiebreak; selectMostSpecific vracia najšpecifickejšie povolené pravidlo zhodné s URL.

Pokrytie udelených originov je samostatný a zámerne širší matcher. permissions.getAll() môže vracať granty v plnej webextension gramatike (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*), ktorú reštriktívny MATCH_PATTERN neprijíma. parseGrantedOrigin ich rozloží do GrantedOriginParts a originCovers hlási, či grant pokrýva pravidlo match. matchAllowsOriginFallback zasa hlási, či je cesta pravidla presne ORIGIN_FALLBACK_PATH (/*), teda bezpečný prienik obmedzení Chromium a Gecko/Firefox 128 pri fallbacku nepriehľadných originov.

Dynamická registrácia

packages/webext/src/contentRegistration.ts registruje jeden dynamický content script pre každé používateľom pridané pravidlo. Built-in pravidlá obsluhujú statické content_scripts a nikdy sa dynamicky neregistrujú. desiredContentScripts filtruje config.sites na nebuilt-in, povolené pravidlá s udeleným originom, každé mapuje na RegisteredContentScript so stabilným id (contentScriptId, FNV-1a hash match patternu) a nastaví matchOriginAsFallback: true iba vtedy, keď matchAllowsOriginFallback(rule.match) platí. Tým sa zabráni tomu, aby jedno pravidlo s nevyhovujúcou cestou zhodilo celú dávku registrácií; takýto skript sa zaregistruje bez pokrytia pop-out okien.

reconcileContentScriptsOnce načíta udelené originy, uloženú konfiguráciu a živé registrácie a potom ich zosúladí cez diff podľa vlastností: sameRegistration normalizuje požadovaný deskriptor aj hodnotu vrátenú API na rovnakú podobu, vrátane skutočných predvolených hodnôt. Keďže neexistuje updateContentScripts, zmenený skript sa odregistruje a zaregistruje znovu. reconcileContentScripts to obaluje in-flight promise (inFlight), aby sa dve takmer súčasné spustenia nesúťažili. registerContentReconciliation pripája triggery permissions.onAdded/onRemoved a onConfigChanged a vykoná jedno zosúladenie pri štarte; importuje sa len z Chrome a Firefox background entry.

Tok voliteľného oprávnenia

Statické manifesty deklarujú optional_host_permissions (http://*/*, https://*/*), aby mohol používateľ za behu udeliť ľubovoľný ďalší webový host. packages/webext/src/options/options.ts addSite validuje celé kandidátne pravidlo (match aj content root) cez normaliseSiteRule ešte pred žiadosťou o oprávnenie, odvádza host origin cez matchOrigin a volá browser.permissions.request zo samotného click gesta na tlačidle Add. Až po udelení oprávnenia pridá riadok builtin:false a uloží ho cez kanonickú zapisovaciu cestu.

Konfigurácia, úložisko a kanonický vzor

Jediný validačný lievik

normaliseConfig v packages/core/src/config.ts je jediná kanonická cesta čítania: každé pole pretypuje, ohraničí a pri neplatnej hodnote sa vráti k predvolenému stavu; nikdy nevyhadzuje výnimku. Stavia na validátoroch normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts, normaliseMatchColour, normaliseMismatchColour, normaliseCssSize, normaliseFontWeight, normaliseContentRoot, normaliseMatchPattern a normaliseSites. normaliseSites zahodí neplatné pravidlá, deduplikuje podľa match, vynúti builtin: true pre pravidlá zhodné s built-in matchmi, znovu doplní chýbajúce built-in pravidlá a zoradí ich alfanumericky podľa match. configFingerprint hashuje iba polia, ktoré ovplyvňujú render.

Úložisko

packages/webext/src/storage.ts obaluje WebExtension storage API. configArea používa browser.storage.sync, keď je dostupné, a inak padá na browser.storage.local. configAreaName hlási, ktorá oblasť je aktívna. getConfig, setConfig a onConfigChanged vždy vedú surovú hodnotu cez normaliseConfig, takže ani poškodené úložisko nevráti neplatný Config. onConfigChanged navyše ignoruje udalosti z neaktívnej storage oblasti a zmeny nesúvisiacich kľúčov.

Oba doplnky znovu používajú rovnaký vzor read → normaliseConfig → return nad vlastnými host úložiskami. Outlook task pane používa extensions/outlook/src/roamingStorage.ts, Gmail doplnok packages/gmail/src/propertiesStorage.ts. Obe vrstvy exponujú getConfig a setConfig aj card-relevant accessor metódy a chýbajúci host surface ošetria samostatnou chybou. Gmail homepage trigger renderuje CardService settings card z getConfig, onSaveSettings mieša parsované ignoreHosts/highlightMismatch do aktuálnej konfigurácie a perzistuje ich cez setConfig; contextual trigger potom bezpečne číta tieto polia s fallbackom na defaulty.

Vzor kanonickej konfigurácie a cielených aktualizácií

Tri zapisovacie cesty najprv načítajú kanonickú konfiguráciu, zmenia presne jednu vec a zapíšu ju späť — nikdy neuložia nerozpracované úpravy formulára a nikdy celý formulár zbytočne neprekresľujú:

Rovnaký vzor používa aj toolbar action toggleEnabled (packages/webext/src/toolbarAction.ts) z background strany.

Build pre jednotlivé enginy a kontrakt manifestu

tooling/build.mjs riadi jeden spoločný esbuild build pre všetky ciele WebExtension. Deskriptor TARGETS pomenúva zdrojový target každého cieľa: Chrome, Firefox a Thunderbird majú vlastné src/; Edge, Opera a Safari deklarujú chrome ako zdroj; Outlook add-in je samostatný zdroj. ACTIVE_TARGETS (Chrome, Edge, Firefox, Opera a Thunderbird — Safari a Outlook nie) sú ciele, ktoré stavia --all/--package. Safari a Outlook sa stavajú len pri explicitnom uvedení a Gmail add-on sa balí oddelene cez make build-gmail. Gmail bundle cieli na Apps Script V8 runtime, ktorý nemá ES moduly ani natívne URL: zostaví sa ako ESM, odstráni sa jeho záverečné export {…} a pribalí sa malý polyfill URL, aby spoločné jadro vedelo server-side vyhodnocovať odkazy.

buildTarget čistí dist/<target>/, kopíruje cieľové manifest.json a icons/, bundluje každý zdrojový skript ako IIFE a zdieľanú stránku options ako ESM a kopíruje options.html/options.css z packages/webext/src/options/. webextension-polyfill sa balí do každého skriptu.

tooling/version.mjs pečiatkuje verzie: z root package.json berie MAJOR.MINOR a pridáva autoinkrementované BUILD číslo do všetkých target manifestov. Väčšina targetov nesie jeden manifest.json; Outlook add-in nesie dva verzionované manifesty (manifest.json a manifest.xml) a oba sa pečiatkujú, rovnako aj scaffold _template. Root package.json zostáva na rovnakej verzii, ale len v platnom semver tvare. tooling/icons.mjs (spúšťané cez make icons) rastorizuje jediný vektorový zdroj assets/icon.svg na icons/icon48.png a icons/icon128.png pre každý target.

Kontrakt manifestu — teda presné povinné polia pre každý engine — je jediným zdrojom pravdy v extensions/<engine>/test/manifest.smoke.test.mjs. Napríklad extensions/chrome/test/manifest.smoke.test.mjs cez assertManifestShape potvrdzuje tvar manifestu a že každý odkazovaný súbor v builde naozaj existuje. Podľa pravidla deduplikácie sa tu preto presný zoznam polí znovu nevymenúva; pozrite si smoke testy a poznámku „Manifest contract“ v manual test plan.

Internacionalizácia (i18n)

Reveal URLs je lokalizované do mnohých jazykov popri angličtine. Zoznam je single-source v packages/core/src/locales.json (SUPPORTED_LOCALES, native names a default), ktorý čítajú balíky rozšírenia aj plain-Node build webu. Každý neanglický reťazec je strojovo preložený a čaká na ľudskú revíziu; značenie pôvodu sa líši podľa formátu.

Existujú tri nezávislé lokalizačné povrchy, zámerne bez vzájomného prepojenia, a každý je rozdelený na build-time a runtime časť.

Potlačenie „Aktívnych stránok“ v poštovom klientovi (AD-3)

Mail-client target (dnes Thunderbird) už vidí každú vykreslenú správu, takže editor „Active sites“ je nadbytočný a odstraňuje sa. Rozhodnutie nesie flag mailClient: true v tooling/build.mjs na TARGETS.thunderbird a implementuje sa v dvoch poloviciach:

Bezpečnostný a súkromný profil

Testovanie