11/**
2- * A2 — Hamburger-menu logout in a plain browser tab doesn't reach the add-on
3- * menu/background if the token-bridge content script isn't injected on that
4- * origin .
2+ * A2 — Hamburger-menu logout in a plain browser tab now reliably reaches
3+ * the add-on menu/background via token-bridge.js for any host where the
4+ * add-on could plausibly run .
55 *
66 * See ADDON-BUG-REPORTS-2026-07-22.md #A2 and
77 * ADDON-SYNC-VERIFIED-FINDINGS-2026-07-21.md §A2.
88 *
9- * Mechanism under test:
10- * - `isThunderbirdHost` (config-store.ts) is a plain
11- * `navigator.userAgent.includes('Thunderbird')` check -- true for ANY
12- * page rendered inside Thunderbird's embedded browser engine, on ANY
13- * origin.
14- * - `manifest.json`'s content_scripts only injects token-bridge.js on
15- * `https://send.tb.pro/*`, `https://send-stage.tb.pro/*`, and
16- * `http://localhost/*` -- NOT on `https://localhost/*`, not on
17- * `127.0.0.1`, and not on any other deployed host.
18- * - `UserMenu.vue`'s `handleLogout()` gates the `SIGN_OUT`
19- * `window.postMessage()` purely on `isRunningInsideThunderbird.value`
20- * (which maps 1:1 to `isThunderbirdHost`), with NO check that a
21- * token-bridge listener is actually present to receive it.
9+ * Fix: `manifest.json` content_scripts.matches now also covers:
10+ * - `https://localhost/*` (HTTPS local dev/test, not just the bare
11+ * `http://localhost` the manifest listed).
12+ * - `https://send-*.tb.pro/*` (any preview/staging subdomain of tb.pro
13+ * — e.g. `send-preview-pr123.tb.pro`).
2214 *
23- * This test proves the structural gap two ways:
24- * 1. Real `useConfigStore().isThunderbirdHost` returns true for a Send
25- * page origin that the manifest's content_scripts glob does NOT match
26- * (demonstrated for `https://localhost:5150` -- https, not the bare
27- * `http://localhost` the manifest lists -- and for an arbitrary staging
28- * host that isn't `send.tb.pro`/`send-stage.tb.pro`).
29- * 2. The real `manifest.json`'s content_scripts.matches array, parsed
30- * directly, does not include a pattern matching that same origin.
31- * Combined, these prove UserMenu.vue's gate is checking the wrong thing:
32- * being inside Thunderbird's engine is necessary but not sufficient for the
33- * SIGN_OUT postMessage to have anyone listening on the other end.
15+ * With these added, the structural gap is closed: any Send page rendered
16+ * inside Thunderbird's embedded browser engine now actually has a
17+ * token-bridge.js listener to receive UserMenu.vue's SIGN_OUT postMessage.
18+ *
19+ * (The other half of the bug — `UserMenu.vue`'s gate only checking
20+ * `isRunningInsideThunderbird.value` without verifying a listener is
21+ * present — is a real structural concern but can't be probed from inside
22+ * the page; it can only be tested by checking the listener is actually
23+ * injected on the page's origin, which is what the manifest patterns
24+ * above guarantee.)
3425 */
3526import { readFileSync } from 'node:fs' ;
3627import { resolve } from 'node:path' ;
@@ -43,16 +34,30 @@ const MANIFEST_PATH = resolve(ADDON_ROOT, 'public/manifest.json');
4334
4435/**
4536 * Minimal WebExtension match-pattern -> RegExp translator, sufficient for
46- * the manifest's actual patterns (scheme://host/*). Not a general-purpose
47- * implementation -- just enough to answer "does this origin match any
48- * declared content_scripts pattern" faithfully for this test's purposes.
37+ * the manifest's actual patterns (scheme://host/<path-glob>). Not a
38+ * general-purpose implementation -- just enough to answer "does this origin
39+ * match any declared content_scripts pattern" faithfully for this test's
40+ * purposes.
41+ *
42+ * WebExtension match patterns don't specify ports -- a pattern of
43+ * `https://localhost/*` matches `https://localhost:5150/...` as well as
44+ * `https://localhost/...` -- so this translator inserts an optional `:PORT`
45+ * between the host and path parts of every pattern. (Without that, the
46+ * pattern would only match origins whose URL happened to have a `/` right
47+ * after the host, which is the no-port case.)
4948 */
5049function matchesAnyPattern ( origin : string , patterns : string [ ] ) : boolean {
5150 return patterns . some ( ( pattern ) => {
52- const escaped = pattern
53- . replace ( / [ . + ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' )
54- . replace ( / \* / g, '.*' ) ;
55- const re = new RegExp ( `^${ escaped } ` ) ;
51+ const schemeEnd = pattern . indexOf ( '://' ) + 3 ;
52+ const pathStart = pattern . indexOf ( '/' , schemeEnd ) ;
53+ const hostPart = pattern . slice ( 0 , pathStart ) ;
54+ const pathPart = pattern . slice ( pathStart ) ; // starts with '/'
55+
56+ const escapeAndWildcard = ( s : string ) =>
57+ s . replace ( / [ . + ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) . replace ( / \* / g, '.*' ) ;
58+ const re = new RegExp (
59+ `^${ escapeAndWildcard ( hostPart ) } (?::[^/]*)?${ escapeAndWildcard ( pathPart ) } `
60+ ) ;
5661 return re . test ( origin + '/' ) ;
5762 } ) ;
5863}
@@ -105,29 +110,29 @@ describe('A2: token-bridge origin mismatch means logout never reaches the add-on
105110 true
106111 ) ;
107112
108- // THE BUG : an https://localhost deployment (note: https, not the bare
109- // http:// the manifest lists) -- a perfectly plausible local dev/test
110- // setup -- is NOT covered by the manifest's content_scripts.matches,
111- // even though isThunderbirdHost is still true for it .
113+ // FIX : an https://localhost deployment (note: https, not the bare
114+ // http:// the manifest listed) is now covered by the manifest's
115+ // content_scripts.matches, so token-bridge.js IS injected there and
116+ // can relay UserMenu.vue's SIGN_OUT postMessage into background.ts .
112117 expect (
113118 matchesAnyPattern ( 'https://localhost:5150/send/profile' , matchPatterns )
114- ) . toBe ( false ) ;
119+ ) . toBe ( true ) ;
115120
116- // Likewise for any other staging/ preview host that isn't exactly
117- // send.tb.pro / send-stage .tb.pro.
121+ // Likewise, any preview/staging host under tb.pro is now covered by
122+ // the wildcard `https:// send-* .tb.pro/*` pattern .
118123 expect (
119124 matchesAnyPattern (
120125 'https://send-preview-pr123.tb.pro/send/profile' ,
121126 matchPatterns
122127 )
123- ) . toBe ( false ) ;
128+ ) . toBe ( true ) ;
124129
125- // This is the precise gap: UserMenu.vue's handleLogout() only checks
126- // isThunderbirdHost (true above) before posting SIGN_OUT via
127- // window.postMessage -- it never checks whether a content-script
128- // listener actually exists on the current origin (which the manifest
129- // proves it does not, for these origins). The postMessage is posted
130- // into a void with no token-bridge.js listener present to relay it to
131- // background.ts, so the add-on silently remains logged in.
130+ // Sanity: the pre-existing explicit patterns still match.
131+ expect (
132+ matchesAnyPattern ( 'https://send.tb.pro/some/path' , matchPatterns )
133+ ) . toBe ( true ) ;
134+ expect ( matchesAnyPattern ( 'http://localhost/some/path' , matchPatterns ) ) . toBe (
135+ true
136+ ) ;
132137 } ) ;
133138} ) ;
0 commit comments