Safari is a supported active target with its own entry points. Built-in providers use static scripts and Safari requests no all-sites access. User-added sites receive a runtime optional permission, then the background injects content.js after navigation only for granted, enabled, non-builtin rules, with allFrames per rule. Safari settings are device-local and do not roam between devices. macOS packaging uses Xcode and the Apple Developer Program/App Store route.
Reveal URLs — Архитектура
Този документ описва как е структурирано разширението Reveal URLs и как
работи неговата основна логика, за сътрудници и технически читатели. Той
постоянно се позовава на реални файлове и символи, във формата path
symbol, така че всяко твърдение да може да се провери в изходния код. За
описанието от гледна точка на потребителя вижте manual. За
стъпките за ръчна проверка вижте
manual test plan.
Преглед
Reveal URLs е една MV3 WebExtension, изградена за Chrome, Edge, Opera,
Firefox, Safari и Thunderbird от една обща чиста кодова основа. Safari
използва статични скриптове само за вградените доставчици и е публикуван в
App Store. Разширението показва целевия 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-а и
иначе се пропуска.
Този път на native находки умишлено остава ограничен до http:/https:.
Кодовата база разделя отговорностите ясно:
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/destination.ts— схема-зависимото класифициране и сравнение на несъответствията на целта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-а,
обработва повторно target само когато е Element вътре в root чрез
isWithinRoot и closest, а късното откриване на root-ове се поема от
ограничения обход discoverMatchedRoots() за всеки batch с едно
querySelectorAll(contentRoot), дедупликирано чрез WeakSet.
pendingShadowHosts проверява отново по-ранни plain host-ове, докато се
появи open shadow root, така че да се покрият и shadow upgrade-ите.
applyConfig прави teardown, restart или reflow според това дали има
активен rule и дали се е сменил contentRoot.
Маркиране на единична връзка
Annotator-ът живее в packages/core/src/linkProcessor.ts. createAnnotator
връща Annotator със Map, token data-ru от generateToken и текущия
config. Неговият process(root):
- Премахва записите в регистъра, чийто анкер вече не е свързан.
- Събира кандидат-анкери с
collectAnchors(потомциa[href]плюс самия корен, когато еa[href], без дубликати). - Разрешава целта на всеки анкер с
resolveAnnotatableUrl, който делегира наresolveAnnotatableHref. Връщаnull(пропускане) само за празен/вътрешен за страницата или неразбираем href, разрешава относителните href-ове спрямоbaseURIи приема всяка разбираема схема, включителноmailto:,tel:,sms:и непрозрачните URL-иjavascript:,data:иblob:.ignoreHostsважи само заhttp:/https:връзки с host. - Поема идемпотентния бърз път, когато
isAnnotationIntactпотвърди, че render fingerprint-ът не е променен и всеки създаден възел още е свързан и на мястото си; иначе отменя остарялата annotation и render-ира отново. - Изчислява несъответствието според схемата с
destinationMismatchвърху чистия текст на анкера:classifySchemeнасочва web връзките къмhostMismatch, mail къмmailMismatch, телефонните къмphoneMismatch, а всички останали къмopaqueMismatch. След това извикваannotateAnchor;showOnlyOnMismatchпотиска само видимото inline разкриване на връзка без несъответствие, като запазва нейния целевиtitle.
annotateAnchor render-ира само SAFE-DOM. Винаги запазва първоначалния
title (TitleSave) и записва title на целта — единственият запис в анкера.
Няма отделен режим за title и няма badge за предупреждение. Освен ако
showOnlyOnMismatch не скрие връзка без несъответствие, добавя отпред
<span class="reveal-urls-url"> и следващ <br>, като оставя собствените деца
на връзката непокътнати. needsLeadingInlineRevealBreak добавя начален <br>,
когато видимо inline съдържание предшества връзката на същия ред. Span-ът показва
написан абсолютен href дословно или относителен href, разрешен спрямо baseURI,
чрез инертен textContent (с префикс стрелка и неразделящ интервал U+00A0,
после съкратен чрез truncateUrl); след свързване получава контрастен фон чрез
applyContrastBackdrop. При подчертано несъответствие
REVEAL_URLS_MISMATCH_CLASS се добавя към REVEAL_URLS_URL_CLASS, което прилага
конфигурирания цвят за несъответствие чрез applyColour и удебеляване по
подразбиране в REVEAL_URLS_CSS; изричното заместване на дебелината на шрифта
чрез applyFontOverrides има предимство. Всеки създаден възел получава
data-ru="<token>" (никога анкерът), а всеки запис пази своя
configFingerprint, така че промяна в render config да принуди reflow.
revertOwned премахва създадените възли по референция и възстановява запазения
title; revertAll прави това за всеки притежаван анкер и изчиства регистъра.
truncateUrl работи върху Unicode CODE POINTS ([...displayHref]), така че
отрязването никога не разделя сурогатна двойка; запазва произхода на схемата и
host-а и скъсява останалото с краен URL_ELLIPSIS.
Несъответствие на host-а
packages/core/src/hostMismatch.ts hostMismatch е web клонът на
destinationMismatch: той сравнява видимия текст на връзката с нейния href
по registrable домейн, а не по суров hostname, така че subdomain без несъответствие не се
маркира, докато look-alike се маркира. extractHostCandidates разделя текста
и свежда всеки token чрез hostCandidate (който изисква точка и разпознат от
tldts ICANN public suffix, като отхвърля обикновен точкуван текст като
e.g). И host-ът на href, и всеки кандидат се свеждат до registrable домейн с
tldts.getDomain, а ВСЕКИ кандидат, чийто домейн се различава от този на href,
дава несъответствие — така назоваването на истинския злонамерен host до host на
примамката не може да потисне предупреждението. Никога не хвърля exception.
За mail връзки mailMismatch сравнява всички registrable домейни на получателите,
включително to, cc и bcc; phoneMismatch сравнява разрешимите набрани
номера. opaqueMismatch винаги маркира SCRIPT_LIKE_SCHEMES — javascript:,
data: и vbscript: — докато други непрозрачни схеми се маркират само когато
видимият текст назовава конкретна web, email или телефонна цел.
Стилове и контраст
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
покриват FastMail, Gmail, Proton с allFrames, Tuta, Yahoo, Zoho и двата 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,
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 връзки за шестте
страници
architecture.html,faq.html,index.html,licence.html,manual.html,privacy.htmlносят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.