Summary
frontend/taskdeck-web/src/store/featureFlagStore.ts trusts the shape and values of taskdeck_feature_flags after JSON parsing and performs every storage read/write without a failure boundary.
This is a source-traced finding on main 307c3b8b50bec1cb0bfaea3e570a942bcb1d4451. The store is restored both during app startup and inside the router feature gate, so these are reachable application boundaries rather than test-only helpers.
Confirmed gaps
- Non-boolean values become route decisions.
restore() spreads arbitrary parsed JSON over defaultFeatureFlags. A persisted value such as { "devTools": "false" } is returned by isEnabled() as a truthy string, even though the public contract says every flag is boolean. The same applies to objects, numbers, arrays, and unknown properties.
- Storage reads can abort startup/navigation.
localStorage.getItem executes outside the current try. Browsers can throw for storage access in restricted/private contexts, allowing App.vue or the router guard's restore() call to fail.
- Storage writes can turn a local preference click into an exception.
setFlag() and resetAll() mutate valid in-memory state, then persist() calls setItem without a boundary. A quota/security exception escapes even though the runtime choice could remain usable for the session.
- Unknown persisted keys survive restoration. They are not part of
FeatureFlags, but remain on the runtime object and participate in allEnabled through Object.values.
This concerns a local display/developer preference. It is not a server authorization boundary, and feature flags must not be treated as one.
Expected contract
- Restore only known keys whose values are actual booleans; invalid or missing values use the declared defaults.
- Unknown keys and prototype-like payload members are discarded.
- A storage read or parse failure restores safe defaults and does not abort app/router startup.
- A storage write failure does not roll back or throw away a valid in-memory choice; public method signatures remain unchanged.
isEnabled() and allEnabled remain strictly boolean and consider only declared feature flags.
- Preserve the current storage key, defaults, route behavior for valid payloads, and profile-settings API.
Acceptance
- Vitest regressions for string/number/object/array values on known flags and unknown properties.
- A valid partial payload still overrides only its boolean keys.
getItem failure leaves defaults and does not throw.
setItem failure after setFlag and resetAll leaves the intended in-memory state and does not throw.
- Round-trip, router guard, App startup and Profile Settings tests remain green.
- Frontend lint, typecheck, build, full Vitest on Ubuntu and Windows, and exact-head hosted CI pass.
Audit provenance
Found during the 2026-09-21 frontend integrity pass while queue ownership PR #3349 was qualifying. Searches found no existing open issue or PR naming featureFlagStore.
Summary
frontend/taskdeck-web/src/store/featureFlagStore.tstrusts the shape and values oftaskdeck_feature_flagsafter JSON parsing and performs every storage read/write without a failure boundary.This is a source-traced finding on
main307c3b8b50bec1cb0bfaea3e570a942bcb1d4451. The store is restored both during app startup and inside the router feature gate, so these are reachable application boundaries rather than test-only helpers.Confirmed gaps
restore()spreads arbitrary parsed JSON overdefaultFeatureFlags. A persisted value such as{ "devTools": "false" }is returned byisEnabled()as a truthy string, even though the public contract says every flag is boolean. The same applies to objects, numbers, arrays, and unknown properties.localStorage.getItemexecutes outside the currenttry. Browsers can throw for storage access in restricted/private contexts, allowingApp.vueor the router guard'srestore()call to fail.setFlag()andresetAll()mutate valid in-memory state, thenpersist()callssetItemwithout a boundary. A quota/security exception escapes even though the runtime choice could remain usable for the session.FeatureFlags, but remain on the runtime object and participate inallEnabledthroughObject.values.This concerns a local display/developer preference. It is not a server authorization boundary, and feature flags must not be treated as one.
Expected contract
isEnabled()andallEnabledremain strictly boolean and consider only declared feature flags.Acceptance
getItemfailure leaves defaults and does not throw.setItemfailure aftersetFlagandresetAllleaves the intended in-memory state and does not throw.Audit provenance
Found during the 2026-09-21 frontend integrity pass while queue ownership PR #3349 was qualifying. Searches found no existing open issue or PR naming
featureFlagStore.