Reveal URLs — Arquitectura
Aquest document descriu com està estructurat Reveal URLs i com funciona la
seva lògica principal, per a col·laboradors i lectors tècnics. Al llarg del
text s'hi esmenten fitxers i símbols reals en la forma path symbol, de
manera que cada afirmació es pugui comprovar al codi font. La descripció
orientada a l'usuari és a manual, i els passos de comprovació
manual es troben al
manual test plan.
Visió general
Reveal URLs és una WebExtension MV3 única que es construeix a partir d'un
nucli de codi comú i net per a Chrome, Edge, Opera, Firefox i Thunderbird.
Safari queda ajornat per a una fase posterior: té manifest i es pot construir
quan s'indica explícitament, però no entra al build predeterminat ni al CI.
L'extensió mostra l'URL de destí de cada enllaç al costat del mateix enllaç
dins del rendered email i marca un enllaç quan el text visible apunta a un
domini registrable diferent del que hi ha a href.
El repositori és un monorepo de pnpm i TypeScript descrit als fitxers
pnpm-workspace.yaml i package.json, i "private": true evita que es
publiqui com a paquet. El bundling el gestiona tooling/build.mjs, que
compila els paquets compartits i els entry point prims de cada engine en un
directori carregable dist/<target>/. webextension-polyfill es bundleja dins
de cada script en lloc de dependre d'un global de runtime.
Dos complements native de correu amplien el mateix nucli de detecció cap a
superfícies on WebExtension no pot arribar: Outlook Add-in sobre Office.js
i Gmail Add-on sobre Apps Script CardService. Tots dos reutilitzen només
l'anàlisi PURE de packages/core/src/findings.ts, juntament amb
analyseAnchors, analyseHtml, el model neutral Finding[], la lògica
hostMismatch i l'obtenció del domini registrable a través de tldts. El que
canvia és només l'adaptador de host i la presentació. Outlook fa servir
DOMParser i funciona client-side. Gmail fa servir node-html-parser i
funciona server-side sobre la infraestructura Google Apps Script V8. Com
que cap d'aquests dos marcs no permet modificar el DOM del missatge renderitzat,
tots dos mostren un panell o una targeta amb troballes, i no pas inline
annotation. El camí de mutació del DOM de linkProcessor.ts i
REVEAL_URLS_CSS no s'hi fan servir. Un href relatiu només es resol respecte
d'un <base href> fiable dins del missatge; si no, s'omet.
La base de codi separa clarament les responsabilitats:
packages/coreguarda la lògica PURE, sense browser API, sense storage i sense markup sink. És l'única font de veritat per a la lògica d'annotation, la validació del config i la comparació de host.packages/webextembolcalla aquest nucli amb WebExtension API, el content lifecycle, la dynamic registration, l'options UI i el background wiring.extensions/<engine>guarda els entry point prims per engine i els seusmanifest.json.toolingguarda els scripts de build, version-stamping i icones.featuresguarda els escenaris Cucumber BDD i els test doubles.
Estructura del repositori
packages/core — el nucli pur
Cap mòdul d'aquí no importa extension API com browser.*, chrome.* o
messenger.*, i cap no fa servir string-to-markup DOM sink com innerHTML o
insertAdjacentHTML. Aquest contracte s'aplica mecànicament a
packages/core/test/purity.test.ts, que recorre src/**/*.ts, elimina els
cossos dels comentaris amb stripComments i comprova que ni
EXTENSION_API_PATTERN ni UNSAFE_DOM_PATTERN coincideixin amb cap mòdul. La
superfície pública es reexporta des de packages/core/src/index.ts, i l'única
dependència de runtime és tldts.
Els mòduls principals són:
packages/core/src/config.ts— esquemesConfigiSiteRule, valors predeterminats i el validational funnelnormaliseConfigpackages/core/src/matchPattern.ts— gramàtica del pattern, matcher i regles d'especificitatpackages/core/src/linkProcessor.ts— annotator a nivell de documentpackages/core/src/hostMismatch.ts— comprovació de discrepància de domini registrablepackages/core/src/styles.tsipackages/core/src/contrast.ts— injectable stylesheet i lògica de contrastpackages/core/src/safeColour.ts— predicatisSafeColour
packages/webext — els embolcalls de browser API
Aquests mòduls PODEN fer servir browser.* i és l'únic lloc on es toquen les
WebExtension API. La superfície pública es reexporta des de
packages/webext/src/index.ts. Aquí hi ha content.ts,
contentRegistration.ts, storage.ts, background.ts, toolbarAction.ts,
messageDisplay.ts, messageDisplayBackground.ts i options/options.ts.
extensions/<engine> — entry point prims i manifests
Cada engine té el seu propi manifest.json i el seu propi icons/. Chrome,
Firefox i Thunderbird també tenen un directori src/ amb entry point. Edge,
Opera i Safari només tenen manifest i icones i reutilitzen el src/ de
Chrome. Per exemple, extensions/chrome/src/content.ts només conté
void createContentController().bootstrap();, i
extensions/chrome/src/background.ts crida registerBackground,
registerContentReconciliation i registerToolbarToggle.
extensions/thunderbird/src/background.ts crida registerBackground i
registerMessageDisplay. extensions/_template és un scaffold, no pas un
build target.
tooling, features i smoke tests
tooling inclou build.mjs, version.mjs i icons.mjs. features inclou
els escenaris Gherkin, les step definitions i els doubles fidels dels fitxers
features/support/webext.ts i features/support/world.ts. Cada engine també
té extensions/<engine>/test/manifest.smoke.test.mjs, mentre que _template
utilitza template.smoke.test.mjs.
El flux de revelació
La ruta de contingut comença en un entry point d'engine com ara
extensions/chrome/src/content.ts, que crida
createContentController().bootstrap() des de packages/webext/src/content.ts.
Bootstrap
createContentController bootstrap s'executa una vegada per frame. Reivindica
el frame abans de cada await amb claimBootstrap, carrega el config amb
loadConfigWithRetry, injecta la stylesheet amb injectStyles només després
d'una lectura correcta i restaura el marcador amb releaseBootstrap si la
càrrega finalment falla. Quan config.enabled és actiu, crida start, i en
qualsevol cas registra el listener onConfigChanged.
Resolució de la regla activa del lloc
start i applyConfig fan servir resolveSiteRule per decidir si s'ha de fer
annotation i on. Primer es prova la ubicació del mateix document mitjançant
resolveForHref, que filtra config.sites fins a les regles activades i
deixa la selecció final a selectMostSpecific. Si el document no coincideix i
el seu origen és opaque o inherited, tal com comprova hasOpaqueOrigin,
s'aplica un fallback a través de readTopHref, readOpenerHref i
readReferrer, amb protecció davant de lectures cross-origin.
Acotació al content root i observació dels canvis
start recorda contentRoot, crea l'annotator amb createAnnotator(config) i
processa cada arrel de selectRoots(doc, contentRoot), que embolcalla
querySelectorAll en un try/catch perquè un selector incorrecte torni
[] de manera segura. Després observa doc.body amb OBSERVER_OPTIONS.
processMutations filtra cada canvi segons el root, i processAddedNode
abasta els nodes dins del root, el root mateix i els nodes que el contenen.
applyConfig fa teardown, restart o reflow segons el canvi de la regla activa
i de contentRoot.
Annotation d'un únic enllaç
L'annotator és a packages/core/src/linkProcessor.ts. createAnnotator
retorna un Annotator amb Map, un token data-ru de generateToken i el
config actiu. El seu process(root) neteja els registres desconnectats,
recull els elements a[href] amb collectAnchors, resol l'URL mitjançant
resolveAnnotatableUrl, utilitza el camí ràpid isAnnotationIntact quan el
render no ha canviat i calcula el mismatch amb hostMismatch abans de cridar
annotateAnchor.
annotateAnchor només renderitza SAFE-DOM. En mode "inline" afegeix
<span class="reveal-urls-url"> amb textContent i <br>. En mode "title"
guarda el title existent, el substitueix per l'URL i, quan el mismatch és
emfatitzat, afegeix <span class="reveal-urls-warn"> amb
REVEAL_URLS_WARN_LABEL. Els nodes creats reben data-ru="<token>", i
configFingerprint serveix per forçar el reflow. truncateUrl treballa sobre
[...displayHref] i utilitza URL_ELLIPSIS.
Discrepància d'host
packages/core/src/hostMismatch.ts hostMismatch compara el text visible de
l'enllaç amb el seu href al nivell del domini registrable, i no pas del host
cru. extractHostCandidates divideix el text en tokens i els redueix amb
hostCandidate, que exigeix un punt i un public suffix reconegut per tldts.
L'host de l'href i els candidats es redueixen mitjançant tldts.getDomain.
Qualsevol candidat amb un domini diferent produeix un mismatch.
Estils i contrast
packages/core/src/styles.ts guarda REVEAL_URLS_CSS i les funcions d'ajuda
applyColour, applyFontOverrides i applyContrastBackdrop.
applyContrastBackdrop llegeix el color calculat de l'element i el primer fons
opaque que troba a través de getComputedStyle, i després fa servir
needsWhiteBackdrop de packages/core/src/contrast.ts per decidir si cal
assignar style.backgroundColor = "white". contrast.ts proporciona
parseColour, relativeLuminance i contrastRatio.
Hosts configurables i acotació del contingut
SiteRule a packages/core/src/config.ts defineix ON funciona l'annotation
amb match i allFrames, i QUIN container la limita amb contentRoot,
juntament amb enabled i la marca builtin. DEFAULT_SITES inclou Gmail,
Proton amb allFrames i els dos host d'Outlook. Config agrupa els
toggle globals, els colours, els font overrides i la llista sites.
El motor de match pattern
packages/core/src/matchPattern.ts conté la gramàtica restrictiva
MATCH_PATTERN: només http i https, host wildcard opcional *., glob path,
sense port, sense * scheme i sense <all_urls>. matchesPattern compara una
URL amb un pattern validat i delega la part del path a pathGlobMatches.
parsePatternParts divideix el pattern en host, path, scheme i
wildcardHost.
Quan les regles se solapen, s'aplica la norma "most specific wins".
compareSiteSpecificity dona preferència a l'host exacte sobre l'host
wildcard, després al host literal més llarg, després al path més literal a
través de literalPathLength, i finalment fa servir un ASCII tiebreak.
selectMostSpecific retorna la regla activada més específica que coincideix
amb l'URL.
permissions.getAll() pot informar de grant a tota la gramàtica WebExtension,
com ara <all_urls>, *://*/*, https://*/*, *://*.host/* i
https://host/*) que MATCH_PATTERN rebutjaria.
parseGrantedOrigin els converteix en GrantedOriginParts, originCovers
comprova la cobertura sobre match, i matchAllowsOriginFallback indica si el
path és exactament ORIGIN_FALLBACK_PATH, és a dir /*.
Registre dinàmic
packages/webext/src/contentRegistration.ts registra un dynamic content script
per cada regla afegida per l'usuari. Les regles built-in se serveixen amb les
entrades static content_scripts i mai no es registren dinàmicament.
desiredContentScripts filtra config.sites, fa servir
isOriginGranted, originCovers i contentScriptId, i estableix
matchOriginAsFallback: true només quan ho permet
matchAllowsOriginFallback(rule.match).
reconcileContentScriptsOnce llegeix els orígens concedits, el config desat i
els registres actius i els harmonitza amb un property-aware diff.
sameRegistration normalitza tant el descriptor desitjat com el live
read-back. Com que no existeix updateContentScripts, un script canviat
primer es desregistra i després es torna a registrar. reconcileContentScripts
utilitza la promise inFlight per serialitzar triggers que poden produir-se
gairebé alhora.
El flux de permís explícit
Els manifests static declaren optional_host_permissions
(http://*/*, https://*/*) perquè l'usuari pugui concedir en runtime permís
per a un host addicional. packages/webext/src/options/options.ts addSite
valida tota la regla candidata mitjançant normaliseSiteRule, extreu l'host
origin amb matchOrigin i crida browser.permissions.request des del click
gesture del botó Add. Només després de l'aprovació afegeix la fila
builtin:false.
Configuració, storage i el patró canònic
L'únic validational funnel
normaliseConfig a packages/core/src/config.ts és l'única ruta de lectura
canònica. Força i limita cada camp, torna al valor predeterminat quan l'entrada
és incorrecta i mai no llança cap exception. Està compost per validators per
camps com normaliseBoolean, normaliseRenderMode,
normaliseMaxLength, normaliseIgnoreHosts, normaliseMatchColour,
normaliseMismatchColour, normaliseCssSize, normaliseFontWeight,
normaliseContentRoot, normaliseMatchPattern i normaliseSites.
normaliseSites expulsa les entrades incorrectes, elimina duplicats per
match, força builtin: true quan la regla coincideix amb una regla built-in
i restaura totes les entrades integrades que falten.
Storage
packages/webext/src/storage.ts embolcalla la WebExtension storage API.
configArea utilitza browser.storage.sync quan és disponible i recorre a
browser.storage.local, per exemple al Thunderbird.
getConfig, setConfig i onConfigChanged sempre fan passar els valors per
normaliseConfig, de manera que un storage corrupte mai no pot retornar un
Config invàlid. Totes dues formes d'add-on utilitzen el mateix funnel sobre
el seu host storage propi.
Configuració canònica i actualitzacions dirigides
Tres rutes de write llegeixen el config canònic, canvien exactament una cosa i l'escriuen de nou, sense desar form edits inacabats:
removeSiteaoptions.tsaddSiteaoptions.tstoggleEnabledaoptions.ts
toggleEnabled a packages/webext/src/toolbarAction.ts segueix el mateix
patró des del costat del background.
Build per engine i el contracte del manifest
tooling/build.mjs executa un build esbuild comú per a cada WebExtension
target. El descriptor TARGETS indica el SOURCE TARGET de cada target. Chrome,
Firefox i Thunderbird tenen el seu propi src/. Edge, Opera i Safari fan
servir chrome com a origen. Outlook add-in té un origen a part.
ACTIVE_TARGETS, és a dir Chrome, Edge, Firefox, Opera i Thunderbird, són els
que es construeixen amb --all i --package. Safari i Outlook només es
construeixen quan s'especifiquen explícitament, i Gmail utilitza
make build-gmail.
El bundle de Gmail apunta al runtime Apps Script V8, que no té ni ES modules
ni URL native. Per això es construeix com a ESM i després se li elimina
l'export {…} final perquè les funcions trigger continuïn sent globals. El
petit URL polyfill només s'hi inclou quan cal. buildTarget neteja
dist/<target>/, copia manifest.json i icons/, bundleja els scripts com a
IIFE, la pàgina d'opcions com a ESM i copia options.html i options.css.
tooling/version.mjs estampa les versions. Pren el MAJOR.MINOR del
package.json arrel, troba el BUILD més alt i l'afegeix a cada manifest de
target. Outlook add-in té tant manifest.json com manifest.xml.
tooling/icons.mjs, executat amb make icons, rasteritza assets/icon.svg
cap a icon48.png i icon128.png.
El contracte del manifest es defineix als fitxers
extensions/<engine>/test/manifest.smoke.test.mjs, on es comprova la forma de
cada manifest i l'existència de cada referenced file.
Internacionalització (i18n)
Reveal URLs està localitzat a molts idiomes a més de l'anglès. L'única font
d'aquest conjunt és packages/core/src/locales.json, que conté
SUPPORTED_LOCALES, els noms locals i l'idioma predeterminat. Cada string no
anglesa és machine-translated i queda pendent de revisió humana. La marca
d'origen depèn del format: _locales la porten al camp description, site
catalogues la registren a site/i18n/README.md, i els docs traduïts duen un
comentari HTML a la primera línia.
Hi ha tres superfícies de localització independents:
- The options page (AD-1). El markup static es lliura en anglès, però cada
node traduïble porta
data-i18n="<key>"odata-i18n-<attr>="<key>". En runtime,packages/webext/src/options/locale.tscarrega_locales/<code>/messages.json, reescriu el text ambtextContenti fixa<html lang>. - The website chrome (AD-2).
tooling/site.mjsrenderitza el site chrome en anglès ambdata-i18ni<html lang="en-GB">. Despréssite/scripts/i18n.mjsel substitueix per la llengua del visitant des desite/i18n/<code>.json. - The documentation pages (AD-8/AD-9). Les pàgines de documentació es
renderitzen per a cada locale durant el build i fan servir noms de fitxer
localitzats des de
packages/core/src/site-page-names.json, per exemplenl/handleiding.html. - Internal-link localisation (Finding 2). Els enllaços interns dels docs
tenen
data-doc-link="<page>", i el runtime loader els reescriu cap a la variant sibling del mateix locale, conservant#hashi?query.
Supressió de «Active sites» al client de correu (AD-3)
Un mail client target com Thunderbird ja veu tots els rendered messages, de
manera que l'editor host-based d'«Active sites» hi és sobrer. La decisió es
transmet amb el camp mailClient: true de tooling/build.mjs.
- Build-time.
copyAssetscomprovadescriptorFor(target).mailClienti, quan és actiu, elimina<section class="sites">ambremoveSitesSection(html). - Runtime.
packages/webext/src/options/options.tstolera l'absència d'aquest apartat, no llançaOptionsFieldMissingErrori restaura el valorsitesdesat abans del normalisation.
Posició de seguretat i privadesa
- Sense crides de xarxa ni exfiltració de dades. Cap mòdul no executa
fetch,XHRni altres crides de xarxa. L'extensió només llegeix el config desat i el DOM de la pàgina. - Només SAFE-DOM. Annotation i injected stylesheet només fan servir
createElement,textContentistyle.*, i no pasinnerHTML,insertAdjacentHTMLoeval. - Tota entrada externa es normalitza. Tota lectura i escriptura passa per
normaliseConfig. Els colours es comproven ambisSafeColour, els pattern ambMATCH_PATTERN, i els content-root selectors es limiten ambCONTENT_ROOT_PATTERN. - Privilegis mínims. Els orígens webmail integrats són
host_permissions. Els host addicionals són opt-in a través deoptional_host_permissionsipermissions.request. - Recollida de dades en entorn Gecko. Els manifests de Firefox i
Thunderbird declaren
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Llicència. El projecte és AGPL-3.0-only, tal com consta a
LICENSEipackage.json.
Proves
- Vitest unit tests cobreixen
packages/core/test/*ipackages/webext/test/*. - Cucumber BDD viu al directori
features/i fa servirfeatures/support/webext.tsifeatures/support/world.ts. - Per-target manifest smoke tests a
extensions/<engine>/test/manifest.smoke.test.mjsfan servirnode:testi comproven la forma del manifest i els referenced files. - CI a
.github/workflows/ci.ymlatura cada push i pull request simake test,make bddimake lintno passen. Version-stamping i packaging són a.github/workflows/release.yml.