Harden host access recovery for v2.6.2

This commit is contained in:
Timo
2026-07-15 12:46:57 +02:00
parent a334605dd5
commit 142153a131
7 changed files with 278 additions and 125 deletions
+28 -10
View File
@@ -7,8 +7,10 @@ import {
addTabHostAccessRequest,
describeTabUrl,
inspectTabHostAccess,
isHostAccessError,
normalizeTabId,
removeTabHostAccessRequest
removeTabHostAccessRequest,
requestOriginPermission
} from '../extension/host-access.js';
assert.equal(HOST_ACCESS_REQUIRED_STATUS, 'host_permission_required');
@@ -17,6 +19,11 @@ assert.equal(normalizeTabId(undefined), null);
assert.equal(normalizeTabId(''), null);
assert.equal(normalizeTabId(0), null);
assert.equal(normalizeTabId('42'), 42);
assert.equal(normalizeTabId(true), null);
assert.equal(normalizeTabId([42]), null);
assert.equal(normalizeTabId('42.5'), null);
assert.equal(normalizeTabId(' 42 '), 42);
assert.equal(normalizeTabId(Number.MAX_SAFE_INTEGER + 1), null);
assert.deepEqual(describeTabUrl('https://emby.example:8443/web/index.html'), {
url: 'https://emby.example:8443/web/index.html',
host: 'emby.example:8443',
@@ -50,30 +57,41 @@ assert.deepEqual(containsRequest, { origins: ['https://video.example/*'] });
let requestedTabId = null;
const requestChrome = {
permissions: {
addHostAccessRequest: async ({ tabId }) => { requestedTabId = tabId; }
addHostAccessRequest: async request => { requestedTabId = request; }
}
};
assert.equal(await addTabHostAccessRequest(requestChrome, 42), true);
assert.equal(requestedTabId, 42);
assert.equal(await addTabHostAccessRequest(requestChrome, 42, 'https://video.example/*'), true);
assert.deepEqual(requestedTabId, { tabId: 42, pattern: 'https://video.example/*' });
assert.equal(await addTabHostAccessRequest({ permissions: {} }, 42), false);
let removedTabId = null;
const removeRequestChrome = {
permissions: {
removeHostAccessRequest: async ({ tabId }) => { removedTabId = tabId; }
removeHostAccessRequest: async request => { removedTabId = request; }
}
};
assert.equal(await removeTabHostAccessRequest(removeRequestChrome, 42), true);
assert.equal(removedTabId, 42);
assert.equal(await removeTabHostAccessRequest(removeRequestChrome, 42, 'https://video.example/*'), true);
assert.deepEqual(removedTabId, { tabId: 42, pattern: 'https://video.example/*' });
assert.equal(await removeTabHostAccessRequest({ permissions: {} }, 42), false);
assert.equal(isHostAccessError(new Error('Missing host permission for the tab')), true);
assert.equal(isHostAccessError(new Error('No tab with id: 42')), false);
const callbackPermissionChrome = {
runtime: {},
permissions: {
request: (_request, callback) => { callback(true); }
}
};
assert.equal(await requestOriginPermission(callbackPermissionChrome, 'https://video.example/*'), true);
assert.equal(await requestOriginPermission({ permissions: {} }, 'https://video.example/*'), null);
const background = fs.readFileSync(path.join(cwd(), 'extension', 'background.js'), 'utf8');
const popup = fs.readFileSync(path.join(cwd(), 'extension', 'popup.js'), 'utf8');
const popupHtml = fs.readFileSync(path.join(cwd(), 'extension', 'popup.html'), 'utf8');
assert.match(background, /await activateTargetTab\(message\.tabId, message\.tabTitle\)/,
'SET_TARGET_TAB must await successful activation before acknowledging it');
assert.match(background, /addTabHostAccessRequest\(chrome, tabId\)/,
assert.match(background, /addTabHostAccessRequest\(chrome, tabId, access\.originPattern\)/,
'failed injection must register Chrome host-access request');
assert.match(background, /retryPendingTarget\(\)/,
'pending target must resume after the user grants access');
@@ -87,11 +105,11 @@ assert.ok(
activateTargetBody.indexOf('await injectContentScript') < activateTargetBody.indexOf('currentTabId = selectedTabId'),
'a tab must not become current until its content script injection succeeds'
);
assert.match(background, /removeTabHostAccessRequest\(chrome, pendingTabId\)/,
assert.match(background, /removeTabHostAccessRequest\([\s\S]*pendingTabId/,
'clearing a pending target must also clear Chrome toolbar access requests');
assert.match(popup, /response\?\.status === 'host_permission_required'/,
'popup must render the structured host-access failure');
assert.match(popup, /chrome\.permissions\.request\(\{ origins: \[originPattern\] \}\)/,
assert.match(popup, /requestOriginPermission\(chrome, requestedOriginPattern\)/,
'retry button must request withheld host access directly');
assert.match(popupHtml, /id="siteAccessNotice"/,
'popup must contain a persistent site-access notice');
-57
View File
@@ -1,57 +0,0 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = relativePath => fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
const template = read('website/template.html');
const foundationCss = read('website/styles/foundation.css');
const supportCss = read('website/styles/support.css');
const helpPage = read('website/site-access-help.html');
const build = read('website/build.cjs');
const localesDir = path.join(repoRoot, 'website', 'locales');
assert.equal((template.match(/class="site-access-banner"/g) || []).length, 1,
'localized landing template must contain exactly one support banner');
assert.match(template, /href="\/site-access-help\.html"/,
'every compiled language must link to the English root help page');
assert.match(template, /\{\{SUPPORT_BANNER_TEXT\}\}/);
assert.match(template, /\{\{SUPPORT_BANNER_CTA\}\}/);
assert.match(foundationCss, /\.site-access-banner\s*\{/,
'landing bundle must style the support banner without deferred CSS');
for (const file of fs.readdirSync(localesDir).filter(file => file.endsWith('.json'))) {
const locale = JSON.parse(fs.readFileSync(path.join(localesDir, file), 'utf8'));
assert.ok(locale.SUPPORT_BANNER_TEXT?.trim(), `${file} is missing SUPPORT_BANNER_TEXT`);
assert.ok(locale.SUPPORT_BANNER_CTA?.trim(), `${file} is missing SUPPORT_BANNER_CTA`);
}
assert.match(helpPage, /<html lang="en"/);
assert.match(helpPage, /rel="canonical" href="https:\/\/sync\.koalastuff\.net\/site-access-help\.html"/);
assert.doesNotMatch(helpPage, /\{\{[A-Z0-9_-]+\}\}/,
'English-only static help page must not contain unresolved template placeholders');
assert.match(helpPage, /Access requested/,
'help page must explain the new Chromium access-request UI');
assert.match(helpPage, /Firefox/,
'help page must include Firefox-specific recovery instructions');
assert.doesNotMatch(helpPage, /Peers \(1\)|Separate issue/,
'website-access help must not include unrelated relay troubleshooting');
assert.match(helpPage, /video and audio stream never leave your browser/,
'help page must explain the privacy boundary of the permission');
assert.match(helpPage, /support\.google\.com\/chrome/);
assert.match(helpPage, /support\.mozilla\.org/);
assert.match(supportCss, /html\.theme-light \.support-card/,
'support cards must define a readable light-theme surface');
assert.match(build, /\{ src: 'site-access-help\.html', dest: 'site-access-help\.html' \}/,
'website build must copy the help page to the root output');
assert.match(build, /'styles\/support\.css'/,
'website build must include support page styles in style.min.css');
assert.match(build, /https:\/\/sync\.koalastuff\.net\/site-access-help\.html/,
'sitemap must include the English-only help page');
console.log('Site-access help page and localized landing banner are complete');
-1
View File
@@ -29,7 +29,6 @@ const checks = [
['background syntax', 'node', ['-c', 'extension/background.js']],
['locale coverage', 'node', ['scripts/test-locales.cjs']],
['website locale coverage', 'node', ['scripts/test-website-locales.mjs']],
['website site-access help', 'node', ['scripts/test-site-access-help.mjs']],
['website theme coverage', 'node', ['scripts/test-website-theme.mjs']],
['lint', 'npm', ['run', 'lint']],
['root production audit', 'npm', ['audit', '--omit=dev']],