Reveal URLs — Arkitektur

Det här dokumentet beskriver hur tillägget Reveal URLs är strukturerat och hur dess kärnlogik fungerar, för bidragsgivare och tekniska läsare. Det hänvisar genomgående till verkliga filer och symboler (på formen path symbol) så att varje påstående kan kontrolleras mot källkoden. För den användarvända beskrivningen, se handboken; för de manuella verifieringsstegen, se den manuella testplanen.

Översikt

Reveal URLs är ett enda MV3-WebExtension byggt för flera motorer — Chrome, Edge, Opera, Firefox och Thunderbird — från en gemensam, ren kodkärna. (Safari är uppskjutet: det har ett manifest och kan byggas när det namnges uttryckligen, men är exkluderat från standardbygget och från CI.) Tillägget avslöjar varje länks destinations-URL bredvid länken i ett renderat e-postmeddelande, och flaggar en länk vars synliga text anger ett annat registrerbart domännamn än dess href — det klassiska nätfisketecknet.

Förrådet är en pnpm/TypeScript-monorepo (pnpm-workspace.yaml, package.json med "private": true). Buntningen är ett enda esbuild-drivet skript, tooling/build.mjs, som kompilerar de delade paketen och varje motors tunna ingångspunkter till en laddbar katalog dist/<target>/. webextension-polyfill buntas in i varje skript snarare än att förlitas på som en global vid körning, så att samma källkod körs oförändrad över Chromium och Gecko.

Två inbyggda e-posttillägg utökar samma detekteringskärna till ytor som tillägget inte kan nå: ett Outlook-tillägg (Office.js-uppgiftsfönster, som når Outlook på webb/Windows/Mac/iOS/Android och Outlook.com) och ett Gmail-tillägg (Apps Script CardService, som når Gmail-apparna för webb/Android/iOS). Båda återanvänder endast den RENA analysen — en ny modul packages/core/src/findings.ts (analyseAnchors/analyseHtml, en plattformsneutral Finding[]-modell) som återanvänder den befintliga hostMismatch-logiken och tldts-härledningen av registrerbar domän. Endast värdadaptern (hur kroppen läses och tolkas) och presentationen skiljer sig åt per yta: Outlook-uppgiftsfönstret tolkar kroppen med sin egen DOMParser och körs på klientsidan; Gmail-tillägget tolkar med node-html-parser och körs på serversidan på Googles Apps Script V8-infrastruktur (gratis hosting, eftersom Google redan har posten; driftsätts lokalt-interaktivt via clasp). Eftersom inget av ramverken kan ändra det renderade meddelande-DOM:et i läsläge (Office:s setAsync/prependAsync är enbart för skrivläge; CardService renderar kort, inte meddelande-HTML), presenterar båda tilläggen en panel/ett kort med fynd, inte annotering på plats — den befintliga DOM-muteringsvägen i linkProcessor.ts och REVEAL_URLS_CSS återanvänds INTE av något av tilläggen. En relativ href löses endast mot en pålitlig <base href> i e-postmeddelandet och hoppas annars över, aldrig mot brevlådans/leverantörens ursprung (vilket skulle fabricera en destination).

Kodbasen separerar ansvarsområden skarpt:

Förrådets struktur

packages/core — den rena kärnan

Varje modul här importerar inget tilläggs-API (browser.*/chrome.*/messenger.*) och använder ingen sträng-till-markup-DOM-sänka (innerHTML/insertAdjacentHTML). Det kontraktet upprätthålls mekaniskt av packages/core/test/purity.test.ts, som globbar varje src/**/*.ts, skalar bort kommentarkroppar via sin egen stripComments (så att de docblock som legitimt NÄMNER de förbjudna tokenen inte utlöser skyddet) och sedan hävdar att varken EXTENSION_API_PATTERN eller UNSAFE_DOM_PATTERN matchar någon modul. Den publika ytan återexporteras från packages/core/src/index.ts. Det enda körningsberoendet är tldts (för public-suffix-medveten domänjämförelse).

