Reveal URLs — Arhitektura

Ovaj dokument opisuje kako je strukturirano proširenje Reveal URLs i kako radi njegova temeljna logika, za suradnike i tehničke čitatelje. U njemu se stalno upućuje na stvarne datoteke i simbole, u obliku path symbol, kako bi se svaka tvrdnja mogla provjeriti u izvornom kodu. Za opis iz perspektive korisnika pogledajte manual. Za korake ručne provjere pogledajte manual test plan.

Pregled

Reveal URLs je jedinstveni MV3 WebExtension izgrađen za više enginea, odnosno Chrome, Edge, Opera, Firefox i Thunderbird, iz jedne zajedničke čiste jezgre koda. Safari je odgođen: ima manifest i može se buildati kada se izričito zatraži, ali je isključen iz zadanog builda i iz CI-ja. Proširenje otkriva odredišni URL svake poveznice uz samu poveznicu u prikazanom emailu i označava poveznicu čiji vidljivi tekst navodi drukčiju registrable domenu od one u njezinu href.

Repozitorij je monorepo za pnpm i TypeScript, opisan u pnpm-workspace.yaml i package.json, pri čemu ga "private": true sprječava da bude javno objavljen kao paket. Bundling vodi jedna jedina skripta pogonjena esbuildom, tooling/build.mjs, koja prevodi zajedničke pakete i tanka entry points svake platforme u učitljiv direktorij dist/<target>/. webextension-polyfill uključuje se u svaki script bundle umjesto da se oslanja na runtime global, tako da se isti izvorni kod neizmijenjen izvršava u Chromiumu i Gecku.

Dva izvorna dodatka za email proširuju istu jezgru otkrivanja na površine do kojih WebExtension ne može doprijeti: Outlook Add-in temeljen na Office.js task paneu i Gmail Add-on temeljen na Apps Script CardServiceu. Oba ponovno koriste samo PURE analizu iz packages/core/src/findings.ts, s analyseAnchors, analyseHtml, platformski neutralnim modelom Finding[], postojećom logikom hostMismatch i izvođenjem registrable domene preko tldts. Razlikuju se samo host adapter, odnosno način čitanja i parsiranja tijela poruke, te prezentacija. Outlook task pane radi parsiranje vlastitim DOMParser-om i izvršava se client-side. Gmail add-on radi parsiranje s node-html-parser i izvršava se server-side na Googleovoj Apps Script V8 infrastrukturi. Budući da nijedan od tih okvira ne dopušta mijenjanje prikazanog DOM-a poruke u načinu čitanja, oba dodatka prikazuju ploču ili karticu s nalazima, a ne inline annotation. Postojeći DOM-mutation put u linkProcessor.ts i REVEAL_URLS_CSS ne koriste se ponovno. Relativni href rješava se samo u odnosu na pouzdani unutar-email <base href> i inače se preskače.

Kodna baza strogo odvaja odgovornosti:

Raspored repozitorija

packages/core — čista jezgra

Nijedan modul ovdje ne uvozi extension API, poput browser.*, chrome.* ili messenger.*, i nijedan ne koristi string-to-markup DOM sink poput innerHTML ili insertAdjacentHTML. Taj se ugovor mehanički provodi kroz packages/core/test/purity.test.ts, koji prikuplja sve src/**/*.ts, uklanja tijela komentara vlastitim stripComments i potom provjerava da se ni EXTENSION_API_PATTERN ni UNSAFE_DOM_PATTERN ne podudaraju ni s jednim modulom. Javna površina ponovno se izvozi iz packages/core/src/index.ts. Jedina runtime ovisnost je tldts, za usporedbu domena uz razumijevanje public suffixa.

Glavni moduli su:

packages/webext — omotači browser API-ja

Moduli ovdje SMIJU koristiti browser.* i to je jedino mjesto na kojem se dodiruju WebExtension API-ji. Javna površina ponovno se izvozi iz packages/webext/src/index.ts. Moduli su content.ts za content lifecycle, contentRegistration.ts za dinamičku registraciju, storage.ts za persistence configa, background.ts za ponašanje pri instalaciji, toolbarAction.ts za toggle i badge, messageDisplay.ts i messageDisplayBackground.ts za Thunderbird te options/options.ts kao controller stranice postavki.

extensions/<engine> — tanke ulazne točke i manifesti

