Reveal URLs — Αρχιτεκτονική

Αυτό το έγγραφο περιγράφει πώς είναι δομημένη η επέκταση Reveal URLs και πώς λειτουργεί η βασική λογική της, για συνεισφέροντες και τεχνικούς αναγνώστες. Παραπέμπει συνεχώς σε πραγματικά αρχεία και σύμβολα, στη μορφή path symbol, ώστε κάθε ισχυρισμός να μπορεί να ελεγχθεί στον πηγαίο κώδικα. Για την περιγραφή από την οπτική του χρήστη δείτε το manual. Για τα βήματα χειροκίνητης επαλήθευσης δείτε το manual test plan.

Επισκόπηση

Το Reveal URLs είναι μία ενιαία MV3 WebExtension που χτίζεται για πολλούς engines, δηλαδή Chrome, Edge, Opera, Firefox και Thunderbird, από έναν κοινό καθαρό πυρήνα κώδικα. Το Safari έχει αναβληθεί: διαθέτει manifest και μπορεί να γίνει build όταν ζητηθεί ρητά, αλλά αποκλείεται από το προεπιλεγμένο build και από το CI. Η επέκταση αποκαλύπτει τον προορισμό κάθε συνδέσμου δίπλα στον σύνδεσμο μέσα σε αποδιδόμενο email και επισημαίνει σύνδεσμο του οποίου το ορατό κείμενο ονομάζει διαφορετικό registrable domain από αυτό του href.

Το αποθετήριο είναι monorepo pnpm και TypeScript, με τα pnpm-workspace.yaml και package.json να το περιγράφουν και το "private": true να το κρατά εκτός δημόσιας δημοσίευσης ως πακέτου. Το bundling γίνεται από ένα μόνο script βασισμένο σε esbuild, το tooling/build.mjs, το οποίο μεταγλωττίζει τα κοινά packages και τα λεπτά entry points κάθε engine σε έναν φορτώσιμο κατάλογο dist/<target>/. Το webextension-polyfill γίνεται bundle μέσα σε κάθε script, αντί να βασίζεται σε global του runtime, ώστε ο ίδιος πηγαίος κώδικας να εκτελείται αμετάβλητος σε Chromium και Gecko.

Δύο εγγενή πρόσθετα email επεκτείνουν τον ίδιο πυρήνα ανίχνευσης σε επιφάνειες που δεν μπορεί να φτάσει η WebExtension: ένα Outlook Add-in βασισμένο σε Office.js task pane και ένα Gmail Add-on βασισμένο σε Apps Script CardService. Και τα δύο επαναχρησιμοποιούν μόνο την PURE ανάλυση, από το packages/core/src/findings.ts, με τα analyseAnchors και analyseHtml, το ουδέτερο μοντέλο Finding[], την υπάρχουσα λογική hostMismatch και την εξαγωγή registrable domain από το tldts. Μόνο ο adapter του host, δηλαδή ο τρόπος ανάγνωσης και parsing του σώματος, και η παρουσίαση διαφέρουν ανά επιφάνεια. Το Outlook task pane κάνει parsing με δικό του DOMParser και τρέχει client-side. Το Gmail add-on κάνει parsing με node-html-parser και τρέχει server-side πάνω στην Apps Script V8 υποδομή της Google. Επειδή κανένα από τα δύο frameworks δεν επιτρέπει μεταβολή του αποδιδόμενου DOM του μηνύματος σε λειτουργία ανάγνωσης, και τα δύο παρουσιάζουν panel ή card με ευρήματα και όχι inline annotation. Η υπάρχουσα διαδρομή DOM mutation του linkProcessor.ts και το REVEAL_URLS_CSS δεν επαναχρησιμοποιούνται από αυτά τα πρόσθετα. Ένα σχετικό href επιλύεται μόνο σε σχέση με αξιόπιστο εντός-email <base href> και διαφορετικά παραλείπεται.

Η βάση κώδικα χωρίζει αυστηρά τις ευθύνες:

Διάταξη αποθετηρίου

packages/core — ο καθαρός πυρήνας