Modulerna är:

packages/webext — webbläsar-API-omslagen

Moduler här FÅR använda browser.* och är den enda plats där WebExtension-API:erna vidrörs. Den publika ytan återexporteras från packages/webext/src/index.ts. Modulerna är: content.ts (innehållslivscykel), contentRegistration.ts (dynamisk registrering), storage.ts (konfigurationspersistens), background.ts (beteende vid installation), toolbarAction.ts (växel + bricka), messageDisplay.ts och messageDisplayBackground.ts (Thunderbird), och options/options.ts (styrenheten för inställningssidan).

extensions/<engine> — tunna ingångspunkter och manifest

Varje motor bär sitt eget manifest.json och icons/. Chrome, Firefox och Thunderbird levererar dessutom en src/ med ingångspunkter; Edge, Opera och Safari bär endast ett manifest och ikoner och ÅTERANVÄNDER Chromes src/ (deklarerat av byggdeskriptorn — se nedan). Ingångarna är medvetet pyttesmå: till exempel är extensions/chrome/src/content.ts bara void createContentController().bootstrap();, och extensions/chrome/src/background.ts anropar registerBackground, registerContentReconciliation och registerToolbarToggle. Firefox bakgrundsingång är identisk med Chromes; Thunderbirds (extensions/thunderbird/src/background.ts) anropar registerBackground och registerMessageDisplay istället (den har inga content-skript). extensions/_template är ett kopieringsklart Chromium-skelett, inte ett byggmål.

tooling, features och smoke-testerna

tooling innehåller build.mjs, version.mjs och icons.mjs. features innehåller Gherkin-scenarierna, deras stegdefinitioner och de trogna testdubbletterna i features/support/webext.ts och features/support/world.ts. Varje motor bär också en extensions/<engine>/test/manifest.smoke.test.mjs (_template:s är template.smoke.test.mjs) — dessa är det auktoritativa manifestkontraktet (se "Per-motor-bygge och manifestkontraktet" nedan).

Avslöjningspipelinen

Innehållsvägen börjar vid en motoringång såsom extensions/chrome/src/content.ts, som anropar createContentController().bootstrap() från packages/webext/src/content.ts.

Uppstart

createContentController bootstrap körs en gång per ram:

  1. Den gör anspråk på ramen synkront, före varje await, via claimBootstrap, som läser/sätter flaggan BOOTSTRAP_MARKER på ramens window. En statisk inbyggd content_scripts-post och ett överlappande dynamiskt användarskript kan båda injicera content.js i samma ram; det delade window i den isolerade världen gör detta till det korrekta engångsskyddet, och att göra anspråk före varje await innebär att två nästan samtidiga injiceringar inte båda kan passera det.
  2. Den laddar den persisterade konfigurationen FÖRST via loadConfigWithRetry (BOOTSTRAP_CONFIG_RETRIES extra försök vid en övergående avvisning), och håller markören över omförsöken så att en överlappande injicering som redan blev en no-op inte blir strandsatt. Först när läsningen lyckas injicerar den formatmallen (injectStyles, som tilldelar REVEAL_URLS_CSS via textContent), så att en misslyckad läsning lägger till ingen <style> och ett omförsök inte kan duplicera den.
  3. Om konfigurationsladdningen misslyckas vid varje försök rullar releaseBootstrap tillbaka markören (endast här, innan annoteringen börjar) så att en senare återinjicering kan göra ett nytt försök; när start väl har körts måste markören kvarstå, eftersom en ny annoterare inte skulle äga den föregående omgångens noder.
  4. När config.enabled anropar den start, och den registrerar alltid en onConfigChanged-lyssnare som tillämpar konfigurationen på nytt (vilket täcker övergången inaktiverad→aktiverad).

Att lösa den aktiva webbplatsregeln

