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

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, Safari i Thunderbird. Safari només utilitza scripts estàtics per als proveïdors integrats i està publicada a l'App Store. 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. Aquest camí de troballes native es manté deliberadament limitat a http:/https:.

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, reprocesa el target només quan és un Element dins del root mitjançant isWithinRoot i closest, i deixa el descobriment tardà dels roots al recorregut limitat discoverMatchedRoots() per batch amb un sol querySelectorAll(contentRoot), deduplicat amb WeakSet. pendingShadowHosts torna a comprovar hosts plain anteriors fins que apareix un open shadow root, de manera que també cobreix els shadow upgrades. 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):

  1. Poda les entrades del registre l'ancoratge de les quals ja no està connectat.
  2. Recull ancoratges candidats amb collectAnchors (descendents a[href] més la mateixa arrel quan és un a[href], sense duplicats).
  3. Resol la destinació de cada ancoratge amb resolveAnnotatableUrl, que delega en resolveAnnotatableHref. Només retorna null (omet) per a un href buit/dins de la pàgina o no analitzable, resol els relatius contra baseURI i accepta qualsevol esquema analitzable, inclosos mailto:, tel:, sms: i els URL opacs javascript:, data: i blob:. ignoreHosts només s'aplica als enllaços http:/https: que tenen host.
  4. Pren el camí ràpid idempotent quan isAnnotationIntact confirma que el render fingerprint no ha canviat i que cada node creat encara està connectat i a la seva posició; altrament reverteix l'annotation obsoleta i torna a renderitzar.
  5. Calcula la discrepància segons l'esquema amb destinationMismatch sobre el text net de l'ancoratge: classifyScheme envia els enllaços web a hostMismatch, el correu a mailMismatch, el telèfon a phoneMismatch i tota la resta a opaqueMismatch. Després crida annotateAnchor; showOnlyOnMismatch només suprimeix la revelació inline visible d'un enllaç sense discrepàncies, mantenint-ne el title de destinació.

annotateAnchor només renderitza SAFE-DOM. Sempre desa el title original (TitleSave) i escriu el title de destinació — l'única escriptura a l'ancoratge. No hi ha un mode de title separat ni cap badge d'avís. Llevat que showOnlyOnMismatch oculti un enllaç sense discrepàncies, anteposa un <span class="reveal-urls-url"> i un <br> final, sense tocar els fills del mateix enllaç. needsLeadingInlineRevealBreak afegeix un <br> inicial quan contingut inline visible precedeix l'enllaç a la mateixa línia. L'span mostra literalment un href absolut escrit, o un href relatiu resolt contra baseURI, mitjançant textContent inert (amb una fletxa i un espai no separable U+00A0 com a prefix, després truncat per truncateUrl); un cop connectat, rep un fons de contrast mitjançant applyContrastBackdrop. Per a una discrepància emfatitzada, s'afegeix REVEAL_URLS_MISMATCH_CLASS a REVEAL_URLS_URL_CLASS, aplicant el color de discrepància configurat amb applyColour més negreta per defecte a REVEAL_URLS_CSS; una substitució explícita del pes de font mitjançant applyFontOverrides té prioritat. Cada node creat s'etiqueta amb data-ru="<token>" (mai l'ancoratge), i cada entrada registra el seu configFingerprint, de manera que un canvi de render config força un reflow. revertOwned elimina els nodes creats per referència i restaura el title desat; revertAll ho fa per a cada ancoratge posseït i buida el registre.

truncateUrl opera sobre Unicode CODE POINTS ([...displayHref]), de manera que un tall mai no divideix un parell surrogate; conserva l'origen d'esquema i host i escurça la resta amb URL_ELLIPSIS al final.

Discrepància d'host

packages/core/src/hostMismatch.ts hostMismatch és la branca web de destinationMismatch: compara el text visible de l'enllaç amb el seu href pel domini registrable, no pel hostname cru, de manera que un subdomini sense discrepàncies no es marca però un look-alike sí. extractHostCandidates divideix el text i redueix cada token amb hostCandidate (que exigeix un punt i un ICANN public suffix reconegut per tldts, i rebutja text puntejat corrent com e.g). Tant l'host de l'href com cada candidat es redueixen al seu domini registrable amb tldts.getDomain, i QUALSEVOL candidat el domini del qual difereixi del de l'href produeix una discrepància — per tant, anomenar el host maliciós real al costat d'un host esquer no pot suprimir l'avís. Mai no llança cap exception.

Per als enllaços de correu, mailMismatch compara tots els dominis registrables dels destinataris, inclosos to, cc i bcc; phoneMismatch compara números marcats resolubles. opaqueMismatch sempre marca SCRIPT_LIKE_SCHEMESjavascript:, data: i vbscript: — mentre que els altres esquemes opacs només es marquen quan el text visible anomena una destinació web, de correu o de telèfon concreta.

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, 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