From 82897df8a3b0e9d3fc5201801e2680b8ba3a5953 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 29 Aug 2026 10:02:18 +0000 Subject: [PATCH] fix(extension): make the packaged build valid for the Chrome Web Store The manifests themselves were structurally fine, but the package the build produced was not, and several declared entries were dead or dev-only. Build: - vite.config.js read the bare `BROWSER` env var to pick its outDir. `BROWSER` is a standard Linux env var (xdg-open et al) and is set on developer machines, so Vite wrote to dist/true (or dist/firefox) while build.js copied the manifest into dist/chrome. The resulting zip shipped only the manifest, icons and rulesets - popup, options page, service worker, blocked.html and the favicon icons were all absent, which the Web Store rejects as a manifest referencing files not in the package. Renamed the switch to EXT_BROWSER, validated against the known targets, and passed it explicitly from build.js. - Dropped the stray root-level background.js: an unbundled copy of the source with bare import specifiers that nothing referenced. - Release builds now strip http://localhost host permissions (EXT_DEV=1 keeps them for local work) instead of shipping them to reviewers. Manifests (chrome, firefox, safari): - Removed the `oauth2` block. Its client_id was still the literal YOUR_GOOGLE_CLIENT_ID placeholder, and it was never read: Google auth goes through identity.launchWebAuthFlow, not identity.getAuthToken. - Removed `optional_permissions: ["tabs"]` - nothing in the codebase calls permissions.request, so it could never be granted. - Removed the `icons/*` web_accessible_resources entry matched to . Extension pages and notification iconUrls do not need WAR; exposing it only made the extension fingerprintable from any page. - Declared `notifications`, which background/index.js already calls when sync hits its retry limit. The call was throwing into its own catch, so the notification was never shown. `key` is kept: it derives to hjcjjcpialiakkalcgadnfnoomdaegjg, the live store item, and the OAuth redirect URIs depend on that ID staying stable. Verified: 786 tests pass, lint clean, and all three packaged manifests resolve every file they reference with BROWSER still polluted in the environment. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012BqLAA6Qqj6bPVkd5nYMVi --- apps/extension/scripts/build.js | 57 ++++++++++++++---------- apps/extension/src/manifest.chrome.json | 18 +------- apps/extension/src/manifest.firefox.json | 12 +---- apps/extension/src/manifest.safari.json | 12 +---- apps/extension/vite.config.js | 13 +++++- 5 files changed, 48 insertions(+), 64 deletions(-) diff --git a/apps/extension/scripts/build.js b/apps/extension/scripts/build.js index 3341420..c5c0c99 100644 --- a/apps/extension/scripts/build.js +++ b/apps/extension/scripts/build.js @@ -66,9 +66,12 @@ function buildFilters() { function buildVite() { console.log('📦 Building with Vite...'); + // Vite writes to dist/; pass it explicitly so an ambient + // BROWSER env var can never redirect the output (see vite.config.js). execSync('pnpm vite build', { cwd: ROOT_DIR, stdio: 'inherit', + env: { ...process.env, EXT_BROWSER: 'chrome' }, }); } @@ -115,6 +118,33 @@ function copyDirectorySync(src, dest) { } } +/** + * Read a source manifest and prepare it for distribution. + * + * `http://localhost:*` host permissions are needed when running against a local + * web app, but shipping them is a standing "unnecessary permissions" rejection + * risk on the Chrome Web Store and AMO. Keep them in the source manifests and + * strip them from release builds; set EXT_DEV=1 to keep them. + */ +function readManifestForRelease(browser) { + const manifest = JSON.parse( + readFileSync(join(ROOT_DIR, `src/manifest.${browser}.json`), 'utf-8') + ); + + if (!process.env.EXT_DEV && Array.isArray(manifest.host_permissions)) { + const before = manifest.host_permissions.length; + manifest.host_permissions = manifest.host_permissions.filter( + (h) => !/^https?:\/\/(localhost|127\.0\.0\.1)([:/]|$)/.test(h) + ); + const dropped = before - manifest.host_permissions.length; + if (dropped > 0) { + console.log(` Stripped ${dropped} localhost host permission(s) (EXT_DEV=1 to keep)`); + } + } + + return `${JSON.stringify(manifest, null, 2)}\n`; +} + /** * Copy manifest and assets for Chrome */ @@ -126,8 +156,7 @@ function buildChrome() { // We need to ensure the manifest and icons are correct // Copy Chrome manifest as manifest.json - const chromeManifest = readFileSync(join(ROOT_DIR, 'src/manifest.chrome.json'), 'utf-8'); - writeFileSync(join(CHROME_DIR, 'manifest.json'), chromeManifest); + writeFileSync(join(CHROME_DIR, 'manifest.json'), readManifestForRelease('chrome')); // Copy icons copyIcons(CHROME_DIR); @@ -135,12 +164,6 @@ function buildChrome() { // Copy adblock rulesets copyRules(CHROME_DIR); - // Copy background script - const bgSrc = join(ROOT_DIR, 'src/background/index.js'); - if (existsSync(bgSrc)) { - copyFileSync(bgSrc, join(CHROME_DIR, 'background.js')); - } - console.log('✅ Chrome build complete'); } @@ -157,8 +180,7 @@ function buildFirefox() { } // Copy Firefox manifest as manifest.json (overwrite Chrome manifest) - const firefoxManifest = readFileSync(join(ROOT_DIR, 'src/manifest.firefox.json'), 'utf-8'); - writeFileSync(join(FIREFOX_DIR, 'manifest.json'), firefoxManifest); + writeFileSync(join(FIREFOX_DIR, 'manifest.json'), readManifestForRelease('firefox')); // Copy icons copyIcons(FIREFOX_DIR); @@ -166,12 +188,6 @@ function buildFirefox() { // Copy adblock rulesets copyRules(FIREFOX_DIR); - // Copy background script - const bgSrc = join(ROOT_DIR, 'src/background/index.js'); - if (existsSync(bgSrc)) { - copyFileSync(bgSrc, join(FIREFOX_DIR, 'background.js')); - } - console.log('✅ Firefox build complete'); } @@ -188,8 +204,7 @@ function buildSafari() { } // Copy Safari manifest as manifest.json (overwrite Chrome manifest) - const safariManifest = readFileSync(join(ROOT_DIR, 'src/manifest.safari.json'), 'utf-8'); - writeFileSync(join(SAFARI_DIR, 'manifest.json'), safariManifest); + writeFileSync(join(SAFARI_DIR, 'manifest.json'), readManifestForRelease('safari')); // Copy icons copyIcons(SAFARI_DIR); @@ -197,12 +212,6 @@ function buildSafari() { // Copy adblock rulesets copyRules(SAFARI_DIR); - // Copy background script - const bgSrc = join(ROOT_DIR, 'src/background/index.js'); - if (existsSync(bgSrc)) { - copyFileSync(bgSrc, join(SAFARI_DIR, 'background.js')); - } - console.log('✅ Safari build complete'); console.log(''); console.log('📝 Note: Safari requires additional steps:'); diff --git a/apps/extension/src/manifest.chrome.json b/apps/extension/src/manifest.chrome.json index e1f10e1..ae2f710 100644 --- a/apps/extension/src/manifest.chrome.json +++ b/apps/extension/src/manifest.chrome.json @@ -28,13 +28,11 @@ "bookmarks", "storage", "alarms", + "notifications", "identity", "declarativeNetRequest", "activeTab" ], - "optional_permissions": [ - "tabs" - ], "declarative_net_request": { "rule_resources": [ { @@ -60,24 +58,10 @@ "https://hole.cert.pl/*", "https://raw.githubusercontent.com/*" ], - "oauth2": { - "client_id": "YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com", - "scopes": [ - "https://www.googleapis.com/auth/drive.file" - ] - }, "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, "web_accessible_resources": [ - { - "resources": [ - "icons/*" - ], - "matches": [ - "" - ] - }, { "resources": [ "blocked.html" diff --git a/apps/extension/src/manifest.firefox.json b/apps/extension/src/manifest.firefox.json index f94c54d..91aa16d 100644 --- a/apps/extension/src/manifest.firefox.json +++ b/apps/extension/src/manifest.firefox.json @@ -32,13 +32,11 @@ "bookmarks", "storage", "alarms", + "notifications", "identity", "declarativeNetRequest", "activeTab" ], - "optional_permissions": [ - "tabs" - ], "declarative_net_request": { "rule_resources": [ { @@ -68,14 +66,6 @@ "extension_pages": "script-src 'self'; object-src 'self'" }, "web_accessible_resources": [ - { - "resources": [ - "icons/*" - ], - "matches": [ - "" - ] - }, { "resources": [ "blocked.html" diff --git a/apps/extension/src/manifest.safari.json b/apps/extension/src/manifest.safari.json index 52df46b..73ce5b9 100644 --- a/apps/extension/src/manifest.safari.json +++ b/apps/extension/src/manifest.safari.json @@ -27,13 +27,11 @@ "bookmarks", "storage", "alarms", + "notifications", "identity", "declarativeNetRequest", "activeTab" ], - "optional_permissions": [ - "tabs" - ], "declarative_net_request": { "rule_resources": [ { @@ -61,14 +59,6 @@ "extension_pages": "script-src 'self'; object-src 'self'" }, "web_accessible_resources": [ - { - "resources": [ - "icons/*" - ], - "matches": [ - "" - ] - }, { "resources": [ "blocked.html" diff --git a/apps/extension/vite.config.js b/apps/extension/vite.config.js index c8d0ae9..933fa74 100644 --- a/apps/extension/vite.config.js +++ b/apps/extension/vite.config.js @@ -5,7 +5,18 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'fs'; const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8')); -const browser = process.env.BROWSER || 'chrome'; +// Deliberately NOT `process.env.BROWSER`: that is a standard Linux env var +// (xdg-open et al) and is commonly set to something like `true` or `firefox`, +// which silently redirected the build into dist/ and shipped a zip +// missing the popup, options page, service worker and icons. +const TARGETS = ['chrome', 'firefox', 'safari']; +const requested = process.env.EXT_BROWSER; +if (requested && !TARGETS.includes(requested)) { + throw new Error( + `EXT_BROWSER must be one of ${TARGETS.join(', ')} (got "${requested}")` + ); +} +const browser = requested || 'chrome'; // Plugin to move HTML files from src/* to root after build function moveHtmlPlugin() {