Skip to content

Resolve extension catalog states against the platform flags - #3029

Merged
greyivy merged 4 commits into
mainfrom
bogdan/chrome-webstore-patching-preview-state
Sep 9, 2026
Merged

greyivy merged 4 commits into
mainfrom
bogdan/chrome-webstore-patching-preview-state

Conversation

@BogdanLykhosherstov

@BogdanLykhosherstov BogdanLykhosherstov commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Asana Task/Github Issue: Fix curated extension catalog not reading when extensionManagement is set to preview

Description

Hotfix for #2991. extensionManagement ships as preview during the phased rollout. The catalog read compared state strings directly and accepted only enabled and internal, so preview read as off, the catalog came back empty, and every curated extension rendered the "Unsupported extension" pill instead of "Add to DuckDuckGo". Caught on Canary during Windows testing.

The same check was wrong in the other direction: ignoring the platform meant internal counted as on for public builds, which would offer a working install button to a browser that cannot install extensions. That is a fail-open in a feature built to fail closed.

readCuratedCatalog now takes the state check as a parameter, and the feature passes ConfigFeature#_isStateEnabled. Both states resolve against the build's platform flags rather than being matched as bare strings, and there is no second copy of the state logic left to drift.

getFeatureSettingEnabled cannot serve here: parseFeatureSettings copies only a feature's settings, so a sub-feature's state and the features map are absent, and the enabled-features filter also applies domain exceptions, which must not decide whether the browser can install extensions.

Testing Steps

  • npm exec --workspace=injected playwright -- test --project=windows chrome-webstore-patching --reporter=list (34 specs)
  • npm exec --workspace=injected jasmine -- --config=unit-test/config.json (1222 specs)
  • npm run lint
  • On a Windows build with extensionManagement and curatedExtensions at preview: a curated extension detail page (Bitwarden) shows "Add to DuckDuckGo" rather than "Unsupported extension"

Worth confirming before this is relied on: preview only takes effect if platform.preview === true in the Canary build. That flag is set Windows-side, and we already know internal comes from being logged in as an internal user rather than from the release channel. Setting the state to enabled bypasses platform flags entirely, so that working is not evidence preview will.

Checklist

Please tick all that apply:

  • I have tested this change locally
  • I have tested this change locally in all supported browsers
  • This change will be visible to users
  • I have added automated tests that cover this change
  • I have ensured the change is gated by config
  • This change was covered by a ship review
  • This change was covered by a tech design
  • Any dependent config has been merged

Note

Medium Risk
Changes fail-closed install-button gating and catalog source selection for extension management; wrong platform flags could still hide or expose install affordances incorrectly.

Overview
Fixes curated Web Store install UI treating preview rollout state as off and internal as on regardless of build flags.

readCuratedCatalog now receives the same platform-aware gate as ConfigFeature#_isStateEnabled (via an injected isEnabled callback) instead of matching only enabled/internal strings. That restores catalog reads when extensionManagement / curatedExtensions ship at preview, and stops public builds from treating internal as installable.

On internal builds (platform.internal), the helper prefers catalogInternal over catalog (replacing the public list, with safe fallback when internal catalog is missing or invalid). Docs and unit tests cover state gating, internal catalog selection, and use real isStateEnabled in specs.

Reviewed by Cursor Bugbot for commit fb1e720. Bugbot is set up for automated code reviews on this repo. Configure here.

extensionManagement ships as 'preview' during a phased rollout. The catalog
read compared state strings directly and accepted only 'enabled' and
'internal', so 'preview' read as off, the catalog came back empty, and every
curated extension rendered the "Unsupported extension" pill. Caught on
Canary during Windows testing.

The same check was wrong in the other direction too: ignoring the platform
meant 'internal' counted as on for public builds, which would offer a
working install button to a browser that cannot install extensions. That is
a fail-open in a feature built to fail closed.

readCuratedCatalog now takes the state check as a parameter and the feature
passes ConfigFeature#_isStateEnabled, so both states resolve against this
build's platform flags instead of being matched as bare strings, and there
is no second copy of the state logic to drift.

