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:

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/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:

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:

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.

Posició de seguretat i privadesa

Proves