Svaki engine nosi vlastiti manifest.json i vlastiti icons/. Chrome, Firefox i Thunderbird dodatno nose direktorij src/ s entry points. Edge, Opera i Safari nose samo manifest i ikone te ponovno koriste Chromeov src/, kako određuje build descriptor. Entry points namjerno su vrlo mali. Na primjer, extensions/chrome/src/content.ts sadrži samo void createContentController().bootstrap();, dok extensions/chrome/src/background.ts poziva registerBackground, registerContentReconciliation i registerToolbarToggle. Firefoxov background entry isti je kao Chromeov. Thunderbirdov, extensions/thunderbird/src/background.ts, poziva registerBackground i registerMessageDisplay, jer nema content scripts. extensions/_template služi kao Chromium scaffold i nije build target.

tooling, features i smoke testovi

tooling sadrži build.mjs, version.mjs i icons.mjs. features sadrži Gherkin scenarije, njihove step definitions i vjerne test doubles u features/support/webext.ts i features/support/world.ts. Svaki engine također nosi extensions/<engine>/test/manifest.smoke.test.mjs, a za _template to je template.smoke.test.mjs. Oni su autoritativni ugovor manifesta.

Tok razotkrivanja

Put sadržaja počinje na entry pointu enginea, kao što je extensions/chrome/src/content.ts, koji poziva createContentController().bootstrap() iz packages/webext/src/content.ts.

Bootstrap

createContentController bootstrap izvršava se jednom po okviru.

  1. Sinkrono prisvaja okvir prije svakog await, preko claimBootstrap, koji čita i postavlja oznaku BOOTSTRAP_MARKER na window okvira. Statički built-in content_scripts entry i preklapajući dynamic user script mogu oba ubrizgati content.js u isti okvir, pa je zajednički isolated-world window ispravan once-only guard.
  2. Najprije učitava spremljeni config preko loadConfigWithRetry, s dodatnim pokušajima BOOTSTRAP_CONFIG_RETRIES pri transient rejectionu. Tek kada čitanje uspije, ubrizgava stylesheet preko injectStyles, koji zapisuje REVEAL_URLS_CSS pomoću textContent.
  3. Ako učitavanje configa propadne u svakom pokušaju, releaseBootstrap vraća marker prije početka annotationa, kako bi kasniji re-injection mogao ponovno pokušati.
  4. Kada je config.enabled istinit, poziva se start, a u svakom se slučaju registrira listener onConfigChanged, kako bi se config ponovno primijenio pri promjeni.

Određivanje pravila aktivnog mjesta

start, kao i applyConfig, poziva resolveSiteRule kako bi odlučio treba li i gdje napraviti annotation. resolveSiteRule najprije pokušava s lokacijom samog dokumenta preko resolveForHref, koji filtrira config.sites na omogućena pravila i konačni izbor prepušta selectMostSpecific iz jezgre, tako da pobjeđuje najspecifičniji pogodak. Ako se lokacija samog dokumenta ne podudara ni s čim, a njegov je origin opaque ili inherited, što provjerava hasOpaqueOrigin, tada redom slijedi fallback na TOP frame preko readTopHref, na OPENER window preko readOpenerHref i naposljetku na document.referrer preko readReferrer. Svako vanjsko čitanje čuva se od cross-origin pristupa i daje kandidata samo kad se doista može pročitati. Običan unmatched document smatra se autoritativnim i ne nasljeđuje pravilo iz okoline.

Ograničavanje na korijen sadržaja i promatranje promjena

start zapisuje selector contentRoot pravila koje odgovara, stvara annotator preko createAnnotator(config) i obrađuje svaki odgovarajući korijen iz selectRoots(doc, contentRoot), koji omata querySelectorAll u try/catch kako bi nevažeći selector sigurno završio kao []. Nakon toga promatra doc.body, stabilan container, pomoću OBSERVER_OPTIONS.

Observer hrani processMutations, koji filtrira svaku promjenu u odnosu na contentRoot. Target se ponovno obrađuje samo kada je Element unutar roota preko isWithinRoot, a svaki dodani Element prolazi kroz processAddedNode, koji annotates čvor koji je unutar roota, sam root ili sadrži root, kao kod lazy SPA mounta. Tako su pokriveni i churn unutar message bodyja i kasnije montiranje susjednog message bodyja, dok ostatak aplikacijskog chromea ostaje netaknut. Null annotator je no-op.