getFeatureSettingEnabled cannot serve here: parseFeatureSettings copies only
a feature's settings, so a sub-feature's state and the features map are
absent, and the enabled-features filter also applies domain exceptions,
which must not decide whether the browser can install extensions.
@github-actions github-actions Bot added the semver-patch Bug fix / internal — no release needed label Sep 9, 2026

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Injected PR Evaluation: Web Compatibility & Security

Web Compatibility Assessment

File Lines Severity Finding
injected/src/features/chrome-webstore-patching/helpers.js 54–66 info No browser API overrides or prototype changes. Scope is limited to Chrome Web Store DOM when patchWebstore is enabled. Replacing the local isStateOn() string comparison with the injected isEnabled callback is a config-gating fix only.
injected/src/features/chrome-webstore-patching.js 405 info Positive UX fix on preview builds. The removed isStateOn() treated preview as off, so the catalog read as empty and every curated extension showed the "Unsupported extension" pill on Canary (documented in the PR). Passing _isStateEnabled restores correct install/remove pill behavior during phased rollout.
injected/docs/chrome-webstore-patching.md 41 info Documentation accurately describes the platform-flag contract. No viewport, MutationObserver, or SPA-navigation behavior changes in this diff.

Security Assessment

File Lines Severity Finding
injected/src/features/chrome-webstore-patching/helpers.js 54–66 info Positive config-trust fix. Old isStateOn() treated internal as enabled on all builds regardless of platform.internal. That could reveal a working install button on public builds that cannot install extensions — a fail-open UX/security boundary violation. The injected isEnabled callback respects platform.internal / platform.preview via isStateEnabled().
injected/src/features/chrome-webstore-patching.js 405 info Correctly delegates to ConfigFeature#_isStateEnabled rather than reimplementing state logic — consistent with computeEnabledFeatures() and other features. No messaging, nativeData, captured-globals, or iframe-access changes.
injected/unit-test/chrome-webstore-patching.spec.js 16–18 info Tests now pin the real isStateEnabled contract via { internal: true } instead of a stand-in. See inline comment on missing preview / no-flag coverage.

Risk Level

Medium Risk — Config-gating logic change in injected Chrome Web Store feature code; no API overrides or messaging changes, but it affects which install buttons are revealed based on platform flags.

Recommendations

  1. Add unit tests for the two fixed regressions (see inline comment on chrome-webstore-patching.spec.js):
    • preview state + { preview: true } → catalog returned
    • internal state + no platform flags → [] (the public-build bug)
  2. Optional: Integration test exercising a preview-state bundled config on a mock preview platform args object, if the integration harness supports platform flag injection.

Local validation: 33/33 chromeWebstorePatching helpers unit specs pass on Node 24.

Open in Web View Automation 

Sent by Cursor Automation: Web compat and sec

Comment thread injected/src/features/chrome-webstore-patching/helpers.js Outdated
Comment thread injected/src/features/chrome-webstore-patching/helpers.js Outdated
Comment thread injected/unit-test/chrome-webstore-patching.spec.js Outdated
@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

[Beta] Generated file diff

Time updated: Wed, 09 Sep 2026 16:13:12 GMT

Integration
    - integration/contentScope.js

File has changed

Windows
    - windows/contentScope.js

File has changed

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread injected/src/features/chrome-webstore-patching.js Outdated
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

⚠️ Cursor review was not successful.

This PR requires a manual review and approval from a member of one of the following teams:

  • @duckduckgo/content-scope-scripts-owners
  • @duckduckgo/apple-devs
  • @duckduckgo/android-devs
  • @duckduckgo/team-windows-development
  • @duckduckgo/extension-owners
  • @duckduckgo/config-aor
  • @duckduckgo/breakage-aor
  • @duckduckgo/breakage

Windows offers a wider set internally (1Password and LastPass alongside
Bitwarden) while the public catalog stays at Bitwarden only, so internal
users read curatedExtensions.settings.catalogInternal in place of catalog.
It replaces the public list rather than extending it, matching the native
behaviour.

Absent on older configs, so a missing or malformed catalogInternal falls
back to catalog. That is the narrower list, so the fallback cannot widen
what an internal user is offered, and internal users keep working against
config that predates the field.

Internal-ness comes from platform.internal, which native passes in at init;
it is not the feature state, so it is read directly rather than through
_isStateEnabled.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 837d7f1. Configure here.

Comment thread injected/src/features/chrome-webstore-patching/helpers.js

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Web Compatibility Assessment