start (och applyConfig) anropar resolveSiteRule för att avgöra om — och var — annotering ska ske. resolveSiteRule provar först det körande dokumentets EGEN plats via resolveForHref, som filtrerar config.sites till de aktiverade reglerna och överlåter till kärnans selectMostSpecific så att den mest specifika matchningen vinner (en specifik inbyggd slår ett brett användarjokertecken). När den egna platsen inte matchar något OCH dokumentets eget ursprung är ogenomskinligt eller ärvt — grindat av hasOpaqueOrigin, sant endast för about:blank/about:srcdoc (ärvt via INHERITED_ORIGIN_URLS) eller blob:/data: (OPAQUE_ORIGIN_SCHEMES) — faller den tillbaka, i ordning, till den ÖVERSTA ramen (readTopHref, det inbyggda fallet med Protons meddelande-iframe), ÖPPNAR-fönstret (readOpenerHref, Outlooks utbrutna läsfönster öppnat via window.open("about:blank")) och slutligen document.referrer (readReferrer). Varje extern läsning är skyddad mot korsursprungsåtkomst (som kastar) och bidrar med en kandidat endast när den är läsbar. Ett VANLIGT omatchat dokument är auktoritativt och ärver aldrig en regel från omgivande kontext, vilket lämnar styrenheten inaktiv.

Avgränsa till innehållsroten och observera mutationer

start registrerar den matchade regelns contentRoot-väljare, skapar annoteraren via createAnnotator(config), och bearbetar varje matchad rot från selectRoots(doc, contentRoot) (en querySelectorAll inlindad i try/catch så att en ogiltig väljare misslyckas säkert till []). Den observerar sedan doc.body (en stabil behållare) med OBSERVER_OPTIONS ({ childList: true, subtree: true }).

Observeraren matar processMutations, som grindar varje mutation på innehållsroten: den bearbetar ett mutations-target på nytt endast när det är ett Element inom en rot (isWithinRoot, via closest), och dirigerar varje tillagt Element genom processAddedNode, som annoterar en nod som är inuti en rot, ÄR en rot, eller INNEHÅLLER en (en lat SPA-montering). Så både omsättning inom roten och en syskonmeddelandetext som monteras senare fångas upp, medan appchrome utanför rötterna lämnas orört. En null-annoterare (tillägget är stoppat) är en no-op.

applyConfig förenar en konfigurationsändring i realtid: den river ner när ingen regel matchar eller aktiveringsflaggan är av; startar om (återställ + observera på nytt) när den lösta contentRoot ändrades; och flödar annars om på plats — annotator.setConfig sedan annotator.process över varje matchad rot — utan att starta om observeraren.

Annotera ett enskilt ankare

Annoteraren bor i packages/core/src/linkProcessor.ts. createAnnotator returnerar en Annotator över closure-tillstånd som sidan inte kan läsa: en stark Map nycklad på ankaridentitet, en data-ru-token per annoterare från generateToken (crypto.getRandomValues), och den aktuella konfigurationen. Dess metod process(root):

  1. Beskär registerposter vars ankare inte längre är anslutet.
  2. Samlar in kandidatankare med collectAnchors (efterkommande a[href] plus roten själv när den är en a[href], avduplicerade).
  3. Löser varje ankares destination med resolveAnnotatableUrl, som returnerar null (hoppa över) för en tom href / href på samma sida, en otolkbar href, vilket annat schema som helst än http:/https:, eller en värd lika med eller under en ignoreHosts-post; den löser relativa href:ar mot baseURI så att resultatet är absolut.
  4. Tar den idempotenta snabbvägen när isAnnotationIntact bekräftar att renderingsfingeravtrycket är oförändrat och varje skapad nod fortfarande är ansluten och på plats; annars återställer den den föråldrade annoteringen och renderar om.
  5. Beräknar avvikelse med hostMismatch på den rena ankartexten, respekterar showOnlyOnMismatch (lämnar ärliga länkar orörda), och anropar annotateAnchor.

