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съдържа PURE логиката, без browser APIs, без storage и без markup sinks. Тя е единственият източник на истина за annotation на връзки, валидиране на config, съпоставяне по шаблон и сравнение на host-ове.packages/webextобвива тази основа с WebExtension APIs, жизнения цикъл на content-а, dynamic registration, UI-то на опциите и background wiring-а.extensions/<engine>съдържа тънките entry points за всеки engine и съответнитеmanifest.json.toolingсъдържа скриптовете за build, version-stamping и икони.featuresсъдържа Cucumber BDD сценариите и test doubles.
Разположение на хранилището
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/core/src/config.ts— схемитеConfigиSiteRule, стойностите по подразбиране и funnel-ътnormaliseConfigpackages/core/src/matchPattern.ts— граматиката на шаблоните, matcher-ът и правилата за specificitetpackages/core/src/linkProcessor.ts— annotator-ът на ниво документpackages/core/src/hostMismatch.ts— проверката за несъвпадение на registrable домейнpackages/core/src/styles.tsиpackages/core/src/contrast.ts— injectable stylesheet и логиката за контрастpackages/core/src/safeColour.ts— предикатътisSafeColour
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:
removeSiteвoptions.tsaddSiteвoptions.tstoggleEnabledвoptions.ts
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 коментар на първия ред.
Има три независими повърхности за локализация:
- The options page (AD-1). Статичният markup е на английски, но всеки
преводим възел носи
data-i18n="<key>"илиdata-i18n-<attr>="<key>". В runtimepackages/webext/src/options/locale.tsзарежда_locales/<code>/messages.json, пренаписва текста чрезtextContentи задава<html lang>. - The website chrome (AD-2).
tooling/site.mjsrender-ира chrome-а на сайта на английски сdata-i18nи<html lang="en-GB">. Послеsite/scripts/i18n.mjsго заменя с езика на посетителя отsite/i18n/<code>.json. - The documentation pages (AD-8/AD-9). Документационните страници се
render-ират за всеки locale по време на build и използват локализирани
имена на файлове от
packages/core/src/site-page-names.json, катоnl/handleiding.html. - Internal-link localisation (Finding 2). Вътрешните doc връзки носят
data-doc-link="<page>", а runtime loader ги пренасочва към sibling variant-а на същия locale, като запазва#hashи?query.
Скриване на „Активни сайтове” в пощенския клиент (AD-3)
Target mail client като Thunderbird вече вижда всички rendered messages,
затова редакторът „Active sites” по host е излишен. Решението се носи чрез
mailClient: true в tooling/build.mjs.
- Build-time.
copyAssetsпроверяваdescriptorFor(target).mailClientи когато той е истинен, премахва<section class="sites">чрезremoveSitesSection(html). - Runtime.
packages/webext/src/options/options.tsтърпи липсата на секцията, не хвърляOptionsFieldMissingErrorи възстановява записаната стойност наsitesпреди normalisation.
Позиция за сигурност и поверителност
- Без мрежови заявки и без извличане на данни. Никой модул не прави
fetch,XHRили друга мрежова заявка. Разширението чете само записания config и DOM-а на страницата. - Само SAFE-DOM. Annotation и инжектираният stylesheet използват само
createElement,textContentиstyle.*, а неinnerHTML,insertAdjacentHTMLилиeval. - Всеки външен вход се нормализира. Всяко четене и запис минава през
normaliseConfig. Цветовете минават презisSafeColour, шаблоните презMATCH_PATTERN, а content-root selector-ите се ограничават отCONTENT_ROOT_PATTERN. - Минимални привилегии. Вградените webmail origin-и живеят в
host_permissions. Допълнителните host-ове са opt-in чрезoptional_host_permissionsиpermissions.request. - Събиране на данни в Gecko. Firefox и Thunderbird manifest-ите
декларират
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Лиценз. Проектът е AGPL-3.0-only, както е обявено в
LICENSEиpackage.json.
Тестване
- Vitest unit tests покриват
packages/core/test/*иpackages/webext/test/*. - Cucumber BDD сценариите живеят в
features/и използватfeatures/support/webext.tsиfeatures/support/world.ts. - Per-target manifest smoke tests в
extensions/<engine>/test/manifest.smoke.test.mjsизползватnode:testи проверяват формата на manifest-ите и referenced files. - CI в
.github/workflows/ci.ymlблокира всеки push и pull request, акоmake test,make bddиmake lintне преминат. Version-stamping и packaging живеят в.github/workflows/release.yml.