Reveal URLs — Архитектура

Този документ описва как е структурирано разширението Reveal URLs и как работи неговата основна логика, за сътрудници и технически читатели. Той постоянно се позовава на реални файлове и символи, във формата path symbol, така че всяко твърдение да може да се провери в изходния код. За описанието от гледна точка на потребителя вижте manual. За стъпките за ръчна проверка вижте manual test plan.

Преглед

Reveal URLs е една MV3 WebExtension, изградена за Chrome, Edge, Opera, Firefox и Thunderbird от една обща чиста кодова основа. Safari е отложен за по-късен етап: има manifest и може да бъде build-нат при изрично искане, но е изключен от стандартния build и от CI. Разширението показва целевия URL на всяка връзка до самата връзка в rendered email и маркира връзка, когато видимият текст назовава различен registrable домейн от този в нейния href.

Хранилището е monorepo за pnpm и TypeScript, описано в pnpm-workspace.yaml и package.json, като "private": true го държи извън публикуване като пакет. Bundling се управлява от tooling/build.mjs, който компилира общите пакети и тънките entry points за всеки engine в зареждаема директория dist/<target>/. webextension-polyfill се пакетира във всеки script bundle, вместо да се оставя като runtime global.

Две native добавки за email разширяват същата логика за откриване върху повърхности, до които WebExtension не достига: Outlook Add-in върху Office.js и Gmail Add-on върху Apps Script CardService. И двете използват само PURE анализа от packages/core/src/findings.ts, с analyseAnchors, analyseHtml, неутралния модел Finding[], логиката hostMismatch и извличането на registrable домейн чрез tldts. Различават се само adapter-ът на host-а и представянето. Outlook използва DOMParser и работи client-side. Gmail използва node-html-parser и работи server-side на Apps Script V8 инфраструктурата на Google. Понеже нито една от тези рамки не позволява да се променя прочетеният DOM на съобщението, и двете показват панел или карта с находки, а не inline annotation. Пътят за DOM mutation в linkProcessor.ts и REVEAL_URLS_CSS не се използват там. Относителен href се resolve-ва само срещу надежден <base href> вътре в email-а и иначе се пропуска.

Кодовата база разделя отговорностите ясно:

Разположение на хранилището

packages/core — чистото ядро

Нито един модул тук не импортва extension API като browser.*, chrome.* или messenger.*, и нито един не използва string-to-markup DOM sinks като innerHTML или insertAdjacentHTML. Това правило се налага механично чрез packages/core/test/purity.test.ts, който обхожда src/**/*.ts, премахва телата на коментарите с stripComments и проверява, че нито EXTENSION_API_PATTERN, нито UNSAFE_DOM_PATTERN съвпадат с някой модул. Публичната повърхност се преекспортира от packages/core/src/index.ts, а единствената runtime зависимост е tldts.

Главните модули са:

packages/webext — обвивките на browser API

Модулите тук МОГАТ да използват browser.* и това е единственото място, където се докосват WebExtension APIs. Публичната повърхност се преекспортира от packages/webext/src/index.ts. Тук се намират content.ts, contentRegistration.ts, storage.ts, background.ts, toolbarAction.ts, messageDisplay.ts, messageDisplayBackground.ts и options/options.ts.

extensions/<engine> — тънки входни точки и manifest-и

Всеки engine носи собствен manifest.json и собствен icons/. Chrome, Firefox и Thunderbird имат и src/ с entry points. Edge, Opera и Safari носят само manifest и икони и използват повторно src/ на Chrome. Например extensions/chrome/src/content.ts съдържа само void createContentController().bootstrap();, а extensions/chrome/src/background.ts извиква registerBackground, registerContentReconciliation и registerToolbarToggle. extensions/thunderbird/src/background.ts извиква registerBackground и registerMessageDisplay. extensions/_template е scaffold, не build target.

tooling, features и smoke tests

tooling включва build.mjs, version.mjs и icons.mjs. features включва Gherkin сценарии, step definitions и верните doubles в features/support/webext.ts и features/support/world.ts. Всеки engine има също extensions/<engine>/test/manifest.smoke.test.mjs, а _template използва template.smoke.test.mjs.

Потокът на разкриване

Пътят на content-а започва от entry point на engine, като extensions/chrome/src/content.ts, който извиква createContentController().bootstrap() от packages/webext/src/content.ts.

Bootstrap

createContentController bootstrap се изпълнява веднъж за всеки frame. Той заявява frame-а преди всеки await чрез claimBootstrap, зарежда config чрез loadConfigWithRetry, инжектира stylesheet-а с injectStyles едва след успешен прочит и връща marker-а чрез releaseBootstrap, ако зареждането окончателно се провали. Когато config.enabled е включен, извиква start, а независимо от това регистрира listener onConfigChanged.

Определяне на правилото за активен сайт

start и applyConfig използват resolveSiteRule, за да решат дали и къде да бъде направен annotation. Първо се проверява собственият URL на документа чрез resolveForHref, който филтрира config.sites до включените правила и оставя избора на selectMostSpecific. Ако документът не съвпада и произходът му е opaque или inherited, както проверява hasOpaqueOrigin, следва fallback чрез readTopHref, readOpenerHref и readReferrer, защитен от cross-origin достъп.

Ограничаване до корена на съдържанието и наблюдение на промените

start запазва contentRoot, създава annotator с createAnnotator(config) и обработва всяка съвпадаща коренова област от selectRoots(doc, contentRoot), който обвива querySelectorAll с try/catch, така че невалиден selector да провали безопасно към []. После наблюдава doc.body с OBSERVER_OPTIONS. processMutations филтрира всяка промяна според root-а, а processAddedNode покрива възли вътре в root-а, самия root и възли, които го съдържат. applyConfig прави teardown, restart или reflow според това дали има активен rule и дали се е сменил contentRoot.

Маркиране на единична връзка

Annotator-ът живее в packages/core/src/linkProcessor.ts. createAnnotator връща Annotator със Map, token data-ru от generateToken и текущия config. process(root) почиства вече несвързани записи, събира a[href] чрез collectAnchors, resolve-ва URL-и чрез resolveAnnotatableUrl, използва fast path-а isAnnotationIntact, когато render-ът е непокътнат, и изчислява mismatch-а с hostMismatch, преди да извика annotateAnchor.

annotateAnchor render-ира само SAFE-DOM. В режим "inline" добавя <span class="reveal-urls-url"> с textContent и <br>. В режим "title" запазва стария title, заменя го с URL-а и при подчертан mismatch добавя <span class="reveal-urls-warn"> с REVEAL_URLS_WARN_LABEL. Създадените възли получават data-ru="<token>", а configFingerprint се използва за reflow. truncateUrl работи върху [...displayHref] и използва URL_ELLIPSIS.

Несъответствие на host-а

packages/core/src/hostMismatch.ts hostMismatch сравнява видимия текст на връзката с нейния href на ниво registrable домейн, не на суров host. extractHostCandidates разделя текста и свива token-ите чрез hostCandidate, който изисква точка и public suffix, разпознаван от tldts. Host-ът на href и кандидатите се редуцират с tldts.getDomain. Всеки кандидат с различен домейн произвежда mismatch.

Стилове и контраст

packages/core/src/styles.ts държи REVEAL_URLS_CSS и helper-ите applyColour, applyFontOverrides и applyContrastBackdrop. applyContrastBackdrop прочита изчисления цвят на елемента и първия opaque фон по веригата нагоре чрез getComputedStyle, след което използва needsWhiteBackdrop от packages/core/src/contrast.ts, за да реши дали да зададе style.backgroundColor = "white". contrast.ts дава parseColour, relativeLuminance и contrastRatio.

Конфигурируеми host-ове и ограничаване на съдържанието

Един SiteRule в packages/core/src/config.ts определя КЪДЕ се изпълнява annotation чрез match и allFrames и КОЙ container го ограничава чрез contentRoot, заедно с enabled и знака builtin. DEFAULT_SITES покриват Gmail, Proton с allFrames и двата Outlook host-а. Config обединява глобалните toggles, colours, font overrides и списъка sites.

Механизмът на match pattern

packages/core/src/matchPattern.ts държи restrictive граматиката MATCH_PATTERN: само http и https, незадължителен *. wildcard host, glob path, без port, без * scheme и без <all_urls>. matchesPattern съпоставя URL към валидиран pattern и делегира path-а на pathGlobMatches. parsePatternParts разделя pattern-а на host, path, scheme и wildcardHost.

При припокриващи се правила важи принципът "most specific wins". compareSiteSpecificity подрежда exact host пред wildcard host, после по-дългия literal host, после по-literal path чрез literalPathLength, а накрая ASCII tiebreak. selectMostSpecific връща най-специфичното включено правило, което съвпада с URL-а.

