Reveal URLs — Arquitetura

Este documento descreve como a extensão Reveal URLs está estruturada e como funciona a sua lógica central, para contribuidores e leitores técnicos. Cita ficheiros e símbolos reais ao longo do texto (na forma path symbol) para que qualquer afirmação possa ser verificada contra o código-fonte. Para a descrição orientada ao utilizador consulte o manual; para os passos de verificação manual consulte o plano de teste manual.

Visão geral

O Reveal URLs é uma única WebExtension MV3 construída para vários motores — Chrome, Edge, Opera, Firefox e Thunderbird — a partir de um núcleo de código partilhado e puro. (O Safari está adiado: tem um manifesto e é construível quando nomeado explicitamente, mas está excluído da compilação predefinida e da CI.) A extensão revela o URL de destino de cada ligação ao lado da ligação num e-mail renderizado, e assinala uma ligação cujo texto visível indica um domínio registável diferente do seu href — o clássico sinal de phishing.

O repositório é um monorepo pnpm/TypeScript (pnpm-workspace.yaml, package.json com "private": true). O empacotamento é um único script conduzido por esbuild, tooling/build.mjs, que compila os pacotes partilhados e os pontos de entrada finos de cada motor num diretório dist/<target>/ carregável. O webextension-polyfill é empacotado em cada script em vez de ser dependente de um global de runtime, pelo que o mesmo código-fonte é executado sem alterações no Chromium e no Gecko.

Dois extras de e-mail nativos estendem o mesmo núcleo de deteção a superfícies que a WebExtension não consegue alcançar: um Suplemento do Outlook (painel de tarefas Office.js, alcançando o Outlook na web/Windows/Mac/iOS/Android e o Outlook.com) e um Extra do Gmail (Apps Script CardService, alcançando as aplicações web/Android/iOS do Gmail). Ambos reutilizam apenas a análise PURA — um novo módulo packages/core/src/findings.ts (analyseAnchors/analyseHtml, um modelo Finding[] neutro em relação à plataforma) que reutiliza a lógica hostMismatch existente e a derivação de domínio registável do tldts. Apenas o adaptador do anfitrião (como o corpo é lido e analisado) e a apresentação diferem por superfície: o painel de tarefas do Outlook analisa o corpo com o seu próprio DOMParser e é executado no lado do cliente; o extra do Gmail analisa com node-html-parser e é executado no lado do servidor na infraestrutura Apps Script V8 da Google (alojamento gratuito, uma vez que a Google já detém o correio; implementado localmente de forma interativa via clasp). Como nenhum dos frameworks consegue mutar o DOM da mensagem renderizada em modo de leitura (os setAsync/prependAsync do Office são apenas de composição; o CardService renderiza cartões, não HTML de mensagem), ambos os extras apresentam um painel/cartão de conclusões, não uma anotação inline — o caminho de mutação do DOM linkProcessor.ts existente e o REVEAL_URLS_CSS NÃO são reutilizados por nenhum dos extras. Um href relativo é resolvido apenas contra um <base href> de confiança no e-mail e de outra forma é ignorado, nunca contra a origem da caixa de correio/fornecedor (o que fabricaria um destino).

A base de código separa as responsabilidades de forma clara:

Estrutura do repositório

packages/core — o núcleo puro

Cada módulo aqui não importa nenhuma API de extensão (browser.*/chrome.*/messenger.*) e não usa nenhum sumidouro de DOM string-para-marcação (innerHTML/insertAdjacentHTML). Esse contrato é mecanicamente imposto por packages/core/test/purity.test.ts, que percorre com glob cada src/**/*.ts, retira os corpos de comentários via o seu próprio stripComments (para que os docblocks que legitimamente NOMEIAM os tokens proibidos não acionem a guarda) e depois afirma que nem EXTENSION_API_PATTERN nem UNSAFE_DOM_PATTERN correspondem a qualquer módulo. A superfície pública é reexportada a partir de packages/core/src/index.ts. A única dependência de runtime é tldts (para comparação de domínios ciente do sufixo público).

