Skip to content

fix(extension): make the packaged build valid for the Chrome Web Store - #21

Merged
ralyodio merged 1 commit into
masterfrom
cws-manifest-check
Aug 29, 2026
Merged

fix(extension): make the packaged build valid for the Chrome Web Store#21
ralyodio merged 1 commit into
masterfrom
cws-manifest-check

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The manifests themselves were structurally fine, but the package the build produced was not, and several declared entries were dead or dev-only.

The blocker

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 — on this box it's true. So Vite wrote to dist/true while build.js copied the manifest into dist/chrome, and marksyncr-chrome.zip shipped with only the manifest, icons and rulesets:

MISSING  icons/favicon-16.png     MISSING  popup/index.html
MISSING  icons/favicon-32.png     MISSING  background/index.js
MISSING  blocked.html             MISSING  options/index.html

A manifest referencing files not present in the package is rejected at upload. Renamed the switch to EXT_BROWSER, validated it against the known targets, and pass it explicitly from build.js.

Other fixes

Build

  • Dropped the stray root-level background.js — an unbundled copy of the source with bare import specifiers that nothing referenced.
  • Release builds strip http://localhost host permissions (EXT_DEV=1 keeps them for local work) rather than shipping a dev-only permission to reviewers.

Manifests (chrome, firefox, safari)

  • Removed the oauth2 block. Its client_id was still the literal YOUR_GOOGLE_CLIENT_ID.apps.googleusercontent.com placeholder, and it was never read — Google auth goes through identity.launchWebAuthFlow, not identity.getAuthToken.
  • Removed optional_permissions: ["tabs"] — nothing calls permissions.request, so it could never be granted.
  • Removed the icons/* web_accessible_resources entry matched to <all_urls>. Extension pages and notification iconUrls don't need WAR; exposing it only made the extension fingerprintable from any page.
  • Declared notifications, which background/index.js:3685 already calls when sync hits its retry limit. The call was throwing into its own catch, so the notification was never shown.

Left alone deliberately

key is kept — it derives to hjcjjcpialiakkalcgadnfnoomdaegjg, the live store item, and the chromiumapp.org OAuth redirect URIs depend on that ID staying stable.

Verification

  • 786 tests pass, lint clean, monorepo build passes.
  • All three packaged manifests resolve every file they reference, with BROWSER=true still polluting the environment.
  • Static dNR rulesets: 30,000 enabled rules (exactly Chrome's guaranteed minimum), unique ids, no regex rules, schema valid.

Not addressed — needs a product call

Single-purpose policy. The item bundles bookmark sync + an ad/tracker blocker + a security shield. That is the likeliest remaining review rejection, and it's a listing/positioning decision rather than a manifest fix.

🤖 Generated with Claude Code

https://claude.ai/code/session_012BqLAA6Qqj6bPVkd5nYMVi

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 <all_urls>.
  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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012BqLAA6Qqj6bPVkd5nYMVi
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

12 finding(s)

MEDIUM: 8 | LOW: 4

Severity Rule Location
MEDIUM js-open-redirect apps/extension/src/blocked/main.js:84
MEDIUM sql-template-interpolation apps/web/app/api/account/delete/route.js:104
MEDIUM js-credential-logged apps/web/app/api/health/route.js:27
MEDIUM js-open-redirect apps/web/app/dashboard/sync-sources-client.jsx:88
MEDIUM js-unescaped-html-sink apps/web/app/layout.jsx:86
MEDIUM js-unescaped-html-sink apps/web/app/layout.jsx:110
MEDIUM js-open-redirect apps/web/app/pricing/page.jsx:178
MEDIUM manifest-install-lifecycle-script package.json:17
LOW secret-generic-credential apps/web/__tests__/auth-api.test.js:541
LOW redos-nested-quantifier packages/sources/__tests__/dropbox-oauth.test.ts:72
LOW secret-generic-credential packages/vault/__tests__/items.test.js:201
LOW secret-generic-credential packages/vault/__tests__/items.test.js:216

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit d808f2b into master Aug 29, 2026
9 checks passed
@ralyodio
ralyodio deleted the cws-manifest-check branch August 29, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant