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:

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/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:

  1. Het claimt het frame synchroon, vóór enige await, via claimBootstrap, dat de BOOTSTRAP_MARKER-vlag op de window van het frame leest/zet. Een statische ingebouwde content_scripts-entry en een overlappend dynamisch gebruikersscript kunnen beide content.js in hetzelfde frame injecteren; de gedeelde geïsoleerde-wereld-window maakt dit de juiste eenmalige bewaker, en het claimen vóór enige await betekent dat twee bijna gelijktijdige injecties hem niet beide kunnen passeren.
  2. Het laadt de gepersisteerde config EERST via loadConfigWithRetry (BOOTSTRAP_CONFIG_RETRIES extra 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, dat REVEAL_URLS_CSS toewijst via textContent), zodat een mislukte lezing geen <style> toevoegt en een herhaling hem niet kan dupliceren.
  3. Als het laden van de config bij elke poging mislukt, draait releaseBootstrap de marker terug (alleen hier, voordat de annotatie begint) zodat een latere herinjectie het opnieuw kan proberen; zodra start heeft gedraaid moet de marker blijven bestaan, omdat een verse annotator de knooppunten van de vorige doorloop niet zou bezitten.
  4. Wanneer config.enabled roept het start aan, en het registreert altijd een onConfigChanged-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:

  1. Snoeit registratie-items waarvan het anker niet langer verbonden is.
  2. Verzamelt kandidaat-ankers met collectAnchors (afstammende a[href] plus de wortel zelf wanneer die een a[href] is, ontdubbeld).
  3. Lost de bestemming van elk anker op met resolveAnnotatableUrl, dat null (overslaan) teruggeeft voor een lege/in-pagina-href, een onparseerbare href, elk ander schema dan http:/https:, of een host die gelijk is aan of onder een ignoreHosts-item valt; het lost relatieve hrefs op tegen baseURI zodat het resultaat absoluut is.
  4. Neemt het idempotente snelle pad wanneer isAnnotationIntact bevestigt 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.
  5. Berekent de mismatch met hostMismatch op de schone ankertekst, eerbiedigt showOnlyOnMismatch (waarbij eerlijke links onaangeroerd blijven), en roept annotateAnchor aan.

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:

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.

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:

Beveiligings- & privacyhouding

Testen