annotateAnchor renderar endast SÄKER-DOM. I läget "inline" lägger den först till en <span class="reveal-urls-url"> vars URL sätts via textContent (prefixad med en pil och ett icke-brytande blanksteg U+00A0, kapad av truncateUrl) följd av en <br>, och lämnar länkens egna barn orörda; den anslutna span:en får sedan en kontrastbakgrund via applyContrastBackdrop. I läget "title" sparar den den ursprungliga title (TitleSave), skriver över den med URL:en — den enda skrivningen till ankaret — och lägger, vid en betonad avvikelse, till en syskonbricka <span class="reveal-urls-warn"> som bär REVEAL_URLS_WARN_LABEL. Varje skapad nod taggas data-ru="<token>" (aldrig ankaret), och varje post registrerar sitt configFingerprint så att en ändring av renderingskonfigurationen tvingar fram ett omflöde. Färger och teckensnittsåsidosättanden tillämpas genom typade element.style.*-egenskaper (applyColour/applyFontOverrides), aldrig interpolerade i en CSS-sträng. revertOwned tar bort skapade noder genom referens och återställer den sparade titeln; revertAll gör detta för varje ägt ankare och rensar registret.

truncateUrl arbetar över Unicode-KODPUNKTER ([...displayHref]), så att ett snitt aldrig delar ett surrogatpar; den bevarar schema-och-värd-ursprunget och förkortar resten med ett efterföljande URL_ELLIPSIS.

Värdavvikelse

packages/core/src/hostMismatch.ts hostMismatch jämför länkens synliga text mot dess href per registrerbar domän, inte per rått värdnamn, så att ett ärligt underdomän inte flaggas medan en efterbildning gör det. extractHostCandidates delar upp texten och reducerar varje token via hostCandidate (som kräver en punkt och ett ICANN-publikt suffix som tldts känner igen, och avvisar vanlig punktad text som e.g). Både href-värden och varje kandidat reduceras till sin registrerbara domän med tldts.getDomain, och VILKEN kandidat som helst vars domän skiljer sig från href:ens ger en avvikelse — så att namnge den verkliga skadliga värden vid sidan av en lockvärd kan inte undertrycka varningen. Den kastar aldrig.

Stilar och kontrast

