Reveal URLs — Arhitektura

Ovaj dokument opisuje kako je Reveal URLs strukturisan i kako radi njegova osnovna logika, za saradnike i tehničke čitaoce. Kroz ceo tekst navode se stvarne datoteke i simboli u obliku path symbol, tako da svaka tvrdnja može da se proveri u izvornom kodu. Korisnički opis nalazi se u manual, a koraci ručne provere u manual test plan.

Pregled

Reveal URLs je jedna MV3 WebExtension koja se iz jednog zajedničkog, čistog jezgra koda gradi za Chrome, Edge, Opera, Firefox i Thunderbird. Safari je odložen za kasniju fazu: ima manifest i može da se izgradi kada se eksplicitno navede, ali nije uključen u podrazumevani build niti u CI. Ekstenzija prikazuje odredišni URL svakog linka pored samog linka u rendered email sadržaju i označava link kada vidljivi tekst upućuje na drugi registrable domen od onog iz href.

Repozitorijum je pnpm i TypeScript monorepo opisan datotekama pnpm-workspace.yaml i package.json, a "private": true sprečava njegovo objavljivanje kao paketa. Bundling vodi tooling/build.mjs, koji kompajlira zajedničke pakete i tanke entry point-e svakog engine-a u učitljiv direktorijum dist/<target>/. webextension-polyfill se bundluje u svaki script umesto da se oslanja na runtime global.

Dva native dodatka za e-poštu proširuju isto jezgro detekcije na površine do kojih WebExtension ne može da dođe: Outlook Add-in na Office.js i Gmail Add-on na Apps Script CardService. Oba ponovo koriste samo PURE analizu iz packages/core/src/findings.ts, zajedno sa analyseAnchors, analyseHtml, neutralnim modelom Finding[], logikom hostMismatch i izvođenjem registrable domena preko tldts. Razlikuju se samo host adapter i prezentacija. Outlook koristi DOMParser i radi client-side. Gmail koristi node-html-parser i radi server-side na Google Apps Script V8 infrastrukturi. Pošto nijedan od ta dva okvira ne dozvoljava menjanje DOM-a prikazane poruke, oba prikazuju panel ili karticu sa nalazima, a ne inline annotation. Putanja DOM mutacije iz linkProcessor.ts i REVEAL_URLS_CSS tu se ne koriste. Relativni href razrešava se samo prema pouzdanom <base href> unutar same poruke, a u suprotnom se preskače.

Baza koda jasno razdvaja odgovornosti:

Raspored repozitorijuma

packages/core — čisto jezgro

Nijedan modul ovde ne importuje extension API kao što su browser.*, chrome.* ili messenger.*, i nijedan ne koristi string-to-markup DOM sinkove kao što su innerHTML ili insertAdjacentHTML. Taj ugovor se mehanički sprovodi u packages/core/test/purity.test.ts, koji prolazi kroz src/**/*.ts, uklanja tela komentara pomoću stripComments i proverava da ni EXTENSION_API_PATTERN ni UNSAFE_DOM_PATTERN ne odgovaraju nijednom modulu. Javna površina se reeksportuje iz packages/core/src/index.ts, a jedina runtime zavisnost je tldts.

Glavni moduli su:

packages/webext — omotači browser API-ja

Ovi moduli SMEJU da koriste browser.* i to je jedino mesto gde se dotiču WebExtension API-ji. Javna površina se reeksportuje iz packages/webext/src/index.ts. Tu se nalaze content.ts, contentRegistration.ts, storage.ts, background.ts, toolbarAction.ts, messageDisplay.ts, messageDisplayBackground.ts i options/options.ts.

extensions/<engine> — tanki entry point-i i manifesti

