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:
packages/coregarda a lóxica PURE, sen browser API, sen storage e sen markup sink. É a única fonte de verdade para a lóxica de annotation, a validación do config e a comparación de host.packages/webextenvolve ese núcleo con WebExtension API, content lifecycle, dynamic registration, options UI e background wiring.extensions/<engine>garda os entry point delgados por engine e os seusmanifest.json.toolinggarda os scripts de build, version-stamping e iconas.featuresgarda os escenarios Cucumber BDD e os test doubles.
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/core/src/config.ts— esquemasConfigeSiteRule, valores predeterminados e o validational funnelnormaliseConfigpackages/core/src/matchPattern.ts— gramática do pattern, matcher e regras de especificidadepackages/core/src/linkProcessor.ts— annotator a nivel de documentopackages/core/src/hostMismatch.ts— comprobación da discrepancia do dominio registrablepackages/core/src/styles.tsepackages/core/src/contrast.ts— injectable stylesheet e lóxica de contrastepackages/core/src/safeColour.ts— predicadoisSafeColour
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:
removeSiteenoptions.tsaddSiteenoptions.tstoggleEnabledenoptions.ts
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:
- The options page (AD-1). O markup static envíase en inglés, pero cada
nodo traducible leva
data-i18n="<key>"oudata-i18n-<attr>="<key>". En runtime,packages/webext/src/options/locale.tscarga_locales/<code>/messages.json, reescribe o texto mediantetextContente fixa<html lang>. - The website chrome (AD-2).
tooling/site.mjsrenderiza o site chrome en inglés condata-i18ne<html lang="en-GB">. Despoissite/scripts/i18n.mjscámbiao á lingua da persoa visitante desdesite/i18n/<code>.json. - The documentation pages (AD-8/AD-9). As páxinas de documentación
renderízanse para cada locale durante o build e usan nomes de ficheiro
localizados desde
packages/core/src/site-page-names.json, por exemplonl/handleiding.html. - Internal-link localisation (Finding 2). As ligazóns internas dos docs
levan
data-doc-link="<page>", e o runtime loader reescríbeas cara á variante sibling do mesmo locale, conservando#hashe?query.
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.
- Build-time.
copyAssetscomprobadescriptorFor(target).mailCliente, cando está activo, elimina<section class="sites">conremoveSitesSection(html). - Runtime.
packages/webext/src/options/options.tstolera a ausencia desa sección, non lanzaOptionsFieldMissingErrore restaura o valorsitesgardado antes do normalisation.
Postura de seguridade e privacidade
- Sen chamadas de rede nin exfiltración de datos. Ningún módulo executa
fetch,XHRnin outras chamadas de rede. A extensión só le o config gardado e o DOM da páxina. - Só SAFE-DOM. Annotation e injected stylesheet usan só
createElement,textContentestyle.*, e noninnerHTML,insertAdjacentHTMLoueval. - Toda entrada externa se normaliza. Toda lectura e escritura pasa por
normaliseConfig. Os colours compróbanse conisSafeColour, os pattern conMATCH_PATTERN, e os content-root selectors limítanse conCONTENT_ROOT_PATTERN. - Privilexios mínimos. As orixes webmail integradas son
host_permissions. Os host adicionais son opt-in a través deoptional_host_permissionsepermissions.request. - Recollida de datos en contorno Gecko. Os manifests de Firefox e
Thunderbird declaran
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Licenza. O proxecto é AGPL-3.0-only, tal e como se indica en
LICENSEepackage.json.
Probas
- Vitest unit tests cobren
packages/core/test/*epackages/webext/test/*. - Cucumber BDD vive no directorio
features/e usafeatures/support/webext.tsefeatures/support/world.ts. - Per-target manifest smoke tests en
extensions/<engine>/test/manifest.smoke.test.mjsusannode:teste comproban a forma do manifest e os referenced files. - CI en
.github/workflows/ci.ymldetén cada push e pull request semake test,make bddemake lintnon pasan. Version-stamping e packaging están en.github/workflows/release.yml.