applyConfig usklađuje živu promjenu configa. Radi teardown kada nijedno pravilo ne odgovara ili kada je enabled isključen, radi restart kada se promijeni razriješeni contentRoot, a inače radi reflow na mjestu pomoću annotator.setConfig i annotator.process nad svakim odgovarajućim korijenom bez restarta observera.

Označavanje pojedinačne poveznice

Annotator živi u packages/core/src/linkProcessor.ts. createAnnotator vraća Annotator s closure stateom koji stranica ne može pročitati: snažan Map vezan uz identitet anchor elementa, token data-ru po annotatoru iz generateToken i trenutni config. Metoda process(root) radi sljedeće:

  1. Orezuje entries registra čiji anchor više nije povezan.
  2. Prikuplja kandidate anchor elemenata preko collectAnchors, odnosno descendant a[href] i, ako je sam root a[href], i sam root.
  3. Rješava odredište svakog anchor elementa preko resolveAnnotatableUrl, koji vraća null za prazan href, in-page href, href koji se ne može parsirati, schemes različite od http: i https:, ili host koji je jednak ili podređen nekom unosu iz ignoreHosts.
  4. Uzima idempotent fast path kada isAnnotationIntact potvrdi da se render fingerprint nije promijenio i da su svi stvoreni čvorovi još uvijek povezani i na pravom mjestu. U suprotnom radi revert starog annotationa i ponovno renderiranje.
  5. Računa mismatch pomoću hostMismatch nad očišćenim tekstom anchor elementa, poštuje showOnlyOnMismatch i na kraju poziva annotateAnchor.

annotateAnchor renderira samo SAFE-DOM. U načinu "inline" na početak umeće <span class="reveal-urls-url"> čiji se URL zapisuje preko textContent, s prefiksom strelice i U+00A0 non-breaking space, a zatim <br>, bez diranja djece same poveznice. U načinu "title" sprema izvorni title, zamjenjuje ga URL-om i, kod naglašenog mismatcha, dodaje susjedni <span class="reveal-urls-warn"> s REVEAL_URLS_WARN_LABEL. Svaki stvoreni čvor dobiva data-ru="<token>", ali nikada sama poveznica, a svaki entry bilježi configFingerprint, tako da promjena polja koja utječu na render prisiljava reflow. Boje i nadjačavanja fonta primjenjuju se preko typed element.style.* properties, nikada interpolacijom u CSS string. revertOwned i revertAll uklanjaju čvorove u vlasništvu annotatora i vraćaju spremljeni title.

truncateUrl radi nad Unicode code points pomoću [...displayHref], kako rez ne bi prekinuo surrogate pair. Zadržava origin scheme-and-host i skraćuje ostatak s trailing URL_ELLIPSIS.

Nepodudaranje hosta

U packages/core/src/hostMismatch.ts, hostMismatch uspoređuje vidljivi tekst poveznice s njezinim href na razini registrable domene, a ne raw hostnama. Tako se poštena sub-domain ne označava, dok se look-alike host označava. extractHostCandidates dijeli tekst na tokene i svodi ih preko hostCandidate, koji zahtijeva točku i ICANN public suffix koji tldts prepoznaje. I host hrefa i svaki kandidat svode se na registrable domenu preko tldts.getDomain. Bilo koji kandidat čija se domena razlikuje od hrefove daje mismatch. Funkcija nikada ne baca exception.

Stilovi i kontrast

packages/core/src/styles.ts drži injectable REVEAL_URLS_CSS, odnosno fixed layout, weight, shape i fixed bijelu potpuno opaque pozadinu chipa, te runtime helpere applyColour, applyFontOverrides i applyContrastBackdrop. applyContrastBackdrop čita resolved colour elementa i prvu opaque pozadinu koju pronađe penjući se od samog elementa prema gore preko getComputedStyle. Kada packages/core/src/contrast.ts needsWhiteBackdrop prijavi da tekst ne zadovoljava WCAG AA CONTRAST_THRESHOLD u odnosu na tu pozadinu i da bijela doista pomaže, postavlja style.backgroundColor = "white". contrast.ts daje parseColour, relativeLuminance i contrastRatio.

Podesivi hostovi i ograničavanje sadržaja