Κανένα module εδώ δεν εισάγει extension API, όπως browser.*, chrome.* ή messenger.*, και κανένα δεν χρησιμοποιεί string-to-markup DOM sink όπως innerHTML ή insertAdjacentHTML. Αυτό το συμβόλαιο επιβάλλεται μηχανικά από το packages/core/test/purity.test.ts, το οποίο συλλέγει όλα τα src/**/*.ts, αφαιρεί τα bodies των σχολίων με το δικό του stripComments και έπειτα ελέγχει ότι ούτε το EXTENSION_API_PATTERN ούτε το UNSAFE_DOM_PATTERN ταιριάζουν σε κάποιο module. Η δημόσια επιφάνεια επανεξάγεται από το packages/core/src/index.ts. Η μοναδική runtime εξάρτηση είναι το tldts, για σύγκριση domains με γνώση public suffix.

Τα βασικά modules είναι τα εξής:

packages/webext — τα περιβλήματα browser API

Τα modules εδώ ΜΠΟΡΕΙ να χρησιμοποιούν browser.* και είναι το μοναδικό σημείο όπου αγγίζονται τα APIs της WebExtension. Η δημόσια επιφάνεια επανεξάγεται από το packages/webext/src/index.ts. Τα modules είναι: content.ts για το content lifecycle, contentRegistration.ts για δυναμική registration, storage.ts για persistence config, background.ts για συμπεριφορά κατά την εγκατάσταση, toolbarAction.ts για toggle και badge, messageDisplay.ts και messageDisplayBackground.ts για Thunderbird, και options/options.ts ως controller της σελίδας ρυθμίσεων.

extensions/<engine> — λεπτά σημεία εισόδου και manifests

Κάθε engine έχει το δικό του manifest.json και τα δικά του icons/. Chrome, Firefox και Thunderbird μεταφέρουν επιπλέον έναν κατάλογο src/ με entry points. Edge, Opera και Safari μεταφέρουν μόνο manifest και icons και επαναχρησιμοποιούν το src/ του Chrome, όπως δηλώνεται από τον descriptor του build. Τα entry points κρατούνται σκόπιμα μικρά. Για παράδειγμα, το extensions/chrome/src/content.ts περιέχει μόνο void createContentController().bootstrap();, ενώ το extensions/chrome/src/background.ts καλεί registerBackground, registerContentReconciliation και registerToolbarToggle. Το background entry του Firefox είναι ίδιο με του Chrome. Του Thunderbird, extensions/thunderbird/src/background.ts, καλεί registerBackground και registerMessageDisplay, επειδή δεν διαθέτει content scripts. Το extensions/_template είναι scaffold για Chromium και όχι build target.

tooling, features και smoke tests

Το tooling κρατά τα build.mjs, version.mjs και icons.mjs. Το features κρατά τα Gherkin scenarios, τα step definitions και τα πιστά test doubles στα features/support/webext.ts και features/support/world.ts. Κάθε engine κουβαλά επίσης ένα extensions/<engine>/test/manifest.smoke.test.mjs, ενώ του _template είναι το template.smoke.test.mjs. Αυτά είναι το αυθεντικό συμβόλαιο των manifests.

Η ροή αποκάλυψης

Η διαδρομή του content ξεκινά από entry point engine, όπως το extensions/chrome/src/content.ts, το οποίο καλεί createContentController().bootstrap() από το packages/webext/src/content.ts.

Bootstrap

Το createContentController bootstrap εκτελείται μία φορά ανά frame.

  1. Διεκδικεί συγχρονισμένα το frame πριν από οποιοδήποτε await, μέσω του claimBootstrap, το οποίο διαβάζει και θέτει το flag BOOTSTRAP_MARKER στο window του frame. Ένα στατικό built-in content_scripts entry και ένα επικαλυπτόμενο dynamic user script μπορούν και τα δύο να εισάγουν το content.js στο ίδιο frame. Το κοινό isolated-world window είναι λοιπόν το σωστό once-only guard.
  2. Φορτώνει πρώτα το persisted config μέσω του loadConfigWithRetry, με BOOTSTRAP_CONFIG_RETRIES επιπλέον προσπάθειες σε transient rejection. Μόνο όταν η ανάγνωση πετύχει γίνεται injection του stylesheet με το injectStyles, το οποίο αποδίδει το REVEAL_URLS_CSS μέσω textContent.
  3. Αν το φόρτωμα του config αποτύχει σε κάθε προσπάθεια, το releaseBootstrap επαναφέρει το marker πριν ξεκινήσει το annotation, ώστε επόμενη re-injection να μπορέσει να ξαναδοκιμάσει.
  4. Όταν το config.enabled είναι αληθές, καλείται το start, και σε κάθε περίπτωση καταχωρίζεται listener onConfigChanged, ώστε το config να επανεφαρμόζεται όταν αλλάζει.

