Reveal URLs — Архітектура

Цей документ описує, як структуровано розширення Reveal URLs і як працює його основна логіка, для дописувачів і технічних читачів. Він цитує реальні файли та символи протягом усього тексту (у формі path symbol), тож будь-яке твердження можна перевірити проти вихідного коду. Опис для користувача див. у посібнику; щодо кроків ручної перевірки див. план ручного тестування.

Огляд

Reveal URLs — це єдине MV3 WebExtension, зібране для кількох рушіїв — Chrome, Edge, Opera, Firefox і Thunderbird — з одного спільного, чистого ядра коду. (Safari відкладено: він має маніфест і збирається, коли названий явно, але виключений із типової збірки та з CI.) Розширення розкриває URL-адресу призначення кожного посилання поряд із посиланням у відображеному листі та позначає посилання, чий видимий текст називає інший реєстрований домен, ніж його href — класичний ознака фішингу.

Репозиторій — це монорепозиторій pnpm/TypeScript (pnpm-workspace.yaml, package.json з "private": true). Пакування — це єдиний скрипт на основі esbuild, tooling/build.mjs, який компілює спільні пакети та тонкі точки входу кожного рушія в завантажуваний каталог dist/<target>/. webextension-polyfill вбудовується в кожен скрипт, а не покладається на нього як на глобальну змінну середовища виконання, тож той самий вихідний код працює незмінно в Chromium і Gecko.