Svaki engine ima sopstveni manifest.json i sopstveni icons/. Chrome, Firefox i Thunderbird dodatno imaju direktorijum src/ sa entry point-ima. Edge, Opera i Safari imaju samo manifest i ikone i ponovo koriste Chrome src/. Na primer, extensions/chrome/src/content.ts sadrži samo void createContentController().bootstrap();, a extensions/chrome/src/background.ts poziva registerBackground, registerContentReconciliation i registerToolbarToggle. extensions/thunderbird/src/background.ts poziva registerBackground i registerMessageDisplay. extensions/_template je scaffold, a ne build target.

tooling, features i smoke testovi

tooling obuhvata build.mjs, version.mjs i icons.mjs. features obuhvata Gherkin scenarije, step definitions i verne doubles datoteke features/support/webext.ts i features/support/world.ts. Svaki engine takođe ima extensions/<engine>/test/manifest.smoke.test.mjs, dok _template koristi template.smoke.test.mjs.

Tok otkrivanja

Content putanja počinje u entry point-u engine-a kao što je extensions/chrome/src/content.ts, koji poziva createContentController().bootstrap() iz packages/webext/src/content.ts.

Bootstrap

createContentController bootstrap radi jednom po frame-u. On preuzima frame pre svakog await preko claimBootstrap, učitava config preko loadConfigWithRetry, ubacuje stylesheet kroz injectStyles tek posle uspešnog čitanja i vraća marker pomoću releaseBootstrap ako učitavanje konačno ne uspe. Kada je config.enabled aktivan, poziva start, a u svakom slučaju registruje listener onConfigChanged.

Određivanje aktivnog pravila za sajt

start i applyConfig koriste resolveSiteRule da odluče da li i gde treba izvršiti annotation. Prvo se pokušava lokacija samog dokumenta preko resolveForHref, koji filtrira config.sites na uključena pravila, a konačan izbor prepušta funkciji selectMostSpecific. Ako se dokument ne poklapa, a njegovo poreklo je opaque ili inherited, kako to proverava hasOpaqueOrigin, tada sledi fallback preko readTopHref, readOpenerHref i readReferrer, uz zaštitu od cross-origin čitanja.

Ograničavanje na koren sadržaja i praćenje promena

start pamti contentRoot, kreira annotator preko createAnnotator(config) i obrađuje svaki koren iz selectRoots(doc, contentRoot), koji omotava querySelectorAll u try/catch kako bi neispravan selector bezbedno vratio []. Posle toga posmatra doc.body koristeći OBSERVER_OPTIONS. processMutations filtrira svaku promenu prema root-u, a processAddedNode obuhvata čvorove unutar root-a, sam root i čvorove koji ga sadrže. applyConfig radi teardown, restart ili reflow u zavisnosti od promene aktivnog pravila i contentRoot.

Označavanje pojedinačnog linka

Annotator se nalazi u packages/core/src/linkProcessor.ts. createAnnotator vraća Annotator sa Map, data-ru tokenom iz generateToken i aktivnim config-om. Njegov process(root) čisti odspojene zapise, skuplja a[href] elemente pomoću collectAnchors, razrešava URL preko resolveAnnotatableUrl, koristi brzu putanju isAnnotationIntact kada se render nije promenio i računa mismatch pomoću hostMismatch pre nego što pozove annotateAnchor.

annotateAnchor renderuje samo SAFE-DOM. U režimu "inline" dodaje <span class="reveal-urls-url"> sa textContent i <br>. U režimu "title" čuva postojeći title, menja ga u URL i, kada je mismatch naglašen, dodaje <span class="reveal-urls-warn"> sa REVEAL_URLS_WARN_LABEL. Kreirani čvorovi dobijaju data-ru="<token>", a configFingerprint služi za prisilni reflow. truncateUrl radi nad [...displayHref] i koristi URL_ELLIPSIS.

Nepodudaranje hosta

packages/core/src/hostMismatch.ts hostMismatch poredi vidljivi tekst linka sa njegovim href na nivou registrable domena, a ne sirovog hosta. extractHostCandidates deli tekst na tokene i svodi ih pomoću hostCandidate, koji traži tačku i public suffix koji tldts prepoznaje. Href host i kandidati svode se preko tldts.getDomain. Svaki kandidat sa drugim domenom proizvodi mismatch.