SiteRule u packages/core/src/config.ts govori GDJE se annotation izvršava, preko match i allFrames, i KOJI ga container ograničava, preko contentRoot, zajedno s enabled i oznakom builtin. DEFAULT_SITES pokrivaju Gmail, Proton s allFrames i oba Outlook hosta. Config objedinjuje globalne toggles, colours, font overrides i polje sites.

Mehanizam obrazaca podudaranja

packages/core/src/matchPattern.ts drži namjerno restrictive add-time gramatiku MATCH_PATTERN: samo http i https, neobavezni početni *. host wildcard, glob path, bez porta, bez scheme *, bez <all_urls>. matchesPattern podudara URL s validiranim patternom, prepuštajući path funkciji pathGlobMatches, gdje svaka * predstavlja proizvoljan niz znakova. parsePatternParts dijeli validirani pattern na host, path, scheme i wildcardHost.

Kod preklapajućih pravila vrijedi "most specific wins". compareSiteSpecificity prvo rangira exact host iznad wildcard hosta, zatim dulji literal host, zatim literalniji path preko literalPathLength, a na kraju deterministic ASCII tiebreak. selectMostSpecific vraća najspecificiranije omogućeno pravilo koje odgovara URL-u.

Provjera granted-origin coveragea zaseban je i namjerno širi matcher. permissions.getAll() može vratiti grants u punoj gramatici WebExtensiona (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*) koje restrictive MATCH_PATTERN odbacuje. parseGrantedOrigin ih pretvara u GrantedOriginParts, a originCovers provjerava pokriva li granted origin ruleov match. matchAllowsOriginFallback prijavljuje je li path pravila točno ORIGIN_FALLBACK_PATH, odnosno /*.

Dinamička registracija

packages/webext/src/contentRegistration.ts registrira jedan dynamic content script za svako pravilo koje korisnik doda. Built-ins se isporučuju preko static content_scripts entries i nikada se ne registriraju dinamički. desiredContentScripts filtrira config.sites na non-builtin omogućena pravila čiji je origin granted, preko isOriginGranted i originCovers, mapira svako pravilo na RegisteredContentScript sa stabilnim id-jem iz contentScriptId i postavlja matchOriginAsFallback: true samo kada to dopušta matchAllowsOriginFallback.

reconcileContentScriptsOnce čita granted origins, spremljeni config i live registrations te konvergira preko property-aware diffa. sameRegistration projicira i željeni descriptor i live read-back u normalised form, primjenjujući stvarna zadana ponašanja WebExtension API-ja, tako da freshly registered script bude jednak descriptoru koji ga je proizveo. Budući da ne postoji updateContentScripts, promijenjeni script uklanja se i ponovno registrira. reconcileContentScripts omata taj tok in-flight promiseom, kako bi se dva gotovo istodobna okidača serializirala umjesto da uđu u race. registerContentReconciliation povezuje triggere i jednom konvergira pri pokretanju. Uvozi se samo iz background entryja za Chrome i Firefox.

Tok izričitog dopuštenja

Static manifests deklariraju optional_host_permissions (http://*/*, https://*/*) kako bi korisnik u runtimeu mogao odobriti proizvoljan dodatni web host. packages/webext/src/options/options.ts addSite validira cijelo kandidatno pravilo, odnosno match i content root, preko normaliseSiteRule prije traženja ikakvog dopuštenja. Zatim izvodi host origin pomoću matchOrigin, poziva browser.permissions.request iz click gesture gumba Add i tek nakon odobrenja dodaje red builtin:false i sprema ga canonical write putem.

Konfiguracija, pohrana i kanonski uzorak

Jedinstveni validacijski lijevak

normaliseConfig u packages/core/src/config.ts jedina je canonical read path. Pretvara, ograničava i validira svako polje, vraćajući se na zadanu vrijednost pri svemu neispravnom i nikada ne baca exception. Izgrađen je od validatora po polju, poput normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts, normaliseMatchColour, normaliseMismatchColour, normaliseCssSize, normaliseFontWeight, normaliseContentRoot, normaliseMatchPattern i normaliseSites. normaliseSites odbacuje nevaljane entries, uklanja duplikate po match, nameće builtin: true kada je neki match jednak built-inu, ponovno sije built-ins koji nedostaju i sortira alfanumerički po match, proizvodeći canonical order. configFingerprint hashira samo polja koja utječu na render.

Pohrana