File Lines Severity Finding
injected/src/features/chrome-webstore-patching.js 404–406 warning getCuratedExtensionIds() does not pass isInternal to readCuratedCatalog. The helper and docs now select catalogInternal on internal builds, but the feature always uses the default isInternal = false, so production code never reads catalogInternal. Internal builds will see the public catalog instead of trial extensions — the opposite of the documented native behaviour.
injected/src/features/chrome-webstore-patching/helpers.js 61–79 info (positive) Replacing isStateOn() with injected isEnabled (backed by _isStateEnabled / isStateEnabled) correctly handles preview and internal states against platform.preview / platform.internal. Fixes the Canary regression where preview state fell through to false and every curated extension showed "Unsupported extension".
injected/src/features/chrome-webstore-patching/helpers.js 78–79 info (positive) catalogInternal selection with safe fallback to catalog when absent or malformed is sound — fallback cannot widen the public list.
injected/unit-test/chrome-webstore-patching.spec.js 17–89 info Tests now use real isStateEnabled and cover catalogInternal paths. Still missing explicit preview + { preview: true } and internal state + no platform flags → [] cases that pin the original shipped bugs.
injected/docs/chrome-webstore-patching.md 41–43 info Documentation accurately describes state gating and catalogInternal semantics.

No API overrides, DOM manipulation changes, prototype patches, or timing/race regressions in this delta.

Security Assessment

File Lines Severity Finding
injected/src/features/chrome-webstore-patching/helpers.js 67, 73 info (positive) Delegating state checks to _isStateEnabled closes the prior fail-open path: old isStateOn() treated internal as on regardless of platform.internal, which could expose a working install button on public builds without native extension support.
injected/src/features/chrome-webstore-patching.js 405 info /** @type {any} */ cast on catalog state before _isStateEnabled — type-safety gap at the install-gating boundary, not a runtime bypass (state values are plain strings from bundled config).

No changes to captured globals, messaging, message bridge, origin validation, postMessage, or config trust boundaries. No nativeData leakage vectors.

Risk Level

Medium Risk — Config-gated catalog resolution logic with platform-aware state checks; no API shims or messaging changes, but incorrect catalog selection affects install-button visibility on the Chrome Web Store.

Recommendations

  1. error — Wire platform.internal through in getCuratedExtensionIds():
    return readCuratedCatalog(
        this.bundledConfig,
        (state) => this._isStateEnabled(/** @type {any} */ (state)),
        this.platform?.internal === true,
    );
  2. warning — Add unit tests pinning the original bugs: preview state + { preview: true } → catalog IDs; internal state + {} platform → [].
  3. info — Consider narrowing the any cast by typing state as FeatureState in the callback once bundledConfig shape is validated upstream.

35/35 chromeWebstorePatching helpers unit specs pass locally (Node 24).

Open in Web View Automation 

Sent by Cursor Automation: Web compat and sec

Comment thread injected/src/features/chrome-webstore-patching.js Outdated
Fixes two review findings on #3029.

getCuratedExtensionIds never passed isInternal, so it defaulted to false
and catalogInternal was dead code: internal builds kept reading the public
catalog and trial-only extensions still showed as unsupported. The earlier
commit claimed this call site but never touched it.

Also drops the any cast on the state value. readCuratedCatalog now narrows
config values through asFeatureState, so an unrecognised state reads as
undefined and fails closed rather than being asserted into the type, and
the caller passes state through untouched.

No test covers the wiring yet. A fixture carrying catalogInternal fails
schema validation against the pinned privacy-configuration, which declares
catalog only, so that test has to wait for the schema change to merge and
the pin to be bumped.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Injected PR Evaluation: Web Compatibility & Security

Re-assessed on synchronize (ea5c2afe, 2026-09-09). The prior warning about missing isInternal wiring is resolved — getCuratedExtensionIds() now passes this.platform?.internal === true.

Web Compatibility Assessment