Επίλυση του κανόνα ενεργού τοποθεσίας

Το start, όπως και το applyConfig, καλεί το resolveSiteRule για να αποφασίσει αν και πού θα γίνει annotation. Το resolveSiteRule δοκιμάζει πρώτα την τοποθεσία του ίδιου του εγγράφου μέσω resolveForHref, το οποίο φιλτράρει το config.sites στα ενεργά rules και αναθέτει την τελική επιλογή στο selectMostSpecific του core, έτσι ώστε να κερδίζει το πιο specific match. Αν η τοποθεσία του ίδιου του εγγράφου δεν ταιριάξει με τίποτα και το origin του είναι opaque ή inherited, κάτι που ελέγχεται από το hasOpaqueOrigin, τότε γίνεται fallback με σειρά προς το TOP frame μέσω readTopHref, προς το OPENER window μέσω readOpenerHref και τέλος προς το document.referrer μέσω readReferrer. Κάθε εξωτερική ανάγνωση φυλάσσεται από cross-origin access και συνεισφέρει υποψήφιο μόνο όταν μπορεί πράγματι να διαβαστεί. Ένα συνηθισμένο unmatched document θεωρείται αυθεντία και δεν κληρονομεί κανόνα από το περιβάλλον του.

Περιορισμός στη ρίζα περιεχομένου και παρακολούθηση μεταβολών

Το start καταγράφει τον selector contentRoot του rule που ταιριάζει, δημιουργεί annotator μέσω createAnnotator(config) και επεξεργάζεται κάθε ταιριαστή ρίζα από το selectRoots(doc, contentRoot), το οποίο τυλίγει querySelectorAll σε try/catch, ώστε ένας άκυρος selector να αποτυγχάνει ασφαλώς προς []. Στη συνέχεια παρακολουθεί το doc.body, έναν σταθερό container, με τα OBSERVER_OPTIONS.

Ο observer τροφοδοτεί το processMutations, το οποίο φιλτράρει κάθε μεταβολή σε σχέση με το contentRoot. Ένας target επανεπεξεργάζεται μόνο όταν είναι Element μέσα σε root μέσω isWithinRoot, και κάθε προστιθέμενο Element περνά από το processAddedNode, το οποίο annotates έναν κόμβο που βρίσκεται μέσα σε root, είναι ο ίδιος root ή περιέχει root, όπως συμβαίνει σε lazy SPA mount. Έτσι καλύπτονται τόσο οι αλλαγές μέσα στο message body όσο και η καθυστερημένη εμφάνιση γειτονικού message body, ενώ το υπόλοιπο chrome της εφαρμογής μένει ανέπαφο. Ένας null annotator είναι no-op.

Το applyConfig συμβιβάζει μια ζωντανή αλλαγή config. Κάνει teardown όταν δεν ταιριάζει κανένα rule ή όταν το flag enabled είναι κλειστό, κάνει restart όταν αλλάξει το resolved contentRoot, και αλλιώς κάνει reflow επιτόπου μέσω annotator.setConfig και annotator.process πάνω σε κάθε ταιριαστή ρίζα χωρίς restart του observer.

Σχολιασμός ενός μόνο συνδέσμου

