Reveal URLs — Architecture

Ce document décrit comment l'extension Reveal URLs est structurée et comment sa logique centrale fonctionne, à l'intention des contributeurs et des lecteurs techniques. Il cite des fichiers et des symboles réels tout du long (sous la forme path symbol) afin que toute affirmation puisse être vérifiée par rapport au code source. Pour la description destinée à l'utilisateur, consultez le manuel ; pour les étapes de vérification manuelle, consultez le plan de test manuel.

Vue d'ensemble

Reveal URLs est une unique WebExtension MV3 construite pour plusieurs moteurs — Chrome, Edge, Opera, Firefox et Thunderbird — à partir d'un seul noyau de code partagé et pur. (Safari est différé : il dispose d'un manifeste et est constructible lorsqu'il est nommé explicitement, mais il est exclu de la construction par défaut et de l'intégration continue.) L'extension révèle l'URL de destination de chaque lien à côté du lien dans un courriel rendu, et signale un lien dont le texte visible nomme un domaine enregistrable différent de celui de son href — l'indice classique de l'hameçonnage.

Le dépôt est un monorepo pnpm/TypeScript (pnpm-workspace.yaml, package.json avec "private": true). Le regroupement (bundling) est assuré par un unique script piloté par esbuild, tooling/build.mjs, qui compile les paquets partagés et les points d'entrée minces de chaque moteur dans un répertoire chargeable dist/<target>/. webextension-polyfill est regroupé dans chaque script plutôt que d'être utilisé comme variable globale à l'exécution, afin que le même code source s'exécute sans modification sur Chromium et Gecko.