Os módulos são:

packages/webext — os invólucros da API do navegador

Os módulos aqui PODEM usar browser.* e são o único sítio onde as APIs de WebExtension são tocadas. A superfície pública é reexportada a partir de packages/webext/src/index.ts. Os módulos são: content.ts (ciclo de vida de conteúdo), contentRegistration.ts (registo dinâmico), storage.ts (persistência de configuração), background.ts (comportamento na instalação), toolbarAction.ts (alternância + emblema), messageDisplay.ts e messageDisplayBackground.ts (Thunderbird), e options/options.ts (o controlador da página de definições).

extensions/<engine> — pontos de entrada finos e manifestos

Cada motor tem o seu próprio manifest.json e icons/. O Chrome, o Firefox e o Thunderbird adicionalmente fornecem um src/ de pontos de entrada; o Edge, o Opera e o Safari têm apenas um manifesto e ícones e REUTILIZAM o src/ do Chrome (declarado pelo descritor de compilação — ver abaixo). Os pontos de entrada são deliberadamente minúsculos: por exemplo extensions/chrome/src/content.ts é apenas void createContentController().bootstrap();, e extensions/chrome/src/background.ts chama registerBackground, registerContentReconciliation e registerToolbarToggle. O ponto de entrada de fundo do Firefox é idêntico ao do Chrome; o do Thunderbird (extensions/thunderbird/src/background.ts) chama registerBackground e registerMessageDisplay em vez disso (não tem content scripts). O extensions/_template é um scaffold Chromium pronto a copiar, não um alvo de compilação.

tooling, features e os testes de fumo

O tooling contém build.mjs, version.mjs e icons.mjs. O features contém os cenários Gherkin, as suas definições de passos e os test doubles fiéis em features/support/webext.ts e features/support/world.ts. Cada motor também tem um extensions/<engine>/test/manifest.smoke.test.mjs (o do _template é template.smoke.test.mjs) — estes são o contrato de manifesto autoritativo (ver "Compilação por motor e o contrato de manifesto" abaixo).

O pipeline de revelação

O caminho de conteúdo começa num ponto de entrada de motor como extensions/chrome/src/content.ts, que chama createContentController().bootstrap() a partir de packages/webext/src/content.ts.

Bootstrap

createContentController bootstrap é executado uma vez por frame:

  1. Reivindica o frame de forma síncrona, antes de qualquer await, via claimBootstrap, que lê/define a flag BOOTSTRAP_MARKER no window do frame. Uma entrada estática incorporada de content_scripts e um user script dinâmico sobreposto podem ambos injetar content.js no mesmo frame; o window de mundo isolado partilhado torna isto na guarda correta de execução única, e reivindicar antes de qualquer await significa que duas injeções quase simultâneas não podem ambas passá-la.
  2. Carrega a configuração persistida PRIMEIRO via loadConfigWithRetry (BOOTSTRAP_CONFIG_RETRIES tentativas extra numa rejeição transitória), mantendo o marcador ao longo das tentativas para que uma injeção sobreposta que já se anulou não fique presa. Só depois de a leitura ter êxito é que injeta a folha de estilos (injectStyles, que atribui REVEAL_URLS_CSS via textContent), pelo que uma leitura falhada não acrescenta nenhum <style> e uma nova tentativa não o pode duplicar.
  3. Se o carregamento da configuração falhar em todas as tentativas, releaseBootstrap reverte o marcador (só aqui, antes de a anotação começar) para que uma reinjeção posterior possa tentar de novo; uma vez que start tenha sido executado o marcador tem de persistir, porque um anotador novo não seria dono dos nós da passagem anterior.
  4. Quando config.enabled chama start, e regista sempre um ouvinte onConfigChanged que reaplica a configuração (cobrindo a transição desativado→ativado).

Resolver a regra de site ativa

