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:
packages/coreinnehåller REN logik — inga webbläsar-API:er, ingen lagring, inga markup-sänkor. Den är enhetstestbar i ren JavaScript och är den enda sanningskällan för länkannotering, konfigurationsvalidering, matchning av matchningsmönster, värdjämförelse och kontrastmedveten färgläggning.packages/webextlindar WebExtension-API:erna kring den kärnan: innehållslivscykeln, dynamisk registrering av content-skript, alternativgränssnittet, bakgrundskopplingen, verktygsfältsåtgärden och Thunderbirds väg för meddelandevisning.extensions/<engine>innehåller de tunna ingångspunkterna per motor plus varje motorsmanifest.json.toolinginnehåller skripten för bygge, versionsstämpling och ikongenerering.featuresinnehåller Cucumber-BDD-scenarierna och deras testdubbletter.
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/core/src/config.ts— schematConfig/SiteRule, standardvärden och den enda valideringstrattennormaliseConfig.packages/core/src/matchPattern.ts— matchningsmönstrets grammatik, matchare, specificitetsrangordning och täckning av beviljat ursprung.packages/core/src/linkProcessor.ts— den dokumentavgränsade annoteraren.packages/core/src/hostMismatch.ts— kontrollen av avvikelse i registrerbar domän.packages/core/src/styles.tsochpackages/core/src/contrast.ts— den injicerbara formatmallen och kontrastmedveten färgläggning.packages/core/src/safeColour.ts— predikatet för säker färg,isSafeColour.
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:
- Den gör anspråk på ramen synkront, före varje
await, viaclaimBootstrap, som läser/sätter flagganBOOTSTRAP_MARKERpå ramenswindow. En statisk inbyggdcontent_scripts-post och ett överlappande dynamiskt användarskript kan båda injiceracontent.jsi samma ram; det deladewindowi den isolerade världen gör detta till det korrekta engångsskyddet, och att göra anspråk före varjeawaitinnebär att två nästan samtidiga injiceringar inte båda kan passera det. - Den laddar den persisterade konfigurationen FÖRST via
loadConfigWithRetry(BOOTSTRAP_CONFIG_RETRIESextra 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 tilldelarREVEAL_URLS_CSSviatextContent), så att en misslyckad läsning lägger till ingen<style>och ett omförsök inte kan duplicera den. - Om konfigurationsladdningen misslyckas vid varje försök rullar
releaseBootstraptillbaka markören (endast här, innan annoteringen börjar) så att en senare återinjicering kan göra ett nytt försök; närstartväl har körts måste markören kvarstå, eftersom en ny annoterare inte skulle äga den föregående omgångens noder. - När
config.enabledanropar denstart, och den registrerar alltid enonConfigChanged-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):
- Beskär registerposter vars ankare inte längre är anslutet.
- Samlar in kandidatankare med
collectAnchors(efterkommandea[href]plus roten själv när den är ena[href], avduplicerade). - Löser varje ankares destination med
resolveAnnotatableUrl, som returnerarnull(hoppa över) för en tom href / href på samma sida, en otolkbar href, vilket annat schema som helst änhttp:/https:, eller en värd lika med eller under enignoreHosts-post; den löser relativa href:ar motbaseURIså att resultatet är absolut. - Tar den idempotenta snabbvägen när
isAnnotationIntactbekrä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. - Beräknar avvikelse med
hostMismatchpå den rena ankartexten, respekterarshowOnlyOnMismatch(lämnar ärliga länkar orörda), och anroparannotateAnchor.
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:
removeSite(options.ts): läsergetConfig, persisterarsetConfigöver ensites-array med endast den borttagna regeln bortfiltrerad (persistera FÖRST, sedanrow.remove(), sedan en villkorligpermissions.removesom avgörs från nästa konfiguration). Ett persisteringsfel kastarSiteRemovalSaveErroroch lämnar DOM:et orört; ett misslyckat återkallande kastarSitePermissionRevokeErrorefter att raden redan är borta.addSite(options.ts): beskrivet ovan — validera, begär tillstånd, lägg till raden, sedansaveOptions.toggleEnabled(options.ts): den omedelbara huvudströmbrytaren Aktivera vänder ENDASTenabledpå den lagrade konfigurationen och skriver tillbaka den, utan att rendera om (så att en pågående redigering aldrig skrivs över); vid en misslyckad skrivning återställer den kryssrutan till det lagrade värdet.
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.
- Inställningssidan (AD-1). Den levererar STATISK engelsk markup; varje översättbar
nod bär en annotering
data-i18n="<key>"(text) ellerdata-i18n-<attr>="<key>"(attribut). Vid körning ärpackages/webext/src/options/locale.tstillämpningslagret: denfetchar sidans egen paketerade_locales/<code>/messages.json(viaruntime.getURL, ingaweb_accessible_resourcesbehövs), skriver om varje annoterad nod ENDAST viatextContent(så att formulärets<code>-exempeltips förblir intakta), och sätter<html lang>. Sidan är FULLSTÄNDIGT lokaliserad: vid sidan av den statiska prosan bär de JS-byggda webbplatsrad-etiketterna och Ta bort-knappen endata-i18n-nyckel (med en inledandetranslate-ad text) så att samma tillämpningslager omlokaliserar renderade rader utan omrendering, och de VERKLIGT dynamiska strängarna — statusraden, lägg-till-webbplats-återkopplingen och versionsraden — renderas genomlocale.ts:stranslate(<key>)(currentMessages → engelskt reservalternativ → nyckel) vid anropstillfället. Det engelska reservalternativet är katalogen_locales/en/messages.jsonsom IMPORTERAS (buntas) in ioptions.jsvid byggtid, så atttranslatelöses till läsbar engelska SYNKRONT från modulladdning — en dynamisk sträng som renderas innan växlaren stabiliserats, eller efter att en kataloginhämtning vid körning misslyckats, degraderas aldrig till en rå meddelandenyckel.locale.tspublicerar den tillämpade meddelandekartan och utlöser sinaonLocaleChange-lyssnare efter varje tillämpning, så attoptions.tsrenderar om den för närvarande visade statusen/återkopplingen/ versionen och pekar om länken Online manual vid varje växling. Versionsraden använder ettoptionsVersion-meddelande vars platshållare{version}ersätts med byggnumret (token översatt, nummer ordagrant). Manuallänken följer den aktiva lokalen till sin dokumentvariant (AD-9): engelska behållermanual.htmlpå toppnivå, varje annan lokal öppnarnl/handleiding.html, och bevarar versionsstämpeln?v=. Den aktiva lokalen är användarens sparade val eller, om det saknas, webbläsarspråket löst till ett stött basspråk av kärnansresolveLocale(AD-5); en ostödd eller misslyckad lokal lämnar den förrenderade engelskan (ochtranslatedegraderas då till det engelska reservalternativet). Manualens växlare Display language persisterar sitt val under en dedikeraduiLocale-nyckel istorage.local— aldrig i den synkroniseradeConfig, så att det är per enhet och aldrig roamar. - Webbplatschromet (AD-2). Den statiska webbplatsgeneratorn
tooling/site.mjsserver-renderar varje chrome-nod på engelska medan den annoterar dendata-i18n, och behåller<html lang="en-GB">(AD-4). Den webbläsarsidiga laddarensite/scripts/i18n.mjs(en beroendefri ES-modul) byter sedan chromet till besökarens språk efter företräde — ett persisteratlocalStorage-val (revealUrlsSiteLocale) → den första stödda basmatchningen inavigator.languages(AD-5) → engelska — och hämtarsite/i18n/<code>.json. Till skillnad från alternativsidans tillämpningslager byter den viainnerHTML, eftersom webbplatschromets värden bär betrodd inline-markup (<code>,<a>); ett paritetstest fäster varje icke-engelskt värdes tagg-/attribut-/URL-sekvens byte-identisk med engelska så att en översättning aldrig kan släppa en länk eller bryta en tagg. En saknad nyckel behåller den förrenderade engelska ögonblicksbilden; en misslyckad inhämtning lämnar den engelska sidan intakt (graciös degradering, aldrig tom). - Dokumentationssidorna (AD-8/AD-9). Dokumentsidor renderas PER LOKAL vid byggtid,
inte utbytta vid körning. Engelska dokument stannar på toppnivåvägarna; varje annan
lokal får en kopia under en
<code>/-mapp, från en översattdocs/<code>/<DOC>.mdnär den finns, annars den engelska källan — ett reservalternativ per sida sombuildSiteLOGGAR snarare än att tyst utelämna. Dokumentets BRÖDTEXT bär ingendata-i18n(den är rå renderad markdown), så körtidens chrome-laddare vidrör den aldrig; endast de delade chrome-noderna byts. Dokumentsidans språkväljare NAVIGERAR till systerlokalens sida snarare än att byta på plats. - Lokalisering av interna länkar (Finding 2). Det valda språket följer med över
dokumentnavigeringen. Varje INTERN dokumentankare (de fem sidorna
architecture.html,index.html,licence.html,manual.html,privacy.html) taggasdata-doc-link="<page>"av generatorn, och för fallet utan JavaScript löses dess href redan till den AKTUELLA sidans egen lokalvariant (navDocHref— de tre per-lokala dokumentsidorna lokaliseras; de enbart engelskaindex.html/licence.htmlstannar på toppnivå). KörtidsladdarensrewriteDocLinksriktar sedan om varje internt dokumentankare — de taggade OCH varje katalog-injicerat enkelt ankare såsommanual.html#installing— till den AKTIVA lokalens variant, och bevarar varje#hash/?queryoch vidrör aldrig externa länkar. På en dokumentsida NAVIGERAR ett LAGRAT val som skiljer sig från sidans egen lokal till den matchande varianten; loopskyddet utlöses endast vid ett uttryckligt lagrat val (aldrig enbart en webbläsarspråkspreferens) och endast när målet skiljer sig från sidan som redan visas. Katalogerna själva behåller sina engelska interna URL:er, så att paritetstestet förblir grönt — lokaliseringen är en ren körtidspassning över DOM:et.
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:
- Byggtid.
copyAssetsutgår fråndescriptorFor(target).mailClient: för ett e-postklientmål kör den den rena transformenremoveSitesSection(html)(som tar bort hela värdredigeraren<section class="sites">, inklusive varje lägg-till-webbplats-kontroll, och kastarMissingSourceErrorom sektionen saknas så att en markup-ändring aldrig tyst kan leverera redigeraren) och skriver resultatet; webbläsarmål fåroptions.htmlbyte-för-byte viacpSync. - Körtid. Den buntade styrenheten
packages/webext/src/options/options.tstolererar den saknade sektionen:renderSites/readSites/applyConfig/readFormsonderar#sites-listoch blir en no-op snarare än att kastaOptionsFieldMissingError, ochreadFormUTELÄMNARsites-nyckeln när sektionen är borta. Eftersom att utelämnasitesskulle låtanormaliseConfigåterinså de inbyggda standardvärdena och släppa användarens webbplatser, återinförsaveOptionsde lagradesitesinnan normalisering närhelst sektionen saknas — det är vad som får en e-postklientsparning att bevara de konfigurerade värdarna. Ett körtidsskydd,guardMailClientSites, tar bort sektionen om den fortfarande är närvarande, och detekterar en e-postklient genom API-/funktionsnärvaro (messages/messageDisplay/scripting.messageDisplaypå den injiceradebrowser), aldrig genom målnamn; det körs först iinitOptionsså att den senare lägg-till/list-kopplingen naturligt hoppar över de saknade elementen. I en webbläsarkörtid är skyddet en no-op.
Säkerhets- och sekretesshållning
- Inget nätverk eller exfiltrering. Ingen modul utför
fetch/XHReller något annat nätverksanrop; tillägget läser endast den lagrade konfigurationen och sidans DOM. - Endast säker-DOM. Annotering och den injicerade formatmallen använder
uteslutande
createElement/textContent/typadestyle.*-egenskaper, aldriginnerHTML/insertAdjacentHTML/eval. För kärnan upprätthålls detta avpackages/core/test/purity.test.ts. - All extern indata/konfiguration normaliseras. Varje läsning och skrivning
trattas genom
normaliseConfig; färger passerarisSafeColour; matchningsmönster passerarMATCH_PATTERN; innehållsrot-väljare begränsas avCONTENT_ROOT_PATTERNoch lämnas endast någonsin tillquerySelectorAll/closest, aldrig interpolerade in i markup eller CSS. - Minsta privilegium. Inbyggda webbmail-ursprung är fasta
host_permissions; ytterligare värdar är opt-in viaoptional_host_permissionsoch en gestbundenpermissions.request. Verktygsfältsåtgärden deklarerar ingen popup. - Gecko-datainsamling. Firefox- och Thunderbird-manifesten deklarerar
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Licens. Projektet är AGPL-3.0-only (
LICENSE,package.json).
Testning
- Vitest-enhetstester täcker båda paketen:
packages/core/test/*(konfiguration, kontrast, värdavvikelse, länkprocessor, stilar, och renhetsskyddet) ochpackages/webext/test/*(innehåll, lagring, alternativ, registrering, verktygsfält, meddelandevisning och ett anpassad-webbplats end-to-end-test). Varje paket körvitest run. - Cucumber-BDD-scenarier bor i
features/(kopplade avcucumber.json) och driver de verkliga@reveal-urls/webext-modulerna genom trogna testdubbletter ifeatures/support/webext.ts— särskiltnormaliseReadBack, som speglar att motorn UTELÄMNAR flaggor med falskt värde vid återinläsning så att en eftergiven testrigg inte kan dölja en återregistreringsloop — över ett jsdom-dokument uppsatt ifeatures/support/world.ts. - Manifest-smoke-tester per mål (
extensions/<engine>/test/manifest.smoke.test.mjs, Nodes inbyggdanode:test) hävdar varje manifests exakta form och att den byggda dist:en löser varje refererad fil.tooling/test/lägger till tester för bygge, version och definition-of-done. - CI (
.github/workflows/ci.yml) grindar varje push och pull request påmake test,make bddochmake lint(körda direkt på löparen, vilket åsidosätter Makefilens Docker-RUN/IMAGE_DEP). Versionsstämpling och paketering är medvetet exkluderade; de bor i den separata, taggutlösta.github/workflows/release.yml.