Deux modules complémentaires de messagerie natifs étendent le même noyau de détection à des surfaces que la WebExtension ne peut pas atteindre : un module complémentaire Outlook (volet des tâches Office.js, atteignant Outlook sur web/Windows/Mac/iOS/Android et Outlook.com) et un module complémentaire Gmail (CardService d'Apps Script, atteignant les applications Gmail web/Android/iOS). Tous deux ne réutilisent que l'analyse PURE — un nouveau module packages/core/src/findings.ts (analyseAnchors/analyseHtml, un modèle Finding[] neutre vis-à-vis de la plateforme) qui réutilise la logique hostMismatch existante et la dérivation de domaine enregistrable de tldts. Seuls l'adaptateur hôte (la façon dont le corps est lu et analysé) et la présentation diffèrent selon la surface : le volet des tâches Outlook analyse le corps avec son propre DOMParser et s'exécute côté client ; le module complémentaire Gmail analyse avec node-html-parser et s'exécute côté serveur sur l'infrastructure V8 d'Apps Script de Google (hébergement gratuit, puisque Google détient déjà le courrier ; déployé de façon interactive et locale via clasp). Parce qu'aucun des deux cadres ne peut modifier le DOM du message rendu en mode lecture (les setAsync/prependAsync d'Office sont réservés à la rédaction ; CardService rend des cartes, et non du HTML de message), les deux modules complémentaires présentent un panneau/une carte de constats, et non une annotation en ligne — le chemin de mutation du DOM existant de linkProcessor.ts et REVEAL_URLS_CSS ne sont réutilisés par aucun des deux modules complémentaires. Un href relatif n'est résolu que par rapport à une balise <base href> fiable présente dans le courriel, et est sinon ignoré, jamais résolu par rapport à l'origine de la boîte aux lettres/du fournisseur (ce qui fabriquerait une destination).

Le code sépare nettement les responsabilités :

Disposition du dépôt

packages/core — le noyau pur

Chaque module ici n'importe aucune API d'extension (browser.*/chrome.*/messenger.*) et n'utilise aucun puits DOM chaîne-vers- balisage (innerHTML/insertAdjacentHTML). Ce contrat est appliqué mécaniquement par packages/core/test/purity.test.ts, qui parcourt par glob chaque src/**/*.ts, retire le corps des commentaires via son propre stripComments (afin que les docblocks qui NOMMENT légitimement les jetons interdits ne déclenchent pas la garde) puis vérifie que ni EXTENSION_API_PATTERN ni UNSAFE_DOM_PATTERN ne correspondent à aucun module. La surface publique est réexportée depuis packages/core/src/index.ts. La seule dépendance d'exécution est tldts (pour la comparaison de domaines tenant compte du suffixe public).

Les modules sont :

packages/webext — les enveloppes d'API de navigateur

Les modules ici PEUVENT utiliser browser.* et sont le seul endroit où les API de la WebExtension sont touchées. La surface publique est réexportée depuis packages/webext/src/index.ts. Les modules sont : content.ts (cycle de vie du contenu), contentRegistration.ts (enregistrement dynamique), storage.ts (persistance de la configuration), background.ts (comportement à l'installation), toolbarAction.ts (bascule + badge), messageDisplay.ts et messageDisplayBackground.ts (Thunderbird), et options/options.ts (le contrôleur de la page des paramètres).

extensions/<engine> — points d'entrée minces et manifestes

Chaque moteur porte son propre manifest.json et ses icons/. Chrome, Firefox et Thunderbird livrent en outre un src/ de points d'entrée ; Edge, Opera et Safari ne portent qu'un manifeste et des icônes et RÉUTILISENT le src/ de Chrome (déclaré par le descripteur de construction — voir ci-dessous). Les points d'entrée sont délibérément minuscules : par exemple extensions/chrome/src/content.ts n'est que void createContentController().bootstrap();, et extensions/chrome/src/background.ts appelle registerBackground, registerContentReconciliation et registerToolbarToggle. Le point d'entrée d'arrière-plan de Firefox est identique à celui de Chrome ; celui de Thunderbird (extensions/thunderbird/src/background.ts) appelle plutôt registerBackground et registerMessageDisplay (il n'a pas de scripts de contenu). extensions/_template est un échafaudage Chromium prêt à copier, et non une cible de construction.

tooling, features et les tests de fumée

tooling contient build.mjs, version.mjs et icons.mjs. features contient les scénarios Gherkin, leurs définitions d'étapes et les doublures de test fidèles dans features/support/webext.ts et features/support/world.ts. Chaque moteur porte également un extensions/<engine>/test/manifest.smoke.test.mjs (celui de _template est template.smoke.test.mjs) — ce sont le contrat de manifeste faisant autorité (voir « Construction par moteur et contrat de manifeste » ci-dessous).

Le pipeline de révélation

Le chemin du contenu commence à un point d'entrée de moteur tel que extensions/chrome/src/content.ts, qui appelle createContentController().bootstrap() depuis packages/webext/src/content.ts.

Amorçage (bootstrap)

createContentController bootstrap s'exécute une fois par cadre (frame) :

  1. Il revendique le cadre de façon synchrone, avant tout await, via claimBootstrap, qui lit/définit le drapeau BOOTSTRAP_MARKER sur le window du cadre. Une entrée content_scripts statique intégrée et un script utilisateur dynamique qui se chevauchent peuvent tous deux injecter content.js dans le même cadre ; le window partagé du monde isolé en fait la garde correcte d'unicité, et revendiquer avant tout await signifie que deux injections quasi simultanées ne peuvent pas la franchir toutes les deux.
  2. Il charge d'ABORD la configuration persistée via loadConfigWithRetry (BOOTSTRAP_CONFIG_RETRIES tentatives supplémentaires sur un rejet transitoire), en maintenant le marqueur pendant les tentatives afin qu'une injection qui se chevauche et qui a déjà fait un no-op ne soit pas laissée en plan. Ce n'est qu'une fois la lecture réussie qu'il injecte la feuille de style (injectStyles, qui assigne REVEAL_URLS_CSS via textContent), de sorte qu'une lecture échouée n'ajoute aucun <style> et qu'une nouvelle tentative ne peut pas le dupliquer.
  3. Si le chargement de la configuration échoue à chaque tentative, releaseBootstrap rétablit le marqueur (uniquement ici, avant le début de l'annotation) afin qu'une réinjection ultérieure puisse réessayer ; une fois que start s'est exécuté, le marqueur doit persister, car un annotateur neuf ne posséderait pas les nœuds de la passe précédente.
  4. Lorsque config.enabled, il appelle start, et il enregistre toujours un écouteur onConfigChanged qui réapplique la configuration (couvrant la transition désactivé→activé).

Résolution de la règle de site active

start (et applyConfig) appelle resolveSiteRule pour décider s'il faut — et où — annoter. resolveSiteRule essaie d'abord l'emplacement PROPRE du document en cours d'exécution via resolveForHref, qui filtre config.sites vers les règles activées et délègue au selectMostSpecific du noyau afin que la correspondance la plus spécifique l'emporte (un élément intégré spécifique bat un caractère générique utilisateur large). Lorsque l'emplacement propre ne correspond à rien ET que l'origine propre du document est opaque ou héritée — conditionné par hasOpaqueOrigin, vrai uniquement pour about:blank/about:srcdoc (hérité via INHERITED_ORIGIN_URLS) ou blob:/data: (OPAQUE_ORIGIN_SCHEMES) — il se rabat, dans l'ordre, sur le cadre du HAUT (readTopHref, le cas intégré de l'iframe de message Proton), la fenêtre OUVREUSE (readOpenerHref, la fenêtre de lecture détachée d'Outlook ouverte via window.open("about:blank")) et enfin document.referrer (readReferrer). Chaque lecture externe est protégée contre l'accès interorigine (qui lève une exception) et ne fournit un candidat que lorsqu'elle est lisible. Un document ORDINAIRE non correspondant fait autorité et n'hérite jamais d'une règle du contexte ambiant, laissant le contrôleur inerte.

Cadrage à la racine du contenu et observation des mutations

start enregistre le sélecteur contentRoot de la règle correspondante, crée l'annotateur via createAnnotator(config), et traite chaque racine correspondante de selectRoots(doc, contentRoot) (un querySelectorAll enveloppé dans un try/catch afin qu'un sélecteur invalide échoue sans risque vers []). Il observe ensuite doc.body (un conteneur stable) avec OBSERVER_OPTIONS ({ childList: true, subtree: true }).

L'observateur alimente processMutations, qui conditionne chaque mutation à la racine du contenu : il ne retraite une target de mutation que lorsqu'il s'agit d'un Element à l'intérieur d'une racine (isWithinRoot, via closest), et achemine chaque Element ajouté via processAddedNode, qui annote un nœud qui est à l'intérieur d'une racine, EST une racine, ou en CONTIENT une (un montage SPA paresseux). Ainsi, le brassage à l'intérieur d'une racine et un corps de message frère qui se monte plus tard sont tous deux pris en compte, tandis que le chrome applicatif en dehors des racines est laissé intact. Un annotateur nul (l'extension est arrêtée) est un no-op.

applyConfig réconcilie un changement de configuration en direct : il démonte lorsqu'aucune règle ne correspond ou que le drapeau d'activation est désactivé ; redémarre (rétablir + ré-observer) lorsque le contentRoot résolu a changé ; et sinon, réagence sur place — annotator.setConfig puis annotator.process sur chaque racine correspondante — sans redémarrer l'observateur.

Annotation d'une ancre unique

L'annotateur réside dans packages/core/src/linkProcessor.ts. createAnnotator renvoie un Annotator sur un état de fermeture (closure) que la page ne peut pas lire : une Map forte indexée par l'identité de l'ancre, un jeton data-ru par annotateur issu de generateToken (crypto.getRandomValues), et la configuration courante. Sa méthode process(root) :

  1. Élague les entrées du registre dont l'ancre n'est plus connectée.
  2. Collecte les ancres candidates avec collectAnchors (les a[href] descendants plus la racine elle-même lorsqu'elle est un a[href], dédupliquées).
  3. Résout la destination de chaque ancre avec resolveAnnotatableUrl, qui renvoie null (ignorer) pour un href vide/dans la page, un href non analysable, tout schéma autre que http:/https:, ou un hôte égal à ou sous une entrée ignoreHosts ; il résout les href relatifs par rapport à baseURI afin que le résultat soit absolu.
  4. Emprunte le chemin rapide idempotent lorsque isAnnotationIntact confirme que l'empreinte de rendu est inchangée et que chaque nœud créé est toujours connecté et en place ; sinon, il rétablit l'annotation périmée et effectue un nouveau rendu.
  5. Calcule la non-concordance avec hostMismatch sur le texte propre de l'ancre, honore showOnlyOnMismatch (laissant les liens honnêtes intacts), et appelle annotateAnchor.

annotateAnchor ne rend que du DOM SÛR. En mode "inline", il préfixe un <span class="reveal-urls-url"> dont l'URL est définie via textContent (préfixée d'une flèche et d'un espace insécable U+00A0, tronquée par truncateUrl) suivi d'un <br>, laissant les enfants propres du lien intacts ; le span connecté reçoit ensuite un arrière-plan de contraste via applyContrastBackdrop. En mode "title", il sauvegarde le title d'origine (TitleSave), l'écrase avec l'URL — la seule écriture sur l'ancre — et, sur une non-concordance accentuée, ajoute un badge frère <span class="reveal-urls-warn"> portant REVEAL_URLS_WARN_LABEL. Chaque nœud créé est étiqueté data-ru="<token>" (jamais l'ancre), et chaque entrée enregistre son configFingerprint afin qu'un changement de configuration de rendu force un réagencement. Les couleurs et les surcharges de police sont appliquées via des propriétés typées element.style.* (applyColour/applyFontOverrides), jamais interpolées dans une chaîne CSS. revertOwned supprime les nœuds créés par référence et restaure le titre sauvegardé ; revertAll fait cela pour chaque ancre possédée et vide le registre.

truncateUrl opère sur les POINTS DE CODE Unicode ([...displayHref]), de sorte qu'une coupure ne sépare jamais une paire de substitution (surrogate) ; il préserve l'origine schéma-et-hôte et raccourcit le reste avec un URL_ELLIPSIS en fin.

Non-concordance d'hôte

packages/core/src/hostMismatch.ts hostMismatch compare le texte visible du lien à son href par domaine enregistrable, et non par nom d'hôte brut, de sorte qu'un sous-domaine honnête n'est pas signalé tandis qu'un sosie l'est. extractHostCandidates découpe le texte et réduit chaque jeton via hostCandidate (qui exige un point et un suffixe public ICANN reconnu par tldts, rejetant le texte pointé ordinaire comme e.g). Tant l'hôte du href que chaque candidat sont réduits à leur domaine enregistrable avec tldts.getDomain, et TOUT candidat dont le domaine diffère de celui du href produit une non-concordance — de sorte que nommer le véritable hôte malveillant aux côtés d'un hôte leurre ne peut pas supprimer l'avertissement. Il ne lève jamais d'exception.

Styles et contraste

packages/core/src/styles.ts contient le REVEAL_URLS_CSS injectable (mise en page, graisse et forme fixes, et un arrière-plan de pastille blanc fixe et entièrement opaque — background: #ffffff, opacity: 1 ; les couleurs n'y sont JAMAIS interpolées) et les utilitaires d'exécution applyColour/applyFontOverrides/applyContrastBackdrop. applyContrastBackdrop lit la couleur résolue de l'élément et le premier arrière-plan opaque trouvé en remontant depuis l'élément LUI-MÊME (son propre arrière-plan — le blanc par défaut fixe de la pastille ou une surcharge plus spécifique d'une page hôte — avant tout ancêtre) via le getComputedStyle standard (provenant de element.ownerDocument?.defaultView, de sorte qu'il est sûr pour les cadres et stubbable) et, lorsque packages/core/src/contrast.ts needsWhiteBackdrop indique que le texte échoue au WCAG AA (CONTRAST_THRESHOLD) par rapport à cet arrière-plan ET que le blanc aide véritablement, définit style.backgroundColor = "white". contrast.ts fournit parseColour, relativeLuminance et contrastRatio ; il analyse les formes rgb()/rgba()/hex que getComputedStyle renvoie et traite une couleur entièrement transparente comme « non trouvée ».

Hôtes configurables et cadrage du contenu

Une SiteRule (packages/core/src/config.ts) indique OÙ l'annotation s'exécute (match, allFrames) et QUEL conteneur la cadre (contentRoot), plus enabled et un drapeau builtin. Les DEFAULT_SITES livrés couvrent Gmail, Proton (avec allFrames) et les deux hôtes Outlook. La Config agrège les bascules globales, les couleurs, les surcharges de police et le tableau sites.

Le moteur de motifs de correspondance

packages/core/src/matchPattern.ts contient une grammaire RESTRICTIVE au moment de l'ajout MATCH_PATTERN (http/https uniquement, caractère générique d'hôte *. optionnel en tête, glob de chemin ; pas de port, pas de schéma *, pas de <all_urls>). matchesPattern fait correspondre une URL à un motif validé, déléguant le chemin à pathGlobMatches (chaque * correspond à n'importe quelle séquence de caractères) ; il échoue sans risque vers false. parsePatternParts découpe un motif validé en host/path/scheme/wildcardHost.

Les règles qui se chevauchent se résolvent par « le plus spécifique l'emporte » : compareSiteSpecificity classe selon (1) l'hôte exact plutôt que l'hôte générique, (2) l'hôte littéral le plus long, (3) le chemin le plus littéral (literalPathLength), puis (4) un départage ASCII déterministe ; selectMostSpecific renvoie la règle activée la plus spécifique correspondant à une URL.

La couverture des origines accordées est un comparateur SÉPARÉ, délibérément plus LARGE. permissions.getAll() peut rapporter des octrois dans la grammaire complète de la WebExtension (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*) que le MATCH_PATTERN restrictif rejetterait. parseGrantedOrigin analyse ceux-ci en GrantedOriginParts, et originCovers indique si une origine accordée couvre le match d'une règle (par exemple, un octroi large https://*/* couvre véritablement toute règle https). matchAllowsOriginFallback indique si le chemin d'une règle est exactement ORIGIN_FALLBACK_PATH (/*) — l'intersection sûre des contraintes de Chromium et de Gecko/Firefox 128 sur le repli d'origine opaque dynamique.

Enregistrement dynamique

packages/webext/src/contentRegistration.ts enregistre un script de contenu dynamique par règle ajoutée par l'utilisateur. Les éléments intégrés sont servis par les entrées content_scripts statiques et ne sont JAMAIS enregistrés dynamiquement. desiredContentScripts filtre config.sites vers les règles non intégrées, activées, dont l'origine est isOriginGranted (déléguant à originCovers), associe chacune à un RegisteredContentScript avec un identifiant stable (contentScriptId, un hachage FNV-1a du match afin que l'identifiant n'utilise que le jeu de caractères sûr pour l'API), et ne définit matchOriginAsFallback: true QUE lorsque matchAllowsOriginFallback(rule.match) est vrai (un chemin /*). Conditionner le drapeau de cette façon empêche qu'une règle au chemin non conforme provoque le rejet de l'enregistrement par lot entier ; une telle règle s'enregistre sans couverture de fenêtre détachée, par conception.

reconcileContentScriptsOnce lit les origines accordées, la configuration stockée et les enregistrements en direct, puis converge par un diff TENANT COMPTE DES PROPRIÉTÉS : sameRegistration projette tant le descripteur désiré que la relecture en direct sur une forme normalisée (en appliquant la vraie valeur par défaut de la WebExtension de chaque champ — allFrames/matchOriginAsFallback par défaut faux, persistAcrossSessions par défaut VRAI, runAt par défaut document_idle) afin qu'un script fraîchement enregistré se compare ÉGAL au descripteur qui l'a produit et qu'aucune boucle de réenregistrement ne puisse survenir. Comme il n'y a pas de updateContentScripts, un script modifié est désenregistré puis réenregistré. reconcileContentScripts enveloppe ceci avec une promesse en cours (inFlight) afin que deux déclencheurs quasi simultanés (un changement de configuration ET permissions.onAdded se déclenchant tous deux lors de l'ajout d'un site) soient sérialisés plutôt qu'en course. registerContentReconciliation câble les déclencheurs (permissions.onAdded/onRemoved, onConfigChanged) et converge une fois au démarrage ; il n'est importé que par les points d'entrée d'arrière-plan de Chrome et de Firefox.

Le flux de permission par adhésion (opt-in)

Les manifestes statiques déclarent optional_host_permissions (http://*/*, https://*/*) afin qu'un utilisateur puisse accorder un hôte web supplémentaire arbitraire à l'exécution. packages/webext/src/options/options.ts addSite valide la règle candidate ENTIÈRE (match ET racine de contenu) via normaliseSiteRule AVANT de demander toute permission, dérive l'origine de l'hôte avec matchOrigin, et appelle browser.permissions.request depuis le geste de clic du bouton Ajouter ; ce n'est que lorsque l'octroi est accordé qu'il ajoute une ligne builtin:false et persiste via le chemin d'écriture canonique.

Configuration, stockage et le motif canonique

L'unique entonnoir de validation

normaliseConfig dans packages/core/src/config.ts est l'unique chemin de lecture canonique : il coerce et borne chaque champ, se rabattant sur la valeur par défaut pour tout ce qui est invalide, et ne lève jamais d'exception. Il est construit à partir de validateurs par champ — normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts (réduisant chaque entrée à un nom d'hôte punycode nu via bareHostname), normaliseMatchColour/normaliseMismatchColour (conditionnés par safeColour.ts isSafeColour), normaliseCssSize, normaliseFontWeight, normaliseContentRoot (borné par CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH), normaliseMatchPattern (conditionné par MATCH_PATTERN) et normaliseSites. normaliseSites écarte les rejets, déduplique par match, FORCE builtin: true sur toute règle dont le match égale celui d'un élément intégré (fermant le vecteur de masquage exact / double injection), réensemence tout élément intégré manquant (afin qu'un magasin falsifié ne puisse pas supprimer un fournisseur central) et trie alphanumériquement par match pour un ordre canonique. configFingerprint hache uniquement les champs affectant le rendu (excluant enabled, ignoreHosts et sites, qui pilotent le démarrage/l'arrêt et le saut plutôt que le réagencement) via fnv1a.

Stockage

packages/webext/src/storage.ts enveloppe l'API de stockage de la WebExtension. configArea utilise browser.storage.sync lorsqu'elle est disponible (afin que les paramètres voyagent) et se rabat sur browser.storage.local (par exemple sur Thunderbird) ; configAreaName indique laquelle est active. getConfig, setConfig et onConfigChanged font tous transiter leur valeur brute par normaliseConfig, de sorte qu'un magasin malformé ou falsifié ne peut jamais remettre une Config invalide à un appelant. onConfigChanged ignore en outre les événements de la zone de stockage INACTIVE et les changements de clés non pertinentes.

Les deux modules complémentaires réutilisent le même entonnoir lecture -> normaliseConfig -> retour sur leurs propres magasins hôtes. Le extensions/outlook/src/roamingStorage.ts du volet des tâches Outlook enveloppe Office.context.roamingSettings (get/set synchrones, validés avec saveAsync) ; le packages/gmail/src/propertiesStorage.ts du module complémentaire Gmail enveloppe PropertiesService.getUserProperties() (par utilisateur, itinérant sur les appareils de cet utilisateur, encodé en JSON sous une seule clé, et ne nécessitant AUCUN champ OAuth supplémentaire — jamais le ScriptProperties partagé). Tous deux exposent getConfig/setConfig plus les accesseurs pertinents pour la carte getIgnoreHosts/getHighlightMismatch et protègent une surface hôte indisponible avec une erreur dédiée. Le déclencheur de page d'accueil de Gmail rend une carte de paramètres CardService à partir de getConfig, et le gestionnaire de soumission de formulaire onSaveSettings fusionne les ignoreHosts/highlightMismatch analysés sur la configuration courante et la persiste via setConfig ; le déclencheur contextuel achemine ignoreHosts dans l'adaptateur et highlightMismatch dans le constructeur de carte, en lisant de façon défensive afin qu'un échec de stockage se rabatte sur les valeurs par défaut plutôt que de casser l'analyse des messages.

Le motif configuration-canonique + mise à jour ciblée

Trois chemins d'écriture lisent la configuration canonique, changent exactement une chose et la réécrivent — sans jamais valider les modifications de formulaire non enregistrées ni effectuer un nouveau rendu du formulaire :

Le toggleEnabled propre à l'action de la barre d'outils (packages/webext/src/toolbarAction.ts) suit le même motif depuis le côté arrière-plan.

Construction par moteur et contrat de manifeste

tooling/build.mjs pilote une unique construction esbuild pour chaque cible de la WebExtension. Le descripteur TARGETS nomme la CIBLE SOURCE de chaque cible : Chrome, Firefox et Thunderbird livrent leur propre src/ ; Edge, Opera et Safari déclarent chrome comme leur source ; le module complémentaire Outlook (Office.js) est sa propre source. ACTIVE_TARGETS (Chrome, Edge, Firefox, Opera et Thunderbird — Safari et Outlook sont exclus) est ce que --all/--package construisent ; Safari et Outlook ne se construisent que lorsqu'ils sont nommés explicitement, et le module complémentaire Gmail (Apps Script) est un bundle esbuild séparé (make build-gmail) en dehors de la boucle --all. Ce bundle Gmail cible le runtime V8 d'Apps Script, qui n'a ni modules ES ni URL natif : il est construit en ESM puis voit son instruction export {…} finale retirée (afin que les fonctions de déclenchement restent des variables globales de premier niveau qu'Apps Script peut invoquer), et il intègre un petit polyfill URL (installé uniquement lorsque URL est absent) afin que le new URL(...) du noyau partagé résolve les liens côté serveur. buildTarget nettoie dist/<target>/, copie le manifest.json et les icons/ par cible verbatim, regroupe chaque script source en IIFE (les content.ts/messageDisplay.ts injectés via INJECTED_SCRIPTS ne peuvent pas être des modules ES, et les arrière-plans sont aussi des workers classiques/pages d'événements) et la page d'options partagée en ESM, et copie options.html/options.css depuis packages/webext/src/options/. webextension-polyfill est regroupé dans chaque script.

tooling/version.mjs estampille les versions : il prend MAJEUR.MINEUR du package.json racine et ajoute un numéro de BUILD à incrémentation automatique (lu à partir du troisième composant existant le plus élevé parmi les manifestes cibles) dans chaque manifeste cible, les gardant synchronisés. La plupart des cibles portent un seul manifest.json ; le module complémentaire Outlook porte deux manifestes versionnés (manifest.json et manifest.xml) et tous deux sont estampillés, et l'échafaudage _template est estampillé de façon synchronisée également. Le package.json racine est maintenu à la même version, mais reste un semver valide (une version à quatre parties est autorisée dans un manifeste, jamais dans package.json). tooling/icons.mjs (exécuté via make icons) rastérise l'unique source vectorielle assets/icon.svg en icons/icon48.png et icons/icon128.png de chaque cible, en essayant cairosvg, puis inkscape, puis ImageMagick.

Le contrat de manifeste — les champs exacts requis par moteur — est l'unique source de vérité dans chaque extensions/<engine>/test/manifest.smoke.test.mjs (par exemple, l'assertManifestShape de extensions/chrome/test/manifest.smoke.test.mjs, qui construit aussi la cible et confirme que chaque fichier référencé se résout dans le dist). Conformément à la convention de déduplication du projet, la forme champ par champ n'est PAS réénumérée ici ; reportez-vous à ces tests de fumée et à la note « Contrat de manifeste » dans le plan de test manuel.

Internationalisation (i18n)

Reveal URLs est localisé dans de nombreuses langues en plus de l'anglais. L'ensemble est mono-sourcé dans packages/core/src/locales.json (SUPPORTED_LOCALES, noms natifs et la valeur par défaut), que lisent à la fois les bundles de l'extension (via l'import JSON résolu) et la construction du site web en Node pur. Chaque chaîne non anglaise est traduite par machine et en attente de relecture humaine ; le marqueur de provenance est propre à chaque format (les _locales utilisent la description de chaque message, les dictionnaires du chrome du site web l'enregistrent dans site/i18n/README.md, et les sources de documentation traduites portent un commentaire HTML en première ligne).

Il existe trois surfaces de localisation indépendantes, délibérément non interconnectées, et chacune est scindée selon une ligne temps de construction / exécution.

Suppression des « Sites actifs » pour les clients de messagerie (AD-3)

Une cible de client de messagerie (Thunderbird aujourd'hui) voit déjà chaque message rendu, de sorte que l'éditeur « Sites actifs » par hôte y est redondant et est supprimé. La décision est portée par un drapeau de descripteur par cible documenté, mailClient: true, sur le TARGETS.thunderbird de tooling/build.mjs — jamais un nom de cible codé en dur dans la logique de construction ou d'interface — et est mise en œuvre en deux moitiés :

Posture de sécurité et de confidentialité

Tests