Safari is a supported active target with its own entry points. Built-in providers use static scripts and Safari requests no all-sites access. User-added sites receive a runtime optional permission, then the background injects content.js after navigation only for granted, enabled, non-builtin rules, with allFrames per rule. Safari settings are device-local and do not roam between devices. macOS packaging uses Xcode and the Apple Developer Program/App Store route.

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

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

Επισκόπηση

Το Reveal URLs είναι μία ενιαία MV3 WebExtension που χτίζεται για πολλούς engines, δηλαδή Chrome, Edge, Opera, Firefox, Safari και Thunderbird, από έναν κοινό καθαρό πυρήνα κώδικα. Το Safari χρησιμοποιεί στατικά script μόνο για ενσωματωμένους παρόχους και δημοσιεύεται στο App Store. Η επέκταση αποκαλύπτει τον προορισμό κάθε συνδέσμου δίπλα στον σύνδεσμο μέσα σε αποδιδόμενο 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> και διαφορετικά παραλείπεται. Αυτή η εγγενής διαδρομή ευρημάτων παραμένει σκόπιμα μόνο για http:/https:.

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

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

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 και closest, ενώ η καθυστερημένη ανακάλυψη root αναλαμβάνεται από την περιορισμένη διαδρομή discoverMatchedRoots() σε κάθε batch, με ένα querySelectorAll(contentRoot) ανά batch και deduplication μέσω WeakSet. Το pendingShadowHosts επανελέγχει προηγούμενα plain hosts μέχρι να εμφανιστεί open shadow root, ώστε να καλύπτονται και τα shadow upgrades. Έτσι καλύπτονται τόσο οι αλλαγές μέσα στο message body όσο και τα shadow-host upgrades, ενώ το υπόλοιπο 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, το οποίο αναθέτει στο resolveAnnotatableHref. Επιστρέφει null μόνο για άδειο, εντός σελίδας ή μη αναλύσιμο href, επιλύει σχετικά href ως προς το baseURI και δέχεται κάθε αναλύσιμο scheme, συμπεριλαμβανομένων των mailto:, tel:, sms: και των αδιαφανών URL javascript:, data: και blob:. Το ignoreHosts εφαρμόζεται μόνο σε συνδέσμους http:/https: με host.
  4. Παίρνει idempotent fast path όταν το isAnnotationIntact επιβεβαιώνει ότι το render fingerprint δεν έχει αλλάξει και ότι οι κόμβοι που δημιουργήθηκαν παραμένουν συνδεδεμένοι και στη σωστή θέση. Διαφορετικά κάνει revert το παλιό annotation και κάνει νέο render.
  5. Υπολογίζει ασυμφωνία με το destinationMismatch στο καθαρισμένο κείμενο του anchor: το classifyScheme στέλνει συνδέσμους web στο hostMismatch, αλληλογραφία στο mailMismatch, τηλέφωνο στο phoneMismatch και τα άλλα στο opaqueMismatch. Έπειτα καλεί το annotateAnchor· το showOnlyOnMismatch καταστέλλει μόνο την ορατή inline αποκάλυψη συνδέσμου χωρίς ασυμφωνία, διατηρώντας όμως το title του προορισμού.

Το annotateAnchor αποδίδει μόνο SAFE-DOM. Πάντα αποθηκεύει το αρχικό title (TitleSave) και γράφει τον τίτλο προορισμού — η μόνη εγγραφή στο anchor. Δεν υπάρχει ξεχωριστή λειτουργία τίτλου ή badge προειδοποίησης. Εκτός αν το showOnlyOnMismatch κρύβει έναν σύνδεσμο χωρίς ασυμφωνία, εισάγει στην αρχή ένα <span class="reveal-urls-url"> και ένα επακόλουθο <br>, αφήνοντας ανέπαφα τα παιδιά του συνδέσμου. Το needsLeadingInlineRevealBreak προσθέτει ένα προπορευόμενο <br> όταν ορατό inline περιεχόμενο προηγείται του συνδέσμου στην ίδια γραμμή. Το span εμφανίζει αυτούσιο ένα συγγραφικό απόλυτο href ή ένα σχετικό href επιλυμένο ως προς το baseURI, με αδρανές textContent (πρόθεμα βέλους και U+00A0 non-breaking space, κατόπιν περικοπή από το truncateUrl)· μόλις συνδεθεί, λαμβάνει υπόβαθρο αντίθεσης μέσω του applyContrastBackdrop. Σε τονισμένη ασυμφωνία, το REVEAL_URLS_MISMATCH_CLASS προστίθεται στο REVEAL_URLS_URL_CLASS, εφαρμόζοντας το ρυθμισμένο χρώμα ασυμφωνίας με applyColour και έντονη γραφή εξ ορισμού στο REVEAL_URLS_CSS· ρητή παράκαμψη πάχους γραμματοσειράς μέσω του applyFontOverrides υπερισχύει. Κάθε δημιουργημένος κόμβος παίρνει data-ru="<token>" (ποτέ το ίδιο το anchor), και κάθε entry καταγράφει configFingerprint, ώστε αλλαγή στη ρύθμιση render να επιβάλλει reflow. Το revertOwned αφαιρεί τους δημιουργημένους κόμβους με αναφορά και επαναφέρει το αποθηκευμένο title· το revertAll το κάνει αυτό για κάθε κατεχόμενο anchor και εκκαθαρίζει το registry.

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

Ασυμφωνία host

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

Για συνδέσμους αλληλογραφίας, το mailMismatch συγκρίνει όλα τα registrable domain παραληπτών, συμπεριλαμβανομένων των to, cc και bcc· το phoneMismatch συγκρίνει επιλύσιμους καλούμενους αριθμούς. Το opaqueMismatch επισημαίνει πάντα τα SCRIPT_LIKE_SCHEMESjavascript:, data: και vbscript: — ενώ τα άλλα αδιαφανή scheme επισημαίνονται μόνο όταν το ορατό κείμενο κατονομάζει συγκεκριμένο web, email ή τηλεφωνικό προορισμό.

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

Το 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 καλύπτουν FastMail, Gmail, Proton με allFrames, Tuta, Yahoo, Zoho και τα δύο 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, 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.

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

Δοκιμές