packages/webext/src/storage.ts omata WebExtension storage API. configArea koristi browser.storage.sync kada je dostupan, kako bi postavke putovale, i radi fallback na browser.storage.local, primjerice u Thunderbirdu. getConfig, setConfig i onConfigChanged uvijek provlače svoje raw vrijednosti kroz normaliseConfig, pa izobličen ili namjerno izmijenjen store nikada ne može predati neispravan Config pozivatelju. onConfigChanged dodatno ignorira događaje iz neaktivnog storage područja i promjene nepovezanih ključeva.

Dva add-ona ponovno koriste isti funnel read → normaliseConfig → return nad vlastitim host storeovima. Outlook task pane omata Office.context.roamingSettings u extensions/outlook/src/roamingStorage.ts. Gmail add-on omata PropertiesService.getUserProperties() u packages/gmail/src/propertiesStorage.ts. Oba izlažu getConfig i setConfig, zajedno s accessorima koje card UI treba, i štite unavailable host surface posebnim errorom.

Uzorak kanonske konfiguracije i ciljanih ažuriranja

Tri write patha čitaju canonical config, mijenjaju točno jednu stvar i zatim ga ponovno zapisuju, bez spremanja unsaved form edits i bez re-renderiranja forme:

toggleEnabled iz packages/webext/src/toolbarAction.ts slijedi isti obrazac sa strane backgrounda.

Build po engineu i ugovor manifesta

tooling/build.mjs vodi jedinstveni esbuild build za svaki target WebExtensiona. Descriptor TARGETS za svaki target navodi njegov SOURCE TARGET. Chrome, Firefox i Thunderbird imaju vlastiti src/. Edge, Opera i Safari navode chrome kao source. Outlook add-in ima vlastiti source. ACTIVE_TARGETS, odnosno Chrome, Edge, Firefox, Opera i Thunderbird, grade se preko --all i --package. Safari i Outlook grade se samo kada se izričito zatraže. Gmail add-on bundlea se zasebno naredbom 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 se uklanja završni export {…}, kako bi trigger functions ostale top-level globals koje Apps Script može pozvati. Istodobno se bundlea mali URL polyfill, koji se instalira samo kada nedostaje. buildTarget čisti dist/<target>/, kopira per-target manifest.json i icons/ bez izmjena, svaki source script bundlea kao IIFE i stranicu postavki kao ESM te kopira options.html i options.css iz packages/webext/src/options/.

tooling/version.mjs utiskuje verzije. Uzima MAJOR.MINOR iz root package.json, pronalazi najveći postojeći BUILD number u manifestima i dodaje ga kao treću komponentu svakom target manifestu, držeći ih u lockstepu. Outlook add-in nosi dva versioned manifesta, manifest.json i manifest.xml, i oba se utiskuju. Root package.json zadržava istu verziju, ali ostaje valjan semver. tooling/icons.mjs, pokrenut preko make icons, rasterises jedini vector source assets/icon.svg u icon48.png i icon128.png za svaki target.

Ugovor manifesta, odnosno točna potrebna polja po engineu, definiran je u extensions/<engine>/test/manifest.smoke.test.mjs. Ondje se također potvrđuje da svaki referenced file doista postoji u izgrađenom distu. Za detaljan shape polja projekt upućuje na smoke testove i odgovarajuću bilješku u manual test plan.

Internacionalizacija (i18n)

Reveal URLs lokaliziran je na mnoge jezike osim engleskog. Skup jezika jedinstveno proizlazi iz packages/core/src/locales.json, koji sadrži SUPPORTED_LOCALES, native names i zadani jezik. I bundleovi proširenja i site build u plain Nodeu čitaju odatle. Svaki neengleski string machine-translated je i čeka ljudski pregled. Marker podrijetla razlikuje se po formatu: _locales ga nose u description, site kataloge bilježe ga u site/i18n/README.md, a prevedeni izvori docs nose HTML komentar u prvom retku.

Postoje tri neovisne lokalizacijske površine, namjerno nepovezane, i svaka je podijeljena na build-time i runtime stranu.

Potiskivanje „Aktivnih mjesta” u klijentu e-pošte (AD-3)

Target mail clienta, danas Thunderbird, već vidi svaku prikazanu poruku, pa je editor "Active sites" po hostu suvišan i uklanja se. Odluka se nosi kroz dokumentirani per-target descriptor flag mailClient: true u tooling/build.mjs, a ne kroz hardcoded target name.

Sigurnosni i privatnosni profil

Testiranje