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:
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/destination.ts— classificació i comparació de discrepàncies de destinació segons l'esquemapackages/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, 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):
- Poda les entrades del registre l'ancoratge de les quals ja no està connectat.
- Recull ancoratges candidats amb
collectAnchors(descendentsa[href]més la mateixa arrel quan és una[href], sense duplicats). - Resol la destinació de cada ancoratge amb
resolveAnnotatableUrl, que delega enresolveAnnotatableHref. Només retornanull(omet) per a un href buit/dins de la pàgina o no analitzable, resol els relatius contrabaseURIi accepta qualsevol esquema analitzable, inclososmailto:,tel:,sms:i els URL opacsjavascript:,data:iblob:.ignoreHostsnomés s'aplica als enllaçoshttp:/https:que tenen host. - Pren el camí ràpid idempotent quan
isAnnotationIntactconfirma 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. - Calcula la discrepància segons l'esquema amb
destinationMismatchsobre el text net de l'ancoratge:classifySchemeenvia els enllaços web ahostMismatch, el correu amailMismatch, el telèfon aphoneMismatchi tota la resta aopaqueMismatch. Després cridaannotateAnchor;showOnlyOnMismatchnomés suprimeix la revelació inline visible d'un enllaç sense discrepàncies, mantenint-ne eltitlede 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_SCHEMES —
javascript:, 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:
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 de les
sis pàgines
architecture.html,faq.html,index.html,licence.html,manual.html,privacy.htmltenendata-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.