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:

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

  1. Zajmuje ramkę synchronicznie, przed jakimkolwiek await, przez claimBootstrap, który odczytuje/ustawia flagę BOOTSTRAP_MARKER na window ramki. Statyczny wbudowany wpis content_scripts oraz nakładający się dynamiczny skrypt użytkownika mogą oba wstrzyknąć content.js do tej samej ramki; współdzielony window izolowanego świata czyni to poprawnym strażnikiem jednorazowości, a zajęcie przed jakimkolwiek await oznacza, że dwa niemal jednoczesne wstrzyknięcia nie mogą oba przez niego przejść.
  2. Najpierw wczytuje utrwaloną konfigurację przez loadConfigWithRetry (BOOTSTRAP_CONFIG_RETRIES dodatkowych 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 przypisuje REVEAL_URLS_CSS przez textContent), więc nieudany odczyt nie dołącza żadnego <style>, a ponowienie nie może go zduplikować.
  3. Jeśli wczytanie konfiguracji zawiedzie przy każdej próbie, releaseBootstrap wycofuje marker (tylko tutaj, zanim rozpocznie się adnotacja), aby późniejsze ponowne wstrzyknięcie mogło spróbować ponownie; gdy start już się wykona, marker musi przetrwać, ponieważ świeży anotator nie byłby właścicielem węzłów z poprzedniego przebiegu.
  4. Gdy config.enabled, wywołuje start i zawsze rejestruje nasłuchiwacz onConfigChanged, 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):

  1. Usuwa wpisy rejestru, których kotwica nie jest już połączona.
  2. Zbiera kotwice kandydatów za pomocą collectAnchors (potomne a[href] plus sam korzeń, gdy jest a[href], z usunięciem duplikatów).
  3. Rozwiązuje miejsce docelowe każdej kotwicy za pomocą resolveAnnotatableUrl, który zwraca null (pominięcie) dla pustego/wewnątrzstronicowego href, href nie do sparsowania, dowolnego schematu innego niż http:/https: lub hosta równego wpisowi ignoreHosts albo będącego jego poddomeną; rozwiązuje względne href względem baseURI, więc wynik jest bezwzględny.
  4. Obiera idempotentną szybką ścieżkę, gdy isAnnotationIntact potwierdza, ż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.
  5. Oblicza niezgodność za pomocą hostMismatch na oczyszczonym tekście kotwicy, respektuje showOnlyOnMismatch (pozostawiając uczciwe odnośniki nietknięte) i wywołuje annotateAnchor.

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:

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.

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:

Postawa bezpieczeństwa i prywatności

Testowanie