packages/core/src/styles.ts innehåller den injicerbara REVEAL_URLS_CSS (fast layout, vikt, form och en fast vit, helt ogenomskinlig chipbakgrund — background: #ffffff, opacity: 1; färger interpoleras ALDRIG in i den) och körningshjälparna applyColour/applyFontOverrides/applyContrastBackdrop. applyContrastBackdrop läser elementets lösta färg och den första ogenomskinliga bakgrunden som hittas när man går från elementet SJÄLVT och uppåt (dess egen bakgrund — chipets fasta vita standard eller en värdsidas mer specifika åsidosättande — före någon förfader) genom standarden getComputedStyle (hämtad från element.ownerDocument?.defaultView, så att den är ramsäker och går att stubba) och, när packages/core/src/contrast.ts needsWhiteBackdrop rapporterar att texten inte uppfyller WCAG AA (CONTRAST_THRESHOLD) mot den bakgrunden OCH vitt verkligen hjälper, sätter style.backgroundColor = "white". contrast.ts tillhandahåller parseColour, relativeLuminance och contrastRatio; den tolkar formerna rgb()/rgba()/hex som getComputedStyle returnerar och behandlar en helt genomskinlig färg som "ej funnen".

Konfigurerbara värdar och innehållsavgränsning

En SiteRule (packages/core/src/config.ts) anger VAR annotering körs (match, allFrames) och VILKEN behållare som avgränsar den (contentRoot), plus enabled och en builtin-flagga. De levererade DEFAULT_SITES täcker Gmail, Proton (med allFrames) och båda Outlook-värdarna. Config aggregerar de globala växlarna, färgerna, teckensnittsåsidosättandena och sites-arrayen.

Matchningsmönstermotorn

packages/core/src/matchPattern.ts innehåller en medvetet RESTRIKTIV grammatik vid tilläggstillfället, MATCH_PATTERN (endast http/https, valfritt inledande *. värdjokertecken, glob-sökväg; ingen port, inget *-schema, inget <all_urls>). matchesPattern matchar en URL mot ett validerat mönster, och delegerar sökvägen till pathGlobMatches (varje * matchar valfri teckensekvens); den misslyckas säkert till false. parsePatternParts delar upp ett validerat mönster i host/path/scheme/wildcardHost.

Överlappande regler löses genom "mest specifik vinner": compareSiteSpecificity rangordnar efter (1) exakt värd före jokerteckenvärd, (2) längre bokstavlig värd, (3) mer bokstavlig sökväg (literalPathLength), sedan (4) en deterministisk ASCII-utslagsfaktor; selectMostSpecific returnerar den mest specifika aktiverade regeln som matchar en URL.

Täckning av beviljat ursprung är en SEPARAT, medvetet BREDARE matchare. permissions.getAll() kan rapportera beviljanden i den fullständiga WebExtension-grammatiken (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*) som den restriktiva MATCH_PATTERN skulle avvisa. parseGrantedOrigin tolkar dessa till GrantedOriginParts, och originCovers rapporterar om ett beviljat ursprung täcker en regels match (t.ex. att ett brett beviljande av https://*/* faktiskt täcker varje https-regel). matchAllowsOriginFallback rapporterar om en regels sökväg är exakt ORIGIN_FALLBACK_PATH (/*) — den säkra skärningen av Chromium- och Gecko/Firefox 128-begränsningarna på det dynamiska reservalternativet för ogenomskinligt ursprung.

Dynamisk registrering

packages/webext/src/contentRegistration.ts registrerar ett dynamiskt content-skript per användartillagd regel. Inbyggda serveras av de statiska content_scripts-posterna och registreras ALDRIG dynamiskt. desiredContentScripts filtrerar config.sites till icke-inbyggda, aktiverade regler vars ursprung isOriginGranted (delegerar till originCovers), mappar var och en till ett RegisteredContentScript med ett stabilt id (contentScriptId, en FNV-1a-hash av matchningen så att id:t använder endast den API-säkra teckenuppsättningen), och sätter matchOriginAsFallback: true ENDAST när matchAllowsOriginFallback(rule.match) håller (en sökväg /*). Att grinda flaggan på detta sätt förhindrar att en regel med icke-konform sökväg får hela batchregistreringen att avvisas; en sådan regel registreras utan utbrytningstäckning avsiktligt.

reconcileContentScriptsOnce läser de beviljade ursprungen, den lagrade konfigurationen och de aktiva registreringarna, och konvergerar sedan via en EGENSKAPSMEDVETEN diff: sameRegistration projicerar både den önskade deskriptorn och den aktiva återinläsningen på en normaliserad form (tillämpar varje fälts verkliga WebExtension-standard — allFrames/matchOriginAsFallback är som standard false, persistAcrossSessions är som standard TRUE, runAt är som standard document_idle) så att ett nyss registrerat skript jämförs som LIKA med deskriptorn som skapade det och ingen återregistreringsloop kan uppstå. Eftersom det inte finns något updateContentScripts, avregistreras ett ändrat skript och registreras sedan om. reconcileContentScripts lindar detta med ett pågående löfte (inFlight) så att två nästan samtidiga utlösare (en konfigurationsändring OCH permissions.onAdded som båda utlöses vid en webbplatsinläggning) serialiseras snarare än kapplöper. registerContentReconciliation kopplar utlösarna (permissions.onAdded/onRemoved, onConfigChanged) och konvergerar en gång vid uppstart; den importeras endast av Chrome- och Firefox-bakgrundsingångarna.

Opt-in-tillståndsflödet

De statiska manifesten deklarerar optional_host_permissions (http://*/*, https://*/*) så att en användare kan bevilja en godtycklig ytterligare webbvärd vid körning. packages/webext/src/options/options.ts addSite validerar HELA kandidatregeln (match OCH innehållsrot) genom normaliseSiteRule INNAN något tillstånd begärs, härleder värdursprunget med matchOrigin, och anropar browser.permissions.request inifrån Lägg till-knappens klickgest; först vid beviljande lägger den till en builtin:false-rad och persisterar genom den kanoniska skrivvägen.

Konfiguration, lagring och det kanoniska mönstret

Den enda valideringstratten

normaliseConfig i packages/core/src/config.ts är den enda kanoniska läsvägen: den tvingar och begränsar varje fält, faller tillbaka till standardvärdet vid allt ogiltigt, och kastar aldrig. Den är byggd av valideringsfunktioner per fält — normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts (som reducerar varje post till ett bart punycode-värdnamn via bareHostname), normaliseMatchColour/normaliseMismatchColour (grindade av safeColour.ts isSafeColour), normaliseCssSize, normaliseFontWeight, normaliseContentRoot (begränsad av CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH), normaliseMatchPattern (grindad av MATCH_PATTERN) och normaliseSites. normaliseSites släpper avvisade, avduplicerar efter match, TVINGAR builtin: true på varje regel vars match är lika med en inbyggds (vilket stänger vektorn exakt-match-skuggar-inbyggd / dubbelinjicering), återinsår varje saknad inbyggd (så att ett manipulerat lager inte kan släppa en kärnleverantör) och sorterar alfanumeriskt efter match för en kanonisk ordning. configFingerprint hashar endast de renderingspåverkande fälten (exklusive enabled, ignoreHosts och sites, som driver start/stopp och överhoppning snarare än omflöde) via fnv1a.

Lagring

packages/webext/src/storage.ts lindar WebExtension-lagrings-API:et. configArea använder browser.storage.sync när det är tillgängligt (så att inställningar roamar) och faller tillbaka till browser.storage.local (t.ex. på Thunderbird); configAreaName rapporterar vilket som är aktivt. getConfig, setConfig och onConfigChanged trattar alla sitt råa värde genom normaliseConfig, så att ett felformat eller manipulerat lager aldrig kan lämna en ogiltig Config till en anropare. onConfigChanged ignorerar dessutom händelser från det INAKTIVA lagringsområdet och ändringar av orelaterade nycklar.

De två tilläggen återanvänder samma tratt läs -> normaliseConfig -> returnera över sina egna värdlager. Outlook-uppgiftsfönstrets extensions/outlook/src/roamingStorage.ts lindar Office.context.roamingSettings (synkron get/set, bekräftad med saveAsync); Gmail-tilläggets packages/gmail/src/propertiesStorage.ts lindar PropertiesService.getUserProperties() (per användare, roamar över den användarens enheter, JSON-kodad under en nyckel, och behöver INGEN extra OAuth-omfattning — aldrig den delade ScriptProperties). Båda exponerar getConfig/setConfig plus de kortrelevanta accessorerna getIgnoreHosts/getHighlightMismatch och skyddar en otillgänglig värdyta med ett dedikerat fel. Gmails hemsideutlösare renderar ett CardService-inställningskort från getConfig, och formulärsändningshanteraren onSaveSettings slår samman de tolkade ignoreHosts/highlightMismatch på den aktuella konfigurationen och persisterar den genom setConfig; den kontextuella utlösaren trär ignoreHosts in i adaptern och highlightMismatch in i kortbyggaren, och läser defensivt så att ett lagringsfel faller tillbaka till standardvärden snarare än att bryta meddelandeanalysen.

Mönstret kanonisk konfiguration + riktad uppdatering

Tre skrivvägar läser den kanoniska konfigurationen, ändrar exakt en sak och skriver tillbaka den — utan att någonsin verkställa osparade formulärändringar eller rendera om formuläret:

Verktygsfältsåtgärdens egen toggleEnabled (packages/webext/src/toolbarAction.ts) följer samma mönster från bakgrundssidan.

Per-motor-bygge och manifestkontraktet

tooling/build.mjs driver ett enda esbuild-bygge för varje WebExtension-mål. TARGETS-deskriptorn namnger varje måls KÄLLMÅL: Chrome, Firefox och Thunderbird levererar sin egen src/; Edge, Opera och Safari deklarerar chrome som sin källa; Outlook-tillägget (Office.js) är sin egen källa. ACTIVE_TARGETS (Chrome, Edge, Firefox, Opera och Thunderbird — Safari och Outlook är exkluderade) är vad --all/--package bygger; Safari och Outlook byggs endast när de namnges uttryckligen, och Gmail-tillägget (Apps Script) är en separat esbuild-bunt (make build-gmail) utanför --all-slingan. Den Gmail-bunten riktar sig mot Apps Scripts V8-körtid, som varken har ES-moduler eller en inbyggd URL: den byggs som ESM och får sedan sin avslutande export {…}-sats borttagen (så att utlösarfunktionerna förblir globala på toppnivå som Apps Script kan anropa), och den buntar in en liten URL-polyfill (installerad endast när URL saknas) så att den delade kärnans new URL(...) löser länkar på serversidan. buildTarget rensar dist/<target>/, kopierar måls-specifika manifest.json och icons/ ordagrant, buntar varje källskript som IIFE (de injicerade INJECTED_SCRIPTS content.ts/messageDisplay.ts kan inte vara ES-moduler, och bakgrunder är klassiska arbetare/händelsesidor också) och den delade alternativsidan som ESM, och kopierar options.html/options.css från packages/webext/src/options/. webextension-polyfill buntas in i varje skript.

tooling/version.mjs stämplar versioner: den tar MAJOR.MINOR från rotens package.json och lägger till ett autoinkrementerande BUILD-nummer (läst från den högsta befintliga tredje komponenten över målmanifesten) i varje målmanifest, och håller dem i lås. De flesta mål bär ett enda manifest.json; Outlook-tillägget bär två versionsmärkta manifest (manifest.json och manifest.xml) och båda stämplas, och _template-skelettet stämplas i lås också. Rotens package.json hålls vid samma version, men förblir giltig semver (en fyrdelad version är tillåten i ett manifest, aldrig i package.json). tooling/icons.mjs (körs via make icons) rastrerar den enda vektorkällan assets/icon.svg till varje måls icons/icon48.png och icons/icon128.png, och provar cairosvg, sedan inkscape, sedan ImageMagick.

Manifestkontraktet — de exakta obligatoriska fälten per motor — är den enda sanningskällan i varje extensions/<engine>/test/manifest.smoke.test.mjs (t.ex. assertManifestShape i extensions/chrome/test/manifest.smoke.test.mjs, som även bygger målet och bekräftar att varje refererad fil löses i dist:en). Enligt projektets avdupliceringskonvention räknas den fält-för-fält-baserade formen INTE upp på nytt här; hänvisa till dessa smoke-tester och till noteringen "Manifest contract" i den manuella testplanen.

Internationalisering (i18n)

Reveal URLs är lokaliserad till många språk utöver engelska. Uppsättningen har en enda källa i packages/core/src/locales.json (SUPPORTED_LOCALES, modersmålsnamn och standardvärdet), som både tilläggets buntar (via den lösta JSON-importen) och webbplatsens enkla Node-bygge läser. Varje icke-engelsk sträng är maskinöversatt och väntar på mänsklig granskning; ursprungsmarkören är per format (_locales använder varje meddelandes description, webbplatschromets ordböcker registrerar den i site/i18n/README.md, och översatta dokumentkällor bär en HTML-kommentar på första raden).

Det finns tre oberoende lokaliseringsytor, medvetet inte korskopplade, och var och en är delad längs en linje mellan byggtid och körtid.

Undertryckning av "Aktiva webbplatser" för e-postklient (AD-3)

Ett e-postklientmål (Thunderbird idag) ser redan varje renderat meddelande, så den per-värd-baserade redigeraren "Active sites" är överflödig där och tas bort. Beslutet bärs av en dokumenterad deskriptorflagga per mål, mailClient: true, på tooling/build.mjs:s TARGETS.thunderbird — aldrig ett hårdkodat målnamn i bygg- eller gränssnittslogik — och implementeras i två halvor:

Säkerhets- och sekretesshållning

Testning