feat(radar): add signed Intel insights and local CLI (#9923)

* feat(radar): sync signed Intel insights

* feat(radar): recognize supporters and add Intel UI

* feat(cli): add Radar status and sync commands

* docs(radar): document Intel and CLI contract

* docs(changelog): add Radar Intel fragment

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-10 20:11:29 -03:00
committed by GitHub
parent 12051f7edd
commit 17b4313b6e
33 changed files with 1834 additions and 82 deletions

View File

@@ -0,0 +1,76 @@
import { apiFetch } from "../api.mjs";
import { t } from "../i18n.mjs";
import { emit } from "../output.mjs";
const statusSchema = [
{ key: "feed", header: "Feed" },
{ key: "available", header: "Available" },
{ key: "version", header: "Version" },
{ key: "tier", header: "Tier" },
{ key: "fetchedAt", header: "Fetched" },
];
const syncSchema = [
{ key: "feed", header: "Feed" },
{ key: "status", header: "Status" },
{ key: "version", header: "Version" },
{ key: "reason", header: "Reason" },
];
function exitCodeFor(response) {
return Number.isInteger(response.exitCode) ? response.exitCode : response.status === 401 ? 4 : 1;
}
export async function runRadarStatusCommand(opts = {}) {
const response = await apiFetch("/api/radar/status", { acceptNotOk: true });
if (!response.ok) return exitCodeFor(response);
const data = await response.json();
if (opts.output === "json") {
emit(data, opts);
return 0;
}
const rows = Object.entries(data.feeds ?? {}).map(([feed, value]) => ({
feed,
...(value && typeof value === "object" ? value : { available: false }),
}));
emit(rows, opts, statusSchema);
return 0;
}
export async function runRadarSyncCommand(opts = {}) {
const response = await apiFetch("/api/radar/sync-all", {
method: "POST",
body: {},
acceptNotOk: true,
});
if (!response.ok) return exitCodeFor(response);
const data = await response.json();
if (opts.output === "json") {
emit(data, opts);
return 0;
}
const rows = Object.entries(data).map(([feed, value]) => ({
feed,
...(value && typeof value === "object" ? value : { status: "error" }),
}));
emit(rows, opts, syncSchema);
return 0;
}
export function registerRadar(program) {
const radar = program.command("radar").description(t("radar.description"));
radar
.command("status")
.description(t("radar.status"))
.action(async (_opts, command) => {
const code = await runRadarStatusCommand(command.optsWithGlobals());
if (code !== 0) process.exitCode = code;
});
radar
.command("sync")
.description(t("radar.sync"))
.action(async (_opts, command) => {
const code = await runRadarSyncCommand(command.optsWithGlobals());
if (code !== 0) process.exitCode = code;
});
}

View File

@@ -78,6 +78,7 @@ import { registerTokens } from "./tokens.mjs";
import { registerConfigure } from "./configure.mjs";
import { registerApiCommands } from "../api-commands/registry.mjs";
import { registerPlugin } from "./plugin.mjs";
import { registerRadar } from "./radar.mjs";
export function registerCommands(program) {
registerMemory(program);
@@ -161,4 +162,5 @@ export function registerCommands(program) {
registerConfigure(program);
registerApiCommands(program);
registerPlugin(program);
registerRadar(program);
}

View File

@@ -921,6 +921,11 @@
"model": "Filter by model"
}
},
"radar": {
"description": "Inspect and synchronize the local Radar catalog feeds",
"status": "Show local Radar settings and feed cache status",
"sync": "Synchronize catalog, referrals, offers, and Intel through the local server"
},
"resilience": {
"description": "Inspect and manage resilience mechanisms",
"status": {

View File

@@ -918,6 +918,11 @@
"model": "Filtrar por model"
}
},
"radar": {
"description": "Inspecionar e sincronizar os feeds locais do catálogo Radar",
"status": "Mostrar configurações locais e estado dos caches do Radar",
"sync": "Sincronizar catálogo, indicações, ofertas e Intel pelo servidor local"
},
"resilience": {
"description": "Inspecionar e gerenciar mecanismos de resiliência",
"status": {

View File

@@ -0,0 +1 @@
- **feat(radar):** add signed Intel insights, supporter recognition, and local Radar CLI commands ([#9923](https://github.com/diegosouzapw/OmniRoute/pull/9923))

View File

@@ -34,9 +34,10 @@ or external integration is currently available.
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Signed catalog client | Implemented behind `RADAR_ENABLED`, with separate opt-in, Ed25519 verification, local encrypted settings/cache, persistent display/enabled overrides, reversible tombstones, scheduler, and dashboard. |
| Contributor activation | The dashboard links to the server-hosted GitHub claim flow and accepts an existing `omr_…` key. Contributor eligibility is resolved by the private service; the OSS client contains no GitHub token or issuance logic. |
| Supporter-key activation | Implemented. The raw key is validated, encrypted at rest, masked on reads, and sent only by server-side sync. Changing or clearing the key invalidates all three entitlement-sensitive feed caches. |
| Supporter-key activation | Implemented. The raw key is validated, encrypted at rest, masked on reads, and sent only by server-side sync. Changing or clearing the key invalidates all four entitlement-sensitive feed caches. |
| Referral links | Implemented as a separately signed, hourly-refreshed feed. Fixed links are available to the community tier immediately; limited campaigns remain live-tier data. |
| Supporter offers | Implemented as a separate signed, live-only feed and dashboard page. The client revalidates the closed benefit schema, preserves the last good cache, filters expired entries, and labels partner offers explicitly. |
| Intel and supporter recognition | Implemented as a strict signed live-only feed with Radar-owned ELO, factual catalog freshness/trend, a verified local supporter badge, dashboard page, and local-only CLI status/sync commands. |
| Payments and transactional email | Not implemented in the OSS client. Purchase, donation, receipt review, and mail delivery belong to the private service and its later operational workstream. |
| Research-agent workstream | Not part of this client release. Curated feed contents remain server-side data; no autonomous research agent runs in an OmniRoute installation. |
@@ -53,7 +54,7 @@ Radar is gated end-to-end by the `RADAR_ENABLED` feature flag
- All `/api/radar/*` endpoints, including local model-state reads and writes,
return `404` before touching any Radar module.
- The dashboard screens (`/dashboard/radar`, `/dashboard/radar/setup`,
`/dashboard/radar/combos`, `/dashboard/radar/offers`) render
`/dashboard/radar/combos`, `/dashboard/radar/offers`, `/dashboard/radar/intel`) render
`notFound()`.
- `getRadarCatalog()` (`src/lib/radar/index.ts`) returns the untouched baseline —
same entry count, same values, every entry tagged `origin: "baseline"` — and never
@@ -87,9 +88,9 @@ When both are on, the sync path is:
plain, unauthenticated-by-default GET. OmniRoute never posts usage data, provider
configuration, or model traffic to the feed service.
3. The response is verified, validated, and cached locally (see
[Security model](#security-model)). Radar has exactly three server-side network paths:
[Security model](#security-model)). Radar has exactly four server-side network paths:
`syncRadar()` for the catalog, `syncRadarReferrals()` for referrals, and
`syncRadarOffers()` for supporter-only offers.
`syncRadarOffers()` / `syncRadarIntel()` for supporter-only offers and Intel.
The **supporter key** is an optional Bearer token (`radar_settings.supporter_key`)
that lets the feed service decide which tier to serve (see
@@ -99,7 +100,7 @@ that lets the feed service decide which tier to serve (see
helpers (`src/lib/db/encryption.ts`) used for provider credentials.
- Set via `POST /api/radar/settings` (`{ supporterKey: "omr_" + 40 hex chars }`) and
**never echoed back** — the response returns a masked form (`omr_****abcd`).
- Changing or clearing it atomically invalidates the catalog, referrals, and offers caches. The
- Changing or clearing it atomically invalidates the catalog, referrals, offers, and Intel caches. The
next sync/read resolves the new entitlement server-side; saving a key does not itself make
a network request or consume a single-use activation key.
- Sent to the feed service as a Bearer token on the sync GET — nothing else about the
@@ -337,15 +338,20 @@ The local Radar route families below back the UI under `src/app/api/radar/`:
| `/api/radar/referrals` | GET | Returns `{ fixed, campaigns, tier }` from the local cache — see [Referral links](#referral-links-free-credits) below. |
| `/api/radar/offers` | GET | Returns active offers from the verified local live cache; never returns the supporter key. |
| `/api/radar/offers/sync` | POST | Triggers the server-side, live-key-only `syncRadarOffers()` pipeline. |
| `/api/radar/intel` | GET | Returns verified local live Intel plus a supporter-recognition boolean; never an identity or key. |
| `/api/radar/intel/sync` | POST | Triggers the server-side, live-key-only `syncRadarIntel()` pipeline. |
| `/api/radar/status` | GET | Returns read-only local settings/cache status for catalog, referrals, offers, and Intel, without secrets. |
| `/api/radar/sync-all` | POST | Runs all four server-side sync modules and returns a separate status for each feed. |
| `/api/radar/local-model-state` | GET | Lists persisted overrides and tombstones for edit/restore controls. |
| `/api/radar/local-model-state` | PATCH | Sets or clears the validated `displayName`/`enabled` override fields. |
| `/api/radar/local-model-state` | PUT | Creates or removes a tombstone with `{ provider, modelId, tombstoned }`. |
| `/api/radar/local-model-state` | DELETE | Clears editable override fields while preserving any tombstone. |
**Hard rule: these routes never proxy the feed service.** The browser only ever talks
to the local OmniRoute server. The three modules that touch the Radar service are
to the local OmniRoute server. The four modules that touch the Radar service are
`src/lib/radar/sync.ts` (catalog), `src/lib/radar/referralsSync.ts` (referrals), and
`src/lib/radar/offersSync.ts` (offers); all run server-side, never client-side. This keeps
`src/lib/radar/offersSync.ts` (offers) plus `src/lib/radar/intelSync.ts` (Intel); all run
server-side, never client-side. This keeps
the feed URL and any supporter key out of client-facing network traffic entirely.
All Radar endpoints return `404` when `RADAR_ENABLED` is off (see
@@ -395,6 +401,29 @@ this release.
---
## Radar Intel, supporter badge, and CLI
Intel is a signed artifact at `GET /v1/intel/latest`. The closed `RadarIntelFeedSchema` accepts
only Radar-owned ELO rankings derived by the private curator from confirmed comparisons and factual
catalog age/count deltas derived from signed catalog snapshots. The methodology is fixed at initial
rating 1000 and K=32. An empty ranking is valid when no comparison has been confirmed; the client
never synthesizes one.
`syncRadarIntel()` applies the same server-side Bearer, 30-second timeout, 10 MiB streamed cap,
exact-byte Ed25519 verification, strict schema, `live` body/header requirement, version floor, and
last-good-cache preservation as offers. After a verified live snapshot is persisted, the client
derives `radar:<sha256(supporter key)>`, stores only that one-way identity, and emits the dedicated
`radar_supporter` recognition event. Its `radar-supporter` badge is idempotent and awards zero XP;
it never updates leaderboards or reuses `token_share`. `/dashboard/radar/intel` renders the badge
only from verified local cache metadata.
The CLI exposes `omniroute radar status` and `omniroute radar sync`. Both communicate only with the
local OmniRoute API. `status` performs a read-only `GET /api/radar/status`; `sync` sends one
`POST /api/radar/sync-all` and prints a result per feed. Neither command reads, accepts, or prints
the supporter key, and neither contacts the Radar service directly.
---
## Referral links (free credits)
Referral links are served from a **standalone, always-current** feed —
@@ -582,6 +611,12 @@ Supporter offers are another optional artifact. To serve them, implement
endpoint keeps the catalog/referrals behavior unchanged; offer refresh fails non-destructively and
the last verified local offer cache remains available.
Intel is optional in the same way. A self-hoster can serve `GET /v1/intel/latest` using
`RadarIntelFeedSchema` (`src/lib/radar/intelFeedSchema.ts`), require live entitlement, return
`x-omniroute-feed-tier: live`, and sign the exact bytes with the shared Ed25519 key. Omitting the
endpoint leaves catalog, referrals, and offers unchanged; Intel refresh preserves any last verified
local snapshot.
---
## Related docs

View File

@@ -1294,7 +1294,7 @@ module doc.
| Variable | Default | Source File | Description |
| -------------------------------- | --------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------ |
| `RADAR_FEED_URL` | `https://radar.omniroute.online` | `src/lib/radar/{sync,referralsSync,offersSync}.ts` | Base URL shared by the separately signed catalog, referrals, and supporter-offers feeds. Override to point at a self-hosted or forked service. |
| `RADAR_FEED_URL` | `https://radar.omniroute.online` | `src/lib/radar/{sync,referralsSync,offersSync,intelSync}.ts` | Base URL shared by the separately signed catalog, referrals, supporter-offers, and Intel feeds. Override to point at a self-hosted or forked service. |
| `RADAR_FEED_PUBKEY` | _(pinned default key)_ | `src/lib/radar/pinnedKeys.ts` | Ed25519 public key (base64-DER SPKI or PEM) used to verify feed signatures from a custom feed. |
| `RADAR_CONTRIBUTOR_CLAIM_URL` | `https://radar.omniroute.online/auth/github` | `src/lib/radar/links.ts` | URL the "I'm a contributor" dashboard button opens (GitHub OAuth supporter-key claim flow). |
| `RADAR_SUPPORTER_PLANS_URL` | `https://radar.omniroute.online/planos` | `src/lib/radar/links.ts` | URL the "Support the project" dashboard button opens (payment/plans page). |

View File

@@ -0,0 +1,197 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { useTranslations } from "next-intl";
import type { RadarIntelFeed } from "@/lib/radar/intelFeedSchema";
import { Card } from "@/shared/components";
interface IntelMeta {
version: string;
tier: "live";
fetchedAt: string;
supporterVerified: true;
}
export default function RadarIntelPage() {
const t = useTranslations("radarIntelPage");
const [intel, setIntel] = useState<RadarIntelFeed | null>(null);
const [meta, setMeta] = useState<IntelMeta | null>(null);
const [loading, setLoading] = useState(true);
const [syncing, setSyncing] = useState(false);
const [flagOff, setFlagOff] = useState(false);
const [error, setError] = useState("");
const load = useCallback(async () => {
const response = await fetch("/api/radar/intel");
if (response.status === 404) {
setFlagOff(true);
return;
}
if (!response.ok) throw new Error("intel_load_failed");
const body = (await response.json()) as {
intel?: RadarIntelFeed | null;
meta?: IntelMeta | null;
};
setIntel(body.intel ?? null);
setMeta(body.meta ?? null);
}, []);
const sync = useCallback(async () => {
setSyncing(true);
setError("");
try {
const response = await fetch("/api/radar/intel/sync", { method: "POST" });
if (response.status === 404) {
setFlagOff(true);
return;
}
if (!response.ok) throw new Error("intel_sync_failed");
const status = (await response.json()) as { status?: string };
if (
["error", "invalid_signature", "invalid_schema", "wrong_tier", "too_large"].includes(
status.status ?? ""
)
) {
setError(t("loadFailed"));
}
await load();
} catch {
setError(t("loadFailed"));
await load().catch(() => undefined);
} finally {
setSyncing(false);
}
}, [load, t]);
useEffect(() => {
load()
.catch(() => setError(t("loadFailed")))
.finally(() => setLoading(false));
}, [load, t]);
if (flagOff) notFound();
return (
<div className="flex flex-col gap-6">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<Link
href="/dashboard/radar"
className="text-sm text-text-muted hover:text-text-main transition-colors"
>
{t("backToRadar")}
</Link>
<h1 className="mt-3 text-2xl font-bold">{t("title")}</h1>
<p className="mt-1 text-sm text-text-muted">{t("subtitle")}</p>
</div>
<div className="flex items-center gap-2">
{meta?.supporterVerified === true && (
<span
data-badge-id="radar-supporter"
className="rounded-full border border-violet-500 px-3 py-1 text-sm text-violet-300"
>
{t("supporterBadge")}
</span>
)}
<button
type="button"
onClick={() => void sync()}
disabled={syncing}
className="rounded-lg border border-violet-500 px-4 py-2 text-sm font-medium text-violet-400 disabled:opacity-50"
>
{syncing ? t("syncing") : t("refresh")}
</button>
</div>
</div>
{error && <div className="rounded-lg bg-red-500/10 p-3 text-sm text-red-400">{error}</div>}
{loading ? (
<div className="flex min-h-48 items-center justify-center text-text-muted">
{t("loading")}
</div>
) : !intel || !meta ? (
<Card>
<p className="py-8 text-center text-text-muted">{t("empty")}</p>
</Card>
) : (
<>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<p className="text-xs uppercase tracking-wide text-text-muted">{t("methodology")}</p>
<p className="mt-2 font-semibold">
{t("eloMethod", {
initial: intel.methodology.initialRating,
factor: intel.methodology.kFactor,
})}
</p>
</Card>
<Card>
<p className="text-xs uppercase tracking-wide text-text-muted">{t("freshness")}</p>
<p className="mt-2 font-semibold">
{t(`freshnessValues.${intel.catalog.freshness}`)}
</p>
<p className="mt-1 text-sm text-text-muted">
{t("ageDays", { days: intel.catalog.ageDays })}
</p>
</Card>
<Card>
<p className="text-xs uppercase tracking-wide text-text-muted">{t("trend")}</p>
<p className="mt-2 font-semibold">{t(`trendValues.${intel.catalog.trend}`)}</p>
<p className="mt-1 text-sm text-text-muted">
{t("modelDelta", {
current: intel.catalog.models.current,
added: intel.catalog.models.added,
removed: intel.catalog.models.removed,
})}
</p>
</Card>
</div>
<Card>
<div className="mb-4 flex items-center justify-between gap-3">
<h2 className="text-lg font-semibold">{t("ranking")}</h2>
<span className="text-xs text-text-muted">{meta.version}</span>
</div>
{intel.rankings.length === 0 ? (
<p className="py-6 text-center text-text-muted">{t("noRankings")}</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="text-text-muted">
<tr>
<th className="pb-3">#</th>
<th className="pb-3">{t("model")}</th>
<th className="pb-3">{t("category")}</th>
<th className="pb-3 text-right">{t("rating")}</th>
<th className="pb-3 text-right">{t("matches")}</th>
</tr>
</thead>
<tbody>
{intel.rankings.map((ranking) => (
<tr
key={`${ranking.category}:${ranking.provider}:${ranking.modelId}`}
className="border-t border-border"
>
<td className="py-3">{ranking.rank}</td>
<td className="py-3 font-mono">
{ranking.provider}/{ranking.modelId}
</td>
<td className="py-3">{ranking.category}</td>
<td className="py-3 text-right">{ranking.rating}</td>
<td className="py-3 text-right">{ranking.matches}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Card>
</>
)}
</div>
);
}

View File

@@ -317,6 +317,14 @@ export default function RadarPage() {
<p className="text-sm text-text-muted mt-1">{t("subtitle")}</p>
</div>
<div className="flex items-center gap-2">
{(pageState === "empty" || pageState === "populated") && (
<Link
href="/dashboard/radar/intel"
className="px-4 py-2 text-sm font-medium rounded-lg border border-border text-text-main hover:border-violet-500 hover:text-violet-400 transition-colors"
>
{t("intel")}
</Link>
)}
{(pageState === "empty" || pageState === "populated") && (
<Link
href="/dashboard/radar/offers"

View File

@@ -0,0 +1,42 @@
/** GET the verified local Radar Intel cache. Never proxies the private service. */
import { NextResponse } from "next/server";
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { getRadarIntel } from "@/lib/radar";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function OPTIONS() {
return handleCorsOptions();
}
export async function GET(request: Request) {
if (!isFeatureFlagEnabled("RADAR_ENABLED")) {
return NextResponse.json(buildErrorBody(404, "Not found"), {
status: 404,
headers: CORS_HEADERS,
});
}
if (!(await isAuthenticated(request))) {
return NextResponse.json(buildErrorBody(401, "Unauthorized"), {
status: 401,
headers: CORS_HEADERS,
});
}
try {
return NextResponse.json(getRadarIntel(), {
headers: { ...CORS_HEADERS, "Cache-Control": "no-store" },
});
} catch (error: unknown) {
return NextResponse.json(
buildErrorBody(500, sanitizeErrorMessage(error) || "Failed to load Radar Intel"),
{ status: 500, headers: CORS_HEADERS }
);
}
}

View File

@@ -0,0 +1,56 @@
/** POST a server-side Radar Intel sync. The browser never receives the supporter key. */
import { NextResponse } from "next/server";
import { z } from "zod";
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { syncRadarIntel } from "@/lib/radar/intelSync";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
export const dynamic = "force-dynamic";
export const revalidate = 0;
const SyncBodySchema = z.object({}).strict().optional();
export async function OPTIONS() {
return handleCorsOptions();
}
export async function POST(request: Request) {
if (!isFeatureFlagEnabled("RADAR_ENABLED")) {
return NextResponse.json(buildErrorBody(404, "Not found"), {
status: 404,
headers: CORS_HEADERS,
});
}
if (!(await isAuthenticated(request))) {
return NextResponse.json(buildErrorBody(401, "Unauthorized"), {
status: 401,
headers: CORS_HEADERS,
});
}
let body: unknown;
try {
body = await request.json();
} catch {
body = undefined;
}
if (!SyncBodySchema.safeParse(body).success) {
return NextResponse.json(buildErrorBody(400, "Invalid request body"), {
status: 400,
headers: CORS_HEADERS,
});
}
try {
return NextResponse.json(await syncRadarIntel(), { headers: CORS_HEADERS });
} catch (error: unknown) {
return NextResponse.json(
buildErrorBody(500, sanitizeErrorMessage(error) || "Radar Intel sync failed"),
{ status: 500, headers: CORS_HEADERS }
);
}
}

View File

@@ -0,0 +1,70 @@
/** Read-only aggregate status of local Radar state. */
import { NextResponse } from "next/server";
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import {
getRadarCache,
getRadarIntelCache,
getRadarOffersCache,
getRadarReferralsCache,
getRadarSettings,
} from "@/lib/db/radar";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function OPTIONS() {
return handleCorsOptions();
}
function cacheStatus(
cache: { version?: string; generatedAt?: string; tier: string; fetchedAt: string } | null
) {
if (!cache) return { available: false };
return {
available: true,
version: cache.version ?? cache.generatedAt,
tier: cache.tier,
fetchedAt: cache.fetchedAt,
};
}
export async function GET(request: Request) {
if (!isFeatureFlagEnabled("RADAR_ENABLED")) {
return NextResponse.json(buildErrorBody(404, "Not found"), {
status: 404,
headers: CORS_HEADERS,
});
}
if (!(await isAuthenticated(request))) {
return NextResponse.json(buildErrorBody(401, "Unauthorized"), {
status: 401,
headers: CORS_HEADERS,
});
}
try {
const settings = getRadarSettings();
return NextResponse.json(
{
settings: { optIn: settings.optIn, hasSupporterKey: settings.supporterKey !== null },
feeds: {
catalog: cacheStatus(getRadarCache()),
referrals: cacheStatus(getRadarReferralsCache()),
offers: cacheStatus(getRadarOffersCache()),
intel: cacheStatus(getRadarIntelCache()),
},
},
{ headers: { ...CORS_HEADERS, "Cache-Control": "no-store" } }
);
} catch (error: unknown) {
return NextResponse.json(
buildErrorBody(500, sanitizeErrorMessage(error) || "Failed to load Radar status"),
{ status: 500, headers: CORS_HEADERS }
);
}
}

View File

@@ -0,0 +1,47 @@
/** Aggregate local trigger for every Radar feed sync. */
import { NextResponse } from "next/server";
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { syncRadarIntel } from "@/lib/radar/intelSync";
import { syncRadarOffers } from "@/lib/radar/offersSync";
import { syncRadarReferrals } from "@/lib/radar/referralsSync";
import { syncRadar } from "@/lib/radar/sync";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
export const dynamic = "force-dynamic";
export const revalidate = 0;
export async function OPTIONS() {
return handleCorsOptions();
}
export async function POST(request: Request) {
if (!isFeatureFlagEnabled("RADAR_ENABLED")) {
return NextResponse.json(buildErrorBody(404, "Not found"), {
status: 404,
headers: CORS_HEADERS,
});
}
if (!(await isAuthenticated(request))) {
return NextResponse.json(buildErrorBody(401, "Unauthorized"), {
status: 401,
headers: CORS_HEADERS,
});
}
try {
const catalog = await syncRadar();
const referrals = await syncRadarReferrals();
const offers = await syncRadarOffers();
const intel = await syncRadarIntel();
return NextResponse.json({ catalog, referrals, offers, intel }, { headers: CORS_HEADERS });
} catch (error: unknown) {
return NextResponse.json(
buildErrorBody(500, sanitizeErrorMessage(error) || "Radar aggregate sync failed"),
{ status: 500, headers: CORS_HEADERS }
);
}
}

View File

@@ -11600,6 +11600,11 @@
"rare": "Rare achievements"
},
"badges": {
"radar-supporter": {
"name": "Radar Supporter",
"description": "Verified a live OmniRoute Radar supporter feed",
"criteria": "Verify a signed live Radar supporter feed."
},
"first-token": {
"name": "First Token",
"description": "Made your first API request",
@@ -12307,7 +12312,8 @@
"modelEnabled": "Enabled locally",
"localStateSaveFailed": "Failed to save local Radar settings",
"guidedCombos": "Guided combos",
"offers": "Offers"
"offers": "Offers",
"intel": "Intel"
},
"radarCombosPage": {
"title": "Radar guided combos",
@@ -12351,6 +12357,31 @@
"addConnectionDescription": "Don't have a connection yet? Add one in the providers dashboard.",
"addConnectionLink": "Go to providers →"
},
"radarIntelPage": {
"title": "Radar Intel",
"subtitle": "Radar-owned ELO rankings and factual catalog movement.",
"backToRadar": "Back to Radar",
"loading": "Loading Intel...",
"refresh": "Refresh Intel",
"syncing": "Refreshing...",
"loadFailed": "We could not refresh Intel. The last verified local cache is kept.",
"empty": "No verified Intel snapshot is available yet.",
"supporterBadge": "Radar Supporter",
"methodology": "Methodology",
"eloMethod": "ELO, initial {initial}, K={factor}",
"freshness": "Catalog freshness",
"ageDays": "{days, plural, one {# day old} other {# days old}}",
"trend": "Catalog trend",
"modelDelta": "{current} models, +{added} / -{removed}",
"ranking": "Model ranking",
"noRankings": "No confirmed comparisons are available yet.",
"model": "Model",
"category": "Category",
"rating": "Rating",
"matches": "Matches",
"freshnessValues": { "fresh": "Fresh", "aging": "Aging", "stale": "Stale" },
"trendValues": { "growing": "Growing", "stable": "Stable", "shrinking": "Shrinking" }
},
"radarOffersPage": {
"title": "Supporter offers",
"subtitle": "Official discounts, credits, trials, and reviewed partner benefits from Radar.",

View File

@@ -11600,6 +11600,11 @@
"rare": "Conquistas raras"
},
"badges": {
"radar-supporter": {
"name": "Apoiador do Radar",
"description": "Verificou um feed ativo de apoiador do OmniRoute Radar",
"criteria": "Verifique um feed ativo e assinado de apoiador do Radar."
},
"first-token": {
"name": "Primeiro Token",
"description": "Fez sua primeira requisição de API",
@@ -12307,7 +12312,8 @@
"modelEnabled": "Ativado localmente",
"localStateSaveFailed": "Falha ao salvar as configurações locais do Radar",
"guidedCombos": "Combos guiados",
"offers": "Ofertas"
"offers": "Ofertas",
"intel": "Intel"
},
"radarCombosPage": {
"title": "Combos guiados pelo Radar",
@@ -12351,6 +12357,31 @@
"addConnectionDescription": "Ainda não tem uma conexão? Adicione uma no painel de provedores.",
"addConnectionLink": "Ir para provedores →"
},
"radarIntelPage": {
"title": "Intel do Radar",
"subtitle": "Ranking ELO próprio do Radar e evolução factual do catálogo.",
"backToRadar": "Voltar ao Radar",
"loading": "Carregando Intel...",
"refresh": "Atualizar Intel",
"syncing": "Atualizando...",
"loadFailed": "Não foi possível atualizar o Intel. O último cache local verificado foi preservado.",
"empty": "Ainda não há um snapshot Intel verificado.",
"supporterBadge": "Apoiador do Radar",
"methodology": "Metodologia",
"eloMethod": "ELO, inicial {initial}, K={factor}",
"freshness": "Atualidade do catálogo",
"ageDays": "{days, plural, one {# dia} other {# dias}}",
"trend": "Tendência do catálogo",
"modelDelta": "{current} modelos, +{added} / -{removed}",
"ranking": "Ranking de modelos",
"noRankings": "Ainda não há comparações confirmadas.",
"model": "Modelo",
"category": "Categoria",
"rating": "Pontuação",
"matches": "Partidas",
"freshnessValues": { "fresh": "Atual", "aging": "Envelhecendo", "stale": "Desatualizado" },
"trendValues": { "growing": "Crescendo", "stable": "Estável", "shrinking": "Diminuindo" }
},
"radarOffersPage": {
"title": "Ofertas para apoiadores",
"subtitle": "Descontos, créditos, testes oficiais e benefícios de parceiros revisados pelo Radar.",

View File

@@ -11600,6 +11600,11 @@
"rare": "Thành tích hiếm"
},
"badges": {
"radar-supporter": {
"name": "Người ủng hộ Radar",
"description": "Đã xác minh nguồn dữ liệu trực tiếp dành cho người ủng hộ OmniRoute Radar",
"criteria": "Xác minh nguồn dữ liệu Radar trực tiếp đã được ký dành cho người ủng hộ."
},
"first-token": {
"name": "Token đầu tiên",
"description": "Đã thực hiện yêu cầu API đầu tiên",
@@ -12307,7 +12312,8 @@
"modelEnabled": "Enabled locally",
"localStateSaveFailed": "Failed to save local Radar settings",
"guidedCombos": "Guided combos",
"offers": "Offers"
"offers": "Offers",
"intel": "Thông tin chuyên sâu"
},
"radarSetupPage": {
"title": "Thiết lập nhà cung cấp",
@@ -12351,6 +12357,39 @@
"loadFailed": "Failed to load Radar combo suggestions.",
"createFailed": "Failed to create the combo. Review your provider connections and try again."
},
"radarIntelPage": {
"title": "Thông tin chuyên sâu Radar",
"subtitle": "Xếp hạng ELO do Radar quản lý và biến động thực tế của danh mục.",
"backToRadar": "Quay lại Radar",
"loading": "Đang tải thông tin chuyên sâu...",
"refresh": "Làm mới thông tin chuyên sâu",
"syncing": "Đang làm mới...",
"loadFailed": "Không thể làm mới thông tin chuyên sâu. Bộ nhớ đệm cục bộ đã xác minh gần nhất được giữ lại.",
"empty": "Chưa có bản chụp thông tin chuyên sâu đã xác minh.",
"supporterBadge": "Người ủng hộ Radar",
"methodology": "Phương pháp",
"eloMethod": "ELO, khởi tạo {initial}, K={factor}",
"freshness": "Độ mới của danh mục",
"ageDays": "{days, plural, one {# ngày tuổi} other {# ngày tuổi}}",
"trend": "Xu hướng danh mục",
"modelDelta": "{current} mô hình, +{added} / -{removed}",
"ranking": "Xếp hạng mô hình",
"noRankings": "Chưa có phép so sánh nào được xác nhận.",
"model": "Mô hình",
"category": "Danh mục",
"rating": "Điểm",
"matches": "Lượt so sánh",
"freshnessValues": {
"fresh": "Mới",
"aging": "Đang cũ dần",
"stale": "Đã cũ"
},
"trendValues": {
"growing": "Đang tăng",
"stable": "Ổn định",
"shrinking": "Đang giảm"
}
},
"radarOffersPage": {
"title": "Supporter offers",
"subtitle": "Official discounts, credits, trials, and reviewed partner benefits from Radar.",

View File

@@ -0,0 +1,11 @@
-- Signed, live-only Radar Intel feed cache. The supporter identity is a
-- one-way SHA-256 marker (`radar:<64 hex>`) and never contains the raw key.
CREATE TABLE IF NOT EXISTS radar_intel_cache (
id INTEGER PRIMARY KEY CHECK (id = 1),
version TEXT NOT NULL,
tier TEXT NOT NULL CHECK (tier = 'live'),
payload TEXT NOT NULL,
signature TEXT NOT NULL,
supporter_identity TEXT NOT NULL,
fetched_at TEXT NOT NULL DEFAULT (datetime('now'))
);

View File

@@ -20,6 +20,10 @@
* Tables (migration 144):
* - radar_offers_cache: single-row signed live offers feed cache.
*
* Tables (migration 145):
* - radar_intel_cache: single-row signed live Intel feed cache plus a
* one-way supporter identity used for local recognition.
*
* The supporter key is encrypted at rest with AES-256-GCM using the same
* `encrypt()`/`decrypt()` helpers from `./encryption.ts` that protect
* provider connection credentials.
@@ -62,6 +66,15 @@ export interface RadarOffersCache {
fetchedAt: string;
}
export interface RadarIntelCache {
version: string;
tier: "live";
payload: string;
signature: string;
supporterIdentity: string;
fetchedAt: string;
}
export interface RadarLocalModelState {
provider: string;
modelId: string;
@@ -180,6 +193,7 @@ export function setRadarKey(key: string | null): void {
const clearCatalogCache = db.prepare("DELETE FROM radar_feed_cache WHERE id = 1");
const clearReferralsCache = db.prepare("DELETE FROM radar_referrals_cache WHERE id = 1");
const clearOffersCache = db.prepare("DELETE FROM radar_offers_cache WHERE id = 1");
const clearIntelCache = db.prepare("DELETE FROM radar_intel_cache WHERE id = 1");
db.transaction(() => {
updateKey.run(encrypted);
@@ -189,6 +203,7 @@ export function setRadarKey(key: string | null): void {
clearCatalogCache.run();
clearReferralsCache.run();
clearOffersCache.run();
clearIntelCache.run();
})();
}
@@ -276,6 +291,52 @@ export function setRadarOffersCache(entry: {
.run(entry.version, entry.tier, entry.payload, entry.signature, fetchedAt);
}
// ---------------------------------------------------------------------------
// radar_intel_cache
// ---------------------------------------------------------------------------
export function getRadarIntelCache(): RadarIntelCache | null {
const row = getDbInstance()
.prepare(
"SELECT version, tier, payload, signature, supporter_identity AS supporterIdentity, " +
"fetched_at AS fetchedAt FROM radar_intel_cache WHERE id = 1"
)
.get() as RadarIntelCache | undefined;
return row ?? null;
}
export function setRadarIntelCache(entry: {
version: string;
tier: "live";
payload: string;
signature: string;
supporterIdentity: string;
fetchedAt?: string;
}): void {
const fetchedAt = entry.fetchedAt ?? new Date().toISOString();
getDbInstance()
.prepare(
`INSERT INTO radar_intel_cache
(id, version, tier, payload, signature, supporter_identity, fetched_at)
VALUES (1, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
version = excluded.version,
tier = excluded.tier,
payload = excluded.payload,
signature = excluded.signature,
supporter_identity = excluded.supporter_identity,
fetched_at = excluded.fetched_at`
)
.run(
entry.version,
entry.tier,
entry.payload,
entry.signature,
entry.supporterIdentity,
fetchedAt
);
}
// ---------------------------------------------------------------------------
// radar_local_model_state
// ---------------------------------------------------------------------------

View File

@@ -161,6 +161,16 @@ export const BUILTIN_BADGES: Omit<BadgeDefinition, "createdAt">[] = [
criteria: JSON.stringify({ type: "threshold", metric: "uptime", threshold: 100, window: 7 }),
hidden: 0,
},
{
id: "radar-supporter",
name: "Radar Supporter",
description: "Verified a live OmniRoute Radar supporter feed",
icon: "radar",
category: "contribution",
rarity: "rare",
criteria: JSON.stringify({ type: "action_count", action: "radar_supporter", threshold: 1 }),
hidden: 0,
},
// ── Streak (Engagement) ──────────────────────────────────────────────────
{

View File

@@ -25,7 +25,8 @@ export async function emitGamificationEvent(params: {
| "combo_use"
| "token_share"
| "invite_redeem"
| "daily_login";
| "daily_login"
| "radar_supporter";
metadata?: Record<string, unknown>;
}): Promise<void> {
const { apiKeyId, action, metadata } = params;
@@ -33,6 +34,13 @@ export async function emitGamificationEvent(params: {
if (!apiKeyId) return; // Skip if no API key
try {
// A verified Radar supporter is a recognition event, not an XP or
// leaderboard action. The caller supplies only a one-way key identity.
if (action === "radar_supporter") {
await checkAndUnlockBadge(apiKeyId, "radar-supporter", false);
return;
}
// 1. Award XP
const xpAmount = getXpForAction(action);
if (xpAmount > 0) {
@@ -87,7 +95,7 @@ export async function emitGamificationEvent(params: {
} catch (err) {
// Never throw — gamification must not break the request pipeline
log.error("events.error", {
apiKeyId,
...(action === "radar_supporter" ? {} : { apiKeyId }),
action,
error: err instanceof Error ? err.message : String(err),
});
@@ -114,22 +122,25 @@ function getXpForAction(action: string): number {
/**
* Check and unlock a specific badge.
*/
async function checkAndUnlockBadge(apiKeyId: string, badgeId: string): Promise<void> {
async function checkAndUnlockBadge(
apiKeyId: string,
badgeId: string,
logIdentity = true
): Promise<void> {
const { unlockBadge, hasBadge } = await import("../db/gamification");
// #3472: dedup via user_badges directly. getBadges() INNER-JOINs badge_definitions, which is
// empty until seeded, so it falsely reported "not earned" and re-emitted the unlock event on
// every request.
if (!hasBadge(apiKeyId, badgeId)) {
unlockBadge(apiKeyId, badgeId);
log.info("events.badge_unlocked", { apiKeyId, badgeId });
log.info("events.badge_unlocked", logIdentity ? { apiKeyId, badgeId } : { badgeId });
// Look up badge details from badge_definitions
const { getDbInstance } = await import("../db/core");
const badgeRow = getDbInstance()
.prepare("SELECT name, description, icon, rarity FROM badge_definitions WHERE id = ?")
.get(badgeId) as
| { name: string; description: string | null; icon: string | null; rarity: string }
| undefined;
{ name: string; description: string | null; icon: string | null; rarity: string } | undefined;
// Record notification for SSE toast
const { recordBadgeUnlock } = await import("./notifications");

View File

@@ -822,6 +822,8 @@ export {
setRadarReferralsCache,
getRadarOffersCache,
setRadarOffersCache,
getRadarIntelCache,
setRadarIntelCache,
listRadarLocalModelState,
setRadarLocalModelOverride,
clearRadarLocalModelOverride,
@@ -833,6 +835,7 @@ export type {
RadarSettings,
RadarReferralsCache,
RadarOffersCache,
RadarIntelCache,
RadarLocalModelState,
RadarLocalModelOverridePatch,
RadarLocalMergeState,

View File

@@ -17,6 +17,7 @@ import {
RadarOffersFeedSchema,
type RadarOffer,
} from "./offersFeedSchema";
import { RadarIntelFeedSchema, type RadarIntelFeed } from "./intelFeedSchema";
import { applyFeed, type MergedEntry, type FeedModel } from "./applyFeed";
import { findDefaultReferral } from "./referrals";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
@@ -24,6 +25,7 @@ import {
getRadarCache,
getRadarLocalMergeState,
getRadarOffersCache,
getRadarIntelCache,
getRadarReferralsCache,
type RadarLocalMergeState,
} from "@/lib/db/radar";
@@ -261,8 +263,51 @@ export function getRadarOffers(deps: GetRadarOffersDeps = {}): RadarOffersResult
}
}
export interface RadarIntelResult {
intel: RadarIntelFeed | null;
meta: {
version: string;
tier: "live";
fetchedAt: string;
supporterVerified: true;
} | null;
}
export interface GetRadarIntelDeps {
getFlag?: (key: string) => boolean;
getCache?: typeof getRadarIntelCache;
}
const EMPTY_INTEL: RadarIntelResult = { intel: null, meta: null };
/** Return only a defensively revalidated live Intel cache. */
export function getRadarIntel(deps: GetRadarIntelDeps = {}): RadarIntelResult {
const { getFlag = isFeatureFlagEnabled, getCache: getCacheFn = getRadarIntelCache } = deps;
if (!getFlag("RADAR_ENABLED")) return EMPTY_INTEL;
const cache = getCacheFn();
if (!cache || cache.tier !== "live" || !/^radar:[a-f0-9]{64}$/.test(cache.supporterIdentity)) {
return EMPTY_INTEL;
}
try {
const feed = RadarIntelFeedSchema.parse(JSON.parse(cache.payload));
if (feed.version !== cache.version || feed.tier !== "live") return EMPTY_INTEL;
return {
intel: feed,
meta: {
version: cache.version,
tier: "live",
fetchedAt: cache.fetchedAt,
supporterVerified: true,
},
};
} catch {
return EMPTY_INTEL;
}
}
// Re-export merge types for convenience
export { applyFeed, type MergedEntry, type FeedModel } from "./applyFeed";
export { findDefaultReferral } from "./referrals";
export type { RadarReferral } from "./feedSchema";
export type { RadarOffer, RadarOfferBenefit, RadarOfferLocalizedText } from "./offersFeedSchema";
export type { RadarIntelFeed, RadarIntelRanking, RadarIntelCatalog } from "./intelFeedSchema";

View File

@@ -0,0 +1,73 @@
/** Closed client mirror of the private Radar Intel feed contract. */
import { z } from "zod";
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,119}$/;
const CATEGORY_PATTERN = /^[a-z0-9][a-z0-9._-]{0,79}$/;
const MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,239}$/;
export const RadarIntelRankingSchema = z
.object({
rank: z.number().int().positive(),
provider: z.string().regex(ID_PATTERN),
modelId: z.string().regex(MODEL_ID_PATTERN),
category: z.string().regex(CATEGORY_PATTERN),
rating: z.number().int(),
matches: z.number().int().nonnegative(),
wins: z.number().int().nonnegative(),
losses: z.number().int().nonnegative(),
draws: z.number().int().nonnegative(),
})
.strict()
.superRefine((ranking, ctx) => {
if (ranking.matches !== ranking.wins + ranking.losses + ranking.draws) {
ctx.addIssue({ code: "custom", path: ["matches"], message: "match counters disagree" });
}
});
const CatalogDeltaSchema = z
.object({
current: z.number().int().nonnegative(),
added: z.number().int().nonnegative(),
removed: z.number().int().nonnegative(),
})
.strict();
export const RadarIntelCatalogSchema = z
.object({
currentVersion: z.string().regex(/^\d{4}\.\d{2}\.\d{2}\.\d+$/),
previousVersion: z
.string()
.regex(/^\d{4}\.\d{2}\.\d{2}\.\d+$/)
.nullable(),
currentGeneratedAt: z.string().datetime(),
ageDays: z.number().int().nonnegative(),
freshness: z.enum(["fresh", "aging", "stale"]),
providers: CatalogDeltaSchema,
models: CatalogDeltaSchema,
trend: z.enum(["growing", "stable", "shrinking"]),
})
.strict();
export const RadarIntelFeedSchema = z
.object({
feed: z.literal("omniroute-radar-intel"),
schemaVersion: z.literal(1),
version: z.string().regex(/^\d{4}\.\d{2}\.\d{2}\.\d+$/),
generatedAt: z.string().datetime(),
tier: z.literal("live"),
methodology: z
.object({
kind: z.literal("elo"),
initialRating: z.literal(1000),
kFactor: z.literal(32),
})
.strict(),
rankings: z.array(RadarIntelRankingSchema),
catalog: RadarIntelCatalogSchema,
})
.strict();
export type RadarIntelFeed = z.infer<typeof RadarIntelFeedSchema>;
export type RadarIntelRanking = z.infer<typeof RadarIntelRankingSchema>;
export type RadarIntelCatalog = z.infer<typeof RadarIntelCatalogSchema>;

173
src/lib/radar/intelSync.ts Normal file
View File

@@ -0,0 +1,173 @@
/** Server-side sync for the signed, supporter-only Radar Intel feed. */
import crypto from "node:crypto";
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
import { RadarIntelFeedSchema, type RadarIntelFeed } from "./intelFeedSchema";
import { compareVersions, type RadarSettingsSnapshot } from "./sync";
import { verifyFeedBytes } from "./verify";
const DEFAULT_FEED_BASE_URL = "https://radar.omniroute.online";
const SYNC_TIMEOUT_MS = 30_000;
const MAX_FEED_BYTES = 10 * 1024 * 1024;
export type IntelSyncStatus =
| { status: "disabled" }
| { status: "opt_out" }
| { status: "no_key" }
| { status: "invalid_signature" }
| { status: "invalid_schema" }
| { status: "wrong_tier" }
| { status: "stale" }
| { status: "too_large" }
| { status: "updated"; version: string }
| { status: "error"; reason: string };
export interface RadarIntelCacheEntry {
version: string;
tier: "live";
payload: string;
signature: string;
supporterIdentity: string;
fetchedAt?: string;
}
export interface IntelSyncDeps {
fetch?: typeof globalThis.fetch;
now?: () => Date;
getFlag?: (key: string) => boolean;
getSettings?: () => RadarSettingsSnapshot;
getCache?: () => RadarIntelCacheEntry | null;
setCache?: (entry: RadarIntelCacheEntry) => void;
recognizeSupporter?: (identity: string) => Promise<void>;
}
async function readBoundedBytes(response: Response): Promise<Buffer | null> {
const contentLength = response.headers.get("content-length");
if (contentLength !== null) {
const declared = Number(contentLength);
if (Number.isFinite(declared) && declared > MAX_FEED_BYTES) return null;
}
const body = response.body as ReadableStream<Uint8Array> | null | undefined;
if (!body || typeof body.getReader !== "function") {
const buffered = Buffer.from(await response.arrayBuffer());
return buffered.byteLength > MAX_FEED_BYTES ? null : buffered;
}
const reader = body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
if (!value) continue;
total += value.byteLength;
if (total > MAX_FEED_BYTES) {
await reader.cancel().catch(() => undefined);
return null;
}
chunks.push(value);
}
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
}
function supporterIdentity(key: string): string {
return `radar:${crypto.createHash("sha256").update(key, "utf8").digest("hex")}`;
}
async function recognizeVerifiedSupporter(identity: string): Promise<void> {
const { emitGamificationEvent } = await import("@/lib/gamification/events");
await emitGamificationEvent({ apiKeyId: identity, action: "radar_supporter" });
}
export async function syncRadarIntel(deps: IntelSyncDeps = {}): Promise<IntelSyncStatus> {
const {
fetch: fetchFn = globalThis.fetch,
now = () => new Date(),
getFlag = isFeatureFlagEnabled,
getSettings: getSettingsFn,
getCache: getCacheFn,
setCache: setCacheFn,
recognizeSupporter = recognizeVerifiedSupporter,
} = deps;
try {
if (!getFlag("RADAR_ENABLED")) return { status: "disabled" };
const settings = getSettingsFn
? getSettingsFn()
: (await import("@/lib/db/radar")).getRadarSettings();
if (!settings.optIn) return { status: "opt_out" };
if (!settings.supporterKey) return { status: "no_key" };
const baseUrl = (process.env.RADAR_FEED_URL || DEFAULT_FEED_BASE_URL).replace(/\/+$/, "");
const response = await fetchFn(`${baseUrl}/v1/intel/latest`, {
method: "GET",
headers: { Authorization: `Bearer ${settings.supporterKey}` },
signal: AbortSignal.timeout(SYNC_TIMEOUT_MS),
});
if (!response.ok) {
return {
status: "error",
reason: `Intel feed request failed with status ${response.status}`,
};
}
const rawBytes = await readBoundedBytes(response);
if (!rawBytes) return { status: "too_large" };
const signature = response.headers.get("x-omniroute-feed-signature") ?? "";
if (!verifyFeedBytes(rawBytes, signature)) return { status: "invalid_signature" };
let feed: RadarIntelFeed;
try {
feed = RadarIntelFeedSchema.parse(JSON.parse(rawBytes.toString("utf8")));
} catch {
return { status: "invalid_schema" };
}
if (response.headers.get("x-omniroute-feed-tier") !== "live" || feed.tier !== "live") {
return { status: "wrong_tier" };
}
const existing = getCacheFn
? getCacheFn()
: (await import("@/lib/db/radar")).getRadarIntelCache();
if (existing && compareVersions(feed.version, existing.version) <= 0) {
return { status: "stale" };
}
const identity = supporterIdentity(settings.supporterKey);
const cacheEntry: RadarIntelCacheEntry = {
version: feed.version,
tier: "live",
payload: rawBytes.toString("utf8"),
signature,
supporterIdentity: identity,
fetchedAt: now().toISOString(),
};
if (setCacheFn) setCacheFn(cacheEntry);
else (await import("@/lib/db/radar")).setRadarIntelCache(cacheEntry);
// Recognition is local and best-effort. It runs only after the signed live
// bytes have been accepted and persisted, and never changes sync success.
await recognizeSupporter(identity).catch(() => undefined);
return { status: "updated", version: feed.version };
} catch (error: unknown) {
const reason = (sanitizeErrorMessage(error) || "Radar Intel sync failed").replace(
/omr_[a-f0-9]{40}/gi,
"[REDACTED]"
);
return { status: "error", reason };
}
}
export const RADAR_INTEL_STALE_MS = 24 * 60 * 60 * 1000;
export function shouldSyncRadarIntel(fetchedAt: string | null, nowMs = Date.now()): boolean {
if (!fetchedAt) return true;
const fetchedMs = Date.parse(fetchedAt);
return !Number.isFinite(fetchedMs) || nowMs - fetchedMs >= RADAR_INTEL_STALE_MS;
}

View File

@@ -24,7 +24,15 @@
*/
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
import { getRadarCache, getRadarSettings, getRadarReferralsCache } from "@/lib/db/radar";
import {
getRadarCache,
getRadarIntelCache,
getRadarOffersCache,
getRadarSettings,
getRadarReferralsCache,
} from "@/lib/db/radar";
import { shouldSyncRadarIntel, syncRadarIntel, type IntelSyncStatus } from "./intelSync";
import { syncRadarOffers, type OffersSyncStatus } from "./offersSync";
import { nextSyncTime, syncRadar, type SyncStatus } from "./sync";
import {
syncRadarReferrals,
@@ -49,6 +57,10 @@ export interface RadarSchedulerDeps {
getReferralsCache?: () => { fetchedAt: string } | null;
/** Referrals sync — separate from `sync` (the catalog sync). */
syncReferrals?: () => Promise<ReferralsSyncStatus>;
getOffersCache?: () => { fetchedAt: string } | null;
syncOffers?: () => Promise<OffersSyncStatus>;
getIntelCache?: () => { fetchedAt: string } | null;
syncIntel?: () => Promise<IntelSyncStatus>;
now?: () => number;
setIntervalFn?: typeof setInterval;
clearIntervalFn?: typeof clearInterval;
@@ -73,6 +85,28 @@ async function maybeSyncReferrals(deps: RadarSchedulerDeps, nowMs: number): Prom
}
}
async function maybeSyncSupporterFeeds(deps: RadarSchedulerDeps, nowMs: number): Promise<void> {
try {
const offersCache = (deps.getOffersCache ?? getRadarOffersCache)();
if (nowMs >= nextSyncTime(offersCache?.fetchedAt ?? null).getTime()) {
await (deps.syncOffers ?? syncRadarOffers)();
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.warn("[RADAR_SYNC] Offers side-sync failed (non-fatal):", msg);
}
try {
const intelCache = (deps.getIntelCache ?? getRadarIntelCache)();
if (shouldSyncRadarIntel(intelCache?.fetchedAt ?? null, nowMs)) {
await (deps.syncIntel ?? syncRadarIntel)();
}
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.warn("[RADAR_SYNC] Intel side-sync failed (non-fatal):", msg);
}
}
/**
* One scheduler evaluation. Exported for tests and for the immediate
* post-start tick.
@@ -92,6 +126,7 @@ export async function radarSchedulerTick(deps: RadarSchedulerDeps = {}): Promise
// Referrals sync on their own staleness window — independent of the
// catalog's due-ness below, same tick.
await maybeSyncReferrals(deps, nowMs);
await maybeSyncSupporterFeeds(deps, nowMs);
const cache = (deps.getCache ?? getRadarCache)();
if (nowMs < nextSyncTime(cache?.fetchedAt ?? null).getTime()) {

View File

@@ -0,0 +1,42 @@
{
"feed": "omniroute-radar-intel",
"schemaVersion": 1,
"version": "2026.08.09.1",
"generatedAt": "2026-08-09T12:00:00.000Z",
"tier": "live",
"methodology": { "kind": "elo", "initialRating": 1000, "kFactor": 32 },
"rankings": [
{
"rank": 1,
"provider": "example-a",
"modelId": "example-model-a",
"category": "general",
"rating": 1016,
"matches": 1,
"wins": 1,
"losses": 0,
"draws": 0
},
{
"rank": 2,
"provider": "example-b",
"modelId": "example-model-b",
"category": "general",
"rating": 984,
"matches": 1,
"wins": 0,
"losses": 1,
"draws": 0
}
],
"catalog": {
"currentVersion": "2026.08.09.1",
"previousVersion": "2026.08.08.1",
"currentGeneratedAt": "2026-08-09T11:00:00.000Z",
"ageDays": 0,
"freshness": "fresh",
"providers": { "current": 2, "added": 1, "removed": 0 },
"models": { "current": 2, "added": 1, "removed": 0 },
"trend": "growing"
}
}

View File

@@ -0,0 +1,97 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
function makeResponse(data: unknown, status = 200): Response {
return {
ok: status >= 200 && status < 300,
status,
headers: new Headers(),
json: async () => data,
text: async () => JSON.stringify(data),
} as Response;
}
async function captureStdout(fn: () => Promise<number>): Promise<{ output: string; code: number }> {
const chunks: string[] = [];
const original = process.stdout.write.bind(process.stdout);
process.stdout.write = ((chunk: string | Uint8Array) => {
if (typeof chunk === "string") chunks.push(chunk);
return true;
}) as typeof process.stdout.write;
try {
const code = await fn();
return { output: chunks.join(""), code };
} finally {
process.stdout.write = original;
}
}
test("radar status is GET-only, read-only, and prints no secret", async () => {
const originalFetch = globalThis.fetch;
let method = "GET";
let url = "";
globalThis.fetch = (async (input, init) => {
url = String(input);
method = init?.method ?? "GET";
return makeResponse({
settings: { optIn: true, hasSupporterKey: true },
feeds: { catalog: { available: true, version: "2026.08.09.1", tier: "live" } },
});
}) as typeof fetch;
try {
const { runRadarStatusCommand } = await import("../../bin/cli/commands/radar.mjs");
const result = await captureStdout(() => runRadarStatusCommand({ output: "json" }));
assert.equal(result.code, 0);
assert.match(url, /\/api\/radar\/status$/);
assert.equal(method, "GET");
assert.ok(!result.output.includes("omr_"));
} finally {
globalThis.fetch = originalFetch;
}
});
test("radar sync posts only to the local aggregate route and prints per-feed results", async () => {
const originalFetch = globalThis.fetch;
let method = "";
let url = "";
globalThis.fetch = (async (input, init) => {
url = String(input);
method = init?.method ?? "GET";
return makeResponse({
catalog: { status: "updated", version: "2026.08.09.1" },
referrals: { status: "stale" },
offers: { status: "no_key" },
intel: { status: "no_key" },
});
}) as typeof fetch;
try {
const { runRadarSyncCommand } = await import("../../bin/cli/commands/radar.mjs");
const result = await captureStdout(() => runRadarSyncCommand({ output: "json" }));
assert.equal(result.code, 0);
assert.match(url, /\/api\/radar\/sync-all$/);
assert.equal(method, "POST");
const parsed = JSON.parse(result.output) as Record<string, unknown>;
assert.deepEqual(Object.keys(parsed).sort(), ["catalog", "intel", "offers", "referrals"]);
} finally {
globalThis.fetch = originalFetch;
}
});
test("CLI registry exposes nested radar status and sync commands with EN/PT strings", async () => {
const { createProgram } = await import("../../bin/cli/program.mjs");
const program = createProgram();
const radar = program.commands.find((command) => command.name() === "radar");
assert.ok(radar);
assert.deepEqual(radar.commands.map((command) => command.name()).sort(), ["status", "sync"]);
for (const locale of ["en", "pt-BR"]) {
const messages = JSON.parse(
fs.readFileSync(path.resolve(process.cwd(), `bin/cli/locales/${locale}.json`), "utf8")
) as { radar?: Record<string, unknown> };
assert.equal(typeof messages.radar?.description, "string");
assert.equal(typeof messages.radar?.status, "string");
assert.equal(typeof messages.radar?.sync, "string");
}
});

View File

@@ -0,0 +1,74 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-radar-intel-db-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.STORAGE_ENCRYPTION_KEY = "test-encryption-key-for-radar-intel-db-32b!";
const core = await import("../../src/lib/db/core.ts");
const radar = await import("../../src/lib/db/radar.ts");
function resetStorage(): void {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(resetStorage);
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("Intel migration provides a byte-preserving single-row cache", () => {
assert.equal(radar.getRadarIntelCache(), null);
radar.setRadarIntelCache({
version: "2026.08.09.1",
tier: "live",
payload: '{"exact":true}\n',
signature: "signed",
supporterIdentity: `radar:${"a".repeat(64)}`,
fetchedAt: "2026-08-09T12:05:00.000Z",
});
assert.deepEqual(radar.getRadarIntelCache(), {
version: "2026.08.09.1",
tier: "live",
payload: '{"exact":true}\n',
signature: "signed",
supporterIdentity: `radar:${"a".repeat(64)}`,
fetchedAt: "2026-08-09T12:05:00.000Z",
});
});
test("changing supporter key invalidates catalog, referrals, offers, and Intel atomically", () => {
radar.setRadarCache({ version: "2026.08.09.1", tier: "live", payload: "{}", signature: "a" });
radar.setRadarReferralsCache({
generatedAt: "2026-08-09T12:00:00.000Z",
tier: "live",
payload: "{}",
signature: "b",
});
radar.setRadarOffersCache({
version: "2026.08.09.1",
tier: "live",
payload: "{}",
signature: "c",
});
radar.setRadarIntelCache({
version: "2026.08.09.1",
tier: "live",
payload: "{}",
signature: "d",
supporterIdentity: `radar:${"a".repeat(64)}`,
});
radar.setRadarKey(`omr_${"b".repeat(40)}`);
assert.equal(radar.getRadarCache(), null);
assert.equal(radar.getRadarReferralsCache(), null);
assert.equal(radar.getRadarOffersCache(), null);
assert.equal(radar.getRadarIntelCache(), null);
});

View File

@@ -0,0 +1,53 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const pagePath = path.resolve(process.cwd(), "src/app/(dashboard)/dashboard/radar/intel/page.tsx");
const radarPagePath = path.resolve(process.cwd(), "src/app/(dashboard)/dashboard/radar/page.tsx");
test("Radar links to a dedicated local-only Intel page", () => {
assert.ok(fs.existsSync(pagePath));
assert.match(fs.readFileSync(radarPagePath, "utf8"), /href="\/dashboard\/radar\/intel"/);
const source = fs.readFileSync(pagePath, "utf8");
assert.match(source, /fetch\("\/api\/radar\/intel"\)/);
assert.match(source, /fetch\("\/api\/radar\/intel\/sync",\s*\{\s*method:\s*"POST"/);
assert.doesNotMatch(source, /RADAR_FEED_URL|radar\.omniroute\.online|omr_|getDbInstance/);
});
test("Intel page exposes methodology, ranking, freshness, trend, and verified supporter badge only", () => {
const source = fs.readFileSync(pagePath, "utf8");
for (const marker of [
"methodology",
"rankings",
"freshness",
"trend",
"supporterVerified",
"radar-supporter",
]) {
assert.match(source, new RegExp(marker));
}
assert.doesNotMatch(source, /\bhealth\b|\buptime\b|\blatency\b|\btelemetry\b/i);
});
test("Intel UI strings exist in English and Brazilian Portuguese", () => {
for (const locale of ["en", "pt-BR"]) {
const messages = JSON.parse(
fs.readFileSync(path.resolve(process.cwd(), `src/i18n/messages/${locale}.json`), "utf8")
) as { radarIntelPage?: Record<string, unknown>; radarPage?: Record<string, unknown> };
for (const key of [
"title",
"subtitle",
"methodology",
"supporterBadge",
"ranking",
"freshness",
"trend",
"empty",
"loadFailed",
]) {
assert.equal(typeof messages.radarIntelPage?.[key], "string", `${locale}: ${key}`);
}
assert.equal(typeof messages.radarPage?.intel, "string", `${locale}: radarPage.intel`);
}
});

View File

@@ -0,0 +1,107 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { SignJWT } from "jose";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-radar-intel-routes-"));
process.env.DATA_DIR = TEST_DATA_DIR;
process.env.STORAGE_ENCRYPTION_KEY = "test-encryption-key-for-radar-intel-routes-32b!";
process.env.JWT_SECRET = "test-jwt-secret-for-radar-intel-routes";
process.env.INITIAL_PASSWORD = "test-bootstrap-password-for-radar-intel-routes";
const core = await import("../../src/lib/db/core.ts");
const radarDb = await import("../../src/lib/db/radar.ts");
async function authHeaders(): Promise<Record<string, string>> {
const token = await new SignJWT({ authenticated: true })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("1h")
.sign(new TextEncoder().encode(process.env.JWT_SECRET));
return { Cookie: `auth_token=${token}` };
}
function request(pathname: string, method: "GET" | "POST", headers: Record<string, string> = {}) {
return new Request(`http://localhost:20128${pathname}`, { method, headers });
}
function resetStorage(): void {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
delete process.env.RADAR_ENABLED;
});
test("Intel, status, and aggregate sync routes are 404 before auth when flag is off", async () => {
resetStorage();
delete process.env.RADAR_ENABLED;
const intel = await import("../../src/app/api/radar/intel/route.ts");
const intelSync = await import("../../src/app/api/radar/intel/sync/route.ts");
const status = await import("../../src/app/api/radar/status/route.ts");
const syncAll = await import("../../src/app/api/radar/sync-all/route.ts");
assert.equal((await intel.GET(request("/api/radar/intel", "GET"))).status, 404);
assert.equal((await intelSync.POST(request("/api/radar/intel/sync", "POST"))).status, 404);
assert.equal((await status.GET(request("/api/radar/status", "GET"))).status, 404);
assert.equal((await syncAll.POST(request("/api/radar/sync-all", "POST"))).status, 404);
});
test("verified local Intel is returned without supporter identity or key material", async () => {
resetStorage();
process.env.RADAR_ENABLED = "true";
const payload = fs.readFileSync(
path.resolve(process.cwd(), "tests/fixtures/radar-intel-canonical.json"),
"utf8"
);
radarDb.setRadarIntelCache({
version: "2026.08.09.1",
tier: "live",
payload,
signature: "fixture-signature",
supporterIdentity: `radar:${"a".repeat(64)}`,
fetchedAt: "2026-08-09T12:05:00.000Z",
});
const { GET } = await import("../../src/app/api/radar/intel/route.ts");
const response = await GET(request("/api/radar/intel", "GET", await authHeaders()));
const body = await response.json();
assert.equal(response.status, 200);
assert.equal(body.intel.rankings.length, 2);
assert.equal(body.meta.supporterVerified, true);
assert.ok(!JSON.stringify(body).includes("radar:"));
assert.ok(!JSON.stringify(body).includes("omr_"));
});
test("Radar status is read-only and aggregate sync reports each feed separately", async () => {
resetStorage();
process.env.RADAR_ENABLED = "true";
const headers = await authHeaders();
const statusRoute = await import("../../src/app/api/radar/status/route.ts");
const status = await statusRoute.GET(request("/api/radar/status", "GET", headers));
const statusBody = await status.json();
assert.deepEqual(statusBody.settings, { optIn: false, hasSupporterKey: false });
assert.deepEqual(Object.keys(statusBody.feeds).sort(), [
"catalog",
"intel",
"offers",
"referrals",
]);
const syncAllRoute = await import("../../src/app/api/radar/sync-all/route.ts");
const synced = await syncAllRoute.POST(request("/api/radar/sync-all", "POST", headers));
const syncBody = await synced.json();
assert.deepEqual(syncBody, {
catalog: { status: "opt_out" },
referrals: { status: "opt_out" },
offers: { status: "opt_out" },
intel: { status: "opt_out" },
});
});

View File

@@ -0,0 +1,210 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import { readFile } from "node:fs/promises";
import test from "node:test";
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
process.env.RADAR_FEED_PUBKEY = publicKey
.export({ type: "spki", format: "der" })
.toString("base64");
const intelSync = await import("../../src/lib/radar/intelSync.ts");
const { RadarIntelFeedSchema } = await import("../../src/lib/radar/intelFeedSchema.ts");
async function fixtureBytes(): Promise<Buffer> {
return readFile(new URL("../fixtures/radar-intel-canonical.json", import.meta.url));
}
function sign(bytes: Buffer): string {
return crypto.sign(null, bytes, privateKey).toString("base64");
}
function response(body: Buffer, headers: Record<string, string> = {}, status = 200): Response {
return {
ok: status >= 200 && status < 300,
status,
headers: new Headers(headers),
arrayBuffer: async () => body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength),
} as Response;
}
const supporterKey = `omr_${"a".repeat(40)}`;
const liveSettings = { optIn: true, supporterKey };
test("canonical Intel fixture is byte-identical to the private contract", async () => {
const bytes = await fixtureBytes();
assert.equal(bytes.byteLength, 1024);
assert.equal(
crypto.createHash("sha256").update(bytes).digest("hex"),
"c36aaa6ad53942afa0325d6b0fad0aa048ef66f24c805b743b9815446b0e6176"
);
assert.equal(RadarIntelFeedSchema.parse(JSON.parse(bytes.toString("utf8"))).tier, "live");
});
test("Intel schema rejects telemetry and inconsistent ranking counters", async () => {
const feed = JSON.parse((await fixtureBytes()).toString("utf8"));
assert.equal(RadarIntelFeedSchema.safeParse({ ...feed, uptime: 99.9 }).success, false);
feed.rankings[0].matches = 2;
assert.equal(RadarIntelFeedSchema.safeParse(feed).success, false);
});
test("Intel sync gates before fetch and only accepts exact signed live bytes", async () => {
for (const expected of ["disabled", "opt_out", "no_key"] as const) {
let fetched = false;
const result = await intelSync.syncRadarIntel({
getFlag: () => expected !== "disabled",
getSettings: () =>
expected === "opt_out"
? { optIn: false, supporterKey: null }
: { optIn: true, supporterKey: null },
fetch: (async () => {
fetched = true;
return response(Buffer.from("{}"));
}) as typeof fetch,
});
assert.equal(result.status, expected);
assert.equal(fetched, false);
}
const bytes = await fixtureBytes();
const writes: intelSync.RadarIntelCacheEntry[] = [];
const supporterIdentities: string[] = [];
let authorization = "";
const result = await intelSync.syncRadarIntel({
getFlag: () => true,
getSettings: () => liveSettings,
getCache: () => null,
setCache: (entry) => writes.push(entry),
recognizeSupporter: async (identity) => supporterIdentities.push(identity),
fetch: (async (_input, init) => {
authorization = new Headers(init?.headers).get("authorization") ?? "";
return response(bytes, {
"x-omniroute-feed-signature": sign(bytes),
"x-omniroute-feed-tier": "live",
});
}) as typeof fetch,
now: () => new Date("2026-08-09T12:05:00.000Z"),
});
assert.deepEqual(result, { status: "updated", version: "2026.08.09.1" });
assert.equal(authorization, `Bearer ${supporterKey}`);
assert.equal(writes[0]?.payload, bytes.toString("utf8"));
assert.equal(writes[0]?.tier, "live");
assert.match(writes[0]?.supporterIdentity ?? "", /^radar:[a-f0-9]{64}$/);
assert.deepEqual(supporterIdentities, [writes[0]?.supporterIdentity]);
assert.ok(!writes[0]?.supporterIdentity.includes(supporterKey));
});
test("Intel sync preserves the good cache on signature, tier, schema, replay, and size failures", async () => {
const bytes = await fixtureBytes();
const validSignature = sign(bytes);
const cases = [
{ expected: "invalid_signature", body: bytes, signature: "bad", tier: "live" },
{ expected: "wrong_tier", body: bytes, signature: validSignature, tier: "community" },
{
expected: "invalid_schema",
body: Buffer.from('{"feed":"wrong"}'),
signature: "",
tier: "live",
},
];
for (const item of cases) {
const signature = item.expected === "invalid_schema" ? sign(item.body) : item.signature;
let written = false;
const result = await intelSync.syncRadarIntel({
getFlag: () => true,
getSettings: () => liveSettings,
getCache: () => ({
version: "2026.08.08.1",
tier: "live",
payload: "last-good",
signature: "old",
supporterIdentity: `radar:${"b".repeat(64)}`,
}),
setCache: () => {
written = true;
},
fetch: (async () =>
response(item.body, {
"x-omniroute-feed-signature": signature,
"x-omniroute-feed-tier": item.tier,
})) as typeof fetch,
});
assert.equal(result.status, item.expected);
assert.equal(written, false);
}
let written = false;
const stale = await intelSync.syncRadarIntel({
getFlag: () => true,
getSettings: () => liveSettings,
getCache: () => ({
version: "2026.08.09.1",
tier: "live",
payload: "last-good",
signature: "old",
supporterIdentity: `radar:${"b".repeat(64)}`,
}),
setCache: () => {
written = true;
},
fetch: (async () =>
response(bytes, {
"x-omniroute-feed-signature": validSignature,
"x-omniroute-feed-tier": "live",
})) as typeof fetch,
});
assert.equal(stale.status, "stale");
const oversized = await intelSync.syncRadarIntel({
getFlag: () => true,
getSettings: () => liveSettings,
getCache: () => null,
setCache: () => {
written = true;
},
fetch: (async () =>
response(Buffer.from("ignored"), {
"content-length": String(10 * 1024 * 1024 + 1),
})) as typeof fetch,
});
assert.equal(oversized.status, "too_large");
assert.equal(written, false);
});
test("Intel sync enforces the byte cap while reading streamed chunks", async () => {
let cancelled = false;
let written = false;
const firstChunk = new Uint8Array(6 * 1024 * 1024);
const secondChunk = new Uint8Array(5 * 1024 * 1024);
const chunks = [firstChunk, secondChunk];
let chunkIndex = 0;
const body = new ReadableStream<Uint8Array>({
pull(controller) {
controller.enqueue(chunks[chunkIndex]);
chunkIndex += 1;
},
cancel() {
cancelled = true;
},
});
const result = await intelSync.syncRadarIntel({
getFlag: () => true,
getSettings: () => liveSettings,
getCache: () => null,
setCache: () => {
written = true;
},
fetch: (async () =>
new Response(body, {
status: 200,
headers: { "x-omniroute-feed-tier": "live" },
})) as typeof fetch,
});
assert.equal(result.status, "too_large");
assert.equal(cancelled, true);
assert.equal(written, false);
});

View File

@@ -46,10 +46,14 @@ function fakeTimers() {
function deps(overrides: Record<string, unknown> = {}) {
const syncCalls: number[] = [];
const referralsSyncCalls: number[] = [];
const offersSyncCalls: number[] = [];
const intelSyncCalls: number[] = [];
const timers = fakeTimers();
return {
syncCalls,
referralsSyncCalls,
offersSyncCalls,
intelSyncCalls,
timers,
d: {
getFlag: () => true,
@@ -66,7 +70,21 @@ function deps(overrides: Record<string, unknown> = {}) {
getReferralsCache: () => ({ fetchedAt: REFERRALS_FRESH }),
syncReferrals: async () => {
referralsSyncCalls.push(1);
return { status: "updated", generatedAt: "2026-08-06T12:00:00.000Z", tier: "live" } as const;
return {
status: "updated",
generatedAt: "2026-08-06T12:00:00.000Z",
tier: "live",
} as const;
},
getOffersCache: () => ({ fetchedAt: FRESH }),
syncOffers: async () => {
offersSyncCalls.push(1);
return { status: "updated", version: "2026.08.06.1" } as const;
},
getIntelCache: () => ({ fetchedAt: FRESH }),
syncIntel: async () => {
intelSyncCalls.push(1);
return { status: "updated", version: "2026.08.06.1" } as const;
},
now: () => NOW,
setIntervalFn: timers.setIntervalFn,
@@ -126,18 +144,21 @@ test("radar sync scheduler", async (t) => {
assert.equal(syncCalls.length, 1);
});
await t.test("ensure: registers one hourly timer, fires an immediate tick, idempotent", async () => {
const { d, timers, syncCalls } = deps();
assert.equal(ensureRadarSyncScheduler(d), true);
assert.equal(timers.registered.length, 1);
assert.equal(timers.registered[0].ms, RADAR_SCHEDULER_TICK_MS);
// The immediate tick is fire-and-forget; give the microtask queue a turn.
await new Promise((resolve) => setImmediate(resolve));
assert.equal(syncCalls.length, 1, "immediate tick should have synced the stale cache");
// Second ensure is a no-op — no second timer.
assert.equal(ensureRadarSyncScheduler(d), false);
assert.equal(timers.registered.length, 1);
});
await t.test(
"ensure: registers one hourly timer, fires an immediate tick, idempotent",
async () => {
const { d, timers, syncCalls } = deps();
assert.equal(ensureRadarSyncScheduler(d), true);
assert.equal(timers.registered.length, 1);
assert.equal(timers.registered[0].ms, RADAR_SCHEDULER_TICK_MS);
// The immediate tick is fire-and-forget; give the microtask queue a turn.
await new Promise((resolve) => setImmediate(resolve));
assert.equal(syncCalls.length, 1, "immediate tick should have synced the stale cache");
// Second ensure is a no-op — no second timer.
assert.equal(ensureRadarSyncScheduler(d), false);
assert.equal(timers.registered.length, 1);
}
);
await t.test("init: flag off => never arms (flag-off boot stays timer-free)", () => {
const { d, timers } = deps({ getFlag: () => false });
@@ -174,61 +195,100 @@ test("radar sync scheduler", async (t) => {
// only) so the catalog-sync result shape/assertions above stay unchanged.
// -------------------------------------------------------------------------
await t.test("tick: referrals cache fresh => referrals sync NOT called (catalog path unaffected)", async () => {
const { d, syncCalls, referralsSyncCalls } = deps();
const result = await radarSchedulerTick(d);
assert.equal(result.action, "synced", "catalog was due and must still sync as before");
assert.equal(syncCalls.length, 1);
assert.equal(referralsSyncCalls.length, 0, "referrals cache was fresh — must not sync");
});
await t.test(
"tick: referrals cache fresh => referrals sync NOT called (catalog path unaffected)",
async () => {
const { d, syncCalls, referralsSyncCalls } = deps();
const result = await radarSchedulerTick(d);
assert.equal(result.action, "synced", "catalog was due and must still sync as before");
assert.equal(syncCalls.length, 1);
assert.equal(referralsSyncCalls.length, 0, "referrals cache was fresh — must not sync");
}
);
await t.test("tick: referrals cache stale => referrals sync called, independent of catalog due-ness", async () => {
const { d, syncCalls, referralsSyncCalls } = deps({
getCache: () => ({ fetchedAt: FRESH }), // catalog NOT due
getReferralsCache: () => ({ fetchedAt: REFERRALS_STALE }), // referrals due
});
const result = await radarSchedulerTick(d);
assert.deepEqual(result, { action: "skipped", reason: "not_due" }, "catalog result shape must stay unchanged");
assert.equal(syncCalls.length, 0, "catalog must not sync — it was not due");
assert.equal(referralsSyncCalls.length, 1, "referrals were due and must sync independently");
});
await t.test(
"tick: referrals cache stale => referrals sync called, independent of catalog due-ness",
async () => {
const { d, syncCalls, referralsSyncCalls } = deps({
getCache: () => ({ fetchedAt: FRESH }), // catalog NOT due
getReferralsCache: () => ({ fetchedAt: REFERRALS_STALE }), // referrals due
});
const result = await radarSchedulerTick(d);
assert.deepEqual(
result,
{ action: "skipped", reason: "not_due" },
"catalog result shape must stay unchanged"
);
assert.equal(syncCalls.length, 0, "catalog must not sync — it was not due");
assert.equal(referralsSyncCalls.length, 1, "referrals were due and must sync independently");
}
);
await t.test("tick: referrals cache missing => referrals sync called (missing counts as stale)", async () => {
const { d, referralsSyncCalls } = deps({
await t.test(
"tick: referrals cache missing => referrals sync called (missing counts as stale)",
async () => {
const { d, referralsSyncCalls } = deps({
getCache: () => ({ fetchedAt: FRESH }),
getReferralsCache: () => null,
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 1);
}
);
await t.test(
"tick: flag off => referrals sync NOT called (stopped before any sync check)",
async () => {
const { d, referralsSyncCalls } = deps({
getFlag: () => false,
getReferralsCache: () => null, // would be due if ever reached
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 0);
}
);
await t.test(
"tick: opt-in off => referrals sync NOT called (skipped before any sync check)",
async () => {
const { d, referralsSyncCalls } = deps({
getSettings: () => ({ optIn: false }),
getReferralsCache: () => null, // would be due if ever reached
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 0);
}
);
await t.test(
"tick: referrals sync throwing => swallowed, catalog tick still completes normally",
async () => {
const { d, syncCalls } = deps({
getReferralsCache: () => ({ fetchedAt: REFERRALS_STALE }),
syncReferrals: async () => {
throw new Error("referrals upstream exploded");
},
});
const result = await radarSchedulerTick(d);
assert.equal(
result.action,
"synced",
"a throwing referrals sync must never break the catalog tick"
);
assert.equal(syncCalls.length, 1);
}
);
await t.test("tick: offers and Intel use independent staleness gates", async () => {
const { d, syncCalls, offersSyncCalls, intelSyncCalls } = deps({
getCache: () => ({ fetchedAt: FRESH }),
getReferralsCache: () => null,
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 1);
});
await t.test("tick: flag off => referrals sync NOT called (stopped before any sync check)", async () => {
const { d, referralsSyncCalls } = deps({
getFlag: () => false,
getReferralsCache: () => null, // would be due if ever reached
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 0);
});
await t.test("tick: opt-in off => referrals sync NOT called (skipped before any sync check)", async () => {
const { d, referralsSyncCalls } = deps({
getSettings: () => ({ optIn: false }),
getReferralsCache: () => null, // would be due if ever reached
});
await radarSchedulerTick(d);
assert.equal(referralsSyncCalls.length, 0);
});
await t.test("tick: referrals sync throwing => swallowed, catalog tick still completes normally", async () => {
const { d, syncCalls } = deps({
getReferralsCache: () => ({ fetchedAt: REFERRALS_STALE }),
syncReferrals: async () => {
throw new Error("referrals upstream exploded");
},
getOffersCache: () => ({ fetchedAt: STALE }),
getIntelCache: () => null,
});
const result = await radarSchedulerTick(d);
assert.equal(result.action, "synced", "a throwing referrals sync must never break the catalog tick");
assert.equal(syncCalls.length, 1);
assert.deepEqual(result, { action: "skipped", reason: "not_due" });
assert.equal(syncCalls.length, 0);
assert.equal(offersSyncCalls.length, 1);
assert.equal(intelSyncCalls.length, 1);
});
});

View File

@@ -0,0 +1,42 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-radar-supporter-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { getDbInstance, resetDbInstance } = await import("../../src/lib/db/core.ts");
const { BUILTIN_BADGES } = await import("../../src/lib/gamification/badges.ts");
const { emitGamificationEvent } = await import("../../src/lib/gamification/events.ts");
test.after(() => {
resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("Radar supporter has a dedicated badge and zero-XP idempotent action", async () => {
const identity = `radar:${"a".repeat(64)}`;
const badge = BUILTIN_BADGES.find((item) => item.id === "radar-supporter");
assert.ok(badge);
assert.equal(JSON.parse(badge.criteria).action, "radar_supporter");
await emitGamificationEvent({ apiKeyId: identity, action: "radar_supporter" });
await emitGamificationEvent({ apiKeyId: identity, action: "radar_supporter" });
const db = getDbInstance();
const userBadges = db
.prepare("SELECT badge_id AS badgeId FROM user_badges WHERE api_key_id = ?")
.all(identity) as Array<{ badgeId: string }>;
const xpRows = db
.prepare("SELECT action FROM xp_audit_log WHERE api_key_id = ?")
.all(identity) as Array<{ action: string }>;
const scoreRows = db
.prepare("SELECT score FROM leaderboard WHERE api_key_id = ?")
.all(identity) as Array<{ score: number }>;
assert.deepEqual(userBadges, [{ badgeId: "radar-supporter" }]);
assert.deepEqual(xpRows, []);
assert.deepEqual(scoreRows, []);
});