Safari is a supported active target with its own entry points. Built-in providers use static scripts and Safari requests no all-sites access. User-added sites receive a runtime optional permission, then the background injects content.js after navigation only for granted, enabled, non-builtin rules, with allFrames per rule. Safari settings are device-local and do not roam between devices. macOS packaging uses Xcode and the Apple Developer Program/App Store route.

Reveal URLs — Arhitektura

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

Pregled

Reveal URLs je jedna MV3 WebExtension koja se iz jedne zajedničke, čiste jezgre koda gradi za Chrome, Edge, Opera, Firefox, Safari i Thunderbird. Safari koristi statične skripte samo za ugrađene pružatelje i objavljen je u App Storeu. Proširenje prikazuje odredišni URL svakog linka pored samog linka u rendered email sadržaju i označava link kada vidljivi tekst upućuje na drugu registrable domenu od one iz href.

Repozitorij je pnpm i TypeScript monorepo opisan datotekama pnpm-workspace.yaml i package.json, a "private": true sprječ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čitljivi direktorij dist/<target>/. webextension-polyfill se bundla u svaki script umjesto da se oslanja na runtime global.

Dva native dodatka za e-poštu proširuju istu jezgru detekcije na površine do kojih WebExtension ne može doći: 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 mijenjanje DOM-a prikazane poruke, oba prikazuju panel ili karticu s nalazima, a ne inline annotation. Putanja DOM mutacije iz linkProcessor.ts i REVEAL_URLS_CSS tu se ne koriste. Relativni href razrješava se samo prema pouzdanom <base href> unutar same poruke, a u suprotnom se preskače. Ta putanja native nalaza namjerno ostaje ograničena na http:/https:.

Kodna baza jasno razdvaja odgovornosti:

Raspored repozitorija

packages/core — čista jezgra

Nijedan modul ovdje 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 provodi u packages/core/test/purity.test.ts, koji prolazi kroz src/**/*.ts, uklanja tijela komentara pomoću stripComments i provjerava 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 SMIJU koristiti browser.* i to je jedino mjesto gdje 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 vlastiti manifest.json i vlastiti icons/. Chrome, Firefox i Thunderbird dodatno imaju direktorij src/ s entry point-ima. Edge, Opera i Safari imaju samo manifest i ikone i ponovo koriste Chrome src/. Na primjer, 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 vjerne doubles datoteke features/support/webext.ts i features/support/world.ts. Svaki engine također 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 prije svakog await preko claimBootstrap, učitava config preko loadConfigWithRetry, ubacuje stylesheet kroz injectStyles tek nakon uspješnog čitanja i vraća marker pomoću releaseBootstrap ako učitavanje konačno ne uspije. 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 gdje 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 porijeklo je opaque ili inherited, kako to provjerava hasOpaqueOrigin, tada slijedi fallback preko readTopHref, readOpenerHref i readReferrer, uz zaštitu od cross-origin čitanja.

Ograničavanje na korijen sadržaja i praćenje promjena

start pamti contentRoot, kreira annotator preko createAnnotator(config) i obrađuje svaki korijen iz selectRoots(doc, contentRoot), koji omotava querySelectorAll u try/catch kako bi neispravan selector sigurno vratio []. Nakon toga posmatra doc.body koristeći OBSERVER_OPTIONS. processMutations filtrira svaku promjenu prema root-u, ponovo obrađuje target samo kada je Element unutar root-a preko isWithinRoot i closest, a kasno otkrivanje root-a vodi ograničeni prolaz discoverMatchedRoots() po batchu sa jednim querySelectorAll(contentRoot), dedupliciranim pomoću WeakSet. pendingShadowHosts ponovo provjerava ranije plain hostove dok se ne pojavi open shadow root, tako da pokrije i shadow upgrade-e. applyConfig radi teardown, restart ili reflow zavisno od promjene aktivnog pravila i contentRoot.

Označavanje pojedinačnog linka

Annotator se nalazi u packages/core/src/linkProcessor.ts. createAnnotator vraća Annotator s Map, data-ru tokenom iz generateToken i aktivnim config-om. Njegov process(root):

  1. Orezuje zapise registra čije sidro više nije povezano.
  2. Prikuplja sidra kandidate pomoću collectAnchors (potomak a[href] i sam korijen kada je a[href], bez duplikata).
  3. Razrješava odredište svakog sidra pomoću resolveAnnotatableUrl, koji delegira na resolveAnnotatableHref. Vraća null (preskoči) samo za prazan/unutar-stranice ili neparsabilan href, razrješava relativne href-ove prema baseURI i prihvata svaku parsabilnu shemu, uključujući mailto:, tel:, sms: i opaque URL-ove javascript:, data: i blob:. ignoreHosts se primjenjuje samo na http:/https: linkove s hostom.
  4. Bira idempotentnu brzu putanju kada isAnnotationIntact potvrdi da je render fingerprint nepromijenjen i da je svaki kreirani čvor još povezan i na svom mjestu; u suprotnom vraća zastarjelu anotaciju i ponovo renderira.
  5. Računa nepodudaranje prema shemi s destinationMismatch na čistom tekstu sidra: classifyScheme usmjerava web linkove na hostMismatch, poštu na mailMismatch, telefon na phoneMismatch, a sve ostalo na opaqueMismatch. Zatim poziva annotateAnchor; showOnlyOnMismatch potiskuje samo vidljivo inline otkrivanje linka bez nepodudarnosti, zadržavajući njegov odredišni title.

