Reveal URLs — Architektura
Ten dokument opisuje, jak zbudowane jest rozszerzenie Reveal URLs i jak działa
jego logika rdzenia, z myślą o współtwórcach i czytelnikach technicznych. W całym
tekście cytuje rzeczywiste pliki i symbole (w postaci path symbol), aby każde
twierdzenie można było sprawdzić względem źródła. Opis dla użytkownika znajdziesz
w podręczniku; ręczne kroki weryfikacji opisuje
plan testów manualnych.
Przegląd
Reveal URLs to pojedyncze rozszerzenie WebExtension w wersji MV3, budowane dla
kilku silników — Chrome, Edge, Opera, Firefox i Thunderbird — z jednego wspólnego,
czystego rdzenia kodu. (Safari jest odroczone: ma manifest i daje się zbudować, gdy
zostanie nazwane jawnie, ale jest wyłączone z domyślnego builda oraz z CI.)
Rozszerzenie ujawnia docelowy adres URL każdego odnośnika obok odnośnika w
wyrenderowanym e-mailu i oznacza odnośnik, którego widoczny tekst wymienia inną
rejestrowalną domenę niż jego href — klasyczny sygnał phishingu.
Repozytorium to monorepo pnpm/TypeScript (pnpm-workspace.yaml, package.json z
"private": true). Pakowanie realizuje jeden skrypt oparty na esbuild,
tooling/build.mjs, który kompiluje wspólne pakiety oraz cienkie punkty wejścia
każdego silnika do ładowalnego katalogu dist/<target>/. webextension-polyfill
jest wbudowany w każdy skrypt, a nie używany jako globalna zmienna środowiska
uruchomieniowego, więc to samo źródło działa bez zmian w Chromium i Gecko.
Dwa natywne dodatki pocztowe rozszerzają ten sam rdzeń wykrywania na powierzchnie,
których WebExtension nie jest w stanie objąć: dodatek do Outlooka (panel zadań
Office.js, obejmujący Outlooka w sieci/na Windowsie/Macu/iOS/Androidzie oraz
Outlook.com) oraz dodatek do Gmaila (CardService w Apps Script, obejmujący
aplikacje Gmaila w sieci/na Androidzie/iOS). Oba ponownie wykorzystują wyłącznie
CZYSTĄ analizę — nowy moduł packages/core/src/findings.ts
(analyseAnchors/analyseHtml, neutralny względem platformy model Finding[]),
który ponownie wykorzystuje istniejącą logikę hostMismatch oraz wyznaczanie
rejestrowalnej domeny przez tldts. Per powierzchnię różni się tylko adapter hosta
(jak odczytywana i parsowana jest treść) oraz prezentacja: panel zadań Outlooka
parsuje treść własnym DOMParser i działa po stronie klienta; dodatek do
Gmaila parsuje za pomocą node-html-parser i działa po stronie serwera, na
infrastrukturze Google Apps Script V8 (darmowy hosting, skoro Google i tak
przechowuje pocztę; wdrażany lokalnie-interaktywnie przez clasp). Ponieważ żaden z
tych frameworków nie może modyfikować wyrenderowanego DOM wiadomości w trybie
odczytu (setAsync/prependAsync w Office działają tylko przy tworzeniu
wiadomości; CardService renderuje karty, a nie HTML wiadomości), oba dodatki
prezentują panel/kartę ustaleń, a nie adnotację w miejscu — istniejąca ścieżka
modyfikacji DOM linkProcessor.ts oraz REVEAL_URLS_CSS NIE są ponownie
wykorzystywane przez żaden z dodatków. Względny href jest rozwiązywany wyłącznie
względem godnego zaufania, zawartego w e-mailu <base href>, a w przeciwnym razie
pomijany, nigdy względem origin skrzynki/dostawcy (co sfabrykowałoby miejsce
docelowe).
Baza kodu ostro rozdziela odpowiedzialności:
packages/coremieści CZYSTĄ logikę — bez API przeglądarki, bez pamięci, bez ujść znaczników. Daje się testować jednostkowo w czystym JavaScript i jest jedynym źródłem prawdy dla adnotacji odnośników, walidacji konfiguracji, dopasowywania wzorców, porównywania hostów oraz kolorowania świadomego kontrastu.packages/webextowija API WebExtension wokół tego rdzenia: cykl życia treści, dynamiczną rejestrację skryptów treści, interfejs opcji, okablowanie tła, akcję paska narzędzi oraz ścieżkę wyświetlania wiadomości w Thunderbirdzie.extensions/<engine>mieści cienkie punkty wejścia per silnik orazmanifest.jsonkażdego silnika.toolingmieści skrypty budowania, stemplowania wersji i generowania ikon.featuresmieści scenariusze BDD Cucumber oraz ich dublery testowe.
Układ repozytorium
packages/core — czysty rdzeń
Każdy moduł tutaj nie importuje żadnego API rozszerzenia
(browser.*/chrome.*/messenger.*) i nie używa żadnego ujścia DOM zamieniającego
ciąg znaków na znaczniki (innerHTML/insertAdjacentHTML). Ten kontrakt jest
mechanicznie egzekwowany przez packages/core/test/purity.test.ts, który obejmuje
globem każdy src/**/*.ts, usuwa treść komentarzy własnym stripComments (aby
docbloki, które zgodnie z prawem NAZYWAJĄ zakazane tokeny, nie uruchamiały
strażnika), a następnie zapewnia, że ani EXTENSION_API_PATTERN, ani
UNSAFE_DOM_PATTERN nie pasuje do żadnego modułu. Publiczna powierzchnia jest
reeksportowana z packages/core/src/index.ts. Jedyną zależnością środowiska
uruchomieniowego jest tldts (do porównywania domen ze świadomością publicznych
sufiksów).
Moduły to:
packages/core/src/config.ts— schematConfig/SiteRule, wartości domyślne oraz jedyny lejek walidacjinormaliseConfig.packages/core/src/matchPattern.ts— gramatyka wzorców dopasowania, matcher, ranking szczegółowości oraz pokrycie przyznanego origin.packages/core/src/linkProcessor.ts— anotator o zasięgu dokumentu.packages/core/src/hostMismatch.ts— sprawdzanie niezgodności rejestrowalnej domeny.packages/core/src/styles.tsorazpackages/core/src/contrast.ts— wstrzykiwalny arkusz stylów i kolorowanie świadome kontrastu.packages/core/src/safeColour.ts— predykat bezpiecznego koloruisSafeColour.
packages/webext — opakowania API przeglądarki
Moduły tutaj MOGĄ używać browser.* i są jedynym miejscem, w którym dotyka się API
WebExtension. Publiczna powierzchnia jest reeksportowana z
packages/webext/src/index.ts. Moduły to: content.ts (cykl życia treści),
contentRegistration.ts (dynamiczna rejestracja), storage.ts (trwałość
konfiguracji), background.ts (zachowanie przy instalacji), toolbarAction.ts
(przełącznik + odznaka), messageDisplay.ts oraz messageDisplayBackground.ts
(Thunderbird) i options/options.ts (kontroler strony ustawień).
extensions/<engine> — cienkie punkty wejścia i manifesty
Każdy silnik niesie własny manifest.json oraz icons/. Chrome, Firefox i
Thunderbird dodatkowo dostarczają src/ z punktami wejścia; Edge, Opera i Safari
niosą tylko manifest oraz ikony i PONOWNIE WYKORZYSTUJĄ src/ Chrome
(zadeklarowane przez deskryptor builda — zobacz niżej). Punkty wejścia są celowo
malutkie: na przykład extensions/chrome/src/content.ts to po prostu
void createContentController().bootstrap();, a extensions/chrome/src/background.ts
wywołuje registerBackground, registerContentReconciliation oraz
registerToolbarToggle. Punkt wejścia tła Firefoksa jest identyczny jak Chrome'a;
punkt Thunderbirda (extensions/thunderbird/src/background.ts) wywołuje zamiast
tego registerBackground oraz registerMessageDisplay (nie ma skryptów treści).
extensions/_template to gotowy do skopiowania szkielet Chromium, a nie cel builda.
tooling, features oraz testy dymne
tooling mieści build.mjs, version.mjs oraz icons.mjs. features mieści
scenariusze Gherkin, ich definicje kroków oraz wierne dublery testowe w
features/support/webext.ts i features/support/world.ts. Każdy silnik niesie
również extensions/<engine>/test/manifest.smoke.test.mjs (dla _template jest to
template.smoke.test.mjs) — to one stanowią autorytatywny kontrakt manifestu
(zobacz „Build per silnik i kontrakt manifestu” niżej).
Potok ujawniania
Ścieżka treści zaczyna się w punkcie wejścia silnika, takim jak
extensions/chrome/src/content.ts, który wywołuje
createContentController().bootstrap() z packages/webext/src/content.ts.
Bootstrap
createContentController bootstrap uruchamia się raz na ramkę:
- Zajmuje ramkę synchronicznie, przed jakimkolwiek
await, przezclaimBootstrap, który odczytuje/ustawia flagęBOOTSTRAP_MARKERnawindowramki. Statyczny wbudowany wpiscontent_scriptsoraz nakładający się dynamiczny skrypt użytkownika mogą oba wstrzyknąćcontent.jsdo tej samej ramki; współdzielonywindowizolowanego świata czyni to poprawnym strażnikiem jednorazowości, a zajęcie przed jakimkolwiekawaitoznacza, że dwa niemal jednoczesne wstrzyknięcia nie mogą oba przez niego przejść. - Najpierw wczytuje utrwaloną konfigurację przez
loadConfigWithRetry(BOOTSTRAP_CONFIG_RETRIESdodatkowych prób przy przejściowym odrzuceniu), trzymając marker przez cały czas ponawiania, aby nakładające się wstrzyknięcie, które już wykonało no-op, nie zostało porzucone. Dopiero gdy odczyt się powiedzie, wstrzykuje arkusz stylów (injectStyles, który przypisujeREVEAL_URLS_CSSprzeztextContent), więc nieudany odczyt nie dołącza żadnego<style>, a ponowienie nie może go zduplikować. - Jeśli wczytanie konfiguracji zawiedzie przy każdej próbie,
releaseBootstrapwycofuje marker (tylko tutaj, zanim rozpocznie się adnotacja), aby późniejsze ponowne wstrzyknięcie mogło spróbować ponownie; gdystartjuż się wykona, marker musi przetrwać, ponieważ świeży anotator nie byłby właścicielem węzłów z poprzedniego przebiegu. - Gdy
config.enabled, wywołujestarti zawsze rejestruje nasłuchiwaczonConfigChanged, który ponownie stosuje konfigurację (obejmując przejście wyłączone→włączone).
Rozwiązywanie aktywnej reguły witryny
start (oraz applyConfig) wywołuje resolveSiteRule, aby zdecydować, czy — i
gdzie — adnotować. resolveSiteRule najpierw próbuje WŁASNEJ lokalizacji
działającego dokumentu przez resolveForHref, który filtruje config.sites do
reguł włączonych i deleguje do selectMostSpecific z rdzenia, aby wygrało
najbardziej szczegółowe dopasowanie (szczegółowy wbudowany wpis bije szeroki symbol
wieloznaczny użytkownika). Gdy własna lokalizacja nie pasuje do niczego ORAZ origin
samego dokumentu jest nieprzezroczysty lub odziedziczony — bramkowane przez
hasOpaqueOrigin, prawdziwe tylko dla about:blank/about:srcdoc (odziedziczone
przez INHERITED_ORIGIN_URLS) lub blob:/data: (OPAQUE_ORIGIN_SCHEMES) —
sięga, w kolejności, po ramkę GÓRNĄ (readTopHref, wbudowany przypadek iframe
wiadomości Proton), okno OTWIERAJĄCE (readOpenerHref, wysuwane okno czytania
Outlooka otwierane przez window.open("about:blank")), a na końcu po
document.referrer (readReferrer). Każdy odczyt zewnętrzny jest chroniony przed
dostępem międzyźródłowym (który rzuca wyjątek) i dostarcza kandydata tylko wtedy,
gdy jest odczytywalny. ZWYKŁY niedopasowany dokument jest autorytatywny i nigdy nie
dziedziczy reguły z kontekstu otoczenia, pozostawiając kontroler bezczynnym.
Ograniczanie do korzenia treści i obserwowanie mutacji
start zapisuje selektor contentRoot dopasowanej reguły, tworzy anotator przez
createAnnotator(config) i przetwarza każdy dopasowany korzeń z
selectRoots(doc, contentRoot) (querySelectorAll opakowany w try/catch, więc
nieprawidłowy selektor bezpiecznie kończy się []). Następnie obserwuje doc.body
(stabilny kontener) z OBSERVER_OPTIONS ({ childList: true, subtree: true }).
Obserwator zasila processMutations, który bramkuje każdą mutację na korzeniu
treści: ponownie przetwarza target mutacji tylko wtedy, gdy jest on Element
wewnątrz korzenia (isWithinRoot, przez closest), i kieruje każdy dodany Element
przez processAddedNode, który adnotuje węzeł znajdujący się wewnątrz korzenia,
BĘDĄCY korzeniem lub ZAWIERAJĄCY korzeń (leniwy montaż SPA). Dzięki temu zarówno
zmiany wewnątrz korzenia, jak i siostrzana treść wiadomości montowana później są
wychwytywane, podczas gdy interfejs aplikacji poza korzeniami pozostaje nietknięty.
Pusty (null) anotator (rozszerzenie jest zatrzymane) to no-op.
applyConfig uzgadnia zmianę konfiguracji na żywo: rozbiera się, gdy żadna reguła
nie pasuje lub flaga włączenia jest wyłączona; restartuje (cofnięcie + ponowna
obserwacja), gdy rozwiązany contentRoot się zmienił; a w przeciwnym razie
przepływa na miejscu — annotator.setConfig, a następnie annotator.process po
każdym dopasowanym korzeniu — bez restartowania obserwatora.
Adnotowanie pojedynczej kotwicy
Anotator znajduje się w packages/core/src/linkProcessor.ts. createAnnotator
zwraca Annotator nad stanem domknięcia, którego strona nie może odczytać: silną
mapę Map kluczowaną tożsamością kotwicy, token data-ru per anotator z
generateToken (crypto.getRandomValues) oraz bieżącą konfigurację. Jego metoda
process(root):
- Usuwa wpisy rejestru, których kotwica nie jest już połączona.
- Zbiera kotwice kandydatów za pomocą
collectAnchors(potomnea[href]plus sam korzeń, gdy jesta[href], z usunięciem duplikatów). - Rozwiązuje miejsce docelowe każdej kotwicy za pomocą
resolveAnnotatableUrl, który zwracanull(pominięcie) dla pustego/wewnątrzstronicowego href, href nie do sparsowania, dowolnego schematu innego niżhttp:/https:lub hosta równego wpisowiignoreHostsalbo będącego jego poddomeną; rozwiązuje względne href względembaseURI, więc wynik jest bezwzględny. - Obiera idempotentną szybką ścieżkę, gdy
isAnnotationIntactpotwierdza, że odcisk renderowania jest niezmieniony, a każdy utworzony węzeł jest nadal połączony i na swoim miejscu; w przeciwnym razie cofa nieaktualną adnotację i renderuje ponownie. - Oblicza niezgodność za pomocą
hostMismatchna oczyszczonym tekście kotwicy, respektujeshowOnlyOnMismatch(pozostawiając uczciwe odnośniki nietknięte) i wywołujeannotateAnchor.
annotateAnchor renderuje wyłącznie BEZPIECZNY DOM. W trybie "inline" poprzedza
zawartość elementem <span class="reveal-urls-url">, którego URL ustawiany jest
przez textContent (poprzedzony strzałką i niełamliwą spacją U+00A0, skrócony przez
truncateUrl), po którym następuje <br>, pozostawiając własne dzieci odnośnika
nietknięte; połączony span otrzymuje następnie tło kontrastowe przez
applyContrastBackdrop. W trybie "title" zapisuje oryginalny title
(TitleSave), nadpisuje go adresem URL — jedyny zapis do kotwicy — a przy
wyróżnionej niezgodności dołącza siostrzaną odznakę
<span class="reveal-urls-warn"> noszącą REVEAL_URLS_WARN_LABEL. Każdy utworzony
węzeł jest oznaczany data-ru="<token>" (nigdy kotwica), a każdy wpis zapisuje swój
configFingerprint, więc zmiana konfiguracji renderowania wymusza przepływ. Kolory
i nadpisania czcionek są stosowane przez typowane właściwości element.style.*
(applyColour/applyFontOverrides), nigdy interpolowane do ciągu CSS.
revertOwned usuwa utworzone węzły przez referencję i przywraca zapisany tytuł;
revertAll robi to dla każdej posiadanej kotwicy i czyści rejestr.
truncateUrl operuje na PUNKTACH KODOWYCH Unicode ([...displayHref]), więc cięcie
nigdy nie rozdziela pary surogatów; zachowuje origin schematu-i-hosta i skraca
resztę kończącym URL_ELLIPSIS.
Niezgodność hosta
packages/core/src/hostMismatch.ts hostMismatch porównuje widoczny tekst
odnośnika z jego href według rejestrowalnej domeny, a nie surowej nazwy hosta,
więc uczciwa poddomena nie jest oznaczana, podczas gdy łudząco podobna jest.
extractHostCandidates dzieli tekst i redukuje każdy token przez hostCandidate
(który wymaga kropki oraz rozpoznanego przez tldts publicznego sufiksu ICANN,
odrzucając zwykły tekst z kropką, jak e.g). Zarówno host z href, jak i każdy
kandydat są redukowani do swojej rejestrowalnej domeny za pomocą tldts.getDomain,
a KAŻDY kandydat, którego domena różni się od domeny href, daje niezgodność — więc
wymienienie prawdziwego złośliwego hosta obok hosta-przynęty nie może stłumić
ostrzeżenia. Nigdy nie rzuca wyjątku.
Style i kontrast
packages/core/src/styles.ts mieści wstrzykiwalny REVEAL_URLS_CSS (stały układ,
grubość, kształt oraz stałe białe, w pełni nieprzezroczyste tło chipa —
background: #ffffff, opacity: 1; kolory NIGDY nie są do niego interpolowane)
oraz pomocniki środowiska uruchomieniowego
applyColour/applyFontOverrides/applyContrastBackdrop. applyContrastBackdrop
odczytuje rozwiązany kolor elementu oraz pierwsze nieprzezroczyste tło znalezione,
idąc od SAMEGO elementu w górę (jego własne tło — stała biała wartość domyślna chipa
lub bardziej szczegółowe nadpisanie strony hosta — przed jakimkolwiek przodkiem)
przez standardowy getComputedStyle (pozyskany z
element.ownerDocument?.defaultView, więc bezpieczny dla ramek i zastępowalny
atrapą), a gdy packages/core/src/contrast.ts needsWhiteBackdrop zgłasza, że
tekst nie spełnia WCAG AA (CONTRAST_THRESHOLD) względem tego tła ORAZ biel
rzeczywiście pomaga, ustawia style.backgroundColor = "white". contrast.ts
dostarcza parseColour, relativeLuminance oraz contrastRatio; parsuje formy
rgb()/rgba()/szesnastkowe zwracane przez getComputedStyle i traktuje w pełni
przezroczysty kolor jako „nieznaleziony”.
Konfigurowalne hosty i ograniczanie treści
SiteRule (packages/core/src/config.ts) mówi, GDZIE działa adnotacja (match,
allFrames) i KTÓRY kontener ją ogranicza (contentRoot), plus enabled oraz
flaga builtin. Dostarczane DEFAULT_SITES obejmują Gmaila, Protona (z
allFrames) oraz oba hosty Outlooka. Config agreguje globalne przełączniki,
kolory, nadpisania czcionek oraz tablicę sites.
Silnik wzorców dopasowania
packages/core/src/matchPattern.ts mieści celowo RESTRYKCYJNĄ gramatykę czasu
dodawania MATCH_PATTERN (tylko http/https, opcjonalny wiodący symbol wieloznaczny
hosta *., ścieżka glob; bez portu, bez schematu *, bez <all_urls>).
matchesPattern dopasowuje URL do zwalidowanego wzorca, delegując ścieżkę do
pathGlobMatches (każdy * dopasowuje dowolny ciąg znaków); bezpiecznie kończy się
false. parsePatternParts dzieli zwalidowany wzorzec na
host/path/scheme/wildcardHost.
Nakładające się reguły rozstrzyga zasada „wygrywa najbardziej szczegółowa”:
compareSiteSpecificity rankuje według (1) dokładnego hosta nad hostem
wieloznacznym, (2) dłuższego dosłownego hosta, (3) bardziej dosłownej ścieżki
(literalPathLength), a następnie (4) deterministycznego rozstrzygnięcia ASCII;
selectMostSpecific zwraca najbardziej szczegółową włączoną regułę dopasowującą URL.
Pokrycie przyznanego origin to ODDZIELNY, celowo SZERSZY matcher.
permissions.getAll() może zgłaszać przyznania w pełnej gramatyce WebExtension
(<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*), które
restrykcyjny MATCH_PATTERN by odrzucił. parseGrantedOrigin parsuje je do
GrantedOriginParts, a originCovers zgłasza, czy przyznany origin pokrywa match
reguły (np. szerokie przyznanie https://*/* faktycznie pokrywa każdą regułę
https). matchAllowsOriginFallback zgłasza, czy ścieżka reguły to dokładnie
ORIGIN_FALLBACK_PATH (/*) — bezpieczne przecięcie ograniczeń Chromium oraz
Gecko/Firefox 128 dotyczących dynamicznego rezerwowego nieprzezroczystego origin.
Dynamiczna rejestracja
packages/webext/src/contentRegistration.ts rejestruje jeden dynamiczny skrypt
treści na każdą regułę dodaną przez użytkownika. Wbudowane są obsługiwane przez
statyczne wpisy content_scripts i NIGDY nie są rejestrowane dynamicznie.
desiredContentScripts filtruje config.sites do niewbudowanych, włączonych reguł,
których origin isOriginGranted (delegując do originCovers), mapuje każdą na
RegisteredContentScript ze stabilnym id (contentScriptId, skrót FNV-1a z match,
więc id używa tylko bezpiecznego dla API zestawu znaków) i ustawia
matchOriginAsFallback: true TYLKO wtedy, gdy zachodzi
matchAllowsOriginFallback(rule.match) (ścieżka /*). Bramkowanie flagi w ten
sposób zapobiega temu, by jedna reguła o niezgodnej ścieżce spowodowała odrzucenie
całej wsadowej rejestracji; taka reguła rejestruje się bez pokrycia okien wysuwanych
zgodnie z założeniem.
reconcileContentScriptsOnce odczytuje przyznane originy, przechowywaną
konfigurację oraz aktywne rejestracje, a następnie zbiega się przez diff ŚWIADOMY
WŁAŚCIWOŚCI: sameRegistration rzutuje zarówno pożądany deskryptor, jak i odczyt
zwrotny na żywo na znormalizowaną postać (stosując rzeczywistą wartość domyślną
WebExtension każdego pola — allFrames/matchOriginAsFallback domyślnie false,
persistAcrossSessions domyślnie TRUE, runAt domyślnie document_idle), więc
świeżo zarejestrowany skrypt porównuje się jako RÓWNY deskryptorowi, który go
wytworzył, i nie może powstać pętla ponownej rejestracji. Ponieważ nie ma
updateContentScripts, zmieniony skrypt jest wyrejestrowywany, a potem rejestrowany
ponownie. reconcileContentScripts owija to w obietnicę w toku (inFlight), więc
dwa niemal jednoczesne wyzwalacze (zmiana konfiguracji ORAZ permissions.onAdded,
oba wyzwalane przy dodaniu witryny) są serializowane, a nie ścigają się.
registerContentReconciliation okabla wyzwalacze
(permissions.onAdded/onRemoved, onConfigChanged) i zbiega się raz przy
starcie; jest importowany tylko przez punkty wejścia tła Chrome i Firefox.
Przepływ zgody na uprawnienia (opt-in)
Statyczne manifesty deklarują optional_host_permissions (http://*/*,
https://*/*), aby użytkownik mógł w czasie działania przyznać dowolny dodatkowy
host sieciowy. packages/webext/src/options/options.ts addSite waliduje CAŁĄ
regułę kandydata (match ORAZ korzeń treści) przez normaliseSiteRule ZANIM poprosi
o jakiekolwiek uprawnienie, wyprowadza origin hosta za pomocą matchOrigin i
wywołuje browser.permissions.request z wnętrza gestu kliknięcia przycisku Dodaj;
tylko po przyznaniu dołącza wiersz builtin:false i utrwala przez kanoniczną
ścieżkę zapisu.
Konfiguracja, pamięć i kanoniczny wzorzec
Jedyny lejek walidacji
normaliseConfig w packages/core/src/config.ts to jedyna kanoniczna ścieżka
odczytu: koercjuje i ogranicza każde pole, wracając do wartości domyślnej przy
czymkolwiek nieprawidłowym, i nigdy nie rzuca wyjątku. Jest zbudowany z walidatorów
per pole — normaliseBoolean, normaliseRenderMode, normaliseMaxLength,
normaliseIgnoreHosts (redukujący każdy wpis do gołej nazwy hosta w punycode przez
bareHostname), normaliseMatchColour/normaliseMismatchColour (bramkowane przez
safeColour.ts isSafeColour), normaliseCssSize, normaliseFontWeight,
normaliseContentRoot (ograniczony przez
CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH), normaliseMatchPattern
(bramkowany przez MATCH_PATTERN) oraz normaliseSites. normaliseSites odrzuca
odrzuty, usuwa duplikaty według match, WYMUSZA builtin: true na każdej regule,
której match równa się wbudowanej (zamykając wektor
dokładne-dopasowanie-przesłania-wbudowane / podwójne wstrzyknięcie), ponownie
zasiewa każdy brakujący wbudowany (więc zmanipulowana pamięć nie może odrzucić
podstawowego dostawcy) i sortuje alfanumerycznie według match dla kanonicznej
kolejności. configFingerprint haszuje tylko pola wpływające na renderowanie (z
wyłączeniem enabled, ignoreHosts oraz sites, które sterują startem/stopem i
pomijaniem, a nie przepływem) przez fnv1a.
Pamięć
packages/webext/src/storage.ts owija API pamięci WebExtension. configArea używa
browser.storage.sync, gdy jest dostępny (więc ustawienia wędrują), i wraca do
browser.storage.local (np. w Thunderbirdzie); configAreaName zgłasza, który jest
aktywny. getConfig, setConfig oraz onConfigChanged przepuszczają swoją surową
wartość przez normaliseConfig, więc zniekształcona lub zmanipulowana pamięć nigdy
nie może wręczyć wywołującemu nieprawidłowego Config. onConfigChanged dodatkowo
ignoruje zdarzenia z NIEAKTYWNEGO obszaru pamięci oraz zmiany niepowiązanych kluczy.
Oba dodatki ponownie wykorzystują ten sam lejek odczyt -> normaliseConfig ->
zwrot nad własnymi pamięciami hosta. extensions/outlook/src/roamingStorage.ts
panelu zadań Outlooka owija Office.context.roamingSettings (synchroniczne
get/set, zatwierdzane przez saveAsync); packages/gmail/src/propertiesStorage.ts
dodatku do Gmaila owija PropertiesService.getUserProperties() (per użytkownik,
wędrujące między urządzeniami tego użytkownika, zakodowane jako JSON pod jednym
kluczem i niewymagające ŻADNEGO dodatkowego zakresu OAuth — nigdy współdzielonego
ScriptProperties). Oba udostępniają getConfig/setConfig oraz istotne dla karty
akcesory getIgnoreHosts/getHighlightMismatch i chronią niedostępną powierzchnię
hosta dedykowanym błędem. Wyzwalacz strony głównej Gmaila renderuje kartę ustawień
CardService z getConfig, a procedura obsługi przesłania formularza
onSaveSettings scala sparsowane ignoreHosts/highlightMismatch z bieżącą
konfiguracją i utrwala ją przez setConfig; wyzwalacz kontekstowy przekazuje
ignoreHosts do adaptera, a highlightMismatch do budowniczego karty, czytając
defensywnie, więc awaria pamięci wraca do wartości domyślnych zamiast psuć analizę
wiadomości.
Wzorzec kanoniczna-konfiguracja + ukierunkowana aktualizacja
Trzy ścieżki zapisu odczytują kanoniczną konfigurację, zmieniają dokładnie jedną rzecz i zapisują ją z powrotem — nigdy nie zatwierdzając niezapisanych edycji formularza ani nie renderując formularza ponownie:
removeSite(options.ts): odczytujegetConfig, utrwalasetConfignad tablicąsitesz odfiltrowaną tylko usuwaną regułą (NAJPIERW utrwalenie, potemrow.remove(), potem warunkowepermissions.removerozstrzygane na podstawie kolejnej konfiguracji). Awaria utrwalenia rzucaSiteRemovalSaveErrori pozostawia DOM nietknięty; nieudane odwołanie rzucaSitePermissionRevokeErrorpo tym, jak wiersz już zniknął.addSite(options.ts): opisane powyżej — waliduj, poproś o uprawnienie, dołącz wiersz, potemsaveOptions.toggleEnabled(options.ts): natychmiastowy główny przełącznik Włącz przełącza WYŁĄCZNIEenabledw przechowywanej konfiguracji i zapisuje ją z powrotem, bez ponownego renderowania (więc trwająca edycja nigdy nie zostaje nadpisana); przy nieudanym zapisie przywraca pole wyboru do przechowywanej wartości.
Własny toggleEnabled akcji paska narzędzi
(packages/webext/src/toolbarAction.ts) podąża za tym samym wzorcem od strony tła.
Build per silnik i kontrakt manifestu
tooling/build.mjs napędza jeden build esbuild dla każdego celu WebExtension.
Deskryptor TARGETS nazywa CEL ŹRÓDŁOWY każdego celu: Chrome, Firefox i Thunderbird
dostarczają własny src/; Edge, Opera i Safari deklarują chrome jako swoje
źródło; dodatek do Outlooka (Office.js) jest własnym źródłem. ACTIVE_TARGETS
(Chrome, Edge, Firefox, Opera i Thunderbird — Safari i Outlook są wyłączone) to to,
co budują --all/--package; Safari i Outlook budują się tylko, gdy zostaną
nazwane jawnie, a dodatek do Gmaila (Apps Script) to oddzielny pakiet esbuild
(make build-gmail) poza pętlą --all. Ten pakiet Gmaila celuje w środowisko
uruchomieniowe V8 Apps Script, które nie ma ani modułów ES, ani natywnego URL:
jest budowany jako ESM, a następnie pozbawiany końcowej instrukcji export {…}
(więc funkcje wyzwalaczy pozostają globalami najwyższego poziomu, które Apps Script
może wywołać) i pakuje mały polyfill URL (instalowany tylko, gdy URL jest
nieobecny), więc new URL(...) ze wspólnego rdzenia rozwiązuje odnośniki po stronie
serwera. buildTarget czyści dist/<target>/, kopiuje per cel manifest.json oraz
icons/ dosłownie, pakuje każdy skrypt źródłowy jako IIFE (wstrzykiwane
INJECTED_SCRIPTS content.ts/messageDisplay.ts nie mogą być modułami ES, a tła
to również klasyczne workery/strony zdarzeń) oraz wspólną stronę opcji jako ESM, a
także kopiuje options.html/options.css z packages/webext/src/options/.
webextension-polyfill jest wbudowany w każdy skrypt.
tooling/version.mjs stempluje wersje: bierze MAJOR.MINOR z głównego package.json
i dołącza automatycznie inkrementowany numer BUILD (odczytany z najwyższego
istniejącego trzeciego komponentu we wszystkich manifestach celów) do każdego
manifestu celu, utrzymując je w jednym rytmie. Większość celów niesie pojedynczy
manifest.json; dodatek do Outlooka niesie dwa wersjonowane manifesty
(manifest.json oraz manifest.xml) i oba są stemplowane, a szkielet _template
również jest stemplowany w jednym rytmie. Główny package.json jest utrzymywany w
tej samej wersji, ale pozostaje prawidłowym semver (czteroczęściowa wersja jest
dozwolona w manifeście, nigdy w package.json). tooling/icons.mjs (uruchamiany
przez make icons) rasteryzuje pojedyncze źródło wektorowe assets/icon.svg do
icons/icon48.png oraz icons/icon128.png każdego celu, próbując cairosvg, potem
inkscape, a potem ImageMagick.
Kontrakt manifestu — dokładne wymagane pola per silnik — to jedyne źródło prawdy w
każdym extensions/<engine>/test/manifest.smoke.test.mjs (np. assertManifestShape
w extensions/chrome/test/manifest.smoke.test.mjs, który też buduje cel i
potwierdza, że każdy odwoływany plik rozwiązuje się w diście). Zgodnie z konwencją
deduplikacji projektu kształt pole-po-polu NIE jest tu ponownie wyliczany; odwołaj
się do tych testów dymnych oraz do uwagi „Kontrakt manifestu” w
planie testów manualnych.
Internacjonalizacja (i18n)
Reveal URLs jest zlokalizowany na wiele języków poza angielskim. Zestaw ma jedno źródło w packages/core/src/locales.json
(SUPPORTED_LOCALES, nazwy natywne oraz domyślny), które odczytują zarówno pakiety
rozszerzenia (przez rozwiązany import JSON), jak i czysto-Nodowy build witryny.
Każdy nieanglojęzyczny ciąg jest przetłumaczony maszynowo i oczekuje na weryfikację
przez człowieka; znacznik pochodzenia jest per format (_locales używają
description każdego komunikatu, słowniki interfejsu witryny zapisują go w
site/i18n/README.md, a przetłumaczone źródła dokumentów niosą komentarz HTML w
pierwszej linii).
Istnieją trzy niezależne powierzchnie lokalizacji, celowo niepołączone ze sobą, a każda jest podzielona wzdłuż linii czas budowania / środowisko uruchomieniowe.
- Strona opcji (AD-1). Dostarcza STATYCZNE angielskie znaczniki; każdy
przetłumaczalny węzeł niesie adnotację
data-i18n="<key>"(tekst) lubdata-i18n-<attr>="<key>"(atrybut). W czasie działaniapackages/webext/src/options/locale.tsjest warstwą stosującą: pobiera (fetch) własny spakowany_locales/<code>/messages.jsonstrony (przezruntime.getURL, bez potrzebyweb_accessible_resources), przepisuje każdy adnotowany węzeł WYŁĄCZNIE przeztextContent(więc przykładowe podpowiedzi<code>w formularzu pozostają nienaruszone) i ustawia<html lang>. Strona jest W PEŁNI zlokalizowana: obok statycznej prozy budowane przez JS etykiety wierszy witryn i przycisk Usuń niosą kluczdata-i18n(z początkowym tekstem przeztranslate), więc ta sama warstwa stosująca ponownie lokalizuje wyrenderowane wiersze bez ponownego renderowania, a NAPRAWDĘ dynamiczne ciągi — wiersz statusu, informacja zwrotna dodawania witryny i wiersz wersji — renderują się przeztranslate(<key>)zlocale.ts(currentMessages → angielski fallback → klucz) w czasie wywołania. Angielskim fallbackiem jest katalog_locales/en/messages.jsonZAIMPORTOWANY (wbudowany) dooptions.jsw czasie budowania, więctranslaterozwiązuje się do czytelnego angielskiego SYNCHRONICZNIE od załadowania modułu — dynamiczny ciąg wyrenderowany przed ustabilizowaniem się przełącznika lub po nieudanym pobraniu katalogu w czasie działania nigdy nie degraduje się do surowego klucza komunikatu.locale.tspublikuje zastosowaną mapę komunikatów i wyzwala swoje nasłuchiwaczeonLocaleChangepo każdym zastosowaniu, więcoptions.tsponownie renderuje aktualnie pokazywany status/informację zwrotną/wersję i ponownie kieruje link Podręcznik online przy każdym przełączeniu. Wiersz wersji używa komunikatuoptionsVersion, którego symbol zastępczy{version}jest zastępowany numerem builda (token przetłumaczony, numer dosłownie). Link do podręcznika podąża za aktywnym językiem do swojego wariantu dokumentu (AD-9): angielski zachowujemanual.htmlnajwyższego poziomu, każdy inny język otwieranl/handleiding.html, zachowując stempel wersji?v=. Aktywny język to zapisany wybór użytkownika lub, w razie jego braku, język przeglądarki rozwiązany do obsługiwanego języka bazowego przezresolveLocalez rdzenia (AD-5); nieobsługiwany lub nieudany język pozostawia wstępnie wyrenderowany angielski (atranslatedegraduje się wtedy do angielskiego fallbacku). Przełącznik Język wyświetlania w podręczniku utrwala swój wybór pod dedykowanym kluczemuiLocalewstorage.local— nigdy w synchronizowanejConfig, więc jest per urządzenie i nigdy nie wędruje. - Interfejs witryny (AD-2). Generator statycznej witryny
tooling/site.mjsrenderuje na serwerze każdy węzeł interfejsu po angielsku, adnotując godata-i18n, i zachowuje<html lang="en-GB">(AD-4). Ładowarka po stronie przeglądarkisite/scripts/i18n.mjs(wolny od zależności moduł ES) podmienia następnie interfejs na język odwiedzającego według pierwszeństwa — utrwalony wybórlocalStorage(revealUrlsSiteLocale) → pierwsze obsługiwane dopasowanie bazowenavigator.languages(AD-5) → angielski — pobierającsite/i18n/<code>.json. W przeciwieństwie do warstwy stosującej opcji podmienia przezinnerHTML, ponieważ wartości interfejsu witryny niosą zaufane znaczniki w treści (<code>,<a>); test parzystości przypina sekwencję znaczników/atrybutów/URL każdej nieanglojęzycznej wartości jako bajt-identyczną z angielskim, więc tłumaczenie nigdy nie może upuścić linku ani zepsuć znacznika. Brakujący klucz zachowuje wstępnie wyrenderowaną angielską migawkę; nieudane pobranie pozostawia angielską stronę nienaruszoną (łagodna degradacja, nigdy pustka). - Strony dokumentacji (AD-8/AD-9). Strony dokumentów są renderowane PER JĘZYK w
czasie budowania, a nie podmieniane w czasie działania. Angielskie dokumenty
pozostają na ścieżkach najwyższego poziomu; każdy inny język otrzymuje kopię w
folderze
<code>/, z przetłumaczonegodocs/<code>/<DOC>.md, gdy jest obecny, w przeciwnym razie z angielskiego źródła — fallback per strona, którybuildSiteLOGUJE, zamiast po cichu pomijać. TREŚĆ dokumentu nie niesie żadnegodata-i18n(to surowo wyrenderowany markdown), więc ładowarka interfejsu w czasie działania nigdy jej nie dotyka; podmieniają się tylko wspólne węzły interfejsu. Przełącznik języka strony dokumentu NAWIGUJE do strony siostrzanego języka, zamiast podmieniać w miejscu. - Lokalizacja linków wewnętrznych (Finding 2). Wybrany język przenosi się przez
nawigację po dokumentach. Każda WEWNĘTRZNA kotwica dokumentu (pięć stron
architecture.html,index.html,licence.html,manual.html,privacy.html) jest oznaczanadata-doc-link="<page>"przez generator, a w przypadku braku JavaScript jej href już rozwiązuje się do wariantu językowego BIEŻĄCEJ strony (navDocHref— trzy strony dokumentów per język lokalizują się; tylko-angielskieindex.html/licence.htmlpozostają na najwyższym poziomie).rewriteDocLinksładowarki w czasie działania ponownie kieruje następnie każdą wewnętrzną kotwicę dokumentu — te oznaczone ORAZ każdą wstrzykniętą z katalogu zwykłą kotwicę, taką jakmanual.html#installing— do wariantu AKTYWNEGO języka, zachowując każdy#hash/?queryi nigdy nie dotykając linków zewnętrznych. Na stronie dokumentu PRZECHOWYWANY wybór różniący się od własnego języka strony NAWIGUJE do pasującego wariantu; strażnik pętli uruchamia się tylko przy jawnym przechowywanym wyborze (nigdy przy samej preferencji języka przeglądarki) i tylko, gdy cel różni się od już pokazanej strony. Same katalogi zachowują swoje angielskie wewnętrzne adresy URL, więc test parzystości pozostaje zielony — lokalizacja to czysty przebieg w czasie działania po DOM.
Wygaszanie „Aktywnych witryn” w kliencie poczty (AD-3)
Cel będący klientem poczty (dziś Thunderbird) i tak widzi każdą wyrenderowaną
wiadomość, więc edytor „Aktywnych witryn” per host jest tam zbędny i zostaje
usunięty. Decyzję niesie udokumentowana flaga deskryptora per cel, mailClient: true, na TARGETS.thunderbird w tooling/build.mjs — nigdy zahardkodowana nazwa
celu w logice budowania lub interfejsu — i jest implementowana w dwóch połowach:
- Czas budowania.
copyAssetsopiera się nadescriptorFor(target).mailClient: dla celu będącego klientem poczty uruchamia czystą transformacjęremoveSitesSection(html)(która usuwa cały edytor hostów<section class="sites">, w tym każdą kontrolkę dodawania witryny, i rzucaMissingSourceError, jeśli sekcja jest nieobecna, więc zmiana znaczników nigdy nie może po cichu dostarczyć edytora) i zapisuje wynik; cele przeglądarkowe otrzymująoptions.htmlbajt-w-bajt przezcpSync. - Czas działania. Wbudowany kontroler
packages/webext/src/options/options.tstoleruje nieobecną sekcję:renderSites/readSites/applyConfig/readFormsondują#sites-listi wykonują no-op zamiast rzucaćOptionsFieldMissingError, areadFormPOMIJA kluczsites, gdy sekcji nie ma. Ponieważ pominięciesitespozwoliłobynormaliseConfigponownie zasiać wbudowane wartości domyślne i odrzucić witryny użytkownika,saveOptionsprzywraca przechowywanesitesprzed normalizacją zawsze, gdy sekcja jest nieobecna — to właśnie sprawia, że zapis w kliencie poczty zachowuje skonfigurowane hosty. Strażnik czasu działania,guardMailClientSites, usuwa sekcję, jeśli jest nadal obecna, wykrywając klienta poczty po obecności API/funkcji (messages/messageDisplay/scripting.messageDisplayna wstrzykniętymbrowser), nigdy po nazwie celu; uruchamia się pierwszy winitOptions, więc późniejsze okablowanie dodawania/listy naturalnie pomija nieobecne elementy. W środowisku przeglądarki strażnik to no-op.
Postawa bezpieczeństwa i prywatności
- Brak sieci ani eksfiltracji. Żaden moduł nie wykonuje
fetch/XHRani żadnego innego wywołania sieciowego; rozszerzenie odczytuje tylko przechowywaną konfigurację oraz DOM strony. - Tylko bezpieczny DOM. Adnotacja i wstrzykiwany arkusz stylów używają wyłącznie
createElement/textContent/typowanych właściwościstyle.*, nigdyinnerHTML/insertAdjacentHTML/eval. Dla rdzenia jest to egzekwowane przezpackages/core/test/purity.test.ts. - Każde wejście zewnętrzne/konfiguracyjne znormalizowane. Każdy odczyt i zapis
przechodzi przez
normaliseConfig; kolory przechodząisSafeColour; wzorce dopasowania przechodząMATCH_PATTERN; selektory korzenia treści są ograniczone przezCONTENT_ROOT_PATTERNi zawsze przekazywane tylko doquerySelectorAll/closest, nigdy interpolowane do znaczników ani CSS. - Najmniejsze uprawnienia. Wbudowane originy webmaila to stałe
host_permissions; dodatkowe hosty są dobrowolne (opt-in) przezoptional_host_permissionsoraz powiązane z gestempermissions.request. Akcja paska narzędzi nie deklaruje wyskakującego okienka. - Zbieranie danych Gecko. Manifesty Firefox i Thunderbird deklarują
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Licencja. Projekt jest AGPL-3.0-only (
LICENSE,package.json).
Testowanie
- Testy jednostkowe Vitest obejmują oba pakiety:
packages/core/test/*(konfiguracja, kontrast, niezgodność hosta, procesor odnośników, style oraz strażnik czystości) ipackages/webext/test/*(treść, pamięć, opcje, rejestracja, pasek narzędzi, wyświetlanie wiadomości oraz test end-to-end witryny własnej). Każdy pakiet uruchamiavitest run. - Scenariusze BDD Cucumber znajdują się w
features/(okablowane przezcucumber.json) i napędzają prawdziwe moduły@reveal-urls/webextprzez wierne dublery testowe wfeatures/support/webext.ts— w szczególnościnormaliseReadBack, który odzwierciedla POMIJANIE przez silnik flag o wartości false przy odczycie zwrotnym, więc pobłażliwa uprząż nie może ukryć pętli ponownej rejestracji — nad dokumentem jsdom skonfigurowanym wfeatures/support/world.ts. - Testy dymne manifestu per cel (
extensions/<engine>/test/manifest.smoke.test.mjs, wbudowanynode:testNode'a) zapewniają dokładny kształt każdego manifestu oraz to, że zbudowany dist rozwiązuje każdy odwoływany plik.tooling/test/dodaje testy budowania, wersji oraz definicji ukończenia. - CI (
.github/workflows/ci.yml) bramkuje każdy push i pull request namake test,make bddorazmake lint(uruchamiane bezpośrednio na runnerze, nadpisując DockeroweRUN/IMAGE_DEPz Makefile). Stemplowanie wersji i pakowanie są celowo wyłączone; znajdują się w oddzielnym, wyzwalanym tagiem.github/workflows/release.yml.