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 — 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, Safari e Thunderbird. Safari usa
scripts estáticos só para provedores integrados e está publicada na App Store.
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.
Este camiño nativo de achados permanece deliberadamente limitado a
http:/https:.
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/destination.ts— clasificación e comparación de discrepancias de destino segundo o esquemapackages/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, reprocesa o target só
cando é un Element dentro do root mediante isWithinRoot e closest, e
reserva a detección tardía de roots ao percorrido limitado
discoverMatchedRoots() por batch cun único querySelectorAll(contentRoot),
deduplicado con WeakSet. pendingShadowHosts volve comprobar hosts plain
anteriores ata que aparece un open shadow root, para cubrir tamén os shadow
upgrades. 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 cun estado de peche que a páxina non pode ler: un Map
forte indexado pola identidade da áncora, un token data-ru por annotator de
generateToken e a configuración actual. O seu método process(root):
- Poda as entradas do rexistro cuxa áncora xa non está conectada.
- Recolle as áncoras candidatas con
collectAnchors(osa[href]descendentes e a propia raíz cando é una[href], sen duplicados). - Resolve o destino de cada áncora con
resolveAnnotatableUrl, que delega enresolveAnnotatableHref. Só devolvenull(saltar) para un href baleiro/dentro da páxina ou non analizábel, resolve os href relativos contrabaseURIe acepta todo esquema analizábel, incluídosmailto:,tel:,sms:e os URL opacosjavascript:,data:eblob:.ignoreHostsaplícase só a ligazónshttp:/https:con host. - Toma a vía rápida idempotente cando
isAnnotationIntactconfirma que a pegada de renderizado non mudou e todos os nodos creados seguen conectados e no seu lugar; se non, reverte a anotación obsoleta e volve renderizar. - Calcula a discrepancia segundo o esquema con
destinationMismatchsobre o texto limpo da áncora:classifySchemeenvía as ligazóns web ahostMismatch, o correo amailMismatch, o teléfono aphoneMismatche o resto aopaqueMismatch. Despois chama aannotateAnchor;showOnlyOnMismatchsó suprime a revelación en liña visible dunha ligazón sen discrepancias, conservando o seutitlede destino.
annotateAnchor renderiza só SAFE-DOM. Garda sempre o title orixinal
(TitleSave) e escribe o título de destino — a única escrita na áncora. Non hai un
modo de título separado nin unha insignia de aviso. Agás cando
showOnlyOnMismatch oculta unha ligazón sen discrepancias, antepón un
<span class="reveal-urls-url"> e un <br> final, deixando intactos os fillos da
ligazón. needsLeadingInlineRevealBreak engade un <br> inicial cando un contido
en liña visible precede a ligazón na mesma liña. O span mostra un href absoluto
redactado literalmente, ou un href relativo resolto contra baseURI, mediante
textContent inerte (co prefixo dunha frecha e un espazo sen quebra U+00A0, e logo
truncado por truncateUrl); unha vez conectado, recibe un fondo de contraste con
applyContrastBackdrop. Para unha discrepancia enfatizada,
REVEAL_URLS_MISMATCH_CLASS engádese a REVEAL_URLS_URL_CLASS, aplicando a cor de
discrepancia configurada mediante applyColour e a negra por defecto en
REVEAL_URLS_CSS; unha substitución explícita do peso da fonte mediante
applyFontOverrides ten preferencia. Todos os nodos creados reciben
data-ru="<token>", e cada entrada rexistra o seu configFingerprint para que un
cambio na configuración de renderizado obrigue a unha reorganización. revertOwned
elimina os nodos creados por referencia e restaura o título gardado; revertAll fai
isto para cada áncora posuída e baleira o rexistro.
truncateUrl traballa sobre PUNTOS DE CÓDIGO Unicode ([...displayHref]), de modo
que un corte nunca divide un par substituto; conserva a orixe de esquema-e-host e
acurta o resto cun URL_ELLIPSIS final.
Discrepancia de host
packages/core/src/hostMismatch.ts hostMismatch é a rama web de
destinationMismatch: compara o texto visible da ligazón co seu href a nivel de
dominio registrable, e non do host en bruto, polo que un subdominio sen discrepancias non se
marca mentres que un sosia si. extractHostCandidates divide o texto en tokens e
reduce cada un mediante hostCandidate (que require un punto e un sufixo público
ICANN recoñecido por tldts, rexeitando texto ordinario con puntos como e.g).
Tanto o host do href como cada candidato redúcense ao seu dominio registrable con
tldts.getDomain, e CALQUERA candidato cuxo dominio difira do href produce unha
discrepancia — así, nomear o host malicioso real xunto a un host engado non pode
suprimir o aviso. Nunca lanza unha excepción.
Para ligazóns de correo, mailMismatch compara todos os dominios registrables dos
destinatarios, incluídos to, cc e bcc; phoneMismatch compara os números
marcados que se poden resolver. opaqueMismatch marca sempre
SCRIPT_LIKE_SCHEMES — javascript:, data: e vbscript: —, mentres que outros
esquemas opacos só se marcan cando o texto visible nomea un destino web, de correo
ou telefónico concreto.
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,
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 seis páxinas internas dos docs
architecture.html,faq.html,index.html,licence.html,manual.html,privacy.htmllevandata-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.