start (e applyConfig) chama resolveSiteRule para decidir se — e onde — anotar. resolveSiteRule tenta primeiro a PRÓPRIA localização do documento em execução via resolveForHref, que filtra config.sites para as regras ativadas e delega ao selectMostSpecific do núcleo para que ganhe a correspondência mais específica (um incorporado específico vence um wildcard de utilizador amplo). Quando a própria localização não corresponde a nada E a própria origem do documento é opaca ou herdada — condicionado por hasOpaqueOrigin, verdadeiro apenas para about:blank/about:srcdoc (herdado via INHERITED_ORIGIN_URLS) ou blob:/data: (OPAQUE_ORIGIN_SCHEMES) — recorre, por ordem, ao frame de TOPO (readTopHref, o caso incorporado do iframe de mensagem do Proton), à janela OPENER (readOpenerHref, a janela de leitura pop-out do Outlook aberta via window.open("about:blank")) e finalmente ao document.referrer (readReferrer). Cada leitura externa é protegida contra o acesso entre origens (que lança exceção) e contribui com um candidato apenas quando legível. Um documento ORDINÁRIO não correspondido é autoritativo e nunca herda uma regra do contexto ambiente, deixando o controlador inerte.

Limitar à raiz de conteúdo e observar mutações

start regista o seletor contentRoot da regra correspondente, cria o anotador via createAnnotator(config), e processa cada raiz correspondente de selectRoots(doc, contentRoot) (um querySelectorAll envolvido em try/catch para que um seletor inválido falhe em segurança para []). Depois observa doc.body (um contentor estável) com OBSERVER_OPTIONS ({ childList: true, subtree: true }).

O observador alimenta processMutations, que condiciona cada mutação à raiz de conteúdo: reprocessa um target de mutação apenas quando é um Element dentro de uma raiz (isWithinRoot, via closest), e encaminha cada Element adicionado através de processAddedNode, que anota um nó que está dentro de uma raiz, É uma raiz, ou CONTÉM uma (uma montagem SPA preguiçosa). Assim, tanto a agitação dentro da raiz como um corpo de mensagem irmão a montar mais tarde são ambos apanhados, enquanto a interface da aplicação fora das raízes é deixada intacta. Um anotador nulo (a extensão está parada) é uma operação nula.

applyConfig reconcilia uma alteração de configuração ao vivo: desmonta quando nenhuma regra corresponde ou a flag de ativação está desligada; reinicia (reverter + reobservar) quando o contentRoot resolvido mudou; e de outra forma reflui no lugar — annotator.setConfig depois annotator.process sobre cada raiz correspondente — sem reiniciar o observador.

Anotar uma única âncora

O anotador vive em packages/core/src/linkProcessor.ts. createAnnotator devolve um Annotator sobre estado de closure que a página não consegue ler: um Map forte com chave por identidade de âncora, um token data-ru por anotador de generateToken (crypto.getRandomValues), e a configuração atual. O seu método process(root):

  1. Poda as entradas de registo cuja âncora já não está ligada.
  2. Recolhe âncoras candidatas com collectAnchors (descendentes a[href] mais a própria raiz quando é um a[href], sem duplicados).
  3. Resolve o destino de cada âncora com resolveAnnotatableUrl, que devolve null (ignorar) para um href vazio/na mesma página, um href não analisável, qualquer esquema diferente de http:/https:, ou um anfitrião igual ou sob uma entrada ignoreHosts; resolve hrefs relativos contra baseURI para que o resultado seja absoluto.
  4. Toma o caminho rápido idempotente quando isAnnotationIntact confirma que a impressão digital de renderização não mudou e que cada nó criado ainda está ligado e no lugar; de outra forma reverte a anotação obsoleta e volta a renderizar.
  5. Calcula a não correspondência com hostMismatch sobre o texto limpo da âncora, honra showOnlyOnMismatch (deixando as ligações honestas intactas), e chama annotateAnchor.

