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 — чистото ядро

Нито един модул тук не импортва 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-а, обработва повторно 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):

  1. Премахва записите в регистъра, чийто анкер вече не е свързан.
  2. Събира кандидат-анкери с collectAnchors (потомци a[href] плюс самия корен, когато е a[href], без дубликати).
  3. Разрешава целта на всеки анкер с resolveAnnotatableUrl, който делегира на resolveAnnotatableHref. Връща null (пропускане) само за празен/вътрешен за страницата или неразбираем href, разрешава относителните href-ове спрямо baseURI и приема всяка разбираема схема, включително mailto:, tel:, sms: и непрозрачните URL-и javascript:, data: и blob:. ignoreHosts важи само за http:/https: връзки с host.
  4. Поема идемпотентния бърз път, когато isAnnotationIntact потвърди, че render fingerprint-ът не е променен и всеки създаден възел още е свързан и на мястото си; иначе отменя остарялата annotation и render-ира отново.
  5. Изчислява несъответствието според схемата с 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_SCHEMESjavascript:, 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:

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.

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

Тестване