mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 09:42:15 +03:00
feat(radar): sync signed Intel insights
This commit is contained in:
42
src/app/api/radar/intel/route.ts
Normal file
42
src/app/api/radar/intel/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
56
src/app/api/radar/intel/sync/route.ts
Normal file
56
src/app/api/radar/intel/sync/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
70
src/app/api/radar/status/route.ts
Normal file
70
src/app/api/radar/status/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
47
src/app/api/radar/sync-all/route.ts
Normal file
47
src/app/api/radar/sync-all/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
11
src/lib/db/migrations/145_radar_intel_cache.sql
Normal file
11
src/lib/db/migrations/145_radar_intel_cache.sql
Normal 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'))
|
||||
);
|
||||
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
73
src/lib/radar/intelFeedSchema.ts
Normal file
73
src/lib/radar/intelFeedSchema.ts
Normal 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
173
src/lib/radar/intelSync.ts
Normal 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;
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
42
tests/fixtures/radar-intel-canonical.json
vendored
Normal file
42
tests/fixtures/radar-intel-canonical.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
74
tests/unit/radar-intel-db.test.ts
Normal file
74
tests/unit/radar-intel-db.test.ts
Normal 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);
|
||||
});
|
||||
107
tests/unit/radar-intel-routes.test.ts
Normal file
107
tests/unit/radar-intel-routes.test.ts
Normal 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" },
|
||||
});
|
||||
});
|
||||
210
tests/unit/radar-intel-sync.test.ts
Normal file
210
tests/unit/radar-intel-sync.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user