File Lines Severity Finding
injected/src/features/chrome-webstore-patching/helpers.js 80–99 info No browser API overrides or prototype changes. Scope remains Chrome Web Store DOM patching when patchWebstore is enabled. Replacing the local isStateOn() string comparison with an injected isEnabled callback correctly enables preview when platform.preview === true — fixes the shipped Canary regression where every curated extension showed the "Unsupported extension" pill.
injected/src/features/chrome-webstore-patching.js 407–408 info catalogInternal selection is now wired end-to-end. Internal builds read the trial catalog; public builds continue reading catalog. Fail-closed semantics preserved: unreadable config → [] → unsupported pill.
injected/src/features/chrome-webstore-patching/helpers.js 97–98 info An explicit catalogInternal: [] (empty array) is treated as present and will not fall back to catalog. Docs describe absence/malformed fallback only. Confirm this matches native extensionManagement behaviour; otherwise an empty internal list could hide all extensions on internal builds.
injected/unit-test/chrome-webstore-patching.spec.js 17–89 info Tests now exercise catalogInternal selection and use the real isStateEnabled helper. Still no explicit cases pinning the two shipped regressions: preview state with { preview: true }, and internal state with no platform flags → []. isStateEnabled is covered in utils-properties.spec.js, but direct readCuratedCatalog cases would guard against future reverts of the isEnabled injection.

Security Assessment

File Lines Severity Finding
injected/src/features/chrome-webstore-patching/helpers.js 86–92 info Positive: Delegating state checks to _isStateEnabled closes the fail-open path where state: 'internal' was treated as on regardless of platform.internal. Public builds no longer risk surfacing a working install button for a browser that cannot install extensions.
injected/src/features/chrome-webstore-patching/helpers.js 97–98 info Positive: catalogInternal replaces (not extends) catalog, and malformed/absent catalogInternal falls back to the narrower public list — cannot widen what public users are offered.
All changed files — info No changes to captured globals, messaging, message bridge, API overrides, postMessage, or load()-time logic. bundledConfig is native-supplied; page scripts cannot influence catalog resolution.

Risk Level

Medium Risk — platform-aware config state gating and internal-catalog selection affect Chrome Web Store install-button visibility, but no browser API shims, messaging, or security-boundary changes.

Recommendations

  1. Optional (info): Add two readCuratedCatalog unit cases: preview + { preview: true } → catalog IDs; internal + {} platform → [].
  2. Optional (info): Confirm empty-array catalogInternal behaviour with native; add a fallback or test if native treats [] as absent.
  3. Ready to merge from a web-compat/security perspective — the isInternal wiring gap from the prior review is closed. 14/14 readCuratedCatalog specs pass locally.
Open in Web View Automation 

Sent by Cursor Automation: Web compat and sec

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Build Branch

Branch pr-releases/bogdan/chrome-webstore-patching-preview-state
Commit e83eef07c5
Updated September 9, 2026 at 4:13:06 PM UTC

Static preview entry points

QR codes (mobile preview)
Entry point QR code
Docs QR for docs preview
Static pages QR for static pages preview
Integration pages QR for integration pages preview

Integration commands

npm (Android / Extension):

npm i github:duckduckgo/content-scope-scripts#pr-releases/bogdan/chrome-webstore-patching-preview-state

Swift Package Manager (Apple):

.package(url: "https://github.com/duckduckgo/content-scope-scripts.git", branch: "pr-releases/bogdan/chrome-webstore-patching-preview-state")

git submodule (Windows):

git -C submodules/content-scope-scripts fetch origin pr-releases/bogdan/chrome-webstore-patching-preview-state
git -C submodules/content-scope-scripts checkout origin/pr-releases/bogdan/chrome-webstore-patching-preview-state
Pin to exact commit

npm (Android / Extension):

npm i github:duckduckgo/content-scope-scripts#e83eef07c54463d912f6eed560dc954063dc9fd0

Swift Package Manager (Apple):

.package(url: "https://github.com/duckduckgo/content-scope-scripts.git", revision: "e83eef07c54463d912f6eed560dc954063dc9fd0")

git submodule (Windows):

git -C submodules/content-scope-scripts fetch origin pr-releases/bogdan/chrome-webstore-patching-preview-state
git -C submodules/content-scope-scripts checkout e83eef07c54463d912f6eed560dc954063dc9fd0

@greyivy
greyivy added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit f859001 Sep 9, 2026
40 of 41 checks passed
@greyivy
greyivy deleted the bogdan/chrome-webstore-patching-preview-state branch September 9, 2026 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-patch Bug fix / internal — no release needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants