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 — Architektura
Tento dokument popisuje, jak je rozšíření Reveal URLs strukturováno a jak funguje
jeho základní logika, pro přispěvatele a technické čtenáře. Cituje v celém rozsahu
skutečné soubory a symboly (ve tvaru path symbol), takže lze jakékoli tvrzení
ověřit oproti zdrojovému kódu. Pro popis zaměřený na uživatele viz příručku; pro
kroky manuálního ověření viz plán manuálních testů.
Přehled
Reveal URLs je jediné MV3 WebExtension sestavené pro několik enginů — Chrome,
Edge, Opera, Firefox, Safari a Thunderbird — z jednoho sdíleného, čistého jádra
kódu. Safari používá statické skripty pouze pro vestavěné poskytovatele a je
publikováno v App Storu. Rozšíření odhaluje cílovou URL každého odkazu vedle
odkazu ve vykreslené zprávě a označuje odkaz, jehož viditelný text uvádí jinou
registrovatelnou doménu než jeho href — klasický příznak phishingu.
Repozitář je monorepo pnpm/TypeScript (pnpm-workspace.yaml,
package.json s "private": true). Bundlování je jediný skript řízený esbuildem,
tooling/build.mjs, který kompiluje sdílené balíčky a tenké vstupní body každého
enginu do načítatelného adresáře dist/<target>/.
webextension-polyfill je zabalen do každého skriptu, spíše než aby se na něj
spoléhalo jako na runtime global, takže tentýž zdroj běží beze změny napříč Chromiem
a Geckem.
Dvě nativní e-mailová rozšíření rozšiřují totéž detekční jádro na povrchy, na které
WebExtension nedosáhne: doplněk pro Outlook (podokno úloh Office.js, dosahující
na Outlook na webu/Windows/Macu/iOS/Androidu a Outlook.com) a doplněk pro Gmail
(CardService Apps Script, dosahující na aplikace Gmail web/Android/iOS). Oba znovu
používají pouze ČISTOU analýzu — nový modul packages/core/src/findings.ts
(analyseAnchors/analyseHtml, platformově neutrální model Finding[]), který
znovu používá stávající logiku hostMismatch a odvození registrovatelné domény
tldts. Per povrch se liší pouze adaptér hostitele (jak se čte a parsuje tělo) a
prezentace: podokno úloh Outlooku parsuje tělo vlastním DOMParser a běží
na straně klienta; doplněk pro Gmail parsuje pomocí node-html-parser a běží
na straně serveru na infrastruktuře V8 Apps Script Googlu (bezplatný hosting,
protože Google již poštu má; nasazováno lokálně-interaktivně přes clasp). Protože
ani jeden framework nemůže mutovat vykreslený DOM zprávy v režimu čtení (Office'ovské
setAsync/prependAsync jsou pouze pro psaní; CardService vykresluje karty, nikoli
HTML zprávy), oba doplňky prezentují panel/kartu zjištění, nikoli vloženou
anotaci — stávající cesta mutace DOM linkProcessor.ts a REVEAL_URLS_CSS NEjsou
žádným z doplňků znovu použity. Relativní href se rozlišuje pouze oproti důvěryhodnému
<base href> v e-mailu a jinak se přeskakuje, nikdy oproti původu mailboxu/poskytovatele
(což by cíl vyfabrikovalo).
Tato cesta nativních zjištění záměrně zůstává omezená na http:/https:.
Kódová základna ostře odděluje odpovědnosti:
packages/coreobsahuje ČISTOU logiku — žádné rozhraní API prohlížeče, žádné úložiště, žádné sinky značkování. Je jednotkově testovatelná v prostém JavaScriptu a je jediným zdrojem pravdy pro anotaci odkazů, validaci konfigurace, párování vzorů shody, porovnání hostitelů a obarvování s ohledem na kontrast.packages/webextobaluje rozhraní API WebExtension kolem tohoto jádra: životní cyklus obsahu, dynamickou registraci obsahových skriptů, uživatelské rozhraní možností, propojení na pozadí, akci panelu nástrojů a cestu zobrazení zpráv Thunderbirdu.extensions/<engine>obsahuje tenké vstupní body pro jednotlivé enginy plusmanifest.jsonkaždého enginu.toolingobsahuje skripty pro sestavení, otiskování verzí a generování ikon.featuresobsahuje BDD scénáře Cucumber a jejich testovací dvojníky.
Rozvržení repozitáře
packages/core — čisté jádro
Každý modul zde neimportuje žádné rozhraní API rozšíření (browser.*/chrome.*/messenger.*)
a nepoužívá žádný DOM sink typu řetězec-do-značkování (innerHTML/insertAdjacentHTML).
Tento kontrakt je mechanicky vynucen packages/core/test/purity.test.ts, který
globuje každý src/**/*.ts, odstraňuje těla komentářů přes vlastní stripComments
(takže docblocky, které legitimně JMENUJÍ zakázané tokeny, nespustí strážce) a poté
tvrdí, že ani EXTENSION_API_PATTERN, ani UNSAFE_DOM_PATTERN neodpovídá žádnému
modulu. Veřejný povrch se znovu exportuje z packages/core/src/index.ts. Jedinou
runtime závislostí je tldts (pro porovnání domén s ohledem na veřejné přípony).
Moduly jsou:
packages/core/src/config.ts— schémaConfig/SiteRule, výchozí hodnoty a jediný validační trychtýřnormaliseConfig.packages/core/src/matchPattern.ts— gramatika vzoru shody, matcher, řazení podle specifičnosti a pokrytí udělených původů.packages/core/src/linkProcessor.ts— anotátor v rozsahu dokumentu.packages/core/src/destination.ts— klasifikace a porovnání neshody cíle podle schématu.packages/core/src/hostMismatch.ts— kontrola neshody registrovatelné domény.packages/core/src/styles.tsapackages/core/src/contrast.ts— vkládatelný stylopis a obarvování s ohledem na kontrast.packages/core/src/safeColour.ts— predikát bezpečné barvyisSafeColour.
packages/webext — obaly rozhraní API prohlížeče
Moduly zde MOHOU používat browser.* a jsou jediným místem, kde se dotýká rozhraní
API WebExtension. Veřejný povrch se znovu exportuje z packages/webext/src/index.ts.
Moduly jsou: content.ts (životní cyklus obsahu), contentRegistration.ts
(dynamická registrace), storage.ts (perzistence konfigurace), background.ts
(chování při instalaci), toolbarAction.ts (přepínač + odznak), messageDisplay.ts
a messageDisplayBackground.ts (Thunderbird) a options/options.ts (řadič
stránky nastavení).
extensions/<engine> — tenké vstupní body a manifesty
Každý engine nese svůj vlastní manifest.json a icons/. Chrome, Firefox a
Thunderbird navíc dodávají src/ vstupních bodů; Edge, Opera a Safari nesou pouze
manifest a ikony a ZNOVU POUŽÍVAJÍ Chromovský src/ (deklarováno deskriptorem
sestavení — viz níže). Vstupy jsou záměrně nepatrné: například
extensions/chrome/src/content.ts je jen
void createContentController().bootstrap(); a
extensions/chrome/src/background.ts volá registerBackground,
registerContentReconciliation a registerToolbarToggle. Firefoxův vstup na pozadí
je totožný s Chromovým; Thunderbirdův
(extensions/thunderbird/src/background.ts) volá místo toho registerBackground a
registerMessageDisplay (nemá žádné obsahové skripty). extensions/_template
je Chromium lešení připravené ke kopírování, nikoli cíl sestavení.
tooling, features a kouřové testy
tooling obsahuje build.mjs, version.mjs a icons.mjs. features obsahuje
Gherkin scénáře, jejich definice kroků a věrné testovací dvojníky v
features/support/webext.ts a features/support/world.ts. Každý engine rovněž
nese extensions/<engine>/test/manifest.smoke.test.mjs (_templateovský je
template.smoke.test.mjs) — to jsou autoritativní kontrakt manifestu (viz
„Sestavení per engine a kontrakt manifestu“ níže).
Odhalovací kanál
Obsahová cesta začíná u vstupu enginu jako
extensions/chrome/src/content.ts, který volá createContentController().bootstrap()
z packages/webext/src/content.ts.
Bootstrap
createContentController bootstrap běží jednou na rámec:
- Nárokuje rámec synchronně, před jakýmkoli
await, přesclaimBootstrap, který čte/nastavuje příznakBOOTSTRAP_MARKERnawindowrámce. Statický vestavěný záznamcontent_scriptsa překrývající se dynamický uživatelský skript mohou oba vložitcontent.jsdo téhož rámce; sdílené izolovanéwindowz toho činí správnou strážku pouze-jednou a nárokování před jakýmkoliawaitznamená, že dvě téměř současná vložení jí nemohou obě projít. - Načte perzistovanou konfiguraci NEJPRVE přes
loadConfigWithRetry(BOOTSTRAP_CONFIG_RETRIESdodatečných pokusů při přechodném odmítnutí), přičemž drží marker napříč opakováními, takže překrývající se vložení, které již provedlo no-op, není osiřelé. Až poté, co čtení uspěje, vloží stylopis (injectStyles, který přiřazujeREVEAL_URLS_CSSpřestextContent), takže neúspěšné čtení nepřipojí žádný<style>a opakování jej nemůže duplikovat. - Pokud načtení konfigurace selže při každém pokusu,
releaseBootstrapmarker vrátí zpět (jen zde, před začátkem anotace), takže pozdější opětovné vložení může opakovat; jakmilestartproběhl, marker musí přetrvat, protože čerstvý anotátor by nevlastnil uzly předchozího průchodu. - Když
config.enabled, volástarta vždy registruje posluchačonConfigChanged, který znovu aplikuje konfiguraci (pokrývaje přechod zakázáno→povoleno).
Rozlišení aktivního pravidla webu
start (a applyConfig) volá resolveSiteRule, aby rozhodl, zda — a kde —
anotovat. resolveSiteRule nejprve zkouší VLASTNÍ umístění běžícího dokumentu
přes resolveForHref, který filtruje config.sites na povolená pravidla a
odkládá se na jádrové selectMostSpecific, takže vyhraje nejspecifičtější shoda
(specifický vestavěný poráží širokou uživatelskou zástupnou masku). Když vlastní
umístění neodpovídá ničemu A původ vlastního dokumentu je neprůhledný nebo zděděný —
řízeno hasOpaqueOrigin, pravda pouze pro about:blank/about:srcdoc (zděděné přes
INHERITED_ORIGIN_URLS) nebo blob:/data: (OPAQUE_ORIGIN_SCHEMES) — vrací se,
v pořadí, na HORNÍ rámec (readTopHref, vestavěný případ message-iframe Protonu),
okno OPENER (readOpenerHref, vyskakovací čtecí okno Outlooku otevřené přes
window.open("about:blank")) a nakonec document.referrer (readReferrer).
Každé externí čtení je chráněno proti přístupu napříč původy (který vyhazuje) a
přispívá kandidátem pouze tehdy, když je čitelné. OBYČEJNÝ dokument bez shody je
autoritativní a nikdy nedědí pravidlo z okolního kontextu, takže řadič zůstává
netečný.
Omezení na kořen obsahu a pozorování mutací
start zaznamenává selektor contentRoot odpovídajícího pravidla, vytváří anotátor
přes createAnnotator(config) a zpracovává každý odpovídající kořen z
selectRoots(doc, contentRoot) (querySelectorAll zabalený v try/catch, takže
neplatný selektor selže bezpečně na []). Poté pozoruje doc.body (stabilní
kontejner) s OBSERVER_OPTIONS ({ childList: true, subtree: true }).
Pozorovatel napájí processMutations, který každou mutaci gatuje na kořeni obsahu:
znovu zpracovává target mutace pouze tehdy, když je Element v rámci kořene
(isWithinRoot, přes closest), zatímco pozdní objevování kořenů zajišťuje
omezený průchod discoverMatchedRoots() pro každý batch (jedno
querySelectorAll(contentRoot) na batch, deduplikované přes WeakSet).
pendingShadowHosts levně znovu kontroluje dříve plain hosty, dokud se
neobjeví open shadow root, takže jsou pokryty i shadow upgrady. Takže churn
v kořeni i shadow-host upgrady jsou zachyceny, zatímco rozhraní aplikace mimo
kořeny zůstává nedotčené. Nulový anotátor (rozšíření je zastaveno) je no-op.
applyConfig uvádí do souladu živou změnu konfigurace: bourá, když neodpovídá žádné
pravidlo nebo je příznak povoleno vypnutý; restartuje (vrácení + opětovné pozorování),
když se rozlišený contentRoot změnil; a jinak přeteče na místě — annotator.setConfig
a poté annotator.process přes každý odpovídající kořen — bez restartování
pozorovatele.
Anotování jediné kotvy
Anotátor žije v packages/core/src/linkProcessor.ts. createAnnotator
vrací Annotator nad stavem uzávěru, který stránka nemůže číst: silná Map
klíčovaná identitou kotvy, token data-ru na anotátor z
generateToken (crypto.getRandomValues) a aktuální konfigurace. Jeho metoda
process(root):
- Prořezává záznamy registru, jejichž kotva již není připojena.
- Sbírá kandidátské kotvy pomocí
collectAnchors(potomeka[href]plus samotný kořen, když jea[href], deduplikováno). - Rozlišuje cíl každé kotvy pomocí
resolveAnnotatableUrl, který deleguje naresolveAnnotatableHref.null(přeskočit) vrací jen pro prázdný/vnitrostránkový nebo neparsovatelný href, relativní hrefy rozlišuje oprotibaseURIa přijímá každé parsovatelné schéma včetněmailto:,tel:,sms:a neprůhledných URLjavascript:,data:ablob:.ignoreHostsplatí jen prohttp:/https:odkazy s hostitelem. - Bere idempotentní rychlou cestu, když
isAnnotationIntactpotvrdí, že otisk vykreslení je nezměněný a každý vytvořený uzel je stále připojen a na místě; jinak vrátí zastaralou anotaci a znovu vykreslí. - Vypočítá neshodu podle schématu pomocí
destinationMismatchna čistém textu kotvy:classifySchemeposílá webové odkazy nahostMismatch, poštu namailMismatch, telefon naphoneMismatcha ostatní naopaqueMismatch. Pak voláannotateAnchor;showOnlyOnMismatchpotlačuje pouze viditelné vložené odhalení odkazu bez nesouladu a zachovává jeho cílovýtitle.
annotateAnchor vykresluje pouze BEZPEČNÝ DOM. Vždy uloží původní title
(TitleSave) a zapíše cílový title — jediný zápis do kotvy. Neexistuje samostatný
title režim ani varovný odznak. Pokud showOnlyOnMismatch neskryje odkaz bez
nesouladu,
předsadí <span class="reveal-urls-url"> a následující <br>, přičemž vlastní
potomky odkazu ponechá nedotčené. needsLeadingInlineRevealBreak přidá vedoucí
<br>, když odkazu na stejném řádku předchází viditelný vložený obsah. Span zobrazuje
autorem napsaný absolutní href doslovně nebo relativní href rozlišený oproti baseURI
přes inertní textContent (s předponou šipky a nezalomitelné mezery U+00A0, pak
zkrácený truncateUrl); po připojení získá kontrastní pozadí přes
applyContrastBackdrop. Při zdůrazněné neshodě se REVEAL_URLS_MISMATCH_CLASS
přidá do REVEAL_URLS_URL_CLASS, což použije nastavenou barvu neshody přes
applyColour a výchozí tučné písmo v REVEAL_URLS_CSS; výslovné přepsání
tloušťky písma přes applyFontOverrides má přednost. Každý vytvořený uzel je
označen data-ru="<token>" (nikdy kotva) a každý záznam zaznamenává svůj
configFingerprint, takže změna konfigurace vykreslení vynutí přetečení.
revertOwned odstraňuje vytvořené uzly podle reference a obnovuje uložený title;
revertAll to dělá pro každou vlastněnou kotvu a čistí registr.
truncateUrl operuje nad kódovými body Unicode ([...displayHref]), takže řez
nikdy nerozdělí surrogátový pár; zachovává původ schématu a hostitele a zkracuje
zbytek s koncovým URL_ELLIPSIS.
Neshoda hostitele
packages/core/src/hostMismatch.ts hostMismatch je webová větev
destinationMismatch: porovnává viditelný text odkazu oproti jeho href podle
registrovatelné domény, nikoli podle syrového názvu hostitele, takže poddoména bez
nesouladu se neoznačí, zatímco napodobenina ano.
extractHostCandidates rozdělí text a redukuje každý token přes hostCandidate
(který vyžaduje tečku a veřejnou příponu ICANN rozpoznanou tldts, odmítaje běžný
tečkovaný text jako e.g). Jak hostitel href, tak každý kandidát se redukují na
svou registrovatelnou doménu pomocí tldts.getDomain a JAKÝKOLI kandidát, jehož
doména se liší od domény href, dává neshodu — takže pojmenování skutečného
škodlivého hostitele vedle lákacího hostitele nemůže varování potlačit. Nikdy
nevyhazuje.
Pro mailové odkazy mailMismatch porovnává všechny registrovatelné domény
příjemců včetně to, cc a bcc; phoneMismatch porovnává rozlišitelná
vytáčená čísla. opaqueMismatch vždy označí SCRIPT_LIKE_SCHEMES —
javascript:, data: a vbscript: — zatímco ostatní neprůhledná schémata
označí jen tehdy, když viditelný text uvádí konkrétní webový, e-mailový nebo
telefonní cíl.
Styly a kontrast
packages/core/src/styles.ts obsahuje vkládatelné REVEAL_URLS_CSS (pevné
rozvržení, tloušťka, tvar a pevné bílé, plně neprůhledné pozadí čipu — background: #ffffff,
opacity: 1; barvy se do něj NIKDY neinterpolují) a runtime pomocníky
applyColour/applyFontOverrides/applyContrastBackdrop.
applyContrastBackdrop čte rozlišenou barvu prvku a první neprůhledné pozadí
nalezené při procházení od prvku SAMOTNÉHO nahoru (jeho vlastní pozadí — pevné bílé
výchozí čipu nebo specifičtější přepsání hostitelské stránky — před jakýmkoli
předkem) přes standardní getComputedStyle (získané z
element.ownerDocument?.defaultView, takže je bezpečné vůči rámcům a
stubovatelné) a když packages/core/src/contrast.ts needsWhiteBackdrop hlásí, že
text selhává WCAG AA (CONTRAST_THRESHOLD) oproti tomuto pozadí A bílá skutečně
pomáhá, nastaví style.backgroundColor = "white". contrast.ts poskytuje parseColour,
relativeLuminance a contrastRatio; parsuje formy rgb()/rgba()/hex, které
getComputedStyle vrací, a plně průhlednou barvu považuje za „nenalezenou“.
Konfigurovatelní hostitelé a omezování obsahu
SiteRule (packages/core/src/config.ts) říká, KDE anotace běží (match,
allFrames) a KTERÝ kontejner ji omezuje (contentRoot), plus enabled a
příznak builtin. Dodávané DEFAULT_SITES pokrývají FastMail, Gmail, Proton (s
allFrames), Tuta, Yahoo, Zoho a oba hostitele Outlooku. Config agreguje globální přepínače,
barvy, přepsání písem a pole sites.
Engine vzorů shody
packages/core/src/matchPattern.ts obsahuje záměrně RESTRIKTIVNÍ gramatiku pro
dobu přidání MATCH_PATTERN (pouze http/https, volitelná úvodní zástupná maska
hostitele *., glob cesty; žádný port, žádné schéma *, žádné <all_urls>).
matchesPattern porovnává URL oproti validovanému vzoru, přičemž cestu deleguje na
pathGlobMatches (každý * odpovídá libovolné sekvenci znaků); selhává bezpečně na
false. parsePatternParts rozdělí validovaný vzor na host/path/scheme/
wildcardHost.
Překrývající se pravidla se rozlišují podle „nejspecifičtější vyhrává“:
compareSiteSpecificity řadí podle (1) přesný hostitel nad zástupným hostitelem,
(2) delší doslovný hostitel, (3) doslovnější cesta (literalPathLength), poté (4)
deterministický ASCII tiebreak; selectMostSpecific vrací nejspecifičtější
povolené pravidlo odpovídající URL.
Pokrytí udělených původů je SAMOSTATNÝ, záměrně ŠIRŠÍ matcher.
permissions.getAll() může hlásit udělení v plné gramatice WebExtension
(<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*), které by
restriktivní MATCH_PATTERN odmítl. parseGrantedOrigin je parsuje do
GrantedOriginParts a originCovers hlásí, zda udělený původ pokrývá match
pravidla (např. široké udělení https://*/* skutečně pokrývá každé pravidlo https).
matchAllowsOriginFallback hlásí, zda je cesta pravidla přesně
ORIGIN_FALLBACK_PATH (/*) — bezpečný průnik omezení Chromia a
Gecko/Firefox 128 na dynamickém záložním řešení neprůhledného původu.
Dynamická registrace
packages/webext/src/contentRegistration.ts registruje jeden dynamický obsahový
skript na uživatelem přidané pravidlo. Vestavěné jsou obsluhovány statickými
záznamy content_scripts a NIKDY se dynamicky neregistrují. desiredContentScripts
filtruje config.sites na nevestavěná, povolená pravidla, jejichž původ
isOriginGranted (delegováno na originCovers), mapuje každé na
RegisteredContentScript se stabilním id (contentScriptId, hash FNV-1a shody,
takže id používá jen znakovou sadu bezpečnou pro API) a nastavuje
matchOriginAsFallback: true POUZE tehdy, když platí
matchAllowsOriginFallback(rule.match) (cesta /*). Gatování příznaku tímto
způsobem brání jednomu pravidlu s nekonformní cestou způsobit odmítnutí celé dávkové
registrace; takové pravidlo se registruje bez pokrytí vyskakovacího okna, záměrně.
reconcileContentScriptsOnce čte udělené původy, uloženou konfiguraci a
živé registrace, poté konverguje diffem VĚDOMÝM VLASTNOSTÍ:
sameRegistration promítá jak požadovaný deskriptor, tak živé zpětné čtení
na normalizovaný tvar (aplikuje reálnou výchozí hodnotu WebExtension každého pole —
allFrames/matchOriginAsFallback výchozí false, persistAcrossSessions
výchozí TRUE, runAt výchozí document_idle), takže čerstvě zaregistrovaný skript
se porovnává STEJNĚ s deskriptorem, který jej vyprodukoval, a nemůže vzniknout žádná
smyčka opětovné registrace. Protože neexistuje žádné updateContentScripts, změněný
skript se odregistruje a poté znovu zaregistruje. reconcileContentScripts to obaluje
probíhajícím příslibem (inFlight), takže dva téměř současné spouštěče (změna
konfigurace A permissions.onAdded obojí spuštěné při přidání webu) jsou
serializovány spíše než v závodu. registerContentReconciliation propojuje spouštěče
(permissions.onAdded/onRemoved, onConfigChanged) a konverguje jednou při
spuštění; je importováno pouze vstupy na pozadí Chrome a Firefoxu.
Přihlašovací tok oprávnění
Statické manifesty deklarují optional_host_permissions (http://*/*,
https://*/*), takže uživatel může za běhu udělit libovolného dalšího webového
hostitele. packages/webext/src/options/options.ts addSite validuje CELÉ
kandidátské pravidlo (shoda A kořen obsahu) přes normaliseSiteRule PŘED
požádáním o jakékoli oprávnění, odvozuje původ hostitele pomocí matchOrigin a
volá browser.permissions.request z gesta kliknutí na tlačítko Přidat; jen při
udělení připojí řádek builtin:false a perzistuje přes kanonickou cestu zápisu.
Konfigurace, úložiště a kanonický vzor
Jediný validační trychtýř
normaliseConfig v packages/core/src/config.ts je jediná kanonická cesta čtení:
koeruje a omezuje každé pole, přičemž se u čehokoli neplatného vrací na výchozí
hodnotu, a nikdy nevyhazuje. Je sestaven z validátorů per pole —
normaliseBoolean, normaliseMaxLength,
normaliseIgnoreHosts (redukuje každý záznam na holý název hostitele punycode přes
bareHostname), normaliseMatchColour/normaliseMismatchColour (gatováno
safeColour.ts isSafeColour), normaliseCssSize, normaliseFontWeight,
normaliseContentRoot (omezeno CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH),
normaliseMatchPattern (gatováno MATCH_PATTERN) a normaliseSites.
normaliseSites zahazuje odmítnutí, deduplikuje podle match, VYNUCUJE builtin: true
u jakéhokoli pravidla, jehož shoda se rovná shodě vestavěného (uzavíraje
vektor přesná-shoda-zastíní-vestavěný / dvojité vložení), znovu naseje jakýkoli
chybějící vestavěný (takže zmanipulované úložiště nemůže vypustit jádrového
poskytovatele) a řadí abecedně-numericky podle match pro kanonické pořadí.
configFingerprint hashuje pouze pole ovlivňující vykreslení (vyjma enabled,
ignoreHosts a sites, které řídí start/stop a přeskočení spíše než přetečení) přes
fnv1a.
Úložiště
packages/webext/src/storage.ts obaluje rozhraní API úložiště WebExtension.
configArea používá browser.storage.sync, když je k dispozici (takže nastavení
putují), a vrací se na browser.storage.local (např. na Thunderbirdu);
configAreaName hlásí, které je aktivní. getConfig, setConfig a onConfigChanged
všechny prohánějí svou syrovou hodnotu přes normaliseConfig, takže poškozené nebo
zmanipulované úložiště nemůže volajícímu nikdy předat neplatný Config.
onConfigChanged navíc ignoruje události z NEAKTIVNÍ oblasti úložiště a změny
nesouvisejících klíčů.
Oba doplňky znovu používají tentýž trychtýř čtení -> normaliseConfig -> návrat nad
svými vlastními hostitelskými úložišti. extensions/outlook/src/roamingStorage.ts
podokna úloh Outlooku obaluje Office.context.roamingSettings (synchronní get/set,
potvrzené saveAsync); packages/gmail/src/propertiesStorage.ts doplňku pro Gmail
obaluje PropertiesService.getUserProperties() (per uživatel, putující napříč
zařízeními tohoto uživatele, kódováno JSON pod jedním klíčem a nevyžadující ŽÁDNÝ
dodatečný rozsah OAuth — nikdy sdílené ScriptProperties). Oba vystavují
getConfig/setConfig plus přístupové prvky relevantní pro kartu
getIgnoreHosts/getHighlightMismatch a chrání nedostupný hostitelský povrch
vyhrazenou chybou. Domovský spouštěč Gmailu vykresluje CardService kartu nastavení
z getConfig a obsluha odeslání formuláře onSaveSettings sloučí parsované
ignoreHosts/highlightMismatch do aktuální konfigurace a perzistuje ji
přes setConfig; kontextový spouštěč provléká ignoreHosts do adaptéru
a highlightMismatch do stavitele karty, čtouc obranně, takže selhání úložiště se
vrátí k výchozím hodnotám spíše než by rozbilo analýzu zprávy.
Vzor kanonická-konfigurace + cílená-aktualizace
Tři cesty zápisu čtou kanonickou konfiguraci, mění přesně jednu věc a zapisují ji zpět — nikdy nepotvrzují neuložené úpravy formuláře ani znovu nevykreslují formulář:
removeSite(options.ts): čtegetConfig, perzistujesetConfignad polemsitess odfiltrovaným pouze odstraněným pravidlem (perzistuj NEJPRVE, potérow.remove(), poté podmíněnépermissions.removerozhodnuté z další konfigurace). Selhání perzistence vyhazujeSiteRemovalSaveErrora ponechává DOM nedotčený; neúspěšné odvolání vyhazujeSitePermissionRevokeErrorpoté, co je řádek už pryč.addSite(options.ts): popsáno výše — validuj, požádej o oprávnění, připoj řádek, potésaveOptions.toggleEnabled(options.ts): okamžitý hlavní přepínač Povolit překlápí POUZEenabledna uložené konfiguraci a zapisuje ji zpět, bez opětovného vykreslení (takže probíhající úprava se nikdy nepřepíše); při neúspěšném zápisu vrátí zaškrtávací políčko na uloženou hodnotu.
Vlastní toggleEnabled akce panelu nástrojů
(packages/webext/src/toolbarAction.ts) sleduje tentýž vzor ze strany pozadí.
Sestavení per engine a kontrakt manifestu
tooling/build.mjs řídí jediné sestavení esbuild pro každý cíl WebExtension.
Deskriptor TARGETS jmenuje ZDROJOVÝ CÍL každého cíle: Chrome, Firefox a
Thunderbird dodávají svůj vlastní src/; Edge, Opera a Safari deklarují chrome
jako svůj zdroj; doplněk Outlook (Office.js) je svým vlastním zdrojem. ACTIVE_TARGETS
(Chrome, Edge, Firefox, Opera a Thunderbird — Safari a Outlook jsou vyloučeny) je to,
co --all/--package sestavuje; Safari a Outlook se sestavují jen když jsou
pojmenovány explicitně, a doplněk Gmail (Apps Script) je samostatný balíček esbuild
(make build-gmail) mimo smyčku --all. Tento balíček Gmailu cílí na runtime V8
Apps Script, který nemá ani ES moduly, ani nativní URL: je sestaven jako ESM a poté
mu je odstraněn koncový příkaz export {…} (takže spouštěcí funkce zůstávají
top-level globals, které Apps Script může vyvolat), a zabaluje malý polyfill URL
(instalovaný jen když URL chybí), takže new URL(...) sdíleného jádra rozlišuje
odkazy na straně serveru. buildTarget čistí dist/<target>/, kopíruje
manifest.json a icons/ per cíl doslovně, zabaluje každý zdrojový skript jako IIFE
(vkládané INJECTED_SCRIPTS content.ts/messageDisplay.ts nemohou být ES moduly a
pozadí jsou také klasické workery/event pages) a sdílenou stránku možností jako
ESM a kopíruje options.html/options.css z packages/webext/src/options/.
webextension-polyfill je zabalen do každého skriptu.
tooling/version.mjs otiskuje verze: bere MAJOR.MINOR z kořenového
package.json a připojuje automaticky se zvyšující číslo BUILD (čtené z
nejvyšší stávající třetí komponenty napříč cílovými manifesty) do každého cílového
manifestu, udržuje je v zámku. Většina cílů nese jediný manifest.json;
doplněk Outlook nese dva verzované manifesty (manifest.json a manifest.xml)
a oba se otiskují a lešení _template se otiskuje v zámku také. Kořenový
package.json je udržován na téže verzi, ale zůstává platným semverem (čtyřdílná
verze je povolena v manifestu, nikdy v package.json). tooling/icons.mjs
(spouštěno přes make icons) rasterizuje jediný vektorový zdroj assets/icon.svg do
icons/icon48.png a icons/icon128.png každého cíle, zkoušeje cairosvg, poté
inkscape, poté ImageMagick.
Kontrakt manifestu — přesná požadovaná pole per engine — je jediným
zdrojem pravdy v každém extensions/<engine>/test/manifest.smoke.test.mjs (např.
assertManifestShape z extensions/chrome/test/manifest.smoke.test.mjs, který
také sestaví cíl a potvrdí, že se každý odkazovaný soubor v distu rozliší).
Per konvence deduplikace projektu se tvar pole po poli zde NEuvádí znovu; odkažte se
na tyto kouřové testy a na poznámku „Kontrakt manifestu“
v plánu manuálních testů.
Internacionalizace (i18n)
Reveal URLs je lokalizováno do mnoha jazyků kromě angličtiny. Sada je
jednozdrojová v packages/core/src/locales.json (SUPPORTED_LOCALES, nativní
názvy a výchozí hodnota), kterou čtou jak balíčky rozšíření (přes rozlišený import
JSON), tak plainové Node sestavení webu. Každý neanglický řetězec je
strojově přeložený a čeká na lidskou revizi; marker provenience je per formát
(_locales používají description každé zprávy, slovníky rozhraní webu jej
zaznamenávají v site/i18n/README.md a přeložené zdroje dokumentů nesou první řádek
HTML komentář).
Existují tři nezávislé lokalizační povrchy, záměrně nepropojené, a každý je rozdělen podél čáry doba-sestavení / runtime.
- Stránka možností (AD-1). Dodává STATICKÉ anglické značkování; každý přeložitelný
uzel nese anotaci
data-i18n="<key>"(text) nebodata-i18n-<attr>="<key>"(atribut). Za běhu jepackages/webext/src/options/locale.tsaplikační vrstvou:fetchuje vlastní zabalený_locales/<code>/messages.jsonstránky (přesruntime.getURL, není potřeba žádnéweb_accessible_resources), přepisuje každý anotovaný uzel POUZE přestextContent(takže formulářové příkladové nápovědy<code>zůstávají nedotčené) a nastavuje<html lang>. Stránka je PLNĚ lokalizována: vedle statické prózy nesou JS-stavěné popisky řádků webu a tlačítko Odstranit klíčdata-i18n(s počátečnímtranslated textem), takže tatáž aplikační vrstva znovu lokalizuje vykreslené řádky bez opětovného vykreslení, a SKUTEČNĚ dynamické řetězce — stavový řádek, zpětná vazba přidání webu a řádek verze — se vykreslují přestranslate(<key>)zlocale.ts(currentMessages → anglický fallback → klíč) v době volání. Anglický fallback je katalog_locales/en/messages.jsonIMPORTOVANÝ (zabalený) dooptions.jsv době sestavení, takžetranslatese rozlišuje na čitelnou angličtinu SYNCHRONNĚ od načtení modulu — dynamický řetězec vykreslený předtím, než se přepínač usadí, nebo poté, co runtime fetch katalogu selže, nikdy nedegraduje na syrový klíč zprávy.locale.tspublikuje aplikovanou mapu zpráv a spouští své posluchačeonLocaleChangepo každé aplikaci, takžeoptions.tsznovu vykresluje aktuálně zobrazený stav/zpětnou vazbu/verzi a znovu směruje odkaz Online příručka při každém přepnutí. Řádek verze používá zprávuoptionsVersion, jejíž zástupný symbol{version}se nahrazuje číslem sestavení (token přeložen, číslo doslovné). Odkaz příručky sleduje aktivní locale k jeho variantě dokumentu (AD-9): angličtina si ponechává top-levelmanual.html, každá jiná locale otevírá svůj lokalizovaný název souboru pod<code>/(např.nl/handleiding.html, přeloženo přes jádrovélocalisedSitePage— tatáž sdílená mapa, pod kterou sestavení webu soubor emituje), zachovávaje otisk verze?v=. Aktivní locale je uživatelova uložená volba nebo, pokud selže, jazyk prohlížeče rozlišený na podporovaný základní jazyk jádrovýmresolveLocale(AD-5); nepodporovaná nebo selhaná locale ponechá předvykreslenou angličtinu (atranslatepak degraduje na anglický fallback). Manuální přepínač Jazyk zobrazení perzistuje svou volbu pod vyhrazeným klíčemuiLocalevstorage.local— nikdy v synchronizovanémConfig, takže je per zařízení a nikdy neputuje. - Rozhraní webu (AD-2). Generátor statického webu
tooling/site.mjsserver-vykresluje každý uzel rozhraní v angličtině, přičemž jej anotujedata-i18n, a ponechává<html lang="en-GB">(AD-4). Loader na straně prohlížečesite/scripts/i18n.mjs(bezzávislostní ES modul) poté prohodí rozhraní do jazyka návštěvníka podle precedence — perzistovaná volbalocalStorage(revealUrlsSiteLocale) → první podporovaná základní shodanavigator.languages(AD-5) → angličtina, fetchujesite/i18n/<code>.json. Na rozdíl od aplikační vrstvy možností prohazuje přesinnerHTML, protože hodnoty rozhraní webu nesou důvěryhodné vložené značkování (<code>,<a>); test parity připíná sekvenci tag/atribut/URL každé neanglické hodnoty byte-totožnou s angličtinou, takže překlad nikdy nemůže vypustit odkaz nebo rozbít tag. Chybějící klíč ponechává předvykreslený anglický snímek; selhaný fetch ponechává anglickou stránku nedotčenou (elegantní degradace, nikdy prázdno). - Stránky dokumentace (AD-8/AD-9). Stránky dokumentů se vykreslují PER LOCALE v
době sestavení, nikoli prohazované za běhu. Anglické dokumenty zůstávají na
top-level cestách; každá jiná locale dostane kopii pod složkou
<code>/, pod názvem souboru PŘELOŽENÝM do locale (nl/handleiding.html,de/datenschutz.html) ze sdílené mapypackages/core/src/site-page-names.json(domovská stránka zůstáváindex.html), z přeloženéhodocs/<code>/<DOC>.md, když je přítomen, jinak anglického zdroje — záložní řešení per stránka, kterébuildSiteLOGUJE spíše než by tiše vynechalo. Protože dřívější model tyto publikoval pod jejich anglickými názvy, sestavení také emituje tenké přesměrovánínoindexna každé přejmenované staré cestě (nl/manual.html→nl/handleiding.html), takže stará záložka, výsledek vyhledávání nebo odkaz příručky již nainstalovaného staršího rozšíření se stále rozliší. TĚLO dokumentu nenese žádnédata-i18n(je to syrový vykreslený markdown), takže runtime loader rozhraní se ho nikdy nedotkne; prohodí se pouze sdílené uzly rozhraní. Přepínač jazyků stránky dokumentů NAVIGUJE na stránku sourozenecké locale spíše než by prohazoval na místě. - Lokalizace vnitřních odkazů (Finding 2). Zvolený jazyk se přenáší napříč
navigací dokumentů. Každá VNITŘNÍ kotva dokumentu (šest stránek
architecture.html,faq.html,index.html,licence.html,manual.html,privacy.html) je označenadata-doc-link="<page>"generátorem a pro případ bez JavaScriptu její href již rozlišuje na variantu locale AKTUÁLNÍ stránky, pod jejím přeloženým názvem souboru (navDocHref— každá vnitřní stránka nyní má variantu per locale; jen domovská stránka si ponecháváindex.htmlv každé locale). Runtime loaderovorewriteDocLinkspoté znovu zacílí každou vnitřní kotvu dokumentu — ty označené A jakoukoli katalogem vloženou prostou kotvu jakomanual.html#installing— na variantu AKTIVNÍ locale, zachovávaje jakýkoli#hash/?querya nikdy se nedotýkaje externích odkazů. Značkadata-doc-linkvždy nese KANONICKÝ anglický název stránky; rozlišený href používá PŘELOŽENÝ název souboru locale (AD-9,nl/handleiding.html), takže loader mapuje lokalizovaný název souboru zpět na jeho kanonickou stránku, aby zůstal idempotentní. Na stránce dokumentu ULOŽENÁ volba, která se liší od vlastní locale stránky, NAVIGUJE na odpovídající variantu; strážka smyčky se spouští jen při explicitní uložené volbě (nikdy jen při preferenci jazyka prohlížeče) a jen když se cíl liší od již zobrazené stránky. Samotné katalogy si ponechávají své anglické vnitřní URL, takže test parity zůstává zelený — lokalizace je čistý runtime průchod nad DOM.
Potlačení „Aktivních webů“ e-mailového klienta (AD-3)
Cíl e-mailového klienta (dnes Thunderbird) již vidí každou vykreslenou zprávu, takže
editor „Aktivní weby“ per hostitel je tam nadbytečný a je odstraněn. Rozhodnutí
je neseno dokumentovaným deskriptorem per cíl, příznakem mailClient: true, na
TARGETS.thunderbird z tooling/build.mjs — nikdy pevně zakódovaným názvem cíle v
logice sestavení nebo UI — a je implementováno ve dvou polovinách:
- Doba sestavení.
copyAssetsse klíčuje nadescriptorFor(target).mailClient: pro cíl e-mailového klienta spouští čistou transformaciremoveSitesSection(html)(která odstraní celý editor hostitelů<section class="sites">, včetně každého ovládacího prvku přidání webu, a vyhodíMissingSourceError, pokud oddíl chybí, takže změna značkování nemůže nikdy tiše dodat editor) a zapisuje výsledek; cíle prohlížeče dostávajíoptions.htmlbyte za bytem přescpSync. - Runtime. Zabalený řadič
packages/webext/src/options/options.tstoleruje chybějící oddíl:renderSites/readSites/applyConfig/readFormsondují#site-selecta provádějí no-op spíše než by vyhodilyOptionsFieldMissingError, areadFormVYNECHÁVÁ klíčsites, když je oddíl pryč. Protože vynechánísitesby nechalonormaliseConfigznovu naseje vestavěné výchozí hodnoty a vypustit weby uživatele,saveOptionsznovu dosadí uloženésitespřed normalizací, kdykoli je oddíl nepřítomen — to je to, co činí, že uložení e-mailového klienta zachová konfigurované hostitele. Runtime strážka,guardMailClientSites, odstraní oddíl, pokud je stále přítomen, detekujíc e-mailového klienta podle přítomnosti API/funkce (messages/messageDisplay/scripting.messageDisplayna vloženémbrowser), nikdy podle názvu cíle; běží první vinitOptions, takže pozdější propojení přidání/seznamu přirozeně přeskočí nepřítomné prvky. V runtime prohlížeče je strážka no-op.
Postoj k bezpečnosti a soukromí
- Žádná síť ani exfiltrace. Žádný modul neprovádí
fetch/XHRani žádné jiné síťové volání; rozšíření čte pouze uloženou konfiguraci a DOM stránky. - Pouze bezpečný DOM. Anotace a vkládaný stylopis používají
createElement/textContent/typované vlastnostistyle.*výhradně, nikdyinnerHTML/insertAdjacentHTML/eval. Pro jádro je to vynucenopackages/core/test/purity.test.ts. - Veškerý externí/konfigurační vstup normalizován. Každé čtení a zápis se trychtýřuje
přes
normaliseConfig; barvy projdouisSafeColour; vzory shody projdouMATCH_PATTERN; selektory kořene obsahu jsou omezenyCONTENT_ROOT_PATTERNa předány pouzequerySelectorAll/closest, nikdy interpolovány do značkování nebo CSS. - Nejmenší oprávnění. Vestavěné webmailové původy jsou pevná
host_permissions; další hostitelé jsou přihlašovací přesoptional_host_permissionsa gestem vázanýpermissions.request. Akce panelu nástrojů nedeklaruje žádný popup. - Sběr dat Gecko. Manifesty Firefoxu a Thunderbirdu deklarují
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Licence. Projekt je AGPL-3.0-only (
LICENSE,package.json).
Testování
- Jednotkové testy Vitest pokrývají oba balíčky:
packages/core/test/*(config, contrast, host mismatch, link processor, styles a strážka čistoty) apackages/webext/test/*(content, storage, options, registration, toolbar, message display a vlastní end-to-end test webu). Každý balíček spouštívitest run. - Cucumber BDD scénáře žijí v
features/(propojenocucumber.json) a ženou skutečné moduly@reveal-urls/webextpřes věrné testovací dvojníky vfeatures/support/webext.ts— zejménanormaliseReadBack, který zrcadlí engine VYNECHÁVAJÍCÍ false-hodnotové příznaky při zpětném čtení, takže shovívavý postroj nemůže skrýt smyčku opětovné registrace — nad dokumentem jsdom nastaveným vfeatures/support/world.ts. - Kouřové testy manifestu per cíl (
extensions/<engine>/test/manifest.smoke.test.mjs, vestavěnénode:testNode) tvrdí přesný tvar každého manifestu a že sestavený dist rozlišuje každý odkazovaný soubor.tooling/test/přidává testy sestavení, verze a definition-of-done. - CI (
.github/workflows/ci.yml) gatuje každý push a pull request namake test,make bddamake lint(spuštěno přímo na runneru, přepisujíc DockerovskéRUN/IMAGE_DEPMakefilu). Otiskování verzí a balíčkování jsou záměrně vyloučeny; žijí v samostatném, tagem spouštěném.github/workflows/release.yml.