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περιέχει PURE λογική, χωρίς browser APIs, χωρίς storage και χωρίς sinks που γράφουν markup. Μπορεί να ελεγχθεί με unit tests σε απλό JavaScript και αποτελεί τη μοναδική πηγή αλήθειας για annotation συνδέσμων, επικύρωση config, match-pattern matching, σύγκριση hosts και contrast-aware χρωματισμό.packages/webextτυλίγει γύρω από αυτόν τον πυρήνα τα APIs της WebExtension: lifecycle περιεχομένου, δυναμική καταχώριση content scripts, UI επιλογών, wiring του background, toolbar action και τη διαδρομή εμφάνισης μηνυμάτων στο Thunderbird.extensions/<engine>κρατά τα λεπτά entry points ανά engine και τα αντίστοιχαmanifest.json.toolingπεριέχει τα scripts για build, version-stamping και παραγωγή εικονιδίων.featuresκρατά τα Cucumber BDD scenarios και τα test doubles τους.
Διάταξη αποθετηρίου
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/core/src/config.ts— το schemaConfigκαιSiteRule, οι προεπιλογές και το μοναδικό funnel επικύρωσηςnormaliseConfigpackages/core/src/matchPattern.ts— η γραμματική των match patterns, ο matcher, η κατάταξη specificity και ο έλεγχος granted-origin coveragepackages/core/src/linkProcessor.ts— ο annotator που λειτουργεί σε scope εγγράφουpackages/core/src/hostMismatch.ts— ο έλεγχος ασυμφωνίας registrable domainpackages/core/src/styles.tsκαιpackages/core/src/contrast.ts— το injectable stylesheet και η λογική contrast-aware colouringpackages/core/src/safeColour.ts— το predicateisSafeColour
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.
- Διεκδικεί συγχρονισμένα το frame πριν από οποιοδήποτε
await, μέσω τουclaimBootstrap, το οποίο διαβάζει και θέτει το flagBOOTSTRAP_MARKERστοwindowτου frame. Ένα στατικό built-incontent_scriptsentry και ένα επικαλυπτόμενο dynamic user script μπορούν και τα δύο να εισάγουν τοcontent.jsστο ίδιο frame. Το κοινό isolated-worldwindowείναι λοιπόν το σωστό once-only guard. - Φορτώνει πρώτα το persisted config μέσω του
loadConfigWithRetry, μεBOOTSTRAP_CONFIG_RETRIESεπιπλέον προσπάθειες σε transient rejection. Μόνο όταν η ανάγνωση πετύχει γίνεται injection του stylesheet με τοinjectStyles, το οποίο αποδίδει τοREVEAL_URLS_CSSμέσωtextContent. - Αν το φόρτωμα του config αποτύχει σε κάθε προσπάθεια, το
releaseBootstrapεπαναφέρει το marker πριν ξεκινήσει το annotation, ώστε επόμενη re-injection να μπορέσει να ξαναδοκιμάσει. - Όταν το
config.enabledείναι αληθές, καλείται τοstart, και σε κάθε περίπτωση καταχωρίζεται listeneronConfigChanged, ώστε το 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) κάνει τα εξής:
- Κλαδεύει entries του registry των οποίων το anchor δεν είναι πλέον συνδεδεμένο.
- Συλλέγει υποψήφια anchors με το
collectAnchors, δηλαδή descendanta[href]και, αν το ίδιο το root είναιa[href], και το root. - Επιλύει τον προορισμό κάθε anchor με το
resolveAnnotatableUrl, το οποίο επιστρέφειnullγια άδειο href, in-page href, μη parseable href, schemes διαφορετικά απόhttp:καιhttps:, ή host που είναι ίσος με ή παιδί κάποιουignoreHostsentry. - Παίρνει idempotent fast path όταν το
isAnnotationIntactεπιβεβαιώνει ότι το render fingerprint δεν έχει αλλάξει και ότι οι κόμβοι που δημιουργήθηκαν παραμένουν συνδεδεμένοι και στη σωστή θέση. Διαφορετικά κάνει revert το παλιό annotation και κάνει νέο render. - Υπολογίζει 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 της φόρμας:
removeSiteστοoptions.ts, το οποίο διαβάζειgetConfig, επιμένειsetConfigπάνω σε arraysitesαπό το οποίο έχει αφαιρεθεί το συγκεκριμένο rule και έπειτα αποφασίζει αν θα κάνειpermissions.removeaddSiteστοoptions.ts, που επικυρώνει, ζητά άδεια, προσθέτει row και έπειτα καλείsaveOptionstoggleEnabledστοoptions.ts, όπου ο στιγμιαίος κύριος διακόπτης αλλάζει μόνο τοenabledτου stored config και το γράφει πίσω, χωρίς 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 πλευρά.
- The options page (AD-1). Στέλνει static αγγλικό markup, αλλά κάθε
μεταφράσιμος κόμβος φέρει
data-i18n="<key>"ήdata-i18n-<attr>="<key>". Σε runtime, τοpackages/webext/src/options/locale.tsλειτουργεί ως apply-layer: κάνειfetchτο packaged_locales/<code>/messages.json, ξαναγράφει κάθε επισημασμένο node μόνο μεtextContentκαι θέτει<html lang>. Η σελίδα τοπικοποιείται πλήρως, ακόμη και για JS-built labels, κουμπί Remove, status line, feedback μηνυμάτων και version line. Το fallback στα αγγλικά είναι το_locales/en/messages.json, bundled μέσα στοoptions.js, οπότε dynamic strings παραμένουν αναγνώσιμα ακόμη και όταν το locale fetch αποτύχει. Η ίδια layer ξαναδείχνει και το Online manual στον σωστό μεταφρασμένο doc path. - The website chrome (AD-2). Ο generator
tooling/site.mjsαποδίδει το chrome του site στα αγγλικά, αλλά μεdata-i18nκαι με<html lang="en-GB">. Έπειτα, το browser-side loadersite/scripts/i18n.mjsτο αλλάζει στη γλώσσα του επισκέπτη βάσει persisted επιλογής,navigator.languagesκαι τελικού fallback στα αγγλικά, φορτώνονταςsite/i18n/<code>.json. Εδώ η εναλλαγή γίνεται μεinnerHTML, επειδή οι τιμές του chrome περιέχουν trusted inline markup όπως<code>και<a>. - The documentation pages (AD-8/AD-9). Οι σελίδες docs αποδίδονται ανά
locale σε build time και δεν αλλάζουν σε runtime. Τα αγγλικά μένουν σε
top-level paths. Κάθε άλλο locale παίρνει δικό του
<code>/κατάλογο και filename μεταφρασμένο μέσω του sharedpackages/core/src/site-page-names.json, όπως τοnl/handleiding.html. Όταν λείπει μετάφραση για κάποιο doc, ο builder χρησιμοποιεί την αγγλική πηγή, αλλά το καταγράφει ρητά. - Internal-link localisation (Finding 2). Η επιλεγμένη γλώσσα
διατηρείται στη doc navigation. Κάθε internal doc anchor που δείχνει σε
architecture.html,index.html,licence.html,manual.htmlήprivacy.htmlεπισημαίνεται μεdata-doc-link="<page>", ώστε το runtime loader να μπορεί να το ξαναστοχεύει στο αδελφό locale variant, διατηρώντας οποιοδήποτε#hashή?query.
Απόκρυψη των «Ενεργών τοποθεσιών» στον πελάτη αλληλογραφίας (AD-3)
Ένα target mail client, σήμερα το Thunderbird, βλέπει ήδη κάθε αποδιδόμενο
μήνυμα, άρα ο editor "Active sites" ανά host είναι πλεονάζων και
αφαιρείται. Η απόφαση μεταφέρεται από documented per-target descriptor flag
mailClient: true στο tooling/build.mjs και όχι από hardcoded target
name.
- Build-time. Το
copyAssetsελέγχει τοdescriptorFor(target).mailClient. Για target mail client τρέχει το pure transformremoveSitesSection(html), που αφαιρεί ολόκληρο το<section class="sites">και αποτυγχάνει μεMissingSourceErrorαν αυτό το section λείπει. - Runtime. Ο controller
packages/webext/src/options/options.tsανέχεται την απουσία του section. ΤαrenderSites,readSites,applyConfigκαιreadFormελέγχουν πρώτα αν υπάρχει#sites-listκαι, αν όχι, δεν ρίχνουνOptionsFieldMissingError. ΤοsaveOptionsεπαναφέρει τα storedsitesπριν από το normalisation όταν το section λείπει, ώστε μια αποθήκευση σε mail client να μη χάσει user-defined hosts. ΤοguardMailClientSitesαφαιρεί το section όταν εξακολουθεί να υπάρχει και αναγνωρίζει mail client από API ή feature presence και όχι από target name.
Θέση ασφάλειας και απορρήτου
- Καμία δικτυακή κλήση ή εξαγωγή δεδομένων. Κανένα module δεν εκτελεί
fetch,XHRή άλλη δικτυακή κλήση. Η επέκταση διαβάζει μόνο το stored config και το DOM της σελίδας. - Μόνο SAFE-DOM. Το annotation και το injected stylesheet
χρησιμοποιούν αποκλειστικά
createElement,textContentκαι typedstyle.*properties, ποτέinnerHTML,insertAdjacentHTMLήeval. - Όλη η εξωτερική είσοδος κανονικοποιείται. Κάθε read και write περνά
από
normaliseConfig. Τα colours περνούν απόisSafeColour, τα match patterns απόMATCH_PATTERN, και οι content-root selectors περιορίζονται απόCONTENT_ROOT_PATTERN. - Ελάχιστο απαραίτητο προνόμιο. Τα built-in webmail origins ζουν στα
host_permissions. Οι επιπλέον hosts είναι opt-in μέσωoptional_host_permissionsκαι gesture-boundpermissions.request. Το toolbar action δεν δηλώνει popup. - Συλλογή δεδομένων Gecko. Τα manifests Firefox και Thunderbird δηλώνουν
browser_specific_settings.gecko.data_collection_permissions.required: ["none"]. - Άδεια. Το έργο είναι AGPL-3.0-only, όπως δηλώνουν τα
LICENSEκαιpackage.json.
Δοκιμές
- Vitest unit tests καλύπτουν και τα δύο packages. Τα
packages/core/test/*ελέγχουν config, contrast, host mismatch, link processor, styles και purity guard. Ταpackages/webext/test/*ελέγχουν content, storage, options, registration, toolbar, message display και ένα custom-site end-to-end test. - Cucumber BDD scenarios ζουν στο
features/, όπως ορίζεται από τοcucumber.json, και οδηγούν τα πραγματικά modules του@reveal-urls/webextμέσω πιστών test doubles σταfeatures/support/webext.tsκαιfeatures/support/world.ts. - Per-target manifest smoke tests στα
extensions/<engine>/test/manifest.smoke.test.mjsχρησιμοποιούν το built-innode:test, επιβεβαιώνουν το ακριβές shape κάθε manifest και ότι το built dist επιλύει κάθε referenced file. Τοtooling/test/προσθέτει build, version και definition-of-done tests. - CI στο
.github/workflows/ci.ymlμπλοκάρει κάθε push και pull request αν δεν περάσουν ταmake test,make bddκαιmake lint. Το version-stamping και το packaging μένουν εκτός αυτής της ροής και ζουν στο ξεχωριστό.github/workflows/release.ymlπου ενεργοποιείται από tags.