fix: call no relay from the worker in no-third-party mode, and handle several accounts in one push side by side

This commit is contained in:
Linus Rath
2026-11-04 03:13:59 +01:00
parent d8e2a4f42c
commit f39cff2377
3 changed files with 58 additions and 6 deletions
+22
View File
@@ -295,4 +295,26 @@ describe('nothing new to say', () => {
});
});
describe('a push naming several accounts, one server slow', () => {
it('does not make the others wait out its deadline', async () => {
const started: string[] = [];
let release = () => {};
const slow = new Promise<void>((resolve) => { release = resolve; });
const fetchSlow = vi.fn(async (input: string) => {
const url = new URL(input, 'https://mail.example.com');
const account = url.searchParams.get('accountId') ?? '';
started.push(account);
if (account === 'slow') await slow;
return { ok: true, json: async () => ({ unreadTotal: 1, slot: 0, email: mail(`m-${account}`) }) };
});
const w = loadWorker(() => null, { globals: { fetch: fetchSlow } });
const done = w.push({ kind: 'jmap-state-change', accountId: 'slow', changed: { slow: { EmailDelivery: 's' }, fast: { EmailDelivery: 's' } } });
await vi.waitFor(() => expect(started).toEqual(['slow', 'fast']));
await vi.waitFor(() => expect(w.shade.has('bulwark-mail:fast')).toBe(true));
release();
await done;
expect(w.shade.size).toBe(2);
});
});
// -- end of cases --
@@ -7,7 +7,7 @@ import { describe, expect, it, vi } from 'vitest';
// in a VM with a minimal ServiceWorkerGlobalScope and fire the event.
const SW_SOURCE = readFileSync(join(process.cwd(), 'public', 'sw.js'), 'utf8');
function loadWorker(tabs: { postMessage: (msg: unknown) => void }[], registrations?: unknown) {
function loadWorker(tabs: { postMessage: (msg: unknown) => void }[], registrations?: unknown, config: { noThirdPartyTraffic?: boolean } | null = {}) {
const listeners = new Map<string, (event: unknown) => void>();
const newSubscription = { toJSON: () => ({ endpoint: 'https://push.example/new', keys: { p256dh: 'p', auth: 'a' } }) };
const subscribe = vi.fn(async (_options: { userVisibleOnly?: boolean; applicationServerKey?: Uint8Array }) => newSubscription);
@@ -23,8 +23,12 @@ function loadWorker(tabs: { postMessage: (msg: unknown) => void }[], registratio
self,
atob,
caches: { open: async () => ({ match: async () => (registrations ? { json: async () => registrations } : undefined) }) },
fetch: vi.fn(async (url: string, init: { body: string }) => {
relayCalls.push({ url, body: JSON.parse(init.body) });
fetch: vi.fn(async (url: string, init?: { body: string }) => {
if (url.endsWith('/api/config')) {
if (!config) throw new TypeError('offline');
return { ok: true, json: async () => config };
}
relayCalls.push({ url, body: JSON.parse(init!.body) });
return { ok: true };
}),
URL, URLSearchParams, Response, Date, console, Uint8Array,
@@ -69,6 +73,18 @@ describe('service worker pushsubscriptionchange', () => {
expect(relayCalls).toHaveLength(2);
});
it('calls no relay while third-party traffic is off: the page moves it', async () => {
const { fire, relayCalls, newSubscription } = loadWorker([], REGISTRATIONS, { noThirdPartyTraffic: true });
await fire('pushsubscriptionchange', { oldSubscription: null, newSubscription });
expect(relayCalls).toEqual([]);
});
it('calls no relay when it cannot find out', async () => {
const { fire, relayCalls, newSubscription } = loadWorker([], REGISTRATIONS, null);
await fire('pushsubscriptionchange', { oldSubscription: null, newSubscription });
expect(relayCalls).toEqual([]);
});
it('calls no relay for a browser where push was never on', async () => {
const { fire, relayCalls, newSubscription } = loadWorker([]);
await fire('pushsubscriptionchange', { oldSubscription: null, newSubscription });
+17 -3
View File
@@ -603,7 +603,9 @@ async function handlePush(event) {
const withNewMail = emailIds.length > 0 || !changed
? []
: Object.keys(changed).filter((id) => changed[id] && typeof changed[id] === "object" && "EmailDelivery" in changed[id]);
for (const accountId of withNewMail.length > 0 ? withNewMail : [named]) {
// Side by side: each has its own preview deadline, and Safari wants the
// notifications within 30 s of the push.
await Promise.all((withNewMail.length > 0 ? withNewMail : [named]).map(async (accountId) => {
try {
await notifyAccount(accountId, emailIds, accountLabel);
} catch (_) {
@@ -617,7 +619,7 @@ async function handlePush(event) {
data: { kind: "mail-list", accountId },
});
}
}
}));
}
async function notifyAccount(accountId, emailIds, accountLabel) {
@@ -1000,6 +1002,18 @@ async function readRelayRegistrations() {
}
}
/** No-third-party mode may have come on since the page left a relay here. Unknown counts as no. */
async function mayCallRelays() {
try {
const res = await fetch(`${BASE_PATH}/api/config`, { cache: "no-store" });
if (!res.ok) return false;
const config = await res.json();
return config.noThirdPartyTraffic !== true;
} catch (_) {
return false;
}
}
function applicationServerKeyOf(base64Url) {
const padded = base64Url + "=".repeat((4 - (base64Url.length % 4)) % 4);
const raw = atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
@@ -1031,7 +1045,7 @@ async function handlePushSubscriptionChange(event) {
}
}
}
if (subscription && registrations && typeof subscription.toJSON === "function") {
if (subscription && registrations && typeof subscription.toJSON === "function" && (await mayCallRelays())) {
const json = subscription.toJSON();
for (const entry of Object.values(registrations.entries)) {
try {