permissions.getAll() може да докладва grants в пълната граматика на WebExtension (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*), които MATCH_PATTERN би отхвърлил. parseGrantedOrigin ги превръща в GrantedOriginParts, originCovers проверява покритието върху match, а matchAllowsOriginFallback казва дали path-ът е точно ORIGIN_FALLBACK_PATH, тоест /*.

Динамична регистрация

packages/webext/src/contentRegistration.ts регистрира по един dynamic content script за всяко правило, добавено от потребителя. Built-ins се обслужват от static content_scripts entries и никога не се регистрират динамично. desiredContentScripts филтрира config.sites, използва isOriginGranted, originCovers и contentScriptId, и задава matchOriginAsFallback: true само когато е позволено от matchAllowsOriginFallback(rule.match).

reconcileContentScriptsOnce прочита granted origins, записания config и live registrations и ги съгласува с property-aware diff. sameRegistration нормализира желания descriptor и live read-back-а. Понеже няма updateContentScripts, променен script се премахва и регистрира отново. reconcileContentScripts използва promise inFlight, за да сериализира trigger-и, които могат да се случат близо един до друг.

Потокът на изричното разрешение

Static manifest-ите декларират optional_host_permissions (http://*/*, https://*/*), така че потребителят да може да дава разрешение за допълнителен host по време на runtime. packages/webext/src/options/options.ts addSite валидира целия кандидатен rule чрез normaliseSiteRule, извежда origin-а чрез matchOrigin и извиква browser.permissions.request от click gesture-а на бутона Add. Едва след одобрение добавя ред builtin:false.

Config, storage и каноничният модел

Единственият funnel за валидиране

normaliseConfig в packages/core/src/config.ts е единственият каноничен път за четене. Той преобразува и ограничава всяко поле, връща се към стойността по подразбиране при невалидни данни и никога не хвърля exception. Изграден е от validators като normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts, normaliseMatchColour, normaliseMismatchColour, normaliseCssSize, normaliseFontWeight, normaliseContentRoot, normaliseMatchPattern и normaliseSites. normaliseSites маха невалидните entries, премахва дубликати по match, налага builtin: true при съвпадение с built-in и връща липсващи built-ins.

Съхранение

packages/webext/src/storage.ts обвива WebExtension storage API. configArea използва browser.storage.sync, когато е наличен, и прави fallback към browser.storage.local, например в Thunderbird. getConfig, setConfig и onConfigChanged винаги прекарват стойността през normaliseConfig, така че повреден store никога не може да върне невалиден Config. Двете add-on форми използват същия funnel върху собствените си host storage-и.

Каноничният модел на config и целевите актуализации

Три write path-а четат каноничния config, променят точно едно нещо и го записват обратно, без да записват незавършени form edits:

toggleEnabled в packages/webext/src/toolbarAction.ts следва същия модел от страната на background-а.

Build по engine и договорът на manifest-а

tooling/build.mjs управлява единен esbuild build за всеки target на WebExtension. Descriptor-ът TARGETS определя SOURCE TARGET за всеки target. Chrome, Firefox и Thunderbird имат собствен src/. Edge, Opera и Safari използват chrome като source. Outlook add-in има собствен source. ACTIVE_TARGETS, тоест Chrome, Edge, Firefox, Opera и Thunderbird, са тези, които се build-ват с --all и --package. Safari и Outlook се build-ват само при изрично задаване, а Gmail използва make build-gmail.

Bundle-ът за Gmail цели Apps Script V8 runtime, който няма ES modules и няма native URL. Затова се build-ва като ESM и после му се маха export {…}, така че trigger функциите да останат глобални. Малък polyfill за URL се включва само когато е необходим. buildTarget почиства dist/<target>/, копира manifest.json и icons/, пакетира scripts като IIFE и страницата с опции като ESM и копира options.html и options.css.

tooling/version.mjs stamp-ва версиите. Той взема MAJOR.MINOR от root package.json, намира най-високия BUILD number и го добавя към всеки target manifest. Outlook add-in носи и manifest.json, и manifest.xml. tooling/icons.mjs, стартиран чрез make icons, rasterises assets/icon.svg до icon48.png и icon128.png.

Договорът на manifest-а се определя в extensions/<engine>/test/manifest.smoke.test.mjs, където се проверяват точната форма на всеки manifest и наличието на всеки referenced file.

Интернационализация (i18n)

Reveal URLs е локализиран на много езици освен английски. Единственият източник на този набор е packages/core/src/locales.json, който съдържа SUPPORTED_LOCALES, местните имена и езика по подразбиране. Всеки неанглийски string е machine-translated и очаква човешки преглед. Marker-ът за произход се различава според формата: _locales го носят в description, site catalogues го записват в site/i18n/README.md, а преведените docs имат HTML коментар на първия ред.

Има три независими повърхности за локализация:

Скриване на „Активни сайтове” в пощенския клиент (AD-3)

Target mail client като Thunderbird вече вижда всички rendered messages, затова редакторът „Active sites” по host е излишен. Решението се носи чрез mailClient: true в tooling/build.mjs.

Позиция за сигурност и поверителност

Тестване