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 a Thunderbird — z jednoho sdíleného, čistého jádra kódu. (Safari
je odloženo: má manifest a je sestavitelné, když je pojmenováno explicitně, ale je
vyloučeno z výchozího sestavení a z CI.) 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).
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/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), a směruje každý přidaný Element přes
processAddedNode, který anotuje uzel, který je uvnitř kořene, JE kořenem nebo
kořen OBSAHUJE (líné SPA mountování). Takže churn v kořeni i sourozenecké tělo
zprávy mountované později jsou oba 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ý vracínull(přeskočit) pro prázdný/vnitrostránkový href, neparsovatelný href, jakékoli schéma jiné nežhttp:/https:nebo hostitele rovného nebo pod záznamemignoreHosts; rozlišuje relativní hrefy oprotibaseURI, takže výsledek je absolutní. - 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 pomocí
hostMismatchna čistém textu kotvy, ctíshowOnlyOnMismatch(poctivé odkazy ponechá nedotčené) a voláannotateAnchor.
annotateAnchor vykresluje pouze BEZPEČNÝ DOM. V režimu "inline" předsazuje
<span class="reveal-urls-url">, jehož URL se nastavuje přes textContent (s
předponou šipky a nezalomitelné mezery U+00A0, zkrácené truncateUrl) následované
<br>, přičemž vlastní potomky odkazu ponechává nedotčené; připojený span pak
dostane kontrastní pozadí přes applyContrastBackdrop. V režimu "title" uloží
původní title (TitleSave), přepíše jej URL — jediný zápis do kotvy — a při
zdůrazněné neshodě připojí sourozenecký odznak <span class="reveal-urls-warn">
nesoucí REVEAL_URLS_WARN_LABEL. 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í. Barvy
a přepsání písma se aplikují přes typované vlastnosti element.style.*
(applyColour/applyFontOverrides), nikdy interpolované do řetězce CSS.
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 porovnává viditelný text odkazu
oproti jeho href podle registrovatelné domény, nikoli podle syrového názvu
hostitele, takže poctivá poddoména 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.
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í Gmail, Proton (s
allFrames) 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, normaliseRenderMode, 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 (pět stránek
architecture.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í#sites-lista 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.