annotateAnchor renderiza apenas SAFE-DOM. No modo "inline" antepõe um <span class="reveal-urls-url"> cujo URL é definido via textContent (prefixado com uma seta e um espaço inquebrável U+00A0, truncado por truncateUrl) seguido de um <br>, deixando os próprios filhos da ligação intactos; o span ligado recebe então um fundo de contraste via applyContrastBackdrop. No modo "title" guarda o title original (TitleSave), sobrescreve-o com o URL — a única escrita na âncora — e, numa não correspondência enfatizada, anexa um emblema irmão <span class="reveal-urls-warn"> com REVEAL_URLS_WARN_LABEL. Cada nó criado é etiquetado data-ru="<token>" (nunca a âncora), e cada entrada regista o seu configFingerprint para que uma alteração de configuração de renderização force um refluxo. As cores e as substituições de tipo de letra são aplicadas através de propriedades tipadas element.style.* (applyColour/applyFontOverrides), nunca interpoladas numa string de CSS. revertOwned remove os nós criados por referência e restaura o título guardado; revertAll faz isto para cada âncora possuída e limpa o registo.

truncateUrl opera sobre PONTOS DE CÓDIGO Unicode ([...displayHref]), pelo que um corte nunca divide um par de substituição; preserva a origem esquema-e-anfitrião e encurta o resto com um URL_ELLIPSIS final.

Não correspondência de anfitrião

packages/core/src/hostMismatch.ts hostMismatch compara o texto visível da ligação contra o seu href por domínio registável, não pelo nome de anfitrião bruto, pelo que um subdomínio honesto não é assinalado enquanto um semelhante é. extractHostCandidates divide o texto e reduz cada token via hostCandidate (que exige um ponto e um sufixo público ICANN reconhecido pelo tldts, rejeitando texto com pontos comum como e.g). Tanto o anfitrião do href como cada candidato são reduzidos ao seu domínio registável com tldts.getDomain, e QUALQUER candidato cujo domínio difira do do href produz uma não correspondência — pelo que nomear o verdadeiro anfitrião malicioso ao lado de um anfitrião isco não pode suprimir o aviso. Nunca lança exceção.

Estilos e contraste

