Skip to content

fix: enforce feature-flag gate in route guard (#524)#562

Merged
Chris0Jeky merged 3 commits intomainfrom
fix/524-feature-flag-route-guard
Mar 29, 2026
Merged

fix: enforce feature-flag gate in route guard (#524)#562
Chris0Jeky merged 3 commits intomainfrom
fix/524-feature-flag-route-guard

Conversation

@Chris0Jeky
Copy link
Copy Markdown
Owner

Summary

Root cause

Feature flags only removed sidebar links via ShellSidebar.vue's availableNavItems computed. The router itself had no guard, so typing /workspace/ops/cli directly always loaded the surface regardless of the flag state.

Affected routes gated

Route Flag
/workspace/activity (4 variants) newActivity
/workspace/review newAutomation
/workspace/automations/queue newAutomation
/workspace/automations/chat newAutomation
/workspace/ops/cli newOps
/workspace/ops/endpoints newOps
/workspace/ops/logs newOps
/workspace/settings/profile newAuth
/workspace/settings/access newAccess
/workspace/archive newArchive

Fix

  1. Added declare module 'vue-router' { interface RouteMeta { requiresFlag?: keyof FeatureFlags } } augmentation for type-safe meta
  2. Added requiresFlag to all gated route definitions
  3. Extended the existing beforeEach guard: reads to.meta.requiresFlag, calls featureFlags.restore() (so it works on hard refresh before App.vue mounts), then redirects to /workspace/home if the flag is disabled

Test plan

  • Navigating to a gated route with flag disabled → redirected to Home
  • Navigating to a gated route with flag enabled → allowed through
  • Direct URL navigation also blocked (not just link clicks)
  • Hard refresh with flag disabled in localStorage → still blocked
  • npm run typecheck passes (zero errors)
  • npx vitest --run passes — 31 new tests, 1107 pre-existing all green

Add `requiresFlag` meta to all feature-flagged routes and a `beforeEach`
guard that redirects to /workspace/home when the flag is disabled. The
guard calls `featureFlags.restore()` before checking, so hard-refresh /
direct URL navigation is blocked even before App.vue mounts. Adds a
`declare module 'vue-router'` RouteMeta augmentation for type safety.

Affected routes: activity (4), automations/queue, review, automations/chat,
ops/cli, ops/endpoints, ops/logs, settings/profile, settings/access, archive.
31 tests covering: no-flag routes (allowed), all 13 flagged routes when
enabled (allowed) and disabled (redirect to home), hard-refresh localStorage
restore scenario, and exhaustive route/flag mapping validation.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Self-review findings

All gated routes covered? Yes — 13 routes across 5 feature flags (newActivity, newAutomation, newOps, newAuth, newAccess, newArchive). Redirect-only routes (/workspace/automations, /workspace/automations/proposals) have no meta, but their targets are gated. newShell has no corresponding routes (it guards the shell UI, not a specific page), so no routes were missed.

Flag read source? featureFlagStore reading from localStorage via restore(). This is the same source as the Settings UI — consistent and correct for a UX gate.

Hard refresh / direct nav? The guard calls restore() before every flag check. This ensures localStorage is always consulted even if App.vue's onMounted hasn't run yet. Verified by test.

Redirect destination? /workspace/home — consistent with auth guard and issue specification.

workbenchBypassesFlag not replicated in guard — intentional. ShellSidebar bypasses flag checks in workbench mode for some items. The route guard does not replicate this, meaning a workbench-mode user who navigates directly to a flagged-off route will still be redirected. This is the more conservative and correct behaviour for a UX gate: the workbench bypass is a sidebar display convenience, not a grant of access. If the product decision changes, the guard can read workspaceStore.mode in a follow-up.

Security note: Feature flags live in localStorage and can be manually edited. This is intentional (Settings UI writes there too) and acceptable — this is a UX/discoverability gate, not a security boundary. Backend endpoints remain independently authenticated.

No issues requiring follow-up fixes.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a feature-flag navigation guard in the Vue router, restricting access to specific routes based on enabled flags. It includes updates to route metadata and a new test suite for the guard logic. Feedback suggests dynamically generating the list of valid flags in the tests to prevent the test from becoming out of sync with the feature flag definitions.

Comment on lines +120 to +123
const validFlags = [
'newShell', 'newAuth', 'newAccess', 'newActivity',
'newOps', 'newAutomation', 'newArchive',
] as const
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

This hardcoded list of validFlags can get out of sync with the FeatureFlags type if keys are added or renamed. This could lead to the test passing while coverage is incomplete.

To make this test more robust, you can generate this list dynamically from defaultFeatureFlags. This ensures that the test always checks against the current set of feature flags.

You will need to add import { defaultFeatureFlags } from '../../types/feature-flags' at the top of the file.

      const validFlags = Object.keys(defaultFeatureFlags)

@Chris0Jeky
Copy link
Copy Markdown
Owner Author

Second adversarial review (independent)

Verdict: No correctness bugs found. One minor maintainability note.

Checklist findings

1. Flag defaults on first visit
newAccess, newActivity, newOps, and newArchive default to false in defaultFeatureFlags. This means those routes are inaccessible on a fresh install before a user visits Settings. This is intentional per the comment in feature-flags.ts ("Advanced/diagnostic surfaces are useful, but noisy on first run") and consistent with the feature-flag UX model. Not a bug.

2. restore() idempotency / double-call
App.vue calls featureFlags.restore() in onMounted. The guard also calls restore() on every navigation. restore() is safe to call multiple times: it reads localStorage, JSON-parses, and overwrites flags.value with { ...defaultFeatureFlags, ...parsed }. setFlag() calls persist() synchronously before returning, so the stored value is always current. Calling restore() on every navigation is mildly wasteful (an extra localStorage.getItem per nav) but introduces no correctness issue. The guard comment correctly explains the intent (protecting hard-refresh / direct-nav before App.vue mounts). Acceptable.

3. Gemini's dynamic flag list suggestion
The test hardcodes validFlags as a const array. This is a genuine (low-severity) maintainability concern: if a flag key is renamed or added, a developer must update both feature-flags.ts and the test array, or the test silently passes with a stale entry. TypeScript would catch misuse at the route meta sites, but the runtime guard test would not detect a new flag that was added to the interface but omitted from FLAGGED_ROUTES. The fix (derive the list from Object.keys(defaultFeatureFlags)) is straightforward but not a blocker for merge.

4. Auth guard interaction / redirect loop
Auth check runs first, feature-flag check runs second — both inside the same beforeEach. If unauthenticated: redirect to /login. If authenticated but flag disabled: redirect to /workspace/home. /workspace/home has no requiresFlag, so no loop is possible. Guard ordering is correct.

5. Exhaustiveness: redirect-only routes
/workspace/automations and /workspace/automations/proposals are pure redirect entries with no meta. They redirect to workspace-review, which carries requiresFlag: 'newAutomation'. Vue Router re-runs beforeEach for the redirect destination, so the flag gate is enforced correctly for those paths as well. No gap.

Summary

The implementation is correct. The only actionable note from this review is the Gemini maintainability suggestion (item 3), which is worth a follow-up but not a merge blocker. No fixes applied.

The route guard added in this PR redirects to Home when a feature flag
is disabled. E2E tests navigate to gated routes (ops, activity, archive)
without setting flags, causing 3 failures. Fix: inject all flags as
enabled into localStorage alongside the auth token.
@Chris0Jeky Chris0Jeky merged commit d26b399 into main Mar 29, 2026
18 checks passed
@github-project-automation github-project-automation bot moved this from Pending to Done in Taskdeck Execution Mar 29, 2026
@Chris0Jeky Chris0Jeky deleted the fix/524-feature-flag-route-guard branch March 29, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

UX: Feature-flag route gating — direct URL access bypasses flag for Ops Console and other flagged surfaces

1 participant