Reveal URLs — Architectuur
Dit document beschrijft hoe de Reveal URLs-extensie is opgebouwd en hoe haar
kernlogica werkt, voor bijdragers en technische lezers. Het verwijst overal naar
echte bestanden en symbolen (in de vorm path symbol) zodat elke bewering kan
worden gecontroleerd aan de hand van de broncode. Voor de gebruikersgerichte
beschrijving zie de handleiding; voor de handmatige verificatiestappen
zie het handmatige testplan.
Overzicht
Reveal URLs is één MV3-WebExtension, gebouwd voor verschillende engines — Chrome,
Edge, Opera, Firefox en Thunderbird — vanuit één gedeelde, pure codekern. (Safari
is uitgesteld: het heeft een manifest en is bouwbaar wanneer het expliciet wordt
benoemd, maar is uitgesloten van de standaardbuild en van CI.) De extensie onthult
naast elke link in een weergegeven e-mail de bestemmings-URL van die link, en
markeert een link waarvan de zichtbare tekst een ander registreerbaar domein noemt
dan zijn href — het klassieke kenmerk van phishing.
De repository is een pnpm/TypeScript-monorepo (pnpm-workspace.yaml,
package.json met "private": true). Het bundelen is één esbuild-aangedreven
script, tooling/build.mjs, dat de gedeelde pakketten en de dunne entry points van
elke engine compileert tot een laadbare dist/<target>/-map.
webextension-polyfill wordt in elk script gebundeld in plaats van te vertrouwen op
een runtime-global, zodat dezelfde broncode ongewijzigd draait op zowel Chromium als
Gecko.
Twee native e-mail-add-ons breiden dezelfde detectiekern uit naar oppervlakken die
de WebExtension niet kan bereiken: een Outlook-add-in (Office.js-taakvenster, dat
Outlook op web/Windows/Mac/iOS/Android en Outlook.com bereikt) en een Gmail-add-on
(Apps Script CardService, dat de Gmail-apps op web/Android/iOS bereikt). Beide
hergebruiken alleen de PURE analyse — een nieuwe module packages/core/src/findings.ts
(analyseAnchors/analyseHtml, een platformneutraal Finding[]-model) die de
bestaande hostMismatch-logica en tldts-afleiding van het registreerbare domein
hergebruikt. Alleen de host-adapter (hoe de berichttekst wordt gelezen en geparseerd)
en de presentatie verschillen per oppervlak: het Outlook-taakvenster parseert de
berichttekst met zijn eigen DOMParser en draait aan de clientzijde; de
Gmail-add-on parseert met node-html-parser en draait aan de serverzijde op Google's
Apps Script V8-infrastructuur (gratis hosting, aangezien Google de mail al bezit;
lokaal-interactief uitgerold via clasp). Omdat geen van beide frameworks de
weergegeven, lees-modus-berichten-DOM kan muteren (Office's setAsync/prependAsync
zijn alleen voor opstellen; CardService rendert kaarten, geen bericht-HTML), tonen
beide add-ons een paneel/kaart met bevindingen, geen inline-annotatie — het
bestaande DOM-mutatiepad linkProcessor.ts en REVEAL_URLS_CSS worden door geen van
beide add-ons hergebruikt. Een relatieve href wordt alleen opgelost tegen een
betrouwbare in-e-mail <base href> en anders overgeslagen, nooit tegen de
oorsprong van de mailbox/provider (wat een bestemming zou verzinnen).
De codebasis scheidt de verantwoordelijkheden scherp:
packages/corebevat PURE logica — geen browser-API's, geen opslag, geen markup-sinks. Het is unit-testbaar in gewoon JavaScript en is de enige bron van waarheid voor linkannotatie, configuratievalidatie, overeenkomstpatroon-matching, hostvergelijking en contrastbewuste kleuring.packages/webextwikkelt de WebExtension-API's rond die kern: de inhoudslevenscyclus, de dynamische registratie van content-scripts, de instellingen-UI, de achtergrondbedrading, de werkbalkactie en het berichtweergavepad van Thunderbird.extensions/<engine>bevat de dunne entry points per engine plus hetmanifest.jsonvan elke engine.toolingbevat de scripts voor build, versie-stempeling en pictogramgeneratie.featuresbevat de Cucumber-BDD-scenario's en hun test-doubles.
Repository-indeling
packages/core — de pure kern
Elke module hier importeert geen extensie-API (browser.*/chrome.*/messenger.*)
en gebruikt geen string-naar-markup-DOM-sink (innerHTML/insertAdjacentHTML). Dat
contract wordt mechanisch afgedwongen door packages/core/test/purity.test.ts, dat
elke src/**/*.ts globt, commentaarinhoud verwijdert via zijn eigen stripComments
(zodat de docblocks die de verboden tokens legitiem NOEMEN de bewaker niet laten
struikelen) en vervolgens vaststelt dat noch EXTENSION_API_PATTERN noch
UNSAFE_DOM_PATTERN op enige module past. Het publieke oppervlak wordt opnieuw
geëxporteerd vanuit packages/core/src/index.ts. De enige runtime-afhankelijkheid is
tldts (voor public-suffix-bewuste domeinvergelijking).
De modules zijn:
packages/core/src/config.ts— hetConfig/SiteRule-schema, de standaardwaarden en de enkele validatietrechternormaliseConfig.packages/core/src/matchPattern.ts— de overeenkomstpatroon-grammatica, de matcher, de specificiteitsrangschikking en de dekking van verleende oorsprongen.packages/core/src/linkProcessor.ts— de document-gescopete annotator.packages/core/src/hostMismatch.ts— de mismatchcontrole op registreerbaar domein.packages/core/src/styles.tsenpackages/core/src/contrast.ts— de injecteerbare stylesheet en de contrastbewuste kleuring.packages/core/src/safeColour.ts— het veilige-kleur-predicaatisSafeColour.
packages/webext — de browser-API-wrappers
Modules hier MOGEN browser.* gebruiken en zijn de enige plek waar de
WebExtension-API's worden aangeraakt. Het publieke oppervlak wordt opnieuw
geëxporteerd vanuit packages/webext/src/index.ts. De modules zijn: content.ts
(inhoudslevenscyclus), contentRegistration.ts (dynamische registratie), storage.ts
(configuratiepersistentie), background.ts (gedrag bij installatie),
toolbarAction.ts (schakelaar + badge), messageDisplay.ts en
messageDisplayBackground.ts (Thunderbird), en options/options.ts (de controller
van de instellingenpagina).
extensions/<engine> — dunne entry points en manifesten
Elke engine draagt zijn eigen manifest.json en icons/. Chrome, Firefox en
Thunderbird leveren daarnaast een src/ met entry points; Edge, Opera en Safari
dragen alleen een manifest en pictogrammen en HERGEBRUIKEN de src/ van Chrome
(gedeclareerd door de build-descriptor — zie hieronder). De entries zijn bewust
piepklein: zo is extensions/chrome/src/content.ts slechts
void createContentController().bootstrap();, en roept
extensions/chrome/src/background.ts registerBackground,
registerContentReconciliation en registerToolbarToggle aan. De achtergrond-entry
van Firefox is identiek aan die van Chrome; die van Thunderbird
(extensions/thunderbird/src/background.ts) roept in plaats daarvan registerBackground
en registerMessageDisplay aan (het heeft geen content-scripts). extensions/_template
is een kopieerklare Chromium-steiger, geen build-doel.
tooling, features en de smoke-tests
tooling bevat build.mjs, version.mjs en icons.mjs. features bevat de
Gherkin-scenario's, hun stapdefinities en de getrouwe test-doubles in
features/support/webext.ts en features/support/world.ts. Elke engine draagt ook
een extensions/<engine>/test/manifest.smoke.test.mjs (die van _template is
template.smoke.test.mjs) — dit zijn het gezaghebbende manifestcontract (zie
"Build per engine & het manifestcontract" hieronder).
De onthulpijplijn
Het inhoudspad begint bij een engine-entry zoals
extensions/chrome/src/content.ts, dat createContentController().bootstrap()
aanroept vanuit packages/webext/src/content.ts.
Bootstrap
createContentController bootstrap draait eenmaal per frame:
- Het claimt het frame synchroon, vóór enige
await, viaclaimBootstrap, dat deBOOTSTRAP_MARKER-vlag op dewindowvan het frame leest/zet. Een statische ingebouwdecontent_scripts-entry en een overlappend dynamisch gebruikersscript kunnen beidecontent.jsin hetzelfde frame injecteren; de gedeelde geïsoleerde-wereld-windowmaakt dit de juiste eenmalige bewaker, en het claimen vóór enigeawaitbetekent dat twee bijna gelijktijdige injecties hem niet beide kunnen passeren. - Het laadt de gepersisteerde config EERST via
loadConfigWithRetry(BOOTSTRAP_CONFIG_RETRIESextra pogingen bij een tijdelijke afwijzing), waarbij de marker over de pogingen heen wordt vastgehouden zodat een overlappende injectie die al een no-op heeft uitgevoerd niet gestrand raakt. Pas zodra het lezen slaagt, injecteert het de stylesheet (injectStyles, datREVEAL_URLS_CSStoewijst viatextContent), zodat een mislukte lezing geen<style>toevoegt en een herhaling hem niet kan dupliceren. - Als het laden van de config bij elke poging mislukt, draait
releaseBootstrapde marker terug (alleen hier, voordat de annotatie begint) zodat een latere herinjectie het opnieuw kan proberen; zodrastartheeft gedraaid moet de marker blijven bestaan, omdat een verse annotator de knooppunten van de vorige doorloop niet zou bezitten. - Wanneer
config.enabledroept hetstartaan, en het registreert altijd eenonConfigChanged-listener die de config opnieuw toepast (wat de uitgeschakeld→ingeschakeld-overgang dekt).
De actieve site-regel oplossen
start (en applyConfig) roept resolveSiteRule aan om te beslissen of — en waar
— te annoteren. resolveSiteRule probeert eerst de EIGEN locatie van het draaiende
document via resolveForHref, dat config.sites filtert tot de ingeschakelde regels
en selectMostSpecific van de kern laat beslissen zodat de meest specifieke
overeenkomst wint (een specifieke ingebouwde verslaat een brede
gebruikers-wildcard). Wanneer de eigen locatie nergens op past EN de eigen oorsprong
van het document ondoorzichtig of geërfd is — afgedwongen door hasOpaqueOrigin,
alleen waar voor about:blank/about:srcdoc (geërfd via INHERITED_ORIGIN_URLS)
of blob:/data: (OPAQUE_ORIGIN_SCHEMES) — valt het terug, in volgorde, op het
TOP-frame (readTopHref, het ingebouwde Proton-bericht-iframe-geval), het
OPENER-venster (readOpenerHref, het Outlook-pop-out-leesvenster geopend via
window.open("about:blank")) en ten slotte document.referrer (readReferrer).
Elke externe lezing wordt afgeschermd tegen cross-origin-toegang (die een
uitzondering werpt) en levert alleen een kandidaat op wanneer leesbaar. Een GEWOON
niet-overeenkomend document is gezaghebbend en erft nooit een regel uit de
omgevingscontext, waardoor de controller inert blijft.
Scopen tot de inhoudswortel en mutaties observeren
start registreert de contentRoot-selector van de overeengekomen regel, maakt de
annotator aan via createAnnotator(config), en verwerkt elke overeenkomende wortel
uit selectRoots(doc, contentRoot) (een querySelectorAll in een try/catch zodat een
ongeldige selector veilig terugvalt op []). Het observeert vervolgens doc.body
(een stabiele container) met OBSERVER_OPTIONS ({ childList: true, subtree: true }).
De observer voedt processMutations, dat elke mutatie afmeet aan de inhoudswortel:
het verwerkt een mutatie-target alleen opnieuw wanneer het een Element binnen een
wortel is (isWithinRoot, via closest), en routeert elk toegevoegd Element via
processAddedNode, dat een knooppunt annoteert dat zich binnen een wortel bevindt, een
wortel IS, of er een BEVAT (een trage SPA-mount). Zo worden zowel het verloop binnen
een wortel als een verwante berichttekst die later mount beide opgevangen, terwijl de
app-chrome buiten de wortels onaangeroerd blijft. Een null-annotator (de extensie is
gestopt) is een no-op.
applyConfig verzoent een live-configwijziging: het bouwt af wanneer geen regel past
of de ingeschakeld-vlag uit is; herstart (terugdraaien + opnieuw observeren) wanneer de
opgeloste contentRoot is gewijzigd; en stroomt anders ter plaatse opnieuw —
annotator.setConfig dan annotator.process over elke overeenkomende wortel — zonder
de observer te herstarten.
Een enkel anker annoteren
De annotator leeft in packages/core/src/linkProcessor.ts. createAnnotator geeft
een Annotator terug over closure-state die de pagina niet kan lezen: een sterke Map
met de ankeridentiteit als sleutel, een data-ru-token per annotator uit
generateToken (crypto.getRandomValues), en de huidige config. Zijn
process(root)-methode:
- Snoeit registratie-items waarvan het anker niet langer verbonden is.
- Verzamelt kandidaat-ankers met
collectAnchors(afstammendea[href]plus de wortel zelf wanneer die eena[href]is, ontdubbeld). - Lost de bestemming van elk anker op met
resolveAnnotatableUrl, datnull(overslaan) teruggeeft voor een lege/in-pagina-href, een onparseerbare href, elk ander schema danhttp:/https:, of een host die gelijk is aan of onder eenignoreHosts-item valt; het lost relatieve hrefs op tegenbaseURIzodat het resultaat absoluut is. - Neemt het idempotente snelle pad wanneer
isAnnotationIntactbevestigt dat de render-vingerafdruk ongewijzigd is en elk aangemaakt knooppunt nog verbonden en op zijn plaats is; anders draait het de verouderde annotatie terug en rendert opnieuw. - Berekent de mismatch met
hostMismatchop de schone ankertekst, eerbiedigtshowOnlyOnMismatch(waarbij eerlijke links onaangeroerd blijven), en roeptannotateAnchoraan.
annotateAnchor rendert alleen VEILIGE-DOM. In de modus "inline" plaatst het
vooraan een <span class="reveal-urls-url"> waarvan de URL via textContent wordt
gezet (voorafgegaan door een pijl en een U+00A0 niet-afbreekbare spatie, ingekort door
truncateUrl) gevolgd door een <br>, waarbij de eigen kinderen van de link
onaangeroerd blijven; de verbonden span krijgt vervolgens een contrast-achtergrond via
applyContrastBackdrop. In de modus "title" bewaart het de oorspronkelijke title
(TitleSave), overschrijft het met de URL — de enige schrijfactie naar het anker —
en voegt het, bij een benadrukte mismatch, een verwante <span class="reveal-urls-warn">-badge
toe met REVEAL_URLS_WARN_LABEL. Elk aangemaakt knooppunt wordt getagd met
data-ru="<token>" (nooit het anker), en elk item registreert zijn
configFingerprint zodat een wijziging van de render-config een herverloop afdwingt.
Kleuren en lettertype-overschrijvingen worden toegepast via getypeerde
element.style.*-eigenschappen (applyColour/applyFontOverrides), nooit
geïnterpoleerd in een CSS-string. revertOwned verwijdert aangemaakte knooppunten op
referentie en herstelt de bewaarde titel; revertAll doet dit voor elk eigen anker en
wist het register.
truncateUrl werkt over Unicode-CODEPUNTEN ([...displayHref]), zodat een knip nooit
een surrogaatpaar splitst; het behoudt de scheme-en-host-oorsprong en verkort de rest
met een afsluitende URL_ELLIPSIS.
Host-mismatch
packages/core/src/hostMismatch.ts hostMismatch vergelijkt de zichtbare tekst van de
link met zijn href op registreerbaar domein, niet op ruwe hostnaam, zodat een eerlijk
subdomein niet wordt gemarkeerd terwijl een look-alike dat wel wordt.
extractHostCandidates splitst de tekst en reduceert elk token via hostCandidate
(dat een punt en een door tldts herkend ICANN-public-suffix vereist, waarbij gewone
tekst met punten zoals e.g wordt afgewezen). Zowel de href-host als elke kandidaat
worden gereduceerd tot hun registreerbare domein met tldts.getDomain, en ELKE
kandidaat waarvan het domein verschilt van dat van de href levert een mismatch op —
zodat het noemen van de echte kwaadaardige host naast een lokhost de waarschuwing niet
kan onderdrukken. Het werpt nooit een uitzondering.
Stijlen en contrast
packages/core/src/styles.ts bevat de injecteerbare REVEAL_URLS_CSS (vaste lay-out,
gewicht, vorm en een vaste witte, volledig ondoorzichtige chip-achtergrond —
background: #ffffff, opacity: 1; kleuren worden er NOOIT in geïnterpoleerd) en de
runtime-helpers applyColour/applyFontOverrides/applyContrastBackdrop.
applyContrastBackdrop leest de opgeloste kleur van het element en de eerste
ondoorzichtige achtergrond die wordt gevonden door vanaf het element ZELF omhoog te
lopen (zijn eigen achtergrond — de vaste witte standaard van de chip of een specifiekere
overschrijving van een hostpagina — vóór enige voorouder) via de standaard
getComputedStyle (afkomstig van element.ownerDocument?.defaultView, dus frame-veilig
en stubbaar) en, wanneer packages/core/src/contrast.ts needsWhiteBackdrop meldt dat
de tekst niet voldoet aan WCAG AA (CONTRAST_THRESHOLD) tegen die achtergrond EN wit
echt helpt, stelt het style.backgroundColor = "white" in. contrast.ts levert
parseColour, relativeLuminance en contrastRatio; het parseert de
rgb()/rgba()/hex-vormen die getComputedStyle teruggeeft en behandelt een volledig
transparante kleur als "niet gevonden".
Configureerbare hosts & inhoudsscoping
Een SiteRule (packages/core/src/config.ts) zegt WAAR de annotatie draait (match,
allFrames) en WELKE container haar scopet (contentRoot), plus enabled en een
builtin-vlag. De geleverde DEFAULT_SITES dekken Gmail, Proton (met allFrames) en
beide Outlook-hosts. De Config aggregeert de globale schakelaars, kleuren,
lettertype-overschrijvingen en de sites-array.
De overeenkomstpatroon-engine
packages/core/src/matchPattern.ts bevat een bewust RESTRICTIEVE
toevoeg-tijd-grammatica MATCH_PATTERN (alleen http/https, optionele voorafgaande
*.-host-wildcard, glob-pad; geen poort, geen *-schema, geen <all_urls>).
matchesPattern matcht een URL tegen een gevalideerd patroon, waarbij het het pad
delegeert aan pathGlobMatches (elke * matcht elke tekenreeks); het valt veilig
terug op false. parsePatternParts splitst een gevalideerd patroon in
host/path/scheme/wildcardHost.
Overlappende regels worden opgelost via "meest specifieke wint":
compareSiteSpecificity rangschikt op (1) exacte host boven wildcard-host, (2) langere
literale host, (3) meer-literaal pad (literalPathLength), dan (4) een deterministische
ASCII-tiebreak; selectMostSpecific geeft de meest specifieke ingeschakelde regel
terug die op een URL past.
De dekking van verleende oorsprongen is een SEPARATE, bewust BREDERE matcher.
permissions.getAll() kan grants rapporteren in de volledige WebExtension-grammatica
(<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*) die de
restrictieve MATCH_PATTERN zou afwijzen. parseGrantedOrigin parseert die in
GrantedOriginParts, en originCovers meldt of een verleende oorsprong de match van
een regel dekt (bijvoorbeeld een brede https://*/* grant dekt echt elke
https-regel). matchAllowsOriginFallback meldt of het pad van een regel exact
ORIGIN_FALLBACK_PATH (/*) is — de veilige doorsnede van de beperkingen van Chromium
en Gecko/Firefox 128 op de dynamische ondoorzichtige-oorsprong-terugval.
Dynamische registratie
packages/webext/src/contentRegistration.ts registreert één dynamisch content-script
per door de gebruiker toegevoegde regel. Ingebouwde worden bediend door de statische
content_scripts-entries en worden NOOIT dynamisch geregistreerd.
desiredContentScripts filtert config.sites tot niet-ingebouwde, ingeschakelde
regels waarvan de oorsprong isOriginGranted (delegerend aan originCovers), mapt elk
naar een RegisteredContentScript met een stabiel id (contentScriptId, een
FNV-1a-hash van de match zodat het id alleen de API-veilige tekenset gebruikt), en zet
matchOriginAsFallback: true ALLEEN wanneer matchAllowsOriginFallback(rule.match)
geldt (een /*-pad). Het op deze manier afmeten van de vlag voorkomt dat één
regel-met-niet-conform-pad de hele batch-registratie laat afwijzen; zo'n regel
registreert zich zonder pop-out-dekking, naar ontwerp.
reconcileContentScriptsOnce leest de verleende oorsprongen, de opgeslagen config en
de live-registraties, en convergeert vervolgens via een EIGENSCHAP-BEWUSTE diff:
sameRegistration projecteert zowel de gewenste descriptor als de live-terugleeswaarde
op een genormaliseerde vorm (waarbij de echte WebExtension-standaard van elk veld wordt
toegepast — allFrames/matchOriginAsFallback standaard false, persistAcrossSessions
standaard TRUE, runAt standaard document_idle) zodat een vers geregistreerd script
GELIJK vergelijkt met de descriptor die het produceerde en er geen
herregistratielus kan ontstaan. Omdat er geen updateContentScripts is, wordt een
gewijzigd script ge-deregistreerd en daarna opnieuw geregistreerd.
reconcileContentScripts wikkelt dit met een in-flight-promise (inFlight) zodat twee
bijna gelijktijdige triggers (een configwijziging EN permissions.onAdded die beide
afgaan bij een site-toevoeging) worden geserialiseerd in plaats van te racen.
registerContentReconciliation bedraadt de triggers
(permissions.onAdded/onRemoved, onConfigChanged) en convergeert eenmaal bij het
opstarten; het wordt alleen geïmporteerd door de achtergrond-entries van Chrome en
Firefox.
De opt-in-toestemmingsstroom
De statische manifesten declareren optional_host_permissions (http://*/*,
https://*/*) zodat een gebruiker tijdens runtime een willekeurige extra webhost kan
verlenen. packages/webext/src/options/options.ts addSite valideert de HELE
kandidaatregel (match EN inhoudswortel) via normaliseSiteRule VOORDAT het enige
toestemming aanvraagt, leidt de host-oorsprong af met matchOrigin, en roept
browser.permissions.request aan vanuit het klikgebaar van de Toevoegen-knop; alleen
bij verlening voegt het een builtin:false-rij toe en persisteert het via het
canonieke schrijfpad.
Config, opslag & het canonieke patroon
De enkele validatietrechter
normaliseConfig in packages/core/src/config.ts is het ene canonieke leespad: het
dwingt elk veld af en begrenst het, valt terug op de standaard bij alles wat ongeldig
is, en werpt nooit een uitzondering. Het is opgebouwd uit per-veld-validators —
normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts
(die elk item reduceert tot een kale punycode-hostnaam via bareHostname),
normaliseMatchColour/normaliseMismatchColour (afgemeten door safeColour.ts
isSafeColour), normaliseCssSize, normaliseFontWeight, normaliseContentRoot
(begrensd door CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH), normaliseMatchPattern
(afgemeten door MATCH_PATTERN) en normaliseSites. normaliseSites laat afwijzingen
vallen, ontdubbelt op match, FORCEERT builtin: true op elke regel waarvan de match
gelijk is aan die van een ingebouwde (waarmee de
exacte-match-overschaduwt-ingebouwde-/dubbele-injectievector wordt gesloten), her-zaait
elke ontbrekende ingebouwde (zodat een gemanipuleerde store geen kernprovider kan laten
vallen) en sorteert alfanumeriek op match voor een canonieke volgorde.
configFingerprint hasht alleen de render-beïnvloedende velden (met uitsluiting van
enabled, ignoreHosts en sites, die start/stop en overslaan aansturen in plaats van
herverloop) via fnv1a.
Opslag
packages/webext/src/storage.ts wikkelt de WebExtension-opslag-API. configArea
gebruikt browser.storage.sync wanneer beschikbaar (zodat instellingen meereizen) en
valt terug op browser.storage.local (bijvoorbeeld op Thunderbird); configAreaName
meldt welke actief is. getConfig, setConfig en onConfigChanged trechteren hun ruwe
waarde allemaal door normaliseConfig, zodat een misvormde of gemanipuleerde store nooit
een ongeldige Config aan een aanroeper kan overhandigen. onConfigChanged negeert
daarnaast gebeurtenissen uit het INACTIEVE opslaggebied en wijzigingen aan
ongerelateerde sleutels.
De twee add-ons hergebruiken dezelfde lees -> normaliseConfig -> teruggeef-trechter
over hun eigen host-stores. Het extensions/outlook/src/roamingStorage.ts van het
Outlook-taakvenster wikkelt Office.context.roamingSettings (synchrone get/set,
vastgelegd met saveAsync); het packages/gmail/src/propertiesStorage.ts van de
Gmail-add-on wikkelt PropertiesService.getUserProperties() (per-gebruiker, meereizend
over de apparaten van die gebruiker, JSON-gecodeerd onder één sleutel, en zonder extra
OAuth-scope nodig — nooit de gedeelde ScriptProperties). Beide stellen
getConfig/setConfig bloot plus de voor de kaart relevante accessors
getIgnoreHosts/getHighlightMismatch en beschermen een onbeschikbaar host-oppervlak
met een speciale fout. De Gmail-startpagina-trigger rendert een
CardService-instellingenkaart vanuit getConfig, en de
onSaveSettings-formulier-indien-handler voegt de geparseerde
ignoreHosts/highlightMismatch samen met de huidige config en persisteert die via
setConfig; de contextuele trigger rijgt ignoreHosts door naar de adapter en
highlightMismatch naar de kaartbouwer, defensief lezend zodat een opslagfout terugvalt
op standaardwaarden in plaats van de berichtanalyse te breken.
Het canonieke-config-+-gerichte-update-patroon
Drie schrijfpaden lezen de canonieke config, wijzigen precies één ding en schrijven het terug — zonder ooit niet-opgeslagen formulierbewerkingen vast te leggen of het formulier opnieuw te renderen:
removeSite(options.ts): leestgetConfig, persisteertsetConfigover eensites-array met alleen de verwijderde regel weggefilterd (persisteer EERST, danrow.remove(), dan een voorwaardelijkepermissions.removebesloten vanuit de volgende config). Een persistentiefout werptSiteRemovalSaveErroren laat de DOM onaangeroerd; een mislukte intrekking werptSitePermissionRevokeErrornadat de rij al weg is.addSite(options.ts): hierboven beschreven — valideer, vraag toestemming aan, voeg de rij toe, dansaveOptions.toggleEnabled(options.ts): de directe hoofd-Inschakelen-schakelaar wisselt ALLEENenabledop de opgeslagen config en schrijft die terug, zonder opnieuw te renderen (zodat een lopende bewerking nooit wordt overschreven); bij een mislukte schrijfactie zet het het selectievakje terug op de opgeslagen waarde.
De eigen toggleEnabled van de werkbalkactie
(packages/webext/src/toolbarAction.ts) volgt hetzelfde patroon vanuit de
achtergrondzijde.
Build per engine & het manifestcontract
tooling/build.mjs stuurt één esbuild-build voor elk WebExtension-doel aan. De
TARGETS-descriptor noemt het BRON-DOEL van elk doel: Chrome, Firefox en Thunderbird
leveren hun eigen src/; Edge, Opera en Safari declareren chrome als hun bron; de
Outlook-add-in (Office.js) is zijn eigen bron. ACTIVE_TARGETS (Chrome, Edge, Firefox,
Opera en Thunderbird — Safari en Outlook zijn uitgesloten) is wat --all/--package
bouwt; Safari en Outlook bouwen alleen wanneer ze expliciet worden benoemd, en de
Gmail-add-on (Apps Script) is een aparte esbuild-bundel (make build-gmail) buiten de
--all-lus. Die Gmail-bundel richt zich op de V8-runtime van Apps Script, die noch
ES-modules noch een native URL heeft: het wordt als ESM gebouwd en heeft daarna zijn
afsluitende export {…}-statement verwijderd (zodat de trigger-functies top-level
globals blijven die Apps Script kan aanroepen), en het bundelt een kleine URL-polyfill
(alleen geïnstalleerd wanneer URL afwezig is) zodat de new URL(...) van de gedeelde
kern links aan de serverzijde oplost. buildTarget ruimt dist/<target>/ op, kopieert
het manifest.json en de icons/ per doel verbatim, bundelt elk bronscript als IIFE
(de geïnjecteerde INJECTED_SCRIPTS content.ts/messageDisplay.ts kunnen geen
ES-modules zijn, en achtergronden zijn ook klassieke workers/event-pages) en de gedeelde
instellingenpagina als ESM, en kopieert options.html/options.css uit
packages/webext/src/options/. webextension-polyfill wordt in elk script gebundeld.
tooling/version.mjs stempelt versies: het neemt MAJOR.MINOR uit de root
package.json en voegt een automatisch oplopend BUILD-nummer toe (gelezen uit de
hoogste bestaande derde component over de doelmanifesten) in elk doelmanifest, waardoor
ze in lockstep blijven. De meeste doelen dragen één manifest.json; de Outlook-add-in
draagt twee geversioneerde manifesten (manifest.json en manifest.xml) en beide
worden gestempeld, en de _template-steiger wordt eveneens in lockstep gestempeld. De
root package.json wordt op dezelfde versie gehouden, maar blijft geldige semver (een
vierdelige versie is toegestaan in een manifest, nooit in package.json).
tooling/icons.mjs (uitgevoerd via make icons) rastert de enkele vectorbron
assets/icon.svg naar de icons/icon48.png en icons/icon128.png van elk doel, waarbij
het cairosvg probeert, dan inkscape, dan ImageMagick.
Het manifestcontract — de exact vereiste velden per engine — is de enige bron van
waarheid in elke extensions/<engine>/test/manifest.smoke.test.mjs (bijvoorbeeld de
assertManifestShape van extensions/chrome/test/manifest.smoke.test.mjs, die ook het
doel bouwt en bevestigt dat elk gerefereerd bestand oplost in de dist). Conform de
de-duplicatieconventie van het project wordt de veld-voor-veld-vorm hier NIET opnieuw
opgesomd; raadpleeg die smoke-tests en de "Manifestcontract"-notitie in het
handmatige testplan.
Internationalisatie (i18n)
Reveal URLs is gelokaliseerd in veel talen naast het Engels. De set wordt enkelvoudig
gesourcet in packages/core/src/locales.json (SUPPORTED_LOCALES, native namen en de
standaard), die zowel de extensie bundelt (via de opgeloste JSON-import) als de
plain-Node-websitebuild leest. Elke niet-Engelse string is machinaal vertaald en wacht op
menselijke controle; de herkomstmarkering is per formaat (de _locales gebruiken de
description van elk bericht, de website-chrome-woordenboeken registreren het in
site/i18n/README.md, en vertaalde docbronnen dragen een
eerste-regel-HTML-commentaar).
Er zijn drie onafhankelijke lokalisatie-oppervlakken, bewust niet onderling bedraad, en elk is gesplitst langs een build-tijd-/runtime-lijn.
- De instellingenpagina (AD-1). Het levert STATISCHE Engelse markup; elk
vertaalbaar knooppunt draagt een
data-i18n="<key>"(tekst) ofdata-i18n-<attr>="<key>"(attribuut)-annotatie. Tijdens runtime ispackages/webext/src/options/locale.tsde toepassingslaag: hetfetcht het eigen gebundelde_locales/<code>/messages.jsonvan de pagina (viaruntime.getURL, geenweb_accessible_resourcesnodig), herschrijft elk geannoteerd knooppunt ALLEEN viatextContent(zodat de<code>-voorbeeldhints van het formulier intact blijven), en stelt<html lang>in. De pagina is VOLLEDIG gelokaliseerd: naast de statische tekst dragen de met JS gebouwde site-rij-labels en de Verwijderen-knop eendata-i18n-sleutel (met een initiëletranslated-tekst) zodat dezelfde toepassingslaag de gerenderde rijen opnieuw lokaliseert zonder herrendering, en de ECHT dynamische strings — de statusregel, de feedback bij site-toevoegen en de versieregel — renderen viatranslate(<key>)vanlocale.ts(currentMessages → Engelse terugval → sleutel) op het moment van aanroep. De Engelse terugval is de_locales/en/messages.json-catalogus die op build-tijd inoptions.jsis GEÏMPORTEERD (gebundeld), zodattranslateSYNCHROON oplost naar leesbaar Engels vanaf het laden van de module — een dynamische string die wordt gerenderd voordat de schakelaar zich heeft gezet, of nadat een runtime-catalogusophaling mislukt, degradeert nooit naar een ruwe berichtsleutel.locale.tspubliceert de toegepaste berichten-map en vuurt zijnonLocaleChange-listeners na elke toepassing af, zodatoptions.tsde momenteel getoonde status/feedback/versie opnieuw rendert en de Online handleiding-link bij elke schakeling opnieuw richt. De versieregel gebruikt eenoptionsVersion-bericht waarvan de{version}-tijdelijke aanduiding wordt vervangen door het build-nummer (token vertaald, nummer verbatim). De handleiding-link volgt de actieve locale naar zijn doc-variant (AD-9): Engels houdt de top-levelmanual.html, elke andere locale opentnl/handleiding.html, met behoud van de?v=-versiestempel. De actieve locale is de opgeslagen keuze van de gebruiker of, bij gebrek daaraan, de browsertaal opgelost naar een ondersteunde basistaal doorresolveLocalevan de kern (AD-5); een niet-ondersteunde of mislukte locale laat het voorgerenderde Engels staan (entranslatedegradeert dan naar de Engelse terugval). De Weergavetaal-schakelaar van de handleiding persisteert zijn keuze onder een specialeuiLocale-sleutel instorage.local— nooit in de gesynchroniseerdeConfig, zodat hij per-apparaat is en nooit meereist. - De website-chrome (AD-2). De statische-site-generator
tooling/site.mjsserver-rendert elk chrome-knooppunt in het Engels terwijl het het annoteert metdata-i18n, en houdt<html lang="en-GB">(AD-4). De browser-zijdige ladersite/scripts/i18n.mjs(een afhankelijkheidsvrije ES-module) wisselt de chrome vervolgens naar de taal van de bezoeker via precedentie — een gepersisteerdelocalStorage-keuze (revealUrlsSiteLocale) → de eerste ondersteundenavigator.languages-basisovereenkomst (AD-5) → Engels — doorsite/i18n/<code>.jsonop te halen. Anders dan de toepassingslaag van de instellingenpagina wisselt het viainnerHTML, omdat de chrome-waarden van de site vertrouwde inline-markup dragen (<code>,<a>); een pariteitstest pint de tag-/attribuut-/URL-volgorde van elke niet-Engelse waarde byte-identiek aan het Engels vast zodat een vertaling nooit een link kan laten vallen of een tag kan breken. Een ontbrekende sleutel houdt de voorgerenderde Engelse momentopname; een mislukte ophaling laat de Engelse pagina intact (gracieuze degradatie, nooit leeg). - De documentatiepagina's (AD-8/AD-9). Doc-pagina's worden PER LOCALE op build-tijd
gerenderd, niet runtime-gewisseld. Engelse docs blijven op de top-level-paden; elke
andere locale krijgt een kopie onder een
<code>/-map, vanuit een vertaaldedocs/<code>/<DOC>.mdindien aanwezig, anders de Engelse bron — een terugval per pagina diebuildSiteLOGT in plaats van stil weg te laten. De doc-BODY draagt geendata-i18n(het is ruw gerenderde markdown), dus de runtime-chrome-lader raakt die nooit aan; alleen de gedeelde chrome-knooppunten wisselen. De taalschakelaar van de doc-pagina NAVIGEERT naar de pagina van de verwante locale in plaats van ter plaatse te wisselen. - Lokalisatie van interne links (Finding 2). De geselecteerde taal reist mee over
doc-navigatie. Elk INTERN doc-anker (de vijf pagina's
architecture.html,index.html,licence.html,manual.html,privacy.html) wordt door de generator getagd metdata-doc-link="<page>", en voor het geval zonder JavaScript lost zijn href al op naar de eigen locale-variant van de HUIDIGE pagina (navDocHref— de drie per-locale-doc-pagina's lokaliseren; de alleen-Engelseindex.html/licence.htmlblijven top-level). DerewriteDocLinksvan de runtime-lader richt vervolgens elk intern doc-anker opnieuw — die getagd ÉN elk door de catalogus geïnjecteerd plat anker zoalsmanual.html#installing— naar de variant van de ACTIEVE locale, met behoud van elke#hash/?queryen zonder ooit externe links aan te raken. Op een doc-pagina NAVIGEERT een OPGESLAGEN keuze die verschilt van de eigen locale van de pagina naar de overeenkomende variant; de lus-bewaker vuurt alleen af bij een expliciete opgeslagen keuze (nooit een browsertaal-voorkeur alleen) en alleen wanneer het doel verschilt van de reeds getoonde pagina. De catalogi zelf houden hun Engelse interne URL's, zodat de pariteitstest groen blijft — de lokalisatie is een pure runtime-doorloop over de DOM.
Onderdrukking van "Actieve sites" bij mailclients (AD-3)
Een mailclient-doel (Thunderbird vandaag) ziet al elk weergegeven bericht, dus de
per-host-editor "Actieve sites" is daar overbodig en wordt verwijderd. De beslissing
wordt gedragen door een gedocumenteerde descriptorvlag per doel, mailClient: true, op
TARGETS.thunderbird van tooling/build.mjs — nooit een hardgecodeerde doelnaam in de
build- of UI-logica — en is geïmplementeerd in twee helften:
- Build-tijd.
copyAssetshaakt in opdescriptorFor(target).mailClient: voor een mailclient-doel draait het de pureremoveSitesSection(html)-transformatie (die de hele<section class="sites">-host-editor verwijdert, inclusief elke site-toevoeg-bediening, enMissingSourceErrorwerpt als de sectie afwezig is zodat een markup-wijziging de editor nooit stil kan meeleveren) en schrijft het resultaat; browser-doelen ontvangenoptions.htmlbyte-voor-byte viacpSync. - Runtime. De gebundelde controller
packages/webext/src/options/options.tstolereert de afwezige sectie:renderSites/readSites/applyConfig/readFormpeilen#sites-listen voeren een no-op uit in plaats vanOptionsFieldMissingErrorte werpen, enreadFormLAAT desites-sleutel WEG wanneer de sectie weg is. Omdat het weglaten vansitesnormaliseConfigde ingebouwde standaardwaarden zou laten her-zaaien en de sites van de gebruiker zou laten vallen, hersteltsaveOptionsde opgeslagensitesvóór het normaliseren wanneer de sectie afwezig is — dat is wat een mailclient-opslag de geconfigureerde hosts laat behouden. Een runtime-bewaker,guardMailClientSites, verwijdert de sectie als die nog aanwezig is, waarbij het een mailclient detecteert via API-/feature-aanwezigheid (messages/messageDisplay/scripting.messageDisplayop de geïnjecteerdebrowser), nooit op doelnaam; het draait als eerste ininitOptionszodat de latere toevoeg-/lijst-bedrading de afwezige elementen vanzelf overslaat. In een browser-runtime is de bewaker een no-op.
Beveiligings- & privacyhouding
- Geen netwerk of exfiltratie. Geen module voert
fetch/XHRof enige andere netwerkaanroep uit; de extensie leest alleen de opgeslagen config en de pagina-DOM. - Alleen veilige-DOM. Annotatie en de geïnjecteerde stylesheet gebruiken uitsluitend
createElement/textContent/getypeerdestyle.*-eigenschappen, nooitinnerHTML/insertAdjacentHTML/eval. Voor de kern wordt dit afgedwongen doorpackages/core/test/purity.test.ts. - Alle externe/config-invoer genormaliseerd. Elke lezing en schrijfactie trechtert
door
normaliseConfig; kleuren passerenisSafeColour; overeenkomstpatronen passerenMATCH_PATTERN; inhoudswortel-selectors worden begrensd doorCONTENT_ROOT_PATTERNen worden alleen ooit aanquerySelectorAll/closestoverhandigd, nooit geïnterpoleerd in markup of CSS. - Minimale privileges. Ingebouwde webmail-oorsprongen zijn vaste
host_permissions; extra hosts zijn opt-in viaoptional_host_permissionsen een gebaar-gebondenpermissions.request. De werkbalkactie declareert geen popup. - Gecko-gegevensverzameling. De Firefox- en Thunderbird-manifesten declareren
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Licentie. Het project is AGPL-3.0-only (
LICENSE,package.json).
Testen
- Vitest-unit-tests dekken beide pakketten:
packages/core/test/*(config, contrast, host-mismatch, link-processor, stijlen en de purity-bewaker) enpackages/webext/test/*(content, opslag, opties, registratie, werkbalk, berichtweergave en een end-to-end-test voor een aangepaste site). Elk pakket draaitvitest run. - Cucumber-BDD-scenario's leven in
features/(bedraad doorcucumber.json) en drijven de echte@reveal-urls/webext-modules aan via getrouwe test-doubles infeatures/support/webext.ts— met namenormaliseReadBack, dat de engine spiegelt die false-waarde-vlaggen WEGLAAT bij het teruglezen zodat een toegeeflijke harnas geen herregistratielus kan verbergen — over een jsdom-document opgezet infeatures/support/world.ts. - Manifest-smoke-tests per doel (
extensions/<engine>/test/manifest.smoke.test.mjs, Node's ingebouwdenode:test) stellen de exacte vorm van elk manifest vast en dat de gebouwde dist elk gerefereerd bestand oplost.tooling/test/voegt build-, versie- en definition-of-done-tests toe. - CI (
.github/workflows/ci.yml) bewaakt elke push en pull request opmake test,make bddenmake lint(rechtstreeks op de runner gedraaid, waarbij de DockerRUN/IMAGE_DEPvan de Makefile worden overschreven). Versie-stempeling en bundelen zijn bewust uitgesloten; die leven in de aparte, tag-getriggerde.github/workflows/release.yml.