Reveal URLs — Arquitectura

Este documento describe como está estruturado Reveal URLs e como funciona a súa lóxica principal, pensado para colaboradores e lectores técnicos. Ao longo do texto cítanse ficheiros e símbolos reais na forma path symbol, para que cada afirmación se poida comprobar no código fonte. A descrición orientada á persoa usuaria está en manual, e os pasos de comprobación manual atópanse en manual test plan.

Visión xeral

Reveal URLs é unha única WebExtension MV3 construída a partir dun núcleo común e limpo para Chrome, Edge, Opera, Firefox e Thunderbird. Safari queda para unha fase posterior: ten manifest e pódese construír cando se indica explicitamente, pero non entra no build predeterminado nin no CI. A extensión mostra o URL de destino de cada ligazón ao carón da propia ligazón no rendered email e marca unha ligazón cando o texto visible apunta a un dominio registrable distinto do que hai en href.

O repositorio é un monorepo de pnpm e TypeScript descrito nos ficheiros pnpm-workspace.yaml e package.json, e "private": true evita que se publique como paquete. O bundling xestiónao tooling/build.mjs, que compila os paquetes compartidos e os entry point delgados de cada engine nun directorio cargable dist/<target>/. webextension-polyfill bundlease dentro de cada script en vez de depender dun global de runtime.

Dous complementos native de correo amplían o mesmo núcleo de detección a superficies ás que WebExtension non pode chegar: Outlook Add-in sobre Office.js e Gmail Add-on sobre Apps Script CardService. Ambos reutilizan só a análise PURE de packages/core/src/findings.ts, xunto con analyseAnchors, analyseHtml, o modelo neutral Finding[], a lóxica hostMismatch e a obtención do dominio registrable mediante tldts. O que cambia é só o adaptador de host e a presentación. Outlook usa DOMParser e funciona client-side. Gmail usa node-html-parser e funciona server-side sobre a infraestrutura Google Apps Script V8. Como ningún destes marcos permite modificar o DOM da mensaxe renderizada, ambos mostran un panel ou unha tarxeta con achados, e non inline annotation. A ruta de mutación do DOM de linkProcessor.ts e REVEAL_URLS_CSS non se usan aí. Un href relativo só se resolve contra un <base href> fiable dentro da mensaxe; se non, descártase.

A base de código separa con claridade as responsabilidades:

Estrutura do repositorio

packages/core — o núcleo puro

Ningún módulo de aquí importa extension API como browser.*, chrome.* ou messenger.*, e ningún usa string-to-markup DOM sink como innerHTML ou insertAdjacentHTML. Ese contrato aplícase mecanicamente en packages/core/test/purity.test.ts, que percorre src/**/*.ts, elimina os corpos dos comentarios mediante stripComments e comproba que nin EXTENSION_API_PATTERN nin UNSAFE_DOM_PATTERN coincidan con ningún módulo. A superficie pública reexpórtase desde packages/core/src/index.ts, e a única dependencia de runtime é tldts.

Os módulos principais son:

packages/webext — os envoltorios de browser API

Estes módulos PODEN usar browser.*, e é o único lugar onde se tocan as WebExtension API. A superficie pública reexpórtase desde packages/webext/src/index.ts. Aquí están content.ts, contentRegistration.ts, storage.ts, background.ts, toolbarAction.ts, messageDisplay.ts, messageDisplayBackground.ts e options/options.ts.

extensions/<engine> — entry point delgados e manifests

Cada engine ten o seu propio manifest.json e o seu propio icons/. Chrome, Firefox e Thunderbird tamén teñen un directorio src/ con entry point. Edge, Opera e Safari só teñen manifest e iconas e reutilizan o src/ de Chrome. Por exemplo, extensions/chrome/src/content.ts só contén void createContentController().bootstrap();, e extensions/chrome/src/background.ts chama a registerBackground, registerContentReconciliation e registerToolbarToggle. extensions/thunderbird/src/background.ts chama a registerBackground e registerMessageDisplay. extensions/_template é un scaffold, non un build target.

tooling, features e smoke tests

tooling inclúe build.mjs, version.mjs e icons.mjs. features inclúe os escenarios Gherkin, as step definitions e os doubles fieis dos ficheiros features/support/webext.ts e features/support/world.ts. Cada engine tamén ten extensions/<engine>/test/manifest.smoke.test.mjs, mentres que _template usa template.smoke.test.mjs.

O fluxo de revelación

A ruta de contido comeza nun entry point dun engine como extensions/chrome/src/content.ts, que chama a createContentController().bootstrap() desde packages/webext/src/content.ts.

Bootstrap

createContentController bootstrap execútase unha vez por frame. Reivindica o frame antes de cada await mediante claimBootstrap, carga o config con loadConfigWithRetry, injecta a stylesheet con injectStyles só despois dunha lectura correcta e restaura o marcador con releaseBootstrap se a carga acaba fallando. Cando config.enabled está activo, chama a start, e en calquera caso rexistra o listener onConfigChanged.