packages/core/src/styles.ts contém o REVEAL_URLS_CSS injetável (disposição fixa, espessura, forma e um fundo de chip branco fixo, totalmente opaco — background: #ffffff, opacity: 1; as cores NUNCA são interpoladas nele) e os auxiliares de runtime applyColour/applyFontOverrides/applyContrastBackdrop. applyContrastBackdrop lê a cor resolvida do elemento e o primeiro fundo opaco encontrado ao percorrer do PRÓPRIO elemento para cima (o seu próprio fundo — o branco predefinido fixo do chip ou uma substituição mais específica da página anfitriã — antes de qualquer ascendente) através do getComputedStyle padrão (obtido de element.ownerDocument?.defaultView, pelo que é seguro em frames e stubbable) e, quando packages/core/src/contrast.ts needsWhiteBackdrop reporta que o texto falha o WCAG AA (CONTRAST_THRESHOLD) contra esse fundo E o branco genuinamente ajuda, define style.backgroundColor = "white". contrast.ts fornece parseColour, relativeLuminance e contrastRatio; analisa as formas rgb()/rgba()/hex que o getComputedStyle devolve e trata uma cor totalmente transparente como "não encontrada".

Anfitriões configuráveis e âmbito de conteúdo

Uma SiteRule (packages/core/src/config.ts) diz ONDE a anotação é executada (match, allFrames) e QUAL contentor a limita (contentRoot), mais enabled e uma flag builtin. Os DEFAULT_SITES fornecidos cobrem o Gmail, o Proton (com allFrames) e ambos os anfitriões do Outlook. A Config agrega os interruptores globais, as cores, as substituições de tipo de letra e o array sites.

O motor de padrões de correspondência

packages/core/src/matchPattern.ts contém uma gramática de tempo de adição deliberadamente RESTRITIVA MATCH_PATTERN (apenas http/https, wildcard de anfitrião principal *. opcional, glob de caminho; sem porta, sem esquema *, sem <all_urls>). matchesPattern faz corresponder um URL contra um padrão validado, delegando o caminho a pathGlobMatches (cada * corresponde a qualquer sequência de caracteres); falha em segurança para false. parsePatternParts divide um padrão validado em host/path/scheme/wildcardHost.

As regras sobrepostas resolvem-se por "o mais específico vence": compareSiteSpecificity classifica por (1) anfitrião exato sobre anfitrião wildcard, (2) anfitrião literal mais longo, (3) caminho mais literal (literalPathLength), depois (4) um desempate ASCII determinístico; selectMostSpecific devolve a regra ativada mais específica que corresponde a um URL.

A cobertura de origem concedida é um correspondente SEPARADO, deliberadamente MAIS AMPLO. permissions.getAll() pode reportar concessões na gramática completa de WebExtension (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*) que o MATCH_PATTERN restritivo rejeitaria. parseGrantedOrigin analisa essas em GrantedOriginParts, e originCovers reporta se uma origem concedida cobre o match de uma regra (por exemplo, uma concessão ampla https://*/* cobre genuinamente cada regra https). matchAllowsOriginFallback reporta se o caminho de uma regra é exatamente ORIGIN_FALLBACK_PATH (/*) — a interseção segura das restrições do Chromium e do Gecko/Firefox 128 sobre o fallback dinâmico de origem opaca.

Registo dinâmico

packages/webext/src/contentRegistration.ts regista um content script dinâmico por regra adicionada pelo utilizador. Os incorporados são servidos pelas entradas estáticas content_scripts e NUNCA são registados dinamicamente. desiredContentScripts filtra config.sites para regras não incorporadas e ativadas cuja origem isOriginGranted (delegando a originCovers), mapeia cada uma para um RegisteredContentScript com um id estável (contentScriptId, um hash FNV-1a do match para que o id use apenas o conjunto de caracteres seguro para a API), e define matchOriginAsFallback: true APENAS quando matchAllowsOriginFallback(rule.match) se verifica (um caminho /*). Condicionar a flag desta forma evita que uma regra de caminho não conforme faça com que todo o registo em lote seja rejeitado; tal regra regista-se sem cobertura pop-out por design.

reconcileContentScriptsOnce lê as origens concedidas, a configuração armazenada e os registos ao vivo, depois converge por uma diferença CIENTE DE PROPRIEDADES: sameRegistration projeta tanto o descritor desejado como a leitura ao vivo numa forma normalizada (aplicando a predefinição real de WebExtension de cada campo — allFrames/matchOriginAsFallback predefinem false, persistAcrossSessions predefine TRUE, runAt predefine document_idle) para que um script recém-registado compare IGUAL ao descritor que o produziu e nenhum ciclo de novo registo possa surgir. Como não há updateContentScripts, um script alterado é anulado o registo e depois registado de novo. reconcileContentScripts envolve isto com uma promessa em curso (inFlight) para que dois acionamentos quase simultâneos (uma alteração de configuração E permissions.onAdded a disparar ambos numa adição de site) sejam serializados em vez de correrem em competição. registerContentReconciliation liga os acionamentos (permissions.onAdded/onRemoved, onConfigChanged) e converge uma vez no arranque; é importado apenas pelos pontos de entrada de fundo do Chrome e do Firefox.

O fluxo de permissão opt-in

Os manifestos estáticos declaram optional_host_permissions (http://*/*, https://*/*) para que um utilizador possa conceder um anfitrião web adicional arbitrário em tempo de execução. packages/webext/src/options/options.ts addSite valida TODA a regra candidata (match E raiz de conteúdo) através de normaliseSiteRule ANTES de pedir qualquer permissão, deriva a origem do anfitrião com matchOrigin, e chama browser.permissions.request a partir do gesto de clique do botão Adicionar; apenas na concessão é que anexa uma linha builtin:false e persiste através do caminho de escrita canónico.

Configuração, armazenamento e o padrão canónico

O único funil de validação

normaliseConfig em packages/core/src/config.ts é o único caminho de leitura canónico: coage e limita cada campo, recorrendo à predefinição em tudo o que for inválido, e nunca lança exceção. É construído a partir de validadores por campo — normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts (reduzindo cada entrada a um nome de anfitrião punycode simples via bareHostname), normaliseMatchColour/normaliseMismatchColour (condicionados pelo isSafeColour de safeColour.ts), normaliseCssSize, normaliseFontWeight, normaliseContentRoot (limitado por CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH), normaliseMatchPattern (condicionado por MATCH_PATTERN) e normaliseSites. normaliseSites descarta os rejeitados, remove duplicados por match, FORÇA builtin: true em qualquer regra cujo match seja igual ao de um incorporado (fechando o vetor de correspondência-exata-oculta-incorporado / dupla injeção), volta a semear qualquer incorporado em falta (para que um armazenamento adulterado não possa remover um fornecedor central) e ordena alfanumericamente por match para uma ordem canónica. configFingerprint faz hash apenas dos campos que afetam a renderização (excluindo enabled, ignoreHosts e sites, que conduzem iniciar/parar e ignorar em vez de refluir) via fnv1a.

Armazenamento

packages/webext/src/storage.ts envolve a API de armazenamento de WebExtension. configArea usa browser.storage.sync quando disponível (para que as definições viagem) e recorre a browser.storage.local (por exemplo no Thunderbird); configAreaName reporta qual está ativo. getConfig, setConfig e onConfigChanged todos canalizam o seu valor bruto através de normaliseConfig, pelo que um armazenamento malformado ou adulterado nunca pode entregar uma Config inválida a um chamador. onConfigChanged adicionalmente ignora eventos da área de armazenamento INATIVA e alterações a chaves não relacionadas.

Os dois extras reutilizam o mesmo funil ler -> normaliseConfig -> devolver sobre os seus próprios armazenamentos de anfitrião. O extensions/outlook/src/roamingStorage.ts do painel de tarefas do Outlook envolve Office.context.roamingSettings (get/set síncronos, confirmados com saveAsync); o packages/gmail/src/propertiesStorage.ts do extra do Gmail envolve PropertiesService.getUserProperties() (por utilizador, com roaming entre os dispositivos desse utilizador, codificado em JSON sob uma chave, e não precisando de NENHUM âmbito OAuth extra — nunca as ScriptProperties partilhadas). Ambos expõem getConfig/setConfig mais os acessores relevantes para o cartão getIgnoreHosts/getHighlightMismatch e protegem uma superfície de anfitrião indisponível com um erro dedicado. O acionamento de página inicial do Gmail renderiza um cartão de definições CardService a partir de getConfig, e o manipulador de submissão de formulário onSaveSettings funde o ignoreHosts/highlightMismatch analisado sobre a configuração atual e persiste-o através de setConfig; o acionamento contextual encaminha ignoreHosts para o adaptador e highlightMismatch para o construtor de cartões, lendo de forma defensiva para que uma falha de armazenamento recorra às predefinições em vez de quebrar a análise de mensagens.

O padrão configuração-canónica + atualização-direcionada

Três caminhos de escrita leem a configuração canónica, alteram exatamente uma coisa e escrevem-na de volta — nunca confirmando edições de formulário não guardadas ou voltando a renderizar o formulário:

O próprio toggleEnabled da ação da barra de ferramentas (packages/webext/src/toolbarAction.ts) segue o mesmo padrão a partir do lado de fundo.

Compilação por motor e o contrato de manifesto

tooling/build.mjs conduz uma única compilação esbuild para cada alvo de WebExtension. O descritor TARGETS nomeia o ALVO DE ORIGEM de cada alvo: o Chrome, o Firefox e o Thunderbird fornecem o seu próprio src/; o Edge, o Opera e o Safari declaram chrome como a sua origem; o suplemento do Outlook (Office.js) é a sua própria origem. ACTIVE_TARGETS (Chrome, Edge, Firefox, Opera e Thunderbird — o Safari e o Outlook estão excluídos) é o que --all/--package compilam; o Safari e o Outlook compilam apenas quando nomeados explicitamente, e o extra do Gmail (Apps Script) é um pacote esbuild separado (make build-gmail) fora do ciclo --all. Esse pacote do Gmail visa o runtime V8 do Apps Script, que não tem nem módulos ES nem um URL nativo: é construído como ESM e depois tem a sua instrução export {…} final retirada (para que as funções de acionamento permaneçam globais de nível superior que o Apps Script pode invocar), e empacota um pequeno polyfill de URL (instalado apenas quando URL está ausente) para que o new URL(...) do núcleo partilhado resolva ligações no lado do servidor. buildTarget limpa dist/<target>/, copia o manifest.json e icons/ por alvo textualmente, empacota cada script de origem como IIFE (os INJECTED_SCRIPTS injetados content.ts/messageDisplay.ts não podem ser módulos ES, e os fundos são workers clássicos/event pages também) e a página de opções partilhada como ESM, e copia options.html/options.css de packages/webext/src/options/. O webextension-polyfill é empacotado em cada script.

tooling/version.mjs apõe versões: pega em MAJOR.MINOR do package.json raiz e anexa um número de BUILD auto-incrementável (lido do terceiro componente existente mais alto entre os manifestos de alvo) em cada manifesto de alvo, mantendo-os em sincronia. A maioria dos alvos tem um único manifest.json; o suplemento do Outlook tem dois manifestos versionados (manifest.json e manifest.xml) e ambos são apostos, e o scaffold _template é aposto em sincronia também. O package.json raiz é mantido na mesma versão, mas permanece semver válido (uma versão de quatro partes é permitida num manifesto, nunca em package.json). tooling/icons.mjs (executado via make icons) rasteriza a única fonte vetorial assets/icon.svg em cada icons/icon48.png e icons/icon128.png de alvo, tentando cairosvg, depois inkscape, depois ImageMagick.

O contrato de manifesto — os campos exatos exigidos por motor — é a única fonte de verdade em cada extensions/<engine>/test/manifest.smoke.test.mjs (por exemplo o assertManifestShape de extensions/chrome/test/manifest.smoke.test.mjs, que também constrói o alvo e confirma que cada ficheiro referenciado resolve na dist). Por convenção de desduplicação do projeto, a forma campo-a-campo NÃO é reenumerada aqui; consulte esses testes de fumo e a nota "Contrato de manifesto" no plano de teste manual.

Internacionalização (i18n)

O Reveal URLs está localizado em muitos idiomas além do inglês. O conjunto é fornecido a partir de uma única fonte em packages/core/src/locales.json (SUPPORTED_LOCALES, nomes nativos e a predefinição), que tanto os pacotes da extensão (via a importação JSON resolvida) como a compilação do sítio web em Node simples leem. Cada string não inglesa é traduzida automaticamente e está em espera de revisão humana; o marcador de proveniência é por formato (os _locales usam a description de cada mensagem, os dicionários de chrome do sítio web registam-no em site/i18n/README.md, e as fontes de documentos traduzidos transportam um comentário HTML na primeira linha).

Há três superfícies de localização independentes, deliberadamente não interligadas, e cada uma é dividida ao longo de uma linha tempo-de-compilação / tempo-de-execução.

Supressão de "Sites ativos" no cliente de e-mail (AD-3)

Um alvo de cliente de e-mail (o Thunderbird hoje) já vê cada mensagem renderizada, pelo que o editor "Sites ativos" por anfitrião é redundante aí e é removido. A decisão é transportada por uma flag de descritor por alvo documentada, mailClient: true, no TARGETS.thunderbird de tooling/build.mjs — nunca um nome de alvo codificado na lógica de compilação ou UI — e é implementada em duas metades:

Postura de segurança e privacidade

Testes