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

Ниту еден модул тука не import-ира 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) и ја обработува секоја совпаѓачка root област од 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) ги чисти веќе неповрзаните entries, собира 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, потоа полiteralниот 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 и ги враќа missing 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 и страницата со options како 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, па editor-от „Active sites” по host е вишок. Одлуката се носи преку mailClient: true во tooling/build.mjs.

Став за безбедност и приватност

Тестирање