Resolver a regra activa do sitio

start e applyConfig usan resolveSiteRule para decidir se hai que facer annotation e onde facelo. Primeiro próbase a localización do propio documento mediante resolveForHref, que filtra config.sites ata as regras activadas e deixa a selección final en selectMostSpecific. Se o documento non coincide e a súa orixe é opaque ou inherited, como comproba hasOpaqueOrigin, faise un fallback con readTopHref, readOpenerHref e readReferrer, con protección fronte a lecturas cross-origin.

Acoutar ao content root e observar os cambios

start lembra contentRoot, crea o annotator con createAnnotator(config) e procesa cada raíz de selectRoots(doc, contentRoot), que envolve querySelectorAll nun try/catch para que un selector incorrecto devolva [] de maneira segura. Despois observa doc.body con OBSERVER_OPTIONS. processMutations filtra cada cambio segundo o root, e processAddedNode abarca os nodos dentro do root, o propio root e os nodos que o conteñen. applyConfig fai teardown, restart ou reflow segundo o cambio da regra activa e de contentRoot.

Annotation dunha ligazón única

O annotator está en packages/core/src/linkProcessor.ts. createAnnotator devolve un Annotator con Map, un token data-ru de generateToken e o config activo. O seu process(root) limpa os rexistros desconectados, recolle os elementos a[href] con collectAnchors, resolve o URL mediante resolveAnnotatableUrl, usa a vía rápida isAnnotationIntact cando o render non cambiou e calcula o mismatch con hostMismatch antes de chamar a annotateAnchor.

annotateAnchor renderiza só SAFE-DOM. En modo "inline" engade <span class="reveal-urls-url"> con textContent e <br>. En modo "title" garda o title existente, substitúeo polo URL e, cando o mismatch está enfatizado, engade <span class="reveal-urls-warn"> con REVEAL_URLS_WARN_LABEL. Os nodos creados reciben data-ru="<token>", e configFingerprint serve para forzar o reflow. truncateUrl traballa sobre [...displayHref] e usa URL_ELLIPSIS.

Discrepancia de host

packages/core/src/hostMismatch.ts hostMismatch compara o texto visible da ligazón co seu href a nivel de dominio registrable, e non do host en bruto. extractHostCandidates divide o texto en tokens e redúceos con hostCandidate, que require un punto e un public suffix recoñecido por tldts. O host do href e os candidatos redúcense mediante tldts.getDomain. Calquera candidato cun dominio distinto produce un mismatch.

Estilos e contraste

packages/core/src/styles.ts garda REVEAL_URLS_CSS e as funcións de axuda applyColour, applyFontOverrides e applyContrastBackdrop. applyContrastBackdrop le a cor calculada do elemento e o primeiro fondo opaque que atopa a través de getComputedStyle, e logo usa needsWhiteBackdrop de packages/core/src/contrast.ts para decidir se cómpre establecer style.backgroundColor = "white". contrast.ts fornece parseColour, relativeLuminance e contrastRatio.

Host configurables e acoutación do contido

SiteRule en packages/core/src/config.ts define ON se executa a annotation mediante match e allFrames, e QUE container a limita mediante contentRoot, xunto con enabled e a marca builtin. DEFAULT_SITES inclúe Gmail, Proton con allFrames e os dous host de Outlook. Config agrupa os toggle globais, os colours, os font overrides e a lista sites.

O motor de match pattern

packages/core/src/matchPattern.ts contén a gramática restritiva MATCH_PATTERN: só http e https, host wildcard opcional *., glob path, sen porto, sen * scheme e sen <all_urls>. matchesPattern compara un URL cun pattern validado e delega a parte do path en pathGlobMatches. parsePatternParts divide o pattern en host, path, scheme e wildcardHost.

Cando as regras se superpoñen, aplícase a norma "most specific wins". compareSiteSpecificity dá preferencia ao host exacto fronte ao host wildcard, despois ao host literal máis longo, despois ao path máis literal a través de literalPathLength, e finalmente usa un ASCII tiebreak. selectMostSpecific devolve a regra activada máis específica que coincide co URL.

permissions.getAll() pode informar de grant en toda a gramática WebExtension, como <all_urls>, *://*/*, https://*/*, *://*.host/* e https://host/*) que MATCH_PATTERN rexeitaría. parseGrantedOrigin convérteos en GrantedOriginParts, originCovers comproba a cobertura sobre match, e matchAllowsOriginFallback indica se o path é exactamente ORIGIN_FALLBACK_PATH, é dicir /*.

Rexistro dinámico