Ο annotator ζει στο packages/core/src/linkProcessor.ts. Το createAnnotator επιστρέφει έναν Annotator με closure state που η σελίδα δεν μπορεί να διαβάσει: ένα ισχυρό Map κλειδωμένο στην ταυτότητα του anchor, ένα token data-ru ανά annotator από το generateToken, και το τρέχον config. Η μέθοδος process(root) κάνει τα εξής:

  1. Κλαδεύει entries του registry των οποίων το anchor δεν είναι πλέον συνδεδεμένο.
  2. Συλλέγει υποψήφια anchors με το collectAnchors, δηλαδή descendant a[href] και, αν το ίδιο το root είναι a[href], και το root.
  3. Επιλύει τον προορισμό κάθε anchor με το resolveAnnotatableUrl, το οποίο επιστρέφει null για άδειο href, in-page href, μη parseable href, schemes διαφορετικά από http: και https:, ή host που είναι ίσος με ή παιδί κάποιου ignoreHosts entry.
  4. Παίρνει idempotent fast path όταν το isAnnotationIntact επιβεβαιώνει ότι το render fingerprint δεν έχει αλλάξει και ότι οι κόμβοι που δημιουργήθηκαν παραμένουν συνδεδεμένοι και στη σωστή θέση. Διαφορετικά κάνει revert το παλιό annotation και κάνει νέο render.
  5. Υπολογίζει mismatch με το hostMismatch πάνω στο καθαρισμένο κείμενο του anchor, σέβεται το showOnlyOnMismatch και τελικά καλεί το annotateAnchor.

Το annotateAnchor αποδίδει μόνο SAFE-DOM. Στη λειτουργία "inline" εισάγει στην αρχή ένα <span class="reveal-urls-url"> του οποίου το URL γράφεται με textContent, με πρόθεμα βέλος και U+00A0 non-breaking space, και έπειτα ένα <br>, αφήνοντας ανέπαφα τα παιδιά του ίδιου του συνδέσμου. Στη λειτουργία "title" αποθηκεύει το αρχικό title, το αντικαθιστά με το URL και, σε mismatch που τονίζεται, προσθέτει αδελφικό <span class="reveal-urls-warn"> με το REVEAL_URLS_WARN_LABEL. Κάθε δημιουργημένος κόμβος παίρνει data-ru="<token>", ποτέ όμως το ίδιο το anchor, και κάθε entry καταγράφει configFingerprint, ώστε μια αλλαγή σε render-affecting πεδίο να αναγκάζει reflow. Τα χρώματα και οι override γραμματοσειρές εφαρμόζονται με typed element.style.* properties και ποτέ με interpolation μέσα σε CSS string. Τα revertOwned και revertAll αφαιρούν τους κόμβους που ανήκουν στον annotator και επαναφέρουν το αποθηκευμένο title.

Το truncateUrl δουλεύει πάνω σε Unicode code points με το [...displayHref], ώστε μια περικοπή να μη σπάει surrogate pair. Διατηρεί το origin scheme-and-host και συντομεύει το υπόλοιπο με trailing URL_ELLIPSIS.

Ασυμφωνία host

Στο packages/core/src/hostMismatch.ts, το hostMismatch συγκρίνει το ορατό κείμενο του συνδέσμου με το href του σε επίπεδο registrable domain και όχι raw hostname. Έτσι μια έντιμη sub-domain δεν επισημαίνεται, ενώ ένα look-alike host επισημαίνεται. Το extractHostCandidates χωρίζει το κείμενο σε tokens και τα μειώνει μέσω του hostCandidate, το οποίο απαιτεί τελεία και ICANN public suffix που να αναγνωρίζεται από το tldts. Τόσο ο host του href όσο και κάθε υποψήφιο μειώνονται στο registrable domain με tldts.getDomain. Οποιοδήποτε candidate whose domain differs from του href παράγει mismatch. Η συνάρτηση δεν ρίχνει ποτέ exception.

Στυλ και αντίθεση

Το packages/core/src/styles.ts κρατά το injectable REVEAL_URLS_CSS, δηλαδή fixed layout, weight, shape και fixed λευκό πλήρως opaque background για το chip, και τους runtime helpers applyColour, applyFontOverrides και applyContrastBackdrop. Το applyContrastBackdrop διαβάζει το resolved colour του element και το πρώτο opaque background που βρίσκει ανεβαίνοντας από το ίδιο το element προς τα πάνω με getComputedStyle. Όταν το packages/core/src/contrast.ts needsWhiteBackdrop αναφέρει ότι το κείμενο αποτυγχάνει στο WCAG AA CONTRAST_THRESHOLD απέναντι σε αυτό το background και ότι το λευκό βοηθά πραγματικά, θέτει style.backgroundColor = "white". Το contrast.ts παρέχει τα parseColour, relativeLuminance και contrastRatio.