Дві рідні поштові надбудови розширюють те саме ядро виявлення на поверхні, до яких WebExtension не може дістатися: надбудова Outlook (панель завдань Office.js, що охоплює Outlook у web/Windows/Mac/iOS/Android і Outlook.com) і надбудова Gmail (Apps Script CardService, що охоплює вебзастосунки Gmail web/Android/iOS). Обидві повторно використовують лише ЧИСТИЙ аналіз — новий модуль packages/core/src/findings.ts (analyseAnchors/analyseHtml, платформо-нейтральна модель Finding[]), який повторно використовує наявну логіку hostMismatch і виведення реєстрованого домену tldts. Лише адаптер хоста (як читається й парситься тіло) і подання різняться на кожній поверхні: панель завдань Outlook парсить тіло власним DOMParser і працює на боці клієнта; надбудова Gmail парсить за допомогою node-html-parser і працює на боці сервера в інфраструктурі Apps Script V8 Google (безкоштовний хостинг, оскільки Google уже зберігає пошту; розгортається локально-інтерактивно через clasp). Оскільки жоден фреймворк не може змінити відображений DOM повідомлення в режимі читання (Office'ові setAsync/prependAsync призначені лише для створення; CardService відображає картки, а не HTML повідомлення), обидві надбудови подають панель/картку знахідок, а не вбудовану анотацію — наявний шлях мутації DOM linkProcessor.ts і REVEAL_URLS_CSS НЕ повторно використовуються жодною надбудовою. Відносний href розв'язується лише проти надійного внутрішньоповідомленого <base href> і в іншому разі пропускається, ніколи проти джерела поштової скриньки/провайдера (що сфабрикувало б призначення).

Кодова база різко розділяє відповідальності:

Структура репозиторію

packages/core — чисте ядро

Кожен модуль тут не імпортує жодного API розширення (browser.*/chrome.*/messenger.*) і не використовує жодного приймача DOM «рядок-у-розмітку» (innerHTML/insertAdjacentHTML). Цей контракт механічно забезпечується packages/core/test/purity.test.ts, який проходить кожен src/**/*.ts, знімає тіла коментарів через власний stripComments (тож докблоки, що легітимно НАЗИВАЮТЬ заборонені токени, не спрацьовують на охоронці) і потім стверджує, що ані EXTENSION_API_PATTERN, ані UNSAFE_DOM_PATTERN не збігається з жодним модулем. Публічна поверхня повторно експортується з packages/core/src/index.ts. Єдина залежність середовища виконання — tldts (для порівняння доменів з обізнаністю про публічні суфікси).

Модулі такі:

packages/webext — обгортки браузерних API

Модулі тут МОЖУТЬ використовувати browser.* і є єдиним місцем, де торкаються API WebExtension. Публічна поверхня повторно експортується з packages/webext/src/index.ts. Модулі такі: content.ts (життєвий цикл вмісту), contentRegistration.ts (динамічна реєстрація), storage.ts (збереження конфігурації), background.ts (поведінка під час установлення), toolbarAction.ts (перемикання + значок), messageDisplay.ts і messageDisplayBackground.ts (Thunderbird) та options/options.ts (контролер сторінки налаштувань).

extensions/<engine> — тонкі точки входу та маніфести

Кожен рушій несе власний manifest.json та icons/. Chrome, Firefox і Thunderbird додатково постачають src/ точок входу; Edge, Opera і Safari несуть лише маніфест та піктограми й ПОВТОРНО ВИКОРИСТОВУЮТЬ Chrome'ів src/ (оголошено дескриптором збірки — див. нижче). Точки входу навмисно крихітні: наприклад extensions/chrome/src/content.ts — це просто void createContentController().bootstrap();, а extensions/chrome/src/background.ts викликає registerBackground, registerContentReconciliation і registerToolbarToggle. Фонова точка входу Firefox ідентична Chrome'овій; Thunderbird'ова (extensions/thunderbird/src/background.ts) натомість викликає registerBackground і registerMessageDisplay (він не має контентних скриптів). extensions/_template — це готовий до копіювання каркас Chromium, а не ціль збірки.

tooling, features та димові тести

tooling містить build.mjs, version.mjs і icons.mjs. features містить Gherkin-сценарії, їхні визначення кроків і достовірні тестові двійники в features/support/webext.ts і features/support/world.ts. Кожен рушій також несе extensions/<engine>/test/manifest.smoke.test.mjs (_template'ів — template.smoke.test.mjs) — це авторитетний контракт маніфеста (див. «Збірка для кожного рушія та контракт маніфеста» нижче).

Конвеєр розкриття

Шлях вмісту починається з точки входу рушія, як-от extensions/chrome/src/content.ts, що викликає createContentController().bootstrap() з packages/webext/src/content.ts.

Ініціалізація

createContentController bootstrap виконується один раз на фрейм:

  1. Він синхронно захоплює фрейм, перед будь-яким await, через claimBootstrap, який читає/встановлює прапорець BOOTSTRAP_MARKER на window фрейму. Статична вбудована запис content_scripts і перекривний динамічний користувацький скрипт можуть обидва інжектувати content.js в той самий фрейм; спільний ізольований window робить це правильним одноразовим охоронцем, а захоплення перед будь-яким await означає, що дві майже одночасні інжекції не можуть обидві його пройти.
  2. Він завантажує збережену конфігурацію ПЕРШОЮ через loadConfigWithRetry (BOOTSTRAP_CONFIG_RETRIES додаткових спроб при тимчасовому відхиленні), утримуючи маркер під час повторних спроб, тож перекривна інжекція, що вже виконала no-op, не залишається покинутою. Лише щойно читання вдається, він інжектує таблицю стилів (injectStyles, яка присвоює REVEAL_URLS_CSS через textContent), тож невдале читання не додає жодного <style>, а повторна спроба не може його дублювати.
  3. Якщо завантаження конфігурації зазнає невдачі на кожній спробі, releaseBootstrap відкочує маркер назад (лише тут, перед початком анотації), тож пізніша повторна інжекція може повторити спробу; щойно start виконається, маркер має зберігатися, оскільки свіжий анотатор не володів би вузлами попереднього проходу.
  4. Коли config.enabled, він викликає start і завжди реєструє слухач onConfigChanged, що повторно застосовує конфігурацію (охоплюючи перехід вимкнено→увімкнено).

Визначення активного правила сайту

startapplyConfig) викликає resolveSiteRule, щоб вирішити, чи — і де — анотувати. resolveSiteRule спершу пробує ВЛАСНЕ розташування виконуваного документа через resolveForHref, який фільтрує config.sites до ввімкнених правил і делегує ядровому selectMostSpecific, тож перемагає найспецифічніший збіг (специфічний вбудований перемагає широкий користувацький підстановний знак). Коли власне розташування нічого не збігає І власне джерело документа непрозоре чи успадковане — обмежено hasOpaqueOrigin, істинне лише для about:blank/about:srcdoc (успадковано через INHERITED_ORIGIN_URLS) чи blob:/data: (OPAQUE_ORIGIN_SCHEMES) — він повертається, у порядку, до ВЕРХНЬОГО фрейму (readTopHref, вбудований випадок повідомлення-iframe Proton), вікна ВІДКРИВАЧА (readOpenerHref, спливне вікно читання Outlook, відкрите через window.open("about:blank")) і нарешті document.referrer (readReferrer). Кожне зовнішнє читання захищене від міжджерельного доступу (що кидає виняток) і робить внесок кандидата лише коли читабельне. ЗВИЧАЙНИЙ незбіжний документ є авторитетним і ніколи не успадковує правило з навколишнього контексту, залишаючи контролер інертним.

Обмеження кореня вмісту та спостереження за мутаціями

start записує селектор contentRoot збіжного правила, створює анотатор через createAnnotator(config) і обробляє кожен збіжний корінь із selectRoots(doc, contentRoot) (querySelectorAll, обгорнутий у try/catch, тож недійсний селектор безпечно зазнає невдачі до []). Потім він спостерігає за doc.body (стабільний контейнер) з OBSERVER_OPTIONS ({ childList: true, subtree: true }).

Спостерігач живить processMutations, який обмежує кожну мутацію коренем вмісту: він повторно обробляє target мутації лише коли він є Element у межах кореня (isWithinRoot, через closest) і скеровує кожен доданий Element через processAddedNode, який анотує вузол, що всередині кореня, Є коренем або МІСТИТЬ його (лінива монтація SPA). Тож внутрішньокоренева зміна й сусіднє тіло повідомлення, що монтується пізніше, обидва вловлюються, тоді як інтерфейс застосунку поза коренями залишається незайманим. Нульовий анотатор (розширення зупинено) — це no-op.

applyConfig узгоджує живу зміну конфігурації: він розбирає, коли жодне правило не збігається чи прапорець увімкнення вимкнено; перезапускає (відкат + повторне спостереження), коли розв'язаний contentRoot змінився; і в іншому разі переливає на місці — annotator.setConfig потім annotator.process над кожним збіжним коренем — без перезапуску спостерігача.

Анотування одного посилання

Анотатор живе в packages/core/src/linkProcessor.ts. createAnnotator повертає Annotator над станом замикання, який сторінка не може прочитати: сильний Map, що ключується за ідентичністю посилання, токен data-ru для кожного анотатора з generateToken (crypto.getRandomValues) і поточна конфігурація. Його метод process(root):

  1. Обрізає записи реєстру, чиє посилання більше не з'єднане.
  2. Збирає кандидатні посилання з collectAnchors (нащадкові a[href] плюс сам корінь, коли він є a[href], з видаленням дублікатів).
  3. Розв'язує призначення кожного посилання з resolveAnnotatableUrl, що повертає null (пропустити) для порожнього/внутрішньосторінкового href, непарсабельного href, будь-якої схеми, крім http:/https:, чи хоста, рівного чи під записом ignoreHosts; він розв'язує відносні href проти baseURI, тож результат абсолютний.
  4. Бере ідемпотентний швидкий шлях, коли isAnnotationIntact підтверджує, що відбиток відображення незмінений і кожен створений вузол досі з'єднаний і на місці; в іншому разі відкочує застарілу анотацію й повторно відображає.
  5. Обчислює невідповідність з hostMismatch на чистому тексті посилання, шанує showOnlyOnMismatch (залишаючи чесні посилання незайманими) і викликає annotateAnchor.

annotateAnchor відображає лише БЕЗПЕЧНИЙ DOM. У режимі "inline" він додає перед <span class="reveal-urls-url">, чия URL встановлюється через textContent (з префіксом стрілки та нерозривного пробілу U+00A0, обрізаного truncateUrl), за яким слідує <br>, залишаючи власних дітей посилання незайманими; з'єднаний span потім отримує контрастний фон через applyContrastBackdrop. У режимі "title" він зберігає оригінальний title (TitleSave), перезаписує його URL-адресою — єдиний запис до посилання — і, при акцентованій невідповідності, додає сусідній значок <span class="reveal-urls-warn">, що несе REVEAL_URLS_WARN_LABEL. Кожен створений вузол позначено data-ru="<token>" (ніколи посилання), і кожен запис записує свій configFingerprint, тож зміна конфігурації відображення змушує перелив. Кольори та перевизначення шрифтів застосовуються через типізовані властивості element.style.* (applyColour/applyFontOverrides), ніколи не інтерполюються в рядок CSS. revertOwned видаляє створені вузли за посиланням і відновлює збережений title; revertAll робить це для кожного власного посилання й очищає реєстр.

truncateUrl оперує над Unicode КОДОВИМИ ТОЧКАМИ ([...displayHref]), тож розріз ніколи не розколює сурогатну пару; він зберігає джерело схеми-та-хоста й скорочує решту з кінцевим URL_ELLIPSIS.

Невідповідність хоста

packages/core/src/hostMismatch.ts hostMismatch порівнює видимий текст посилання проти його href за реєстрованим доменом, а не сирим іменем хоста, тож чесний піддомен не позначається, тоді як схожий на справжній позначається. extractHostCandidates розколює текст і зводить кожен токен через hostCandidate (що вимагає крапку й розпізнаний tldts публічний суфікс ICANN, відкидаючи звичайний точковий текст як e.g). І хост href, і кожен кандидат зводяться до їхнього реєстрованого домену з tldts.getDomain, і БУДЬ-ЯКИЙ кандидат, чий домен відрізняється від href'ового, дає невідповідність — тож називання справжнього шкідливого хоста поряд із хостом-приманкою не може придушити попередження. Він ніколи не кидає винятку.

Стилі та контраст

packages/core/src/styles.ts містить інжектований REVEAL_URLS_CSS (фіксований макет, товщина, форма й фіксований білий, повністю непрозорий фон чіпа — background: #ffffff, opacity: 1; кольори НІКОЛИ не інтерполюються в нього) і середовища виконання помічники applyColour/applyFontOverrides/applyContrastBackdrop. applyContrastBackdrop читає розв'язаний колір елемента та перший непрозорий фон, знайдений при обході від САМОГО елемента вгору (його власний фон — фіксований білий типовий чіпа чи специфічніше перевизначення хост-сторінки — перед будь-яким предком) через стандартний getComputedStyle (з джерела element.ownerDocument?.defaultView, тож він безпечний для фреймів і заглушуваний) і, коли packages/core/src/contrast.ts needsWhiteBackdrop повідомляє, що текст не проходить WCAG AA (CONTRAST_THRESHOLD) проти цього фону І білий справді допомагає, встановлює style.backgroundColor = "white". contrast.ts надає parseColour, relativeLuminance і contrastRatio; він парсить форми rgb()/rgba()/hex, які повертає getComputedStyle, і трактує повністю прозорий колір як «не знайдено».

Налаштовувані хости та обмеження вмісту

SiteRule (packages/core/src/config.ts) каже, ДЕ виконується анотація (match, allFrames) і ЯКИЙ контейнер її обмежує (contentRoot), плюс enabled і прапорець builtin. Постачені DEFAULT_SITES охоплюють Gmail, Proton (з allFrames) і обидва хости Outlook. Config агрегує глобальні перемикачі, кольори, перевизначення шрифтів і масив sites.

Рушій шаблонів відповідності

packages/core/src/matchPattern.ts містить навмисно ОБМЕЖУВАЛЬНУ граматику для часу додавання MATCH_PATTERN (лише http/https, необов'язковий провідний підстановний знак хоста *., glob-шлях; без порту, без схеми *, без <all_urls>). matchesPattern зіставляє URL проти валідованого шаблону, делегуючи шлях pathGlobMatches (кожен * збігає будь-яку послідовність символів); він безпечно зазнає невдачі до false. parsePatternParts розколює валідований шаблон на host/path/scheme/ wildcardHost.

Перекривні правила розв'язуються за «найспецифічніший перемагає»: compareSiteSpecificity ранжує за (1) точний хост над підстановним хостом, (2) довший буквальний хост, (3) більш буквальний шлях (literalPathLength), потім (4) детермінований ASCII-розв'язник; selectMostSpecific повертає найспецифічніше ввімкнене правило, що збігає URL.

Охоплення наданого джерела — це ОКРЕМИЙ, навмисно ШИРШИЙ зіставник. permissions.getAll() може повідомляти надання в повній граматиці WebExtension (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*), яку обмежувальний MATCH_PATTERN відкинув би. parseGrantedOrigin парсить їх у GrantedOriginParts, а originCovers повідомляє, чи надане джерело охоплює match правила (наприклад широке надання https://*/* справді охоплює кожне https-правило). matchAllowsOriginFallback повідомляє, чи шлях правила точно ORIGIN_FALLBACK_PATH (/*) — безпечний перетин обмежень Chromium і Gecko/Firefox 128 на динамічний непрозоро-джерельний запасний варіант.

Динамічна реєстрація

packages/webext/src/contentRegistration.ts реєструє один динамічний контентний скрипт на кожне додане користувачем правило. Вбудовані обслуговуються статичними записами content_scripts і НІКОЛИ не реєструються динамічно. desiredContentScripts фільтрує config.sites до невбудованих, увімкнених правил, чиє джерело isOriginGranted (делегуючи originCovers), відображає кожне в RegisteredContentScript зі стабільним id (contentScriptId, хеш FNV-1a збігу, тож id використовує лише API-безпечний набір символів) і встановлює matchOriginAsFallback: true ЛИШЕ коли matchAllowsOriginFallback(rule.match) виконується (шлях /*). Обмеження прапорця цим способом запобігає тому, щоб одне правило з невідповідним шляхом спричинило відхилення всієї пакетної реєстрації; таке правило реєструється без охоплення спливних вікон за задумом.

reconcileContentScriptsOnce читає надані джерела, збережену конфігурацію та живі реєстрації, потім сходиться ВЛАСТИВІСНО-ОБІЗНАНИМ diff'ом: sameRegistration проектує як бажаний дескриптор, так і живе зчитування на нормалізовану форму (застосовуючи реальне типове значення WebExtension кожного поля — allFrames/matchOriginAsFallback типово false, persistAcrossSessions типово TRUE, runAt типово document_idle), тож щойно зареєстрований скрипт порівнюється РІВНИМ дескриптору, що його породив, і жоден цикл повторної реєстрації не може виникнути. Оскільки немає updateContentScripts, змінений скрипт скасовується-реєстрацією потім реєструється знову. reconcileContentScripts обгортає це проміжним обіцянням (inFlight), тож два майже одночасні тригери (зміна конфігурації І permissions.onAdded, що обидва спрацьовують при додаванні сайту) серіалізуються, а не конкурують. registerContentReconciliation з'єднує тригери (permissions.onAdded/onRemoved, onConfigChanged) і сходиться один раз при запуску; він імпортується лише фоновими точками входу Chrome і Firefox.

Процес добровільного надання дозволів

Статичні маніфести оголошують optional_host_permissions (http://*/*, https://*/*), тож користувач може надати довільний додатковий вебхост під час виконання. packages/webext/src/options/options.ts addSite валідує ВСЕ кандидатне правило (match І content-root) через normaliseSiteRule ПЕРЕД запитом будь-якого дозволу, виводить джерело хоста з matchOrigin і викликає browser.permissions.request зсередини жесту клацання кнопки Додати; лише при наданні він додає рядок builtin:false і зберігає через канонічний шлях запису.

Конфігурація, сховище та канонічний шаблон

Єдина воронка валідації

normaliseConfig у packages/core/src/config.ts — це єдиний канонічний шлях читання: він приводить і обмежує кожне поле, повертаючись до типового значення при будь-чому недійсному, і ніколи не кидає винятку. Він побудований із валідаторів для кожного поля — normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts (зводячи кожен запис до простого punycode-імені хоста через bareHostname), normaliseMatchColour/normaliseMismatchColour (обмежено safeColour.ts isSafeColour), normaliseCssSize, normaliseFontWeight, normaliseContentRoot (обмежено CONTENT_ROOT_PATTERN/CONTENT_ROOT_MAX_LENGTH), normaliseMatchPattern (обмежено MATCH_PATTERN) і normaliseSites. normaliseSites відкидає відхилені, видаляє дублікати за match, ПРИМУШУЄ builtin: true на будь-якому правилі, чий match дорівнює вбудованому (закриваючи вектор точний-збіг-затіняє-вбудований / подвійна-інжекція), повторно засіває будь-який відсутній вбудований (тож підроблене сховище не може відкинути ядрового провайдера) і сортує алфавітно-цифрово за match для канонічного порядку. configFingerprint хешує лише поля, що впливають на відображення (виключаючи enabled, ignoreHosts і sites, які керують запуском/зупинкою та пропуском, а не переливом) через fnv1a.

Сховище

packages/webext/src/storage.ts обгортає API сховища WebExtension. configArea використовує browser.storage.sync, коли доступно (тож налаштування роумінгом переміщуються), і повертається до browser.storage.local (наприклад у Thunderbird); configAreaName повідомляє, яке активне. getConfig, setConfig і onConfigChanged усі провадять своє сире значення через normaliseConfig, тож спотворене чи підроблене сховище ніколи не може передати недійсний Config викликачеві. onConfigChanged додатково ігнорує події з НЕАКТИВНОЇ області сховища та зміни незв'язаних ключів.

Дві надбудови повторно використовують ту саму воронку читання -> normaliseConfig -> повернення над їхніми власними хост-сховищами. extensions/outlook/src/roamingStorage.ts панелі завдань Outlook обгортає Office.context.roamingSettings (синхронні get/set, підтверджені saveAsync); packages/gmail/src/propertiesStorage.ts надбудови Gmail обгортає PropertiesService.getUserProperties() (для кожного користувача, роумінгом переміщується між пристроями цього користувача, JSON-закодовано під одним ключем і не потребує жодної додаткової області OAuth — ніколи спільних ScriptProperties). Обидві надають getConfig/setConfig плюс релевантні для картки аксесори getIgnoreHosts/getHighlightMismatch і захищають недоступну хост-поверхню виділеною помилкою. Тригер домашньої сторінки Gmail відображає картку налаштувань CardService з getConfig, а обробник надсилання форми onSaveSettings зливає розібрані ignoreHosts/highlightMismatch на поточну конфігурацію й зберігає її через setConfig; контекстний тригер протягує ignoreHosts в адаптер і highlightMismatch у будівник картки, читаючи захисно, тож збій сховища повертається до типових значень, а не ламає аналіз повідомлень.

Шаблон канонічної конфігурації та точкового оновлення

Три шляхи запису читають канонічну конфігурацію, змінюють точно одну річ і записують її назад — ніколи не фіксуючи незбережені правки форми чи повторно не відображаючи форму:

Власний toggleEnabled дії панелі інструментів (packages/webext/src/toolbarAction.ts) слідує тому самому шаблону з фонового боку.

Збірка для кожного рушія та контракт маніфеста

tooling/build.mjs керує єдиною збіркою esbuild для кожної цілі WebExtension. Дескриптор TARGETS називає ЦІЛЬОВЕ ДЖЕРЕЛО кожної цілі: Chrome, Firefox і Thunderbird постачають власний src/; Edge, Opera і Safari оголошують chrome як своє джерело; надбудова Outlook (Office.js) — це власне джерело. ACTIVE_TARGETS (Chrome, Edge, Firefox, Opera і Thunderbird — Safari і Outlook виключені) — це те, що збирають --all/--package; Safari і Outlook збираються лише коли названі явно, а надбудова Gmail (Apps Script) — це окрема збірка esbuild (make build-gmail) поза циклом --all. Ця збірка Gmail націлена на середовище виконання V8 Apps Script, яке не має ані ES-модулів, ані рідного URL: вона зібрана як ESM і потім має свою кінцеву інструкцію export {…} знятою (тож функції-тригери залишаються глобальними верхнього рівня, які Apps Script може викликати), і вона вбудовує невеликий поліфіл URL (встановлений лише коли URL відсутній), тож ядрове new URL(...) розв'язує посилання на боці сервера. buildTarget очищає dist/<target>/, копіює manifest.json кожної цілі та icons/ дослівно, вбудовує кожен вихідний скрипт як IIFE (інжектовані INJECTED_SCRIPTS content.ts/messageDisplay.ts не можуть бути ES-модулями, а фони — теж класичні воркери/сторінки подій) і спільну сторінку опцій як ESM, і копіює options.html/options.css з packages/webext/src/options/. webextension-polyfill вбудовується в кожен скрипт.

tooling/version.mjs штампує версії: він бере MAJOR.MINOR з кореневого package.json і додає авто-інкрементний номер BUILD (зчитаний із найвищого наявного третього компонента серед маніфестів цілей) у кожен маніфест цілі, тримаючи їх у синхронізації. Більшість цілей несуть єдиний manifest.json; надбудова Outlook несе два версіоновані маніфести (manifest.json і manifest.xml) і обидва штампуються, і каркас _template штампується в синхронізації теж. Кореневий package.json тримається на тій самій версії, але залишається валідним semver (чотиричастинна версія дозволена в маніфесті, ніколи в package.json). tooling/icons.mjs (запускається через make icons) растеризує єдине векторне джерело assets/icon.svg в кожної цілі icons/icon48.png і icons/icon128.png, пробуючи cairosvg, потім inkscape, потім ImageMagick.

Контракт маніфеста — точні обов'язкові поля на кожен рушій — це єдине джерело істини в кожному extensions/<engine>/test/manifest.smoke.test.mjs (наприклад assertManifestShape в extensions/chrome/test/manifest.smoke.test.mjs, яка також збирає ціль і підтверджує, що кожен файл, на який посилаються, розв'язується в dist). За конвенцією проєкту про видалення дублювання, форма поле-за-полем НЕ перелічується тут знову; звертайтеся до цих димових тестів і до нотатки «Контракт маніфеста» у плані ручного тестування.

Інтернаціоналізація (i18n)

Reveal URLs локалізовано багатьма мовами, крім англійської. Набір єдиноджерельний у packages/core/src/locales.json (SUPPORTED_LOCALES, рідні імена й типове значення), який читають і збірки розширення (через розв'язаний JSON-імпорт), і чиста-Node збірка вебсайту. Кожен неанглійський рядок машинно перекладений і очікує перевірки людиною; маркер походження — на кожен формат (_locales використовують description кожного повідомлення, словники чрому вебсайту записують його в site/i18n/README.md, а перекладені джерела документів несуть першорядний HTML-коментар).

Є три незалежні поверхні локалізації, навмисно не перехресно з'єднані, і кожна розділена вздовж лінії час-збірки / час-виконання.

Придушення «Активних сайтів» у поштовому клієнті (AD-3)

Ціль поштового клієнта (Thunderbird сьогодні) уже бачить кожне відображене повідомлення, тож редактор «Активні сайти» за хостами там надлишковий і видаляється. Рішення несеться документованим дескриптором на кожну ціль, прапорцем mailClient: true, на TARGETS.thunderbird tooling/build.mjs — ніколи жорстко закодованим ім'ям цілі в логіці збірки чи UI — і реалізується у дві половини:

Постава безпеки та конфіденційності

Тестування