Skip to content

fix(radar): close the audit gaps — auth, merged feed fields, opt-in state, sidebar gate, size cap + daily scheduler - #9686

Merged
diegosouzapw merged 6 commits into
release/v3.8.50from
fix/radar-client-gaps
Aug 7, 2026
Merged

diegosouzapw merged 6 commits into
release/v3.8.50from
fix/radar-client-gaps

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

⚠️ base-red inherited: #9679 (migration collision 134 + typecheck hard failures on the tip — pre-existing, not from this branch)

Closes the gaps found in the 2026-08-06 Radar audit (PR #9515 follow-up), plus the spec'd daily sync.

Fixes

  • Auth on the 3 /api/radar/* routes (401 after the flag gate; flag-off 404 stays first, so flag-off inertia is byte-identical). New GET /api/radar/settings returns opt-in state + masked key — the raw key never leaves the server.
  • applyFeed now carries setup/capabilities/contextWindow/limits through both merge paths — the per-provider setup CTA, capability badges and context column render again.
  • Opt-in state survives reloads (the page derives it from the new GET instead of always showing the activation screen).
  • Rule 1 honored in feedModelToMerged — a local enabled:true override survives a feed disable, matching mergeOne.
  • Sidebar item gated by RADAR_ENABLED (flag off ⇒ no menu entry; piggy-backs the existing /api/settings fetch, fail-open for unrelated items).
  • 10 MB response cap in syncRadar (Content-Length preflight + streamed running total; exact bytes preserved for signature verification; new too_large status).
  • Daily scheduler + auto-sync on page open: hourly staleness tick armed only when flag+opt-in are on (boot or right after opting in) — a flag-off install never creates the timer; the page auto-syncs once per mount when the cache is older than 6h.
  • Docs: ENVIRONMENT.md stale feed URL fixed; RADAR.md documents the tier header (x-omniroute-feed-tier is the only tier truth — the signed body always says live), auth, GET settings, size cap, corrected data path.

Validation

  • New/extended tests: radar-api-routes 19/19 (auth, 404/401, no stack leak, raw key never echoed), radar-apply-feed 23/23, radar-sync 48/48, sidebar-costs-section 13/13, radar-scheduler + radar-auto-sync 20/20, radar-page-state 5/5, radar-flag-default 6/6, radar-inertia 5/5 — all green locally (route tests run against a deduplicated migrations dir because of the inherited 134 collision, see 🔴 Release branch not green: release/v3.8.50 #9679).
  • typecheck:core: zero errors in touched files (pre-existing tip failures tracked in 🔴 Release branch not green: release/v3.8.50 #9679). Focused ESLint on all touched files: clean. check:docs-all: clean for touched docs.

…ride

applyFeed()'s MergedEntry shape omitted contextWindow/capabilities/limits/
setup even though FeedModel always carries them, so the dashboard's setup
link, Context column, and capability badges never rendered and the setup
page's provider lookup always failed. Both merge paths (mergeOne and
feedModelToMerged) now copy the four fields through, respecting rule 1
(local override wins) same as every other field.

feedModelToMerged() also unconditionally forced enabled:false when the feed
disabled a feed-only entry, even when the operator had locally overridden
enabled:true — mergeOne() already applies overrides after the disable rule
and got this right. feedModelToMerged() now only force-disables when there
is no local `enabled` override, matching mergeOne()'s semantics.
syncRadar() buffered the entire feed response via
Buffer.from(await res.arrayBuffer()) with no size limit, so a
misconfigured or hostile RADAR_FEED_URL (or an upstream serving garbage)
could force an unbounded in-memory buffer. Enforcement is two-layered: a
Content-Length preflight skips reading an already-oversized body entirely,
and a running-total check while reading the stream enforces the cap even
when Content-Length is absent or understates the real size — concatenating
the accumulated chunks preserves the exact bytes the signature check needs.

Exceeding the cap returns a new { status: "too_large" } SyncStatus and
leaves the cache untouched, following the same non-destructive pattern as
every other sync failure (invalid_signature/invalid_schema/stale).
The "radar" sidebar item was registered unconditionally in
sidebarVisibility/sections.ts, but Sidebar.tsx has no feature-flag
awareness (it's a client component), so the link stayed visible and
clickable with RADAR_ENABLED off, landing on a 404 dashboard page.

Sidebar items gain an opt-in `featureFlagKey` field plus a pure
isSidebarItemVisibleForFlags() filter (fails open when a flag isn't in the
map, so a missing/not-yet-loaded key never hides an unrelated item). The
resolved flag value piggy-backs on the /api/settings response the sidebar
already fetches on mount (new `radarEnabled` field) rather than adding a
dedicated round trip.
GET /api/radar/catalog, POST /api/radar/sync, and POST /api/radar/settings
had zero authentication — any client that could reach the local server
could read the merged catalog, trigger a sync, or flip the opt-in/set the
supporter key. All three (plus the new GET below) now call
isAuthenticated() from the shared apiAuth guard, same gate as the rest of
/api/settings/*. The RADAR_ENABLED flag-off 404 check keeps running FIRST
so flag-off inertia stays byte-identical (no auth prompt just to learn the
surface doesn't exist); auth runs after it, before any DB access.

Adds GET /api/radar/settings, returning { optIn, hasSupporterKey,
supporterKeyMasked } — the raw key never leaves the server on either verb.
The dashboard page's fetchSettings() now calls this endpoint instead of
inferring opt-in state from the catalog response (which always defaulted
to unknown/null), so an already-activated operator no longer sees the
activation screen on every reload. handleSync() also handles the new
too_large sync status introduced by the response-cap fix, reusing the
existing generic sync-failed copy (no new UI strings).
- RADAR_FEED_URL default was documented as radar.omniroute.dev in
  ENVIRONMENT.md; the actual default (src/lib/radar/sync.ts) and every
  other reference use radar.omniroute.online — fix the one stale spot.
- Correct the FREE_MODEL_BUDGETS source path: it's declared in
  freeModelCatalog.data.ts, not freeModelCatalog.ts (which only
  re-exports it).
- Document that the signed feed body's `tier` is always "live" (one
  signed artifact per version) and the actually-served tier comes from
  the `x-omniroute-feed-tier` response header, resolved with a Zod parse
  + fallback to the body field.
- Document that all four /api/radar/* routes now require auth
  (isAuthenticated(), same gate as /api/settings/*), the new
  GET /api/radar/settings route, and the new too_large sync status from
  the 10MB response cap.
Spec asks for a 1x/day sync while opted in and fresh data on every page
open. The scheduler only arms itself when RADAR_ENABLED AND the opt-in are
already on (boot) or right after the user opts in (settings route) — a
flag-off install never creates the timer, preserving the inertia contract.
The page auto-syncs once per mount when the cached feed is older than 6h.
@diegosouzapw

Copy link
Copy Markdown
Owner Author

CI triage — every red check is inherited from the base (#9679), none is from this branch

Check Cause Evidence
Unit Tests fast-path 1/4, 2/4, 3/4 Migration collision 134_ccr_blocks.sql × 134_proxy_logs_egress_ip.sql (landed via #9198, after this branch was cut) Migration version collision detected present in all three job logs; zero not ok for any radar/sidebar test
Vitest (fast-path) Same collision 13 occurrences of the collision error; failing files are open-sse/mcp-server/__tests__/*, untouched here
Fast Quality Gates The collision gate itself ✗ prefixo de versão duplicado 134: [134_ccr_blocks.sql, 134_proxy_logs_egress_ip.sql]
Docs Gates (fast-path) stale-version hits in docs untouched by this branch (2.1.220, 3.1.1) matches the base-red class in #9679; check:docs-all is clean for the docs touched here
No new ESLint warnings Orphan suppressions in config/quality/eslint-suppressions.json — file not modified by this branch; the suppressed violations in files touched here still occur at their exact frozen counts (focused ESLint exits 0) inherited drift on the shared suppressions hot-file
Merge integrity changelog.d/features/9415-… fragment missing its leading bullet (from PR #9415) file not touched here
Build App / dast-smoke Known advisory always-red (Turbopack memory) / known flaky class pre-existing on every branch

All radar-scope validation is green locally: 119 radar/sidebar tests across 8 files (route tests run against a deduplicated migrations dir to bypass the inherited 134 collision), focused ESLint clean, typecheck clean for touched files, docs checks clean for touched docs. Merging per operator instruction with the base-red inheritance documented (base drain in progress via #9634).

@diegosouzapw
diegosouzapw merged commit 1e15583 into release/v3.8.50 Aug 7, 2026
9 of 10 checks passed
@diegosouzapw
diegosouzapw deleted the fix/radar-client-gaps branch August 7, 2026 11:26
diegosouzapw pushed a commit that referenced this pull request Aug 25, 2026
…11550)

Validated in a combined 4-PR batch worktree off release/v3.8.51 tip. Also removed an unused `crypto` import from the new test file (one-line lint fix, 228→229 unrelated-drift comparison confirmed it was the only new finding) — pushed to this branch.
- Focused tests: free-tier-summary-radar-overlay.test.ts (9/9) + free-model-catalog/free-catalog-2026-07-expansion/free-providers-batch-2026-07 — part of batch's 60/60 node:test run
- typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity, check:docs-counts-sync — all OK
- Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff (after the crypto-import fix)

Thanks for this — the community/live feed entitlement distinction (never re-publishing paid feed content to anonymous callers) mirrors #9686's treatment carefully, and the catalogUpdatedAt honesty (null over a fabricated download-time stand-in) is the right call.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…r gate, size cap) + daily sync scheduler (diegosouzapw#9686)

* fix(radar): preserve extended feed fields and honor local enable override

applyFeed()'s MergedEntry shape omitted contextWindow/capabilities/limits/
setup even though FeedModel always carries them, so the dashboard's setup
link, Context column, and capability badges never rendered and the setup
page's provider lookup always failed. Both merge paths (mergeOne and
feedModelToMerged) now copy the four fields through, respecting rule 1
(local override wins) same as every other field.

feedModelToMerged() also unconditionally forced enabled:false when the feed
disabled a feed-only entry, even when the operator had locally overridden
enabled:true — mergeOne() already applies overrides after the disable rule
and got this right. feedModelToMerged() now only force-disables when there
is no local `enabled` override, matching mergeOne()'s semantics.

* fix(radar): cap feed sync response body at 10MB

syncRadar() buffered the entire feed response via
Buffer.from(await res.arrayBuffer()) with no size limit, so a
misconfigured or hostile RADAR_FEED_URL (or an upstream serving garbage)
could force an unbounded in-memory buffer. Enforcement is two-layered: a
Content-Length preflight skips reading an already-oversized body entirely,
and a running-total check while reading the stream enforces the cap even
when Content-Length is absent or understates the real size — concatenating
the accumulated chunks preserves the exact bytes the signature check needs.

Exceeding the cap returns a new { status: "too_large" } SyncStatus and
leaves the cache untouched, following the same non-destructive pattern as
every other sync failure (invalid_signature/invalid_schema/stale).

* fix(radar): gate the sidebar radar item behind RADAR_ENABLED

The "radar" sidebar item was registered unconditionally in
sidebarVisibility/sections.ts, but Sidebar.tsx has no feature-flag
awareness (it's a client component), so the link stayed visible and
clickable with RADAR_ENABLED off, landing on a 404 dashboard page.

Sidebar items gain an opt-in `featureFlagKey` field plus a pure
isSidebarItemVisibleForFlags() filter (fails open when a flag isn't in the
map, so a missing/not-yet-loaded key never hides an unrelated item). The
resolved flag value piggy-backs on the /api/settings response the sidebar
already fetches on mount (new `radarEnabled` field) rather than adding a
dedicated round trip.

* fix(radar): require auth on management routes, add GET settings

GET /api/radar/catalog, POST /api/radar/sync, and POST /api/radar/settings
had zero authentication — any client that could reach the local server
could read the merged catalog, trigger a sync, or flip the opt-in/set the
supporter key. All three (plus the new GET below) now call
isAuthenticated() from the shared apiAuth guard, same gate as the rest of
/api/settings/*. The RADAR_ENABLED flag-off 404 check keeps running FIRST
so flag-off inertia stays byte-identical (no auth prompt just to learn the
surface doesn't exist); auth runs after it, before any DB access.

Adds GET /api/radar/settings, returning { optIn, hasSupporterKey,
supporterKeyMasked } — the raw key never leaves the server on either verb.
The dashboard page's fetchSettings() now calls this endpoint instead of
inferring opt-in state from the catalog response (which always defaulted
to unknown/null), so an already-activated operator no longer sees the
activation screen on every reload. handleSync() also handles the new
too_large sync status introduced by the response-cap fix, reusing the
existing generic sync-failed copy (no new UI strings).

* docs(radar): fix stale feed URL, document tier header/auth/size cap

- RADAR_FEED_URL default was documented as radar.omniroute.dev in
  ENVIRONMENT.md; the actual default (src/lib/radar/sync.ts) and every
  other reference use radar.omniroute.online — fix the one stale spot.
- Correct the FREE_MODEL_BUDGETS source path: it's declared in
  freeModelCatalog.data.ts, not freeModelCatalog.ts (which only
  re-exports it).
- Document that the signed feed body's `tier` is always "live" (one
  signed artifact per version) and the actually-served tier comes from
  the `x-omniroute-feed-tier` response header, resolved with a Zod parse
  + fallback to the body field.
- Document that all four /api/radar/* routes now require auth
  (isAuthenticated(), same gate as /api/settings/*), the new
  GET /api/radar/settings route, and the new too_large sync status from
  the 10MB response cap.

* feat(radar): daily sync scheduler + auto-sync on page open

Spec asks for a 1x/day sync while opted in and fresh data on every page
open. The scheduler only arms itself when RADAR_ENABLED AND the opt-in are
already on (boot) or right after the user opts in (settings route) — a
flag-off install never creates the timer, preserving the inertia contract.
The page auto-syncs once per mount when the cached feed is older than 6h.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…iegosouzapw#11550)

Validated in a combined 4-PR batch worktree off release/v3.8.51 tip. Also removed an unused `crypto` import from the new test file (one-line lint fix, 228→229 unrelated-drift comparison confirmed it was the only new finding) — pushed to this branch.
- Focused tests: free-tier-summary-radar-overlay.test.ts (9/9) + free-model-catalog/free-catalog-2026-07-expansion/free-providers-batch-2026-07 — part of batch's 60/60 node:test run
- typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity, check:docs-counts-sync — all OK
- Full-repo lint: 228 pre-existing dashboard react-hooks/* findings, unrelated to this diff (after the crypto-import fix)

Thanks for this — the community/live feed entitlement distinction (never re-publishing paid feed content to anonymous callers) mirrors diegosouzapw#9686's treatment carefully, and the catalogUpdatedAt honesty (null over a fabricated download-time stand-in) is the right call.
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