Safari is a supported active target with its own entry points. Built-in providers use static scripts and Safari requests no all-sites access. User-added sites receive a runtime optional permission, then the background injects content.js after navigation only for granted, enabled, non-builtin rules, with allFrames per rule. Safari settings are device-local and do not roam between devices. macOS packaging uses Xcode and the Apple Developer Program/App Store route.
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, Safari och Thunderbird — från en gemensam, ren kodkärna. Safari
använder statiska skript endast för inbyggda leverantörer och är publicerad i
App Store. 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). Den inbyggda sökvägen för fynd är avsiktligt begränsad
till http:/https:.
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/destination.ts— schemamedveten klassificering och jämförelse av destinationsavvikelser.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), medan sen rotupptäckt hanteras av den
avgränsade rundan discoverMatchedRoots() per batch (en
querySelectorAll(contentRoot) per batch, deduplicerad med WeakSet).
pendingShadowHosts återkontrollerar tidigare plain hosts tills en open shadow
root dyker upp, så att också shadow-uppgraderingar täcks. Så både omsättning inom
roten och shadow-host-uppgraderingar 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 returnerar delegerar tillresolveAnnotatableHref. Den returnerarnull(hoppa över) bara för en tom href, href på samma sida eller en otolkbar href, löser relativa href:ar motbaseURIoch accepterar varje tolkbart schema, inklusivemailto:,tel:,sms:och de ogenomskinliga URL:ernajavascript:,data:ochblob:.ignoreHostsgäller endasthttp:/https:-länkar med en värd. - 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 den schemamedvetna avvikelsen med
destinationMismatchpå den rena ankartexten:classifySchemeskickar webblänkar tillhostMismatch, e-post tillmailMismatch, telefon tillphoneMismatchoch övriga tillopaqueMismatch. Därefter anropasannotateAnchor;showOnlyOnMismatchdöljer bara den synliga inbäddade visningen för en länk utan avvikelse, men behåller dess destinations-title.
annotateAnchor renderar endast SÄKER-DOM. Den sparar alltid den ursprungliga
title (TitleSave) och skriver destinations-title — den enda skrivningen till
ankaret. Det finns inget separat titelläge eller någon varningsbricka. Om inte
showOnlyOnMismatch döljer en länk utan avvikelse, lägger den först till en
<span class="reveal-urls-url"> och ett efterföljande <br>, medan länkens egna
barn lämnas orörda. needsLeadingInlineRevealBreak lägger till ett inledande <br>
när synligt inbäddat innehåll föregår länken på samma rad. Span:en visar en av
författaren skriven absolut href ordagrant, eller en relativ href löst mot baseURI,
med inert textContent (prefixad med en pil och ett icke-brytande blanksteg U+00A0,
sedan kapad av truncateUrl); när den har anslutits får den en kontrastbakgrund via
applyContrastBackdrop. Vid en betonad avvikelse läggs
REVEAL_URLS_MISMATCH_CLASS till i REVEAL_URLS_URL_CLASS, vilket tillämpar den
konfigurerade avvikelsefärgen via applyColour och fetstil som standard i
REVEAL_URLS_CSS; ett uttryckligt åsidosättande av teckensnittsvikten via
applyFontOverrides har företräde. 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. 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 är webbgrenen av
destinationMismatch: den jämför länkens synliga text mot dess href per
registrerbar domän, inte per rått värdnamn, så att en underdomän utan avvikelse
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.
För e-postlänkar jämför mailMismatch alla mottagares registrerbara domäner,
inklusive to, cc och bcc; phoneMismatch jämför tolkningsbara uppringda
nummer. opaqueMismatch flaggar alltid SCRIPT_LIKE_SCHEMES — javascript:,
data: och vbscript: — medan andra ogenomskinliga scheman bara flaggas när den
synliga texten namnger en konkret webb-, e-post- eller telefondestination.
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, 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 sex sidorna
architecture.html,faq.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#site-selectoch 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.