Παραμετροποιήσιμοι hosts και περιορισμός περιεχομένου

Ένα SiteRule στο packages/core/src/config.ts λέει ΠΟΥ τρέχει το annotation, μέσω των match και allFrames, και ΠΟΙΟ container το περιορίζει, μέσω του contentRoot, μαζί με enabled και flag builtin. Τα DEFAULT_SITES καλύπτουν Gmail, Proton με allFrames, και τα δύο hosts του Outlook. Το Config συγκεντρώνει τα global toggles, τα colours, τα font overrides και το array sites.

Η μηχανή match pattern

Το packages/core/src/matchPattern.ts κρατά μια σκόπιμα restrictive γραμμή add-time, τη MATCH_PATTERN: μόνο http και https, προαιρετικό leading *. wildcard host, glob path, χωρίς port, χωρίς scheme *, χωρίς <all_urls>. Το matchesPattern ταιριάζει ένα URL με validated pattern, αναθέτοντας το path στο pathGlobMatches, όπου κάθε * σημαίνει οποιαδήποτε ακολουθία χαρακτήρων. Το parsePatternParts χωρίζει validated pattern σε host, path, scheme και wildcardHost.

Σε επικαλυπτόμενα rules ισχύει το "most specific wins". Το compareSiteSpecificity κατατάσσει πρώτα exact host πάνω από wildcard host, έπειτα μεγαλύτερο literal host, έπειτα πιο literal path μέσω literalPathLength, και τέλος deterministic ASCII tiebreak. Το selectMostSpecific επιστρέφει το πιο specific enabled rule που ταιριάζει σε URL.

