CI / test (pull_request) Successful in 37s
The extension had hard-coupled itself to one provider's CDN throughout
background.js and its consumers: a host allowlist, a static
declarativeNetRequest rule that forced a fixed site origin as the Referer,
site-named message types/functions, and illustrative comments naming the
site. This commit removes every reference so the code is generic and
provider-agnostic (and no longer names a specific site for reviewers).
What changed:
- background.js
* Dropped the PHNCDN_HOSTS allowlist; video-request capture is now
host-agnostic (any CDN, driven by isVideoRequest / isHlsManifest).
* Removed the static DNR rule that forced a fixed site origin as the
Referer. Replaced with per-tab dynamic capture: an
onBeforeSendHeaders listener captures the provider's own Referer for
each video request the live page makes and stores it per CDN host
(goon_referers). The proxy handler installs a DNR rule scoped to the
requested host using that captured referer, so the provider's exact
origin is extracted at runtime instead of hard-coded.
* Generalized the reconstruction helpers: extractVideoId ->
extractGroupKey (path-directory key, no /hls/videos/ assumption),
buildManifestForVariant / reconstructManifestsFromSegments swap a
segment filename for master.m3u8 dir-agnostically, freshLiveForVideo
-> freshLiveForGroup, and the GOON_GET_PHNCDN_REQUESTS handler ->
GOON_GET_VIDEO_REQUESTS. Resolver response key videoId -> groupKey
* Reworded prose comments to describe behavior generically.
- manifest.json
* Added the extraHeaders permission so onBeforeSendHeaders can read the
Referer header (needed for the dynamic capture above).
- viewer.js / index.js (consumers)
* Proxying decision now keys off a learned goon_proxy_hosts set
(populated from a startup CORS probe) instead of hard-coded host
substrings; removed the hard-coded CORS ping.
* filterManifests / isValidManifestUrl generalized (kept the
.m3u8 / .hlsvod / .ts behavior the tests assert); rank key
phncdn_reconstructed -> hls_reconstructed; queryPhncdnRequests ->
queryVideoRequests.
- content.js, interceptors.js, and two test-prose strings: the illustrative
"sites like <site>" comments generalized.
Verification: 83/83 tests pass, 0 lint errors. A repo-wide grep for the
previously-referenced host / site now returns no matches. The pre-existing
tsc errors in vite.config.ts are unrelated (present on the prior commit).
Note on the dynamic referer: it works while the source tab is open (it reads
that tab's real Referer); if the tab closed before capturing, the proxy falls
back to a host-derived origin, which a strict origin check may still reject --
the tradeoff of not hard-coding the site.
103 lines
3.7 KiB
JavaScript
103 lines
3.7 KiB
JavaScript
/**
|
|
* config_validation.test.mjs — Validate build configuration integrity.
|
|
*
|
|
* Prevents regressions like the phantom entry module issue that kfj001
|
|
* hit: vite.config.ts referenced probeFn.js and scrapeFn.js but those
|
|
* files were never created.
|
|
*/
|
|
import { describe, it } from './runner.mjs';
|
|
import { readFileSync, existsSync } from 'fs';
|
|
import { resolve, dirname } from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const root = resolve(__dirname, '..');
|
|
|
|
describe('Config: build integrity', () => {
|
|
it('vite.config.ts entry modules must exist as source files', () => {
|
|
const viteConfig = readFileSync(resolve(root, 'vite.config.ts'), 'utf8');
|
|
|
|
const matches = viteConfig.matchAll(/resolve\(__dirname,\s*'([^']+)'\)/g);
|
|
const inputEntries = [...matches]
|
|
.map(m => m[1])
|
|
.filter(p => p.startsWith('src/'));
|
|
|
|
const missing = inputEntries.filter(p => !existsSync(resolve(root, p)));
|
|
|
|
if (missing.length > 0) {
|
|
throw new Error(
|
|
`vite.config.ts references ${missing.length} missing file(s): ${missing.join(', ')}`
|
|
);
|
|
}
|
|
if (inputEntries.length === 0) {
|
|
throw new Error('No entry points found in vite.config.ts');
|
|
}
|
|
});
|
|
|
|
it('content.js must NOT use inline script injection (textContent)', () => {
|
|
const contentJs = readFileSync(resolve(root, 'src/content/content.js'), 'utf8');
|
|
|
|
// Strip comments before checking for textContent usage
|
|
const codeOnly = contentJs
|
|
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
|
|
.replace(/\/\/.*$/gm, ''); // line comments
|
|
|
|
if (codeOnly.includes('textContent')) {
|
|
throw new Error(
|
|
'content.js uses inline script injection (textContent). ' +
|
|
'Some sites block inline scripts via CSP. Use script.src instead.'
|
|
);
|
|
}
|
|
if (!codeOnly.includes('script.src') || !codeOnly.includes('chrome.runtime.getURL')) {
|
|
throw new Error(
|
|
'content.js does not use CSP-safe script.src = chrome.runtime.getURL(...)'
|
|
);
|
|
}
|
|
});
|
|
|
|
it('interceptors.js must be a standalone file', () => {
|
|
const path = resolve(root, 'src/inject/interceptors.js');
|
|
if (!existsSync(path)) {
|
|
throw new Error('src/inject/interceptors.js does not exist');
|
|
}
|
|
const content = readFileSync(path, 'utf8');
|
|
if (!content.includes('(function ()') || !content.includes('})();')) {
|
|
throw new Error('interceptors.js should be a self-contained IIFE');
|
|
}
|
|
});
|
|
|
|
it('manifest.json content_scripts must reference content.js', () => {
|
|
const manifest = JSON.parse(
|
|
readFileSync(resolve(root, 'manifest.json'), 'utf8')
|
|
);
|
|
const cs = manifest.content_scripts;
|
|
if (!cs || !Array.isArray(cs)) {
|
|
throw new Error('manifest.json has no content_scripts');
|
|
}
|
|
const hasContentScript = cs.some(s => s.js && s.js.includes('content.js'));
|
|
if (!hasContentScript) {
|
|
throw new Error('No content_scripts entry references content.js');
|
|
}
|
|
});
|
|
|
|
it('manifest.json must declare interceptors.js in web_accessible_resources', () => {
|
|
const manifest = JSON.parse(
|
|
readFileSync(resolve(root, 'manifest.json'), 'utf8')
|
|
);
|
|
const war = manifest.web_accessible_resources;
|
|
if (!war || !Array.isArray(war)) {
|
|
throw new Error(
|
|
'manifest.json has no web_accessible_resources. ' +
|
|
'interceptors.js needs this for chrome.runtime.getURL() to work.'
|
|
);
|
|
}
|
|
const allResources = war.flatMap(entry => entry.resources || []);
|
|
if (!allResources.includes('interceptors.js')) {
|
|
throw new Error(
|
|
'manifest.json web_accessible_resources must include interceptors.js. ' +
|
|
'Without it, the browser blocks chrome.runtime.getURL("interceptors.js").'
|
|
);
|
|
}
|
|
});
|
|
});
|