packages/webext/src/contentRegistration.ts rexistra un dynamic content script por cada regra engadida pola persoa usuaria. As regras built-in sérvense coas entradas static content_scripts e nunca se rexistran de forma dinámica. desiredContentScripts filtra config.sites, usa isOriginGranted, originCovers e contentScriptId, e establece matchOriginAsFallback: true só cando o permite matchAllowsOriginFallback(rule.match).

reconcileContentScriptsOnce le as orixes concedidas, o config gardado e os rexistros activos e harmonízaos cun property-aware diff. sameRegistration normaliza tanto o descriptor desexado como o live read-back. Como non existe updateContentScripts, un script modificado primeiro desrexístrase e despois vólvese rexistrar. reconcileContentScripts usa a promise inFlight para serializar triggers que poden producirse case ao mesmo tempo.

O fluxo de permiso explícito

Os manifests static declaran optional_host_permissions (http://*/*, https://*/*) para que a persoa usuaria poida conceder en runtime permiso para un host adicional. packages/webext/src/options/options.ts addSite valida toda a regra candidata mediante normaliseSiteRule, extrae o host origin con matchOrigin e chama a browser.permissions.request desde o click gesture do botón Add. Só despois da aprobación engade a fila builtin:false.

Configuración, storage e o patrón canónico

O único validational funnel

normaliseConfig en packages/core/src/config.ts é a única ruta de lectura canónica. Forza e limita cada campo, volve ao valor predeterminado cando a entrada é incorrecta e nunca lanza unha exception. Está composto por validators por campos como normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts, normaliseMatchColour, normaliseMismatchColour, normaliseCssSize, normaliseFontWeight, normaliseContentRoot, normaliseMatchPattern e normaliseSites. normaliseSites expulsa as entradas incorrectas, elimina duplicados por match, forza builtin: true cando a regra coincide cunha regra built-in e restaura todas as entradas integradas que falten.

Storage

packages/webext/src/storage.ts envolve a WebExtension storage API. configArea usa browser.storage.sync cando está dispoñible e recorre a browser.storage.local, por exemplo en Thunderbird. getConfig, setConfig e onConfigChanged fan pasar sempre os valores por normaliseConfig, de maneira que un storage corrompido nunca pode devolver un Config inválido. As dúas formas de add-on usan o mesmo funnel sobre o seu host storage propio.

Configuración canónica e actualizacións dirixidas

Tres rutas de write len o config canónico, cambian exactamente unha cousa e escríbeno de novo, sen gardar form edits inacabados:

toggleEnabled en packages/webext/src/toolbarAction.ts segue o mesmo patrón desde o lado do background.

Build por engine e o contrato do manifest

tooling/build.mjs executa un build esbuild común para cada WebExtension target. O descriptor TARGETS indica o SOURCE TARGET de cada target. Chrome, Firefox e Thunderbird teñen o seu propio src/. Edge, Opera e Safari usan chrome como orixe. Outlook add-in ten unha orixe á parte. ACTIVE_TARGETS, é dicir, Chrome, Edge, Firefox, Opera e Thunderbird, son os que se constrúen con --all e --package. Safari e Outlook só se constrúen cando se especifican de maneira explícita, e Gmail usa make build-gmail.

O bundle de Gmail apunta ao runtime Apps Script V8, que non ten nin ES modules nin URL native. Por iso constrúese como ESM e, despois, elimínaselle o export {…} final para que as funcións trigger continúen a ser globais. O pequeno URL polyfill só se inclúe cando fai falta. buildTarget limpa dist/<target>/, copia manifest.json e icons/, bundlea os scripts como IIFE, a páxina de options como ESM e copia options.html e options.css.

tooling/version.mjs estampa as versións. Toma o MAJOR.MINOR do package.json raíz, atopa o BUILD máis alto e engádeo a cada manifest de target. Outlook add-in ten tanto manifest.json como manifest.xml. tooling/icons.mjs, executado con make icons, rasteriza assets/icon.svg cara a icon48.png e icon128.png.

O contrato do manifest defínese nos ficheiros extensions/<engine>/test/manifest.smoke.test.mjs, onde se comproba a forma de cada manifest e a existencia de cada referenced file.

Internacionalización (i18n)

Reveal URLs está localizado en moitos idiomas ademais do inglés. A única fonte deste conxunto é packages/core/src/locales.json, que contén SUPPORTED_LOCALES, os nomes locais e o idioma predeterminado. Cada string non inglesa é machine-translated e queda pendente de revisión humana. A marca de orixe depende do formato: _locales lévana no campo description, site catalogues rexístrana en site/i18n/README.md, e os docs traducidos levan un comentario HTML na primeira liña.

Hai tres superficies de localización independentes:

Supresión de «Active sites» no cliente de correo (AD-3)

Un mail client target como Thunderbird xa ve todos os rendered messages, polo que o editor host-based de «Active sites» resulta sobrante. A decisión transmítese co campo mailClient: true de tooling/build.mjs.

Postura de seguridade e privacidade

Probas