annotateAnchor renderira samo SAFE-DOM. Uvijek čuva izvorni title (TitleSave) i zapisuje title odredišta — jedini zapis u sidro. Nema zasebnog title režima ni badge-a upozorenja. Osim ako showOnlyOnMismatch sakrije link bez nepodudarnosti, dodaje na početak <span class="reveal-urls-url"> i završni <br>, bez mijenjanja vlastite djece linka. needsLeadingInlineRevealBreak dodaje vodeći <br> kada vidljivi inline sadržaj prethodi linku u istom redu. Span prikazuje doslovno napisani apsolutni href ili relativni href razriješen prema baseURI preko inertnog textContent (s prefiksom strelice i neraskidivog razmaka U+00A0, zatim skraćenog s truncateUrl); nakon povezivanja dobija kontrastnu pozadinu preko applyContrastBackdrop. Za naglašeno nepodudaranje, REVEAL_URLS_MISMATCH_CLASS se dodaje u REVEAL_URLS_URL_CLASS, primjenjujući konfiguriranu boju nepodudaranja pomoću applyColour i podebljanje prema zadanim postavkama u REVEAL_URLS_CSS; eksplicitno nadjačavanje debljine fonta putem applyFontOverrides ima prednost. Svaki kreirani čvor označen je s data-ru="<token>" (nikada sidro), a svaki zapis čuva svoj configFingerprint, tako da promjena render config-a prisiljava reflow. revertOwned uklanja kreirane čvorove po referenci i vraća sačuvani title; revertAll to radi za svako posjedovano sidro i čisti registar.

truncateUrl radi preko Unicode CODE POINTS ([...displayHref]), tako da rez nikada ne dijeli surrogate par; čuva porijeklo sheme i hosta te skraćuje ostatak završnim URL_ELLIPSIS.

Nepodudaranje hosta

packages/core/src/hostMismatch.ts hostMismatch je web grana destinationMismatch: poredi vidljivi tekst linka s njegovim href prema registrable domeni, a ne sirovom hostu, tako da se poddomen bez nepodudarnosti ne označava, dok se look-alike označava. extractHostCandidates dijeli tekst i svodi svaki token preko hostCandidate (koji zahtijeva tačku i ICANN public suffix koji tldts prepoznaje, odbijajući običan tačkasti tekst kao e.g). I href host i svaki kandidat svode se na registrable domenu pomoću tldts.getDomain, a BILO KOJI kandidat čija se domena razlikuje od href-ove daje nepodudaranje — zato navođenje stvarnog zlonamjernog hosta pored hosta mamca ne može potisnuti upozorenje. Nikada ne baca exception.

Za mail linkove mailMismatch poredi sve registrable domene primatelja, uključujući to, cc i bcc; phoneMismatch poredi razrješive birane brojeve. opaqueMismatch uvijek označava SCRIPT_LIKE_SCHEMESjavascript:, data: i vbscript: — dok se druge opaque sheme označavaju samo kada vidljivi tekst navodi konkretno web, email ili telefonsko odredište.

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 treba li 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 GDJE annotation radi putem match i allFrames, te KOJI container je ograničava kroz contentRoot, zajedno s enabled i oznakom builtin. DEFAULT_SITES obuhvata FastMail, Gmail, Proton sa allFrames, Tuta, Yahoo, Zoho 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 s validiranim pattern-om i delegira path dio na pathGlobMatches. parsePatternParts dijeli pattern na host, path, scheme i wildcardHost.

Kada se pravila preklapaju, primjenjuje 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 prijaviti 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 provjerava 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 dodijeljena porijekla, pohranjeni config i aktivne registracije te ih usklađuje kroz property-aware diff. sameRegistration normalizira i željeni descriptor i live read-back. Pošto ne postoji updateContentScripts, izmijenjeni script se prvo uklanja pa ponovo registruje. reconcileContentScripts koristi promise inFlight kako bi serijalizirao triggere koji se mogu dogoditi gotovo istovremeno.

Tok eksplicitne dozvole

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

Konfiguracija, pohrana i kanonski obrazac

Jedinstveni validacijski lijevak

normaliseConfig u packages/core/src/config.ts je jedina kanonska read putanja. On prisilno pretvara i ograničava svako polje, vraća se na zadanu vrijednost kada je unos neispravan i nikada ne baca exception. Sastavljen je od validatora po poljima kao što su normaliseBoolean, 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 s built-in pravilom i vraća sve ugrađene stavke koje nedostaju.

Pohrana

packages/webext/src/storage.ts obavija WebExtension storage API. configArea koristi browser.storage.sync kada je dostupan, a prelazi na browser.storage.local, na primjer u Thunderbirdu. getConfig, setConfig i onConfigChanged uvijek provlače vrijednosti kroz normaliseConfig, tako da pokvarena pohrana nikada ne može vratiti neispravan Config. Oba add-on oblika koriste isti lijevak nad vlastitim host storage-om.

Kanonska konfiguracija i ciljane izmjene

Tri write putanje čitaju kanonski config, mijenjaju tačno jednu stvar i zapisuju ga nazad, bez spremanja nedovršenih form edits:

toggleEnabled u packages/webext/src/toolbarAction.ts slijedi 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 vlastiti 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/, bundla script-ove kao IIFE, options stranicu kao ESM i kopira options.html i options.css.

tooling/version.mjs utiskuje verzije. Uzima MAJOR.MINOR iz korijenskog 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, rasterizira assets/icon.svg u icon48.png i icon128.png.

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

Internacionalizacija (i18n)

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

Postoje tri nezavisne lokalizacijske 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.

Sigurnosna i privatnosna pozicija

Testiranje