Ο έλεγχος granted-origin coverage είναι ξεχωριστός και σκόπιμα ευρύτερος. Το permissions.getAll() μπορεί να επιστρέψει grants στην πλήρη γραμματική της WebExtension (<all_urls>, *://*/*, https://*/*, *://*.host/*, https://host/*) τα οποία η restrictive MATCH_PATTERN απορρίπτει. Το parseGrantedOrigin τα μετατρέπει σε GrantedOriginParts, και το originCovers ελέγχει αν ένα granted origin καλύπτει το match ενός rule. Το matchAllowsOriginFallback αναφέρει αν το path του rule είναι ακριβώς ORIGIN_FALLBACK_PATH, δηλαδή /*.

Δυναμική καταχώριση

Το packages/webext/src/contentRegistration.ts καταχωρίζει ένα dynamic content script για κάθε rule που προσθέτει ο χρήστης. Τα built-ins εξυπηρετούνται από τα static content_scripts entries και ποτέ δεν καταχωρίζονται δυναμικά. Το desiredContentScripts φιλτράρει το config.sites στα non-builtin enabled rules των οποίων το origin είναι granted, μέσω isOriginGranted και originCovers, χαρτογραφεί κάθε rule σε RegisteredContentScript με σταθερό id από contentScriptId, και θέτει matchOriginAsFallback: true μόνο όταν το matchAllowsOriginFallback το επιτρέπει.

Το reconcileContentScriptsOnce διαβάζει τα granted origins, το stored config και τις live registrations και συγκλίνει μέσω property-aware diff. Το sameRegistration προβάλλει τόσο τον desired descriptor όσο και το live read-back σε normalised form, εφαρμόζοντας τα πραγματικά defaults του WebExtension API, ώστε ένα freshly registered script να συγκρίνεται ίσο με τον descriptor που το δημιούργησε. Επειδή δεν υπάρχει updateContentScripts, ένα changed script αφαιρείται και ξαναγράφεται. Το reconcileContentScripts τυλίγει αυτή τη ροή με in-flight promise, ώστε δύο σχεδόν ταυτόχρονες ενεργοποιήσεις να σειριοποιούνται αντί να τρέχουν σε race. Το registerContentReconciliation συνδέει τα triggers και εκτελεί μία σύγκλιση κατά την εκκίνηση. Εισάγεται μόνο από τα background entries των Chrome και Firefox.

Η ροή ρητής άδειας

Τα static manifests δηλώνουν optional_host_permissions (http://*/*, https://*/*) ώστε ο χρήστης να μπορεί να παραχωρεί σε runtime οποιονδήποτε πρόσθετο web host. Το packages/webext/src/options/options.ts addSite επικυρώνει ολόκληρο το υποψήφιο rule, δηλαδή match και content root, μέσω normaliseSiteRule πριν ζητήσει οποιαδήποτε άδεια. Στη συνέχεια παράγει το host origin με matchOrigin, καλεί browser.permissions.request από το click gesture του κουμπιού Add, και μόνο μετά από grant προσθέτει row builtin:false και επιμένει στην canonical write path.

Διαμόρφωση, αποθήκευση και κανονικό πρότυπο

Το ενιαίο χωνί επικύρωσης

Το normaliseConfig στο packages/core/src/config.ts είναι η μία canonical read path. Μετατρέπει, περιορίζει και επικυρώνει κάθε πεδίο, επιστρέφοντας στην προεπιλογή σε οτιδήποτε άκυρο και χωρίς να πετά exception. Χτίζεται από validators ανά πεδίο, όπως normaliseBoolean, normaliseRenderMode, normaliseMaxLength, normaliseIgnoreHosts, normaliseMatchColour, normaliseMismatchColour, normaliseCssSize, normaliseFontWeight, normaliseContentRoot, normaliseMatchPattern και normaliseSites. Το normaliseSites απορρίπτει invalid entries, αφαιρεί διπλότυπα με βάση το match, επιβάλλει builtin: true όταν κάποιο match είναι ίσο με built-in, επανασπέρνει built-ins που λείπουν και ταξινομεί αλφαριθμητικά κατά match, παράγοντας canonical order. Το configFingerprint κάνει hash μόνο στα πεδία που επηρεάζουν το render.

Αποθήκευση

Το packages/webext/src/storage.ts τυλίγει το WebExtension storage API. Το configArea χρησιμοποιεί browser.storage.sync όταν υπάρχει, ώστε οι ρυθμίσεις να μεταφέρονται, και κάνει fallback στο browser.storage.local, για παράδειγμα στο Thunderbird. Τα getConfig, setConfig και onConfigChanged περνούν πάντοτε την raw τιμή τους από normaliseConfig, οπότε ένα παραμορφωμένο ή πειραγμένο store δεν μπορεί ποτέ να παραδώσει άκυρο Config σε caller. Το onConfigChanged αγνοεί επιπλέον events από ανενεργή storage area και changes σε άσχετα keys.

Τα δύο add-ons επαναχρησιμοποιούν το ίδιο funnel read → normaliseConfig → return πάνω στα δικά τους host stores. Το Outlook task pane τυλίγει το Office.context.roamingSettings στο extensions/outlook/src/roamingStorage.ts. Το Gmail add-on τυλίγει το PropertiesService.getUserProperties() στο packages/gmail/src/propertiesStorage.ts. Και τα δύο εκθέτουν getConfig και setConfig, μαζί με accessors που χρειάζεται το card UI, και προστατεύουν το unavailable host surface με dedicated error.

Το πρότυπο canonical-config και στοχευμένης ενημέρωσης

Τρεις write paths διαβάζουν το canonical config, αλλάζουν ακριβώς ένα πράγμα και το ξαναγράφουν, χωρίς ποτέ να σώζουν unsaved form edits ούτε να κάνουν re-render της φόρμας:

Το toggleEnabled του packages/webext/src/toolbarAction.ts ακολουθεί το ίδιο μοτίβο από την πλευρά του background.

Build ανά engine και συμβόλαιο manifest

Το tooling/build.mjs οδηγεί ένα ενιαίο esbuild build για κάθε target της WebExtension. Ο descriptor TARGETS δηλώνει για κάθε target το SOURCE TARGET του. Chrome, Firefox και Thunderbird έχουν δικό τους src/. Edge, Opera και Safari δηλώνουν chrome ως source. Το Outlook add-in είναι δικό του source. Τα ACTIVE_TARGETS, δηλαδή Chrome, Edge, Firefox, Opera και Thunderbird, είναι αυτά που χτίζονται από --all και --package. Safari και Outlook χτίζονται μόνο όταν ζητηθούν ρητά. Το Gmail add-on γίνεται bundle χωριστά με make build-gmail.

Το Gmail bundle στοχεύει το Apps Script V8 runtime, το οποίο δεν διαθέτει ούτε ES modules ούτε native URL. Γι' αυτό χτίζεται ως ESM και έπειτα αφαιρείται το τελικό export {…}, ώστε οι trigger functions να μείνουν top-level globals που το Apps Script μπορεί να καλέσει. Ταυτόχρονα γίνεται bundle και ένα μικρό polyfill για URL, το οποίο εγκαθίσταται μόνο όταν λείπει. Το buildTarget καθαρίζει το dist/<target>/, αντιγράφει το per-target manifest.json και τα icons/ χωρίς αλλαγές, κάνει bundle κάθε source script ως IIFE και τη σελίδα επιλογών ως ESM, και αντιγράφει τα options.html και options.css από το packages/webext/src/options/.

Το tooling/version.mjs σφραγίζει εκδόσεις. Παίρνει MAJOR.MINOR από το root package.json, βρίσκει τον μεγαλύτερο υπάρχοντα BUILD number στα manifests και τον προσθέτει ως τρίτο component σε κάθε manifest target, κρατώντας τα σε lockstep. Το Outlook add-in μεταφέρει δύο versioned manifests, manifest.json και manifest.xml, και σφραγίζονται και τα δύο. Το root package.json κρατιέται στην ίδια έκδοση, αλλά παραμένει έγκυρο semver. Το tooling/icons.mjs, που τρέχει μέσω make icons, rasterises το μοναδικό vector source assets/icon.svg σε icon48.png και icon128.png για κάθε target.

Το συμβόλαιο των manifests, δηλαδή τα ακριβή απαιτούμενα πεδία ανά engine, ορίζεται στα extensions/<engine>/test/manifest.smoke.test.mjs. Εκεί επιβεβαιώνεται και ότι κάθε referenced file όντως υπάρχει στο built dist. Για την αναλυτική μορφή των πεδίων, το έργο παραπέμπει στα smoke tests και στη σχετική σημείωση του manual test plan.

Διεθνοποίηση (i18n)

Το Reveal URLs είναι τοπικοποιημένο σε πολλές γλώσσες πέρα από τα αγγλικά. Το σύνολο των γλωσσών πηγάζει αποκλειστικά από το packages/core/src/locales.json, το οποίο περιέχει τα SUPPORTED_LOCALES, τα native names και τη default γλώσσα. Τόσο τα bundles της επέκτασης όσο και το site build σε plain Node διαβάζουν από εκεί. Κάθε μη αγγλικό string είναι machine-translated και εκκρεμεί ανθρώπινη αναθεώρηση. Το marker προέλευσης διαφέρει ανά format: τα _locales το μεταφέρουν στις description, τα catalogues του site το καταγράφουν στο site/i18n/README.md, και οι μεταφρασμένες πηγές docs μεταφέρουν HTML comment στην πρώτη γραμμή.

Υπάρχουν τρεις ανεξάρτητες επιφάνειες τοπικοποίησης, εσκεμμένα χωρίς σταυροδρόμηση, και κάθε μία χωρίζεται σε build-time και runtime πλευρά.

Απόκρυψη των «Ενεργών τοποθεσιών» στον πελάτη αλληλογραφίας (AD-3)

Ένα target mail client, σήμερα το Thunderbird, βλέπει ήδη κάθε αποδιδόμενο μήνυμα, άρα ο editor "Active sites" ανά host είναι πλεονάζων και αφαιρείται. Η απόφαση μεταφέρεται από documented per-target descriptor flag mailClient: true στο tooling/build.mjs και όχι από hardcoded target name.

Θέση ασφάλειας και απορρήτου

Δοκιμές