Resolve extension catalog states against the platform flags - #3029
Conversation
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.
There was a problem hiding this comment.
Stale comment
Injected PR Evaluation: Web Compatibility & Security
Web Compatibility Assessment
File Lines Severity Finding injected/src/features/chrome-webstore-patching/helpers.js54–66 info No browser API overrides or prototype changes. Scope is limited to Chrome Web Store DOM when patchWebstoreis enabled. Replacing the localisStateOn()string comparison with the injectedisEnabledcallback is a config-gating fix only.injected/src/features/chrome-webstore-patching.js405 info Positive UX fix on preview builds. The removed isStateOn()treatedpreviewas off, so the catalog read as empty and every curated extension showed the "Unsupported extension" pill on Canary (documented in the PR). Passing_isStateEnabledrestores correct install/remove pill behavior during phased rollout.injected/docs/chrome-webstore-patching.md41 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.js54–66 info Positive config-trust fix. Old isStateOn()treatedinternalas enabled on all builds regardless ofplatform.internal. That could reveal a working install button on public builds that cannot install extensions — a fail-open UX/security boundary violation. The injectedisEnabledcallback respectsplatform.internal/platform.previewviaisStateEnabled().injected/src/features/chrome-webstore-patching.js405 info Correctly delegates to ConfigFeature#_isStateEnabledrather than reimplementing state logic — consistent withcomputeEnabledFeatures()and other features. No messaging,nativeData, captured-globals, or iframe-access changes.injected/unit-test/chrome-webstore-patching.spec.js16–18 info Tests now pin the real isStateEnabledcontract via{ internal: true }instead of a stand-in. See inline comment on missingpreview/ 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
- Add unit tests for the two fixed regressions (see inline comment on
chrome-webstore-patching.spec.js):
previewstate +{ preview: true }→ catalog returnedinternalstate + no platform flags →[](the public-build bug)- 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 helpersunit specs pass on Node 24.Sent by Cursor Automation: Web compat and sec
[Beta] Generated file diffTime updated: Wed, 09 Sep 2026 16:13:12 GMT IntegrationFile has changed WindowsFile has changed |
|
This PR requires a manual review and approval from a member of one of the following teams:
|
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.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ 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.
There was a problem hiding this comment.
Stale comment
Web Compatibility Assessment
File Lines Severity Finding injected/src/features/chrome-webstore-patching.js404–406 warning getCuratedExtensionIds()does not passisInternaltoreadCuratedCatalog. The helper and docs now selectcatalogInternalon internal builds, but the feature always uses the defaultisInternal = false, so production code never readscatalogInternal. 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.js61–79 info (positive) Replacing isStateOn()with injectedisEnabled(backed by_isStateEnabled/isStateEnabled) correctly handlespreviewandinternalstates againstplatform.preview/platform.internal. Fixes the Canary regression wherepreviewstate fell through tofalseand every curated extension showed "Unsupported extension".injected/src/features/chrome-webstore-patching/helpers.js78–79 info (positive) catalogInternalselection with safe fallback tocatalogwhen absent or malformed is sound — fallback cannot widen the public list.injected/unit-test/chrome-webstore-patching.spec.js17–89 info Tests now use real isStateEnabledand covercatalogInternalpaths. Still missing explicitpreview+{ preview: true }andinternalstate + no platform flags →[]cases that pin the original shipped bugs.injected/docs/chrome-webstore-patching.md41–43 info Documentation accurately describes state gating and catalogInternalsemantics.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.js67, 73 info (positive) Delegating state checks to _isStateEnabledcloses the prior fail-open path: oldisStateOn()treatedinternalas on regardless ofplatform.internal, which could expose a working install button on public builds without native extension support.injected/src/features/chrome-webstore-patching.js405 info /** @type {any} */cast on catalogstatebefore_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. NonativeDataleakage 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
- error — Wire
platform.internalthrough ingetCuratedExtensionIds():return readCuratedCatalog( this.bundledConfig, (state) => this._isStateEnabled(/** @type {any} */ (state)), this.platform?.internal === true, );- warning — Add unit tests pinning the original bugs:
previewstate +{ preview: true }→ catalog IDs;internalstate +{}platform →[].- info — Consider narrowing the
anycast by typingstateasFeatureStatein the callback oncebundledConfigshape is validated upstream.35/35
chromeWebstorePatching helpersunit specs pass locally (Node 24).Sent by Cursor Automation: Web compat and sec
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.
There was a problem hiding this comment.
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
- Optional (info): Add two
readCuratedCatalogunit cases:preview+{ preview: true }→ catalog IDs;internal+{}platform →[]. - Optional (info): Confirm empty-array
catalogInternalbehaviour with native; add a fallback or test if native treats[]as absent. - Ready to merge from a web-compat/security perspective — the
isInternalwiring gap from the prior review is closed. 14/14readCuratedCatalogspecs pass locally.
Sent by Cursor Automation: Web compat and sec
Build Branch
Static preview entry points
QR codes (mobile preview)
Integration commandsnpm (Android / Extension): 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-statePin to exact commitnpm (Android / Extension): 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 |



Asana Task/Github Issue: Fix curated extension catalog not reading when extensionManagement is set to preview
Description
Hotfix for #2991.
extensionManagementships aspreviewduring the phased rollout. The catalog read compared state strings directly and accepted onlyenabledandinternal, sopreviewread 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
internalcounted 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.readCuratedCatalognow takes the state check as a parameter, and the feature passesConfigFeature#_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.getFeatureSettingEnabledcannot serve here:parseFeatureSettingscopies only a feature'ssettings, so a sub-feature'sstateand thefeaturesmap 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 lintextensionManagementandcuratedExtensionsatpreview: a curated extension detail page (Bitwarden) shows "Add to DuckDuckGo" rather than "Unsupported extension"Worth confirming before this is relied on:
previewonly takes effect ifplatform.preview === truein the Canary build. That flag is set Windows-side, and we already knowinternalcomes from being logged in as an internal user rather than from the release channel. Setting the state toenabledbypasses platform flags entirely, so that working is not evidencepreviewwill.Checklist
Please tick all that apply:
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
previewrollout state as off andinternalas on regardless of build flags.readCuratedCatalognow receives the same platform-aware gate asConfigFeature#_isStateEnabled(via an injectedisEnabledcallback) instead of matching onlyenabled/internalstrings. That restores catalog reads whenextensionManagement/curatedExtensionsship atpreview, and stops public builds from treatinginternalas installable.On internal builds (
platform.internal), the helper preferscatalogInternalovercatalog(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 realisStateEnabledin specs.Reviewed by Cursor Bugbot for commit fb1e720. Bugbot is set up for automated code reviews on this repo. Configure here.