Skip to content

Commit e201ccf

Browse files
committed
fix(addon): cover https://localhost and *.tb.pro preview hosts in token-bridge manifest pattern
manifest.json's content_scripts.matches for token-bridge.js previously listed only: - https://send.tb.pro/* - https://send-stage.tb.pro/* - http://localhost/* That left two real-world gaps where token-bridge.js would NOT be injected, but isThunderbirdHost (config-store.ts) was still true and UserMenu.vue's SIGN_OUT postMessage would silently go to a void with no listener to relay it into background.ts: - https://localhost:<port>/... (HTTPS local dev/test, not just the bare http://localhost the manifest listed) - https://send-<preview>.tb.pro/... (any preview/staging subdomain) Add the missing patterns. The matchesAnyPattern() helper in the test also gets a small update so its minimal match-pattern-to-regex translator inserts an optional :PORT between host and path -- the real WebExtension match-pattern spec matches any port by default. Refs #1020 Closes #1020
1 parent 352e73c commit e201ccf

2 files changed

Lines changed: 57 additions & 50 deletions

File tree

packages/addon/public/manifest.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,9 @@
145145
"matches": [
146146
"https://send.tb.pro/*",
147147
"https://send-stage.tb.pro/*",
148-
"http://localhost/*"
148+
"https://send-*.tb.pro/*",
149+
"http://localhost/*",
150+
"https://localhost/*"
149151
],
150152
"js": [
151153
"token-bridge.js"

packages/addon/src/test/integration/a2-origin-mismatch-logout-not-delivered.test.ts

Lines changed: 54 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,27 @@
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
*/
3526
import { readFileSync } from 'node:fs';
3627
import { 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
*/
5049
function 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

Comments
 (0)