Stilovi i kontrast

packages/core/src/styles.ts čuva REVEAL_URLS_CSS i pomoćne funkcije applyColour, applyFontOverrides i applyContrastBackdrop. applyContrastBackdrop čita izračunatu boju elementa i prvu opaque pozadinu koju pronađe kroz getComputedStyle, a zatim koristi needsWhiteBackdrop iz packages/core/src/contrast.ts da odluči da li treba postaviti style.backgroundColor = "white". contrast.ts pruža parseColour, relativeLuminance i contrastRatio.

Podesivi hostovi i ograničavanje sadržaja

SiteRule u packages/core/src/config.ts određuje GDE annotation radi putem match i allFrames, i KOJI container je ograničava kroz contentRoot, zajedno sa enabled i oznakom builtin. DEFAULT_SITES obuhvata Gmail, Proton sa allFrames i oba Outlook hosta. Config objedinjuje globalne toggle-ove, colours, font overrides i listu sites.

Mehanizam match patterna

packages/core/src/matchPattern.ts sadrži restriktivnu gramatiku MATCH_PATTERN: samo http i https, opcionalni *. wildcard host, glob path, bez porta, bez * scheme-a i bez <all_urls>. matchesPattern poredi URL sa validiranim pattern-om i delegira path deo na pathGlobMatches. parsePatternParts deli pattern na host, path, scheme i wildcardHost.

Kada se pravila preklapaju, primenjuje se pravilo "most specific wins". compareSiteSpecificity daje prednost tačnom hostu nad wildcard hostom, zatim dužem literal hostu, zatim literalnijem path-u preko literalPathLength, a na kraju koristi ASCII tiebreak. selectMostSpecific vraća najspecifičnije uključeno pravilo koje odgovara URL-u.

permissions.getAll() može da prijavi grant-ove u punoj WebExtension gramatici, kao što su <all_urls>, *://*/*, https://*/*, *://*.host/* i https://host/*) koje bi MATCH_PATTERN odbio. parseGrantedOrigin ih pretvara u GrantedOriginParts, originCovers proverava pokrivenost preko match, a matchAllowsOriginFallback govori da li je path tačno ORIGIN_FALLBACK_PATH, odnosno /*.

Dinamička registracija

packages/webext/src/contentRegistration.ts registruje jedan dynamic content script po svakom pravilu koje je dodao korisnik. Built-in pravila opslužuju se preko static content_scripts unosa i nikada se ne registruju dinamički. desiredContentScripts filtrira config.sites, koristi isOriginGranted, originCovers i contentScriptId, te postavlja matchOriginAsFallback: true samo kada to dozvoljava matchAllowsOriginFallback(rule.match).

reconcileContentScriptsOnce čita dodeljena porekla, sačuvani config i aktivne registracije te ih usklađuje kroz property-aware diff. sameRegistration normalizuje i željeni descriptor i live read-back. Pošto ne postoji updateContentScripts, izmenjeni script se prvo uklanja pa ponovo registruje. reconcileContentScripts koristi promise inFlight kako bi serijalizovao triggere koji mogu da se dogode gotovo istovremeno.

Tok eksplicitne dozvole

Static manifesti deklarišu optional_host_permissions (http://*/*, https://*/*) kako bi korisnik u runtime-u mogao da dodeli dozvolu za dodatni host. packages/webext/src/options/options.ts addSite validira celo kandidat-pravilo preko normaliseSiteRule, izvodi host origin preko matchOrigin i poziva browser.permissions.request iz click gesture-a na Add dugmetu. Tek posle odobrenja dodaje red builtin:false.

Konfiguracija, skladištenje i kanonski obrazac

Jedinstveni validacioni levak

normaliseConfig u packages/core/src/config.ts je jedina kanonska read putanja. On prinudno pretvara i ograničava svako polje, vraća se na podrazumevanu vrednost kada je unos neispravan i nikada ne baca exception. Sastavljen je od validatora po poljima kao što su normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts, normaliseMatchColour, normaliseMismatchColour, normaliseCssSize, normaliseFontWeight, normaliseContentRoot, normaliseMatchPattern i normaliseSites. normaliseSites izbacuje neispravne unose, uklanja duplikate po match, prisiljava builtin: true kada se pravilo poklapa sa built-in pravilom i vraća sve ugrađene stavke koje nedostaju.

Skladištenje

packages/webext/src/storage.ts obavija WebExtension storage API. configArea koristi browser.storage.sync kada je dostupan, a prelazi na browser.storage.local, na primer u Thunderbirdu. getConfig, setConfig i onConfigChanged uvek provlače vrednosti kroz normaliseConfig, tako da pokvareno skladištenje nikada ne može da vrati neispravan Config. Oba add-on oblika koriste isti levak nad sopstvenim host storage-om.

Kanonska konfiguracija i ciljane izmene

Tri write putanje čitaju kanonski config, menjaju tačno jednu stvar i upisuju ga nazad, bez čuvanja nedovršenih form edits:

toggleEnabled u packages/webext/src/toolbarAction.ts prati isti obrazac sa background strane.

Build po engine-u i ugovor manifesta

tooling/build.mjs pokreće jedan zajednički esbuild build za svaki WebExtension target. Descriptor TARGETS navodi SOURCE TARGET za svaki target. Chrome, Firefox i Thunderbird imaju sopstveni src/. Edge, Opera i Safari koriste chrome kao izvor. Outlook add-in ima poseban izvor. ACTIVE_TARGETS, odnosno Chrome, Edge, Firefox, Opera i Thunderbird, jesu oni koji se grade preko --all i --package. Safari i Outlook grade se samo kada su eksplicitno navedeni, a Gmail koristi make build-gmail.

Gmail bundle cilja Apps Script V8 runtime, koji nema ni ES modules ni native URL. Zato se gradi kao ESM, a zatim mu se uklanja završni export {…} kako bi trigger funkcije ostale globalne. Mali URL polyfill uključuje se samo kada je potreban. buildTarget čisti dist/<target>/, kopira manifest.json i icons/, bundluje script-ove kao IIFE, options stranicu kao ESM i kopira options.html i options.css.

tooling/version.mjs utiskuje verzije. Uzima MAJOR.MINOR iz korenskog package.json, pronalazi najveći BUILD broj i dodaje ga svakom target manifestu. Outlook add-in ima i manifest.json i manifest.xml. tooling/icons.mjs, pokrenut kroz make icons, rasterizuje assets/icon.svg u icon48.png i icon128.png.

Manifest ugovor definisan je u datotekama extensions/<engine>/test/manifest.smoke.test.mjs, gde se proverava oblik svakog manifesta i postojanje svakog referenced file-a.

Internacionalizacija (i18n)

Reveal URLs je lokalizovan na mnogo jezika osim engleskog. Jedini izvor za taj skup je packages/core/src/locales.json, koji sadrži SUPPORTED_LOCALES, lokalna imena i podrazumevani jezik. Svaki neengleski string je machine-translated i čeka ljudsku proveru. Oznaka porekla zavisi od formata: _locales je nose u polju description, site catalogues je beleže u site/i18n/README.md, a prevedeni docs u prvoj liniji nose HTML komentar.

Postoje tri nezavisne lokalizacione površine:

Potiskivanje „Active sites” u klijentu e-pošte (AD-3)

Mail client target kao što je Thunderbird već vidi sve rendered messages, pa je host-based editor za „Active sites” tu suvišan. Odluka se prenosi kroz polje mailClient: true u tooling/build.mjs.

Bezbednosna i privatnosna pozicija

Testiranje