mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 01:32:22 +03:00
feat(radar): sync signed supporter offers
This commit is contained in:
41
src/app/api/radar/offers/route.ts
Normal file
41
src/app/api/radar/offers/route.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/** GET the verified local Radar offers cache. Never proxies the private service. */
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { buildErrorBody, sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { getRadarOffers } 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(getRadarOffers(), {
|
||||
headers: { ...CORS_HEADERS, "Cache-Control": "no-store" },
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
buildErrorBody(500, sanitizeErrorMessage(error) || "Failed to load Radar offers"),
|
||||
{ status: 500, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
}
|
||||
55
src/app/api/radar/offers/sync/route.ts
Normal file
55
src/app/api/radar/offers/sync/route.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/** POST a server-side Radar offers 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 { syncRadarOffers } from "@/lib/radar/offersSync";
|
||||
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 syncRadarOffers(), { headers: CORS_HEADERS });
|
||||
} catch (error: unknown) {
|
||||
return NextResponse.json(
|
||||
buildErrorBody(500, sanitizeErrorMessage(error) || "Radar offers sync failed"),
|
||||
{ status: 500, headers: CORS_HEADERS }
|
||||
);
|
||||
}
|
||||
}
|
||||
11
src/lib/db/migrations/144_radar_offers_cache.sql
Normal file
11
src/lib/db/migrations/144_radar_offers_cache.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- 144_radar_offers_cache.sql
|
||||
-- Single-row cache for the separately signed, live-only Radar offers feed.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS radar_offers_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,
|
||||
fetched_at TEXT NOT NULL
|
||||
);
|
||||
@@ -17,6 +17,9 @@
|
||||
* - radar_local_model_state: operator-owned display/enabled overrides and
|
||||
* deletion tombstones, keyed by provider + model ID.
|
||||
*
|
||||
* Tables (migration 144):
|
||||
* - radar_offers_cache: single-row signed live offers feed cache.
|
||||
*
|
||||
* 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.
|
||||
@@ -51,6 +54,14 @@ export interface RadarReferralsCache {
|
||||
fetchedAt: string;
|
||||
}
|
||||
|
||||
export interface RadarOffersCache {
|
||||
version: string;
|
||||
tier: "live";
|
||||
payload: string;
|
||||
signature: string;
|
||||
fetchedAt: string;
|
||||
}
|
||||
|
||||
export interface RadarLocalModelState {
|
||||
provider: string;
|
||||
modelId: string;
|
||||
@@ -168,14 +179,16 @@ 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");
|
||||
|
||||
db.transaction(() => {
|
||||
updateKey.run(encrypted);
|
||||
// Both signed feeds are entitlement-sensitive. Clearing their cached
|
||||
// All signed feeds are entitlement-sensitive. Clearing their cached
|
||||
// variants forces the next sync/read to resolve the new key server-side
|
||||
// instead of serving data fetched under the previous entitlement.
|
||||
clearCatalogCache.run();
|
||||
clearReferralsCache.run();
|
||||
clearOffersCache.run();
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -226,6 +239,43 @@ export function setRadarReferralsCache(entry: {
|
||||
).run(entry.generatedAt, entry.tier, entry.payload, entry.signature, fetchedAt);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// radar_offers_cache
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function getRadarOffersCache(): RadarOffersCache | null {
|
||||
const row = getDbInstance()
|
||||
.prepare(
|
||||
"SELECT version, tier, payload, signature, fetched_at AS fetchedAt " +
|
||||
"FROM radar_offers_cache WHERE id = 1"
|
||||
)
|
||||
.get() as RadarOffersCache | undefined;
|
||||
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
export function setRadarOffersCache(entry: {
|
||||
version: string;
|
||||
tier: "live";
|
||||
payload: string;
|
||||
signature: string;
|
||||
fetchedAt?: string;
|
||||
}): void {
|
||||
const fetchedAt = entry.fetchedAt ?? new Date().toISOString();
|
||||
getDbInstance()
|
||||
.prepare(
|
||||
`INSERT INTO radar_offers_cache (id, version, tier, payload, signature, fetched_at)
|
||||
VALUES (1, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
version = excluded.version,
|
||||
tier = excluded.tier,
|
||||
payload = excluded.payload,
|
||||
signature = excluded.signature,
|
||||
fetched_at = excluded.fetched_at`
|
||||
)
|
||||
.run(entry.version, entry.tier, entry.payload, entry.signature, fetchedAt);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// radar_local_model_state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -820,6 +820,8 @@ export {
|
||||
setRadarKey,
|
||||
getRadarReferralsCache,
|
||||
setRadarReferralsCache,
|
||||
getRadarOffersCache,
|
||||
setRadarOffersCache,
|
||||
listRadarLocalModelState,
|
||||
setRadarLocalModelOverride,
|
||||
clearRadarLocalModelOverride,
|
||||
@@ -830,6 +832,7 @@ export type {
|
||||
RadarCache,
|
||||
RadarSettings,
|
||||
RadarReferralsCache,
|
||||
RadarOffersCache,
|
||||
RadarLocalModelState,
|
||||
RadarLocalModelOverridePatch,
|
||||
RadarLocalMergeState,
|
||||
|
||||
@@ -12,12 +12,18 @@
|
||||
import { FREE_MODEL_BUDGETS } from "@omniroute/open-sse/config/freeModelCatalog";
|
||||
import { RadarFeedSchema, type RadarFeed, type RadarReferral } from "./feedSchema";
|
||||
import { RadarReferralsFeedSchema, type RadarReferralsFeed } from "./referralsFeedSchema";
|
||||
import {
|
||||
filterActiveRadarOffers,
|
||||
RadarOffersFeedSchema,
|
||||
type RadarOffer,
|
||||
} from "./offersFeedSchema";
|
||||
import { applyFeed, type MergedEntry, type FeedModel } from "./applyFeed";
|
||||
import { findDefaultReferral } from "./referrals";
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import {
|
||||
getRadarCache,
|
||||
getRadarLocalMergeState,
|
||||
getRadarOffersCache,
|
||||
getRadarReferralsCache,
|
||||
type RadarLocalMergeState,
|
||||
} from "@/lib/db/radar";
|
||||
@@ -209,7 +215,54 @@ export function getDefaultReferralFor(
|
||||
return findDefaultReferral(fixed, provider);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getRadarOffers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface RadarOffersResult {
|
||||
offers: RadarOffer[];
|
||||
meta: { version: string; tier: "live"; fetchedAt: string } | null;
|
||||
}
|
||||
|
||||
export interface GetRadarOffersDeps {
|
||||
getFlag?: (key: string) => boolean;
|
||||
getCache?: () => {
|
||||
version: string;
|
||||
tier: string;
|
||||
payload: string;
|
||||
fetchedAt: string;
|
||||
} | null;
|
||||
now?: () => Date;
|
||||
}
|
||||
|
||||
const EMPTY_OFFERS: RadarOffersResult = { offers: [], meta: null };
|
||||
|
||||
/** Return only revalidated, unexpired offers from the local live cache. */
|
||||
export function getRadarOffers(deps: GetRadarOffersDeps = {}): RadarOffersResult {
|
||||
const {
|
||||
getFlag = isFeatureFlagEnabled,
|
||||
getCache: getCacheFn = getRadarOffersCache,
|
||||
now = () => new Date(),
|
||||
} = deps;
|
||||
if (!getFlag("RADAR_ENABLED")) return EMPTY_OFFERS;
|
||||
|
||||
const cache = getCacheFn();
|
||||
if (!cache || cache.tier !== "live") return EMPTY_OFFERS;
|
||||
|
||||
try {
|
||||
const feed = RadarOffersFeedSchema.parse(JSON.parse(cache.payload));
|
||||
if (feed.version !== cache.version || feed.tier !== "live") return EMPTY_OFFERS;
|
||||
return {
|
||||
offers: filterActiveRadarOffers(feed.offers, now()),
|
||||
meta: { version: cache.version, tier: "live", fetchedAt: cache.fetchedAt },
|
||||
};
|
||||
} catch {
|
||||
return EMPTY_OFFERS;
|
||||
}
|
||||
}
|
||||
|
||||
// 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";
|
||||
|
||||
147
src/lib/radar/offersFeedSchema.ts
Normal file
147
src/lib/radar/offersFeedSchema.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Closed client mirror of the private Radar offers feed contract.
|
||||
*
|
||||
* Keep this shape byte-compatible with `src/offers/schema.ts` in the private
|
||||
* server. The canonical fixture in `tests/fixtures/` pins that cross-repo
|
||||
* contract without embedding any real offer or partner data.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
const OFFER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,119}$/;
|
||||
const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,119}$/;
|
||||
|
||||
export const RadarOfferLocalizedTextSchema = z
|
||||
.object({
|
||||
en: z.string().min(1),
|
||||
pt: z.string().min(1).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type RadarOfferLocalizedText = z.infer<typeof RadarOfferLocalizedTextSchema>;
|
||||
|
||||
const HttpsUrlSchema = z
|
||||
.string()
|
||||
.url()
|
||||
.superRefine((value, ctx) => {
|
||||
const parsed = new URL(value);
|
||||
if (parsed.protocol !== "https:" || parsed.username || parsed.password) {
|
||||
ctx.addIssue({ code: "custom", message: "offer URL must be credential-free HTTPS" });
|
||||
}
|
||||
});
|
||||
|
||||
const PercentBenefitSchema = z
|
||||
.object({
|
||||
kind: z.literal("percent_off"),
|
||||
basisPoints: z.number().int().min(1).max(10_000),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const CreditBenefitSchema = z
|
||||
.object({
|
||||
kind: z.literal("credit"),
|
||||
amountMinor: z.number().int().positive(),
|
||||
currency: z.string().regex(/^[A-Z]{3}$/),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const TrialBenefitSchema = z
|
||||
.object({
|
||||
kind: z.literal("trial_days"),
|
||||
days: z.number().int().min(1).max(3_650),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const RadarOfferBenefitSchema = z.discriminatedUnion("kind", [
|
||||
PercentBenefitSchema,
|
||||
CreditBenefitSchema,
|
||||
TrialBenefitSchema,
|
||||
]);
|
||||
|
||||
export type RadarOfferBenefit = z.infer<typeof RadarOfferBenefitSchema>;
|
||||
|
||||
function isStrictlyBetter(benefit: RadarOfferBenefit, publicBenefit: RadarOfferBenefit): boolean {
|
||||
if (benefit.kind !== publicBenefit.kind) return false;
|
||||
if (benefit.kind === "percent_off" && publicBenefit.kind === "percent_off") {
|
||||
return benefit.basisPoints > publicBenefit.basisPoints;
|
||||
}
|
||||
if (benefit.kind === "trial_days" && publicBenefit.kind === "trial_days") {
|
||||
return benefit.days > publicBenefit.days;
|
||||
}
|
||||
if (benefit.kind === "credit" && publicBenefit.kind === "credit") {
|
||||
return (
|
||||
benefit.currency === publicBenefit.currency && benefit.amountMinor > publicBenefit.amountMinor
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export const RadarOfferSchema = z
|
||||
.object({
|
||||
id: z.string().regex(OFFER_ID_PATTERN),
|
||||
provider: z.string().regex(PROVIDER_ID_PATTERN),
|
||||
title: RadarOfferLocalizedTextSchema,
|
||||
description: RadarOfferLocalizedTextSchema,
|
||||
benefit: RadarOfferBenefitSchema,
|
||||
publicBenefit: RadarOfferBenefitSchema.nullable(),
|
||||
conditions: RadarOfferLocalizedTextSchema,
|
||||
validUntil: z.string().datetime().nullable(),
|
||||
url: HttpsUrlSchema,
|
||||
partner: z.boolean(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((offer, ctx) => {
|
||||
if (!offer.partner && offer.publicBenefit !== null) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["publicBenefit"],
|
||||
message: "official offer has no partner baseline",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
offer.partner &&
|
||||
(offer.publicBenefit === null || !isStrictlyBetter(offer.benefit, offer.publicBenefit))
|
||||
) {
|
||||
ctx.addIssue({
|
||||
code: "custom",
|
||||
path: ["publicBenefit"],
|
||||
message: "partner benefit must be strictly better than a comparable public benefit",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type RadarOffer = z.infer<typeof RadarOfferSchema>;
|
||||
|
||||
export const RadarOffersFeedSchema = z
|
||||
.object({
|
||||
feed: z.literal("omniroute-radar-offers"),
|
||||
schemaVersion: z.literal(1),
|
||||
version: z.string().regex(/^\d{4}\.\d{2}\.\d{2}\.\d+$/),
|
||||
generatedAt: z.string().datetime(),
|
||||
tier: z.literal("live"),
|
||||
count: z.number().int().nonnegative(),
|
||||
offers: z.array(RadarOfferSchema),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((feed, ctx) => {
|
||||
if (feed.count !== feed.offers.length) {
|
||||
ctx.addIssue({ code: "custom", path: ["count"], message: "offer count mismatch" });
|
||||
}
|
||||
});
|
||||
|
||||
export type RadarOffersFeed = z.infer<typeof RadarOffersFeedSchema>;
|
||||
|
||||
export function filterActiveRadarOffers(
|
||||
offers: readonly RadarOffer[],
|
||||
now: Date = new Date()
|
||||
): RadarOffer[] {
|
||||
const nowMs = now.getTime();
|
||||
return offers.filter(
|
||||
(offer) => offer.validUntil === null || Date.parse(offer.validUntil) > nowMs
|
||||
);
|
||||
}
|
||||
|
||||
export function localizeRadarOfferText(text: RadarOfferLocalizedText, locale: string): string {
|
||||
return locale.toLowerCase().startsWith("pt") && text.pt ? text.pt : text.en;
|
||||
}
|
||||
152
src/lib/radar/offersSync.ts
Normal file
152
src/lib/radar/offersSync.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Server-side sync for the separately signed, supporter-only Radar offers feed.
|
||||
* Every failure preserves the last verified local cache.
|
||||
*/
|
||||
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import { RadarOffersFeedSchema, type RadarOffersFeed } from "./offersFeedSchema";
|
||||
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 OffersSyncStatus =
|
||||
| { 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 RadarOffersCacheEntry {
|
||||
version: string;
|
||||
tier: "live";
|
||||
payload: string;
|
||||
signature: string;
|
||||
fetchedAt?: string;
|
||||
}
|
||||
|
||||
export interface OffersSyncDeps {
|
||||
fetch?: typeof globalThis.fetch;
|
||||
now?: () => Date;
|
||||
getFlag?: (key: string) => boolean;
|
||||
getSettings?: () => RadarSettingsSnapshot;
|
||||
getCache?: () => RadarOffersCacheEntry | null;
|
||||
setCache?: (entry: RadarOffersCacheEntry) => 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)));
|
||||
}
|
||||
|
||||
export async function syncRadarOffers(deps: OffersSyncDeps = {}): Promise<OffersSyncStatus> {
|
||||
const {
|
||||
fetch: fetchFn = globalThis.fetch,
|
||||
now = () => new Date(),
|
||||
getFlag = isFeatureFlagEnabled,
|
||||
getSettings: getSettingsFn,
|
||||
getCache: getCacheFn,
|
||||
setCache: setCacheFn,
|
||||
} = 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/offers/latest`, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${settings.supporterKey}` },
|
||||
signal: AbortSignal.timeout(SYNC_TIMEOUT_MS),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: "error",
|
||||
reason: `Offers 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: RadarOffersFeed;
|
||||
try {
|
||||
feed = RadarOffersFeedSchema.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")).getRadarOffersCache();
|
||||
if (existing && compareVersions(feed.version, existing.version) <= 0) {
|
||||
return { status: "stale" };
|
||||
}
|
||||
|
||||
const cacheEntry: RadarOffersCacheEntry = {
|
||||
version: feed.version,
|
||||
tier: "live",
|
||||
payload: rawBytes.toString("utf8"),
|
||||
signature,
|
||||
fetchedAt: now().toISOString(),
|
||||
};
|
||||
if (setCacheFn) {
|
||||
setCacheFn(cacheEntry);
|
||||
} else {
|
||||
(await import("@/lib/db/radar")).setRadarOffersCache(cacheEntry);
|
||||
}
|
||||
|
||||
return { status: "updated", version: feed.version };
|
||||
} catch (error: unknown) {
|
||||
const reason = (sanitizeErrorMessage(error) || "Radar offers sync failed").replace(
|
||||
/omr_[a-f0-9]{40}/gi,
|
||||
"[REDACTED]"
|
||||
);
|
||||
return { status: "error", reason };
|
||||
}
|
||||
}
|
||||
63
tests/fixtures/radar-offers-canonical.json
vendored
Normal file
63
tests/fixtures/radar-offers-canonical.json
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"feed": "omniroute-radar-offers",
|
||||
"schemaVersion": 1,
|
||||
"version": "2026.08.09.1",
|
||||
"generatedAt": "2026-08-09T12:00:00.000Z",
|
||||
"tier": "live",
|
||||
"count": 2,
|
||||
"offers": [
|
||||
{
|
||||
"id": "example-official-trial",
|
||||
"provider": "example",
|
||||
"title": {
|
||||
"en": "Official trial",
|
||||
"pt": "Teste oficial"
|
||||
},
|
||||
"description": {
|
||||
"en": "Canonical official-offer fixture",
|
||||
"pt": "Fixture canônico de oferta oficial"
|
||||
},
|
||||
"benefit": {
|
||||
"kind": "trial_days",
|
||||
"days": 14
|
||||
},
|
||||
"publicBenefit": null,
|
||||
"conditions": {
|
||||
"en": "Fixture only; not a real offer",
|
||||
"pt": "Somente fixture; não é uma oferta real"
|
||||
},
|
||||
"validUntil": "2099-12-31T23:59:59.000Z",
|
||||
"url": "https://provider.example/official-trial",
|
||||
"partner": false
|
||||
},
|
||||
{
|
||||
"id": "example-partner-credit",
|
||||
"provider": "example",
|
||||
"title": {
|
||||
"en": "Partner credit",
|
||||
"pt": "Crédito de parceiro"
|
||||
},
|
||||
"description": {
|
||||
"en": "Canonical partner-offer fixture",
|
||||
"pt": "Fixture canônico de oferta de parceiro"
|
||||
},
|
||||
"benefit": {
|
||||
"kind": "credit",
|
||||
"amountMinor": 1000,
|
||||
"currency": "USD"
|
||||
},
|
||||
"publicBenefit": {
|
||||
"kind": "credit",
|
||||
"amountMinor": 500,
|
||||
"currency": "USD"
|
||||
},
|
||||
"conditions": {
|
||||
"en": "Fixture only; not a real offer",
|
||||
"pt": "Somente fixture; não é uma oferta real"
|
||||
},
|
||||
"validUntil": null,
|
||||
"url": "https://provider.example/partner-credit",
|
||||
"partner": true
|
||||
}
|
||||
]
|
||||
}
|
||||
60
tests/unit/radar-offers-accessor.test.ts
Normal file
60
tests/unit/radar-offers-accessor.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
import { getRadarOffers } from "../../src/lib/radar/index.ts";
|
||||
|
||||
async function fixturePayload(): Promise<string> {
|
||||
return readFile(new URL("../fixtures/radar-offers-canonical.json", import.meta.url), "utf8");
|
||||
}
|
||||
|
||||
test("offers accessor short-circuits before cache when Radar is disabled", () => {
|
||||
let reads = 0;
|
||||
const result = getRadarOffers({
|
||||
getFlag: () => false,
|
||||
getCache: () => {
|
||||
reads += 1;
|
||||
throw new Error("cache must not be read");
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { offers: [], meta: null });
|
||||
assert.equal(reads, 0);
|
||||
});
|
||||
|
||||
test("offers accessor fails closed for missing, corrupt, or non-live cache", () => {
|
||||
for (const cache of [
|
||||
null,
|
||||
{ version: "x", tier: "live", payload: "not-json", fetchedAt: "now" },
|
||||
{ version: "x", tier: "community", payload: "{}", fetchedAt: "now" },
|
||||
]) {
|
||||
assert.deepEqual(getRadarOffers({ getFlag: () => true, getCache: () => cache }), {
|
||||
offers: [],
|
||||
meta: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("offers accessor revalidates the cache and removes expired entries", async () => {
|
||||
const payload = await fixturePayload();
|
||||
const result = getRadarOffers({
|
||||
getFlag: () => true,
|
||||
getCache: () => ({
|
||||
version: "2026.08.09.1",
|
||||
tier: "live",
|
||||
payload,
|
||||
fetchedAt: "2026-08-09T12:05:00.000Z",
|
||||
}),
|
||||
now: () => new Date("2100-01-01T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
result.offers.map(({ id }) => id),
|
||||
["example-partner-credit"]
|
||||
);
|
||||
assert.deepEqual(result.meta, {
|
||||
version: "2026.08.09.1",
|
||||
tier: "live",
|
||||
fetchedAt: "2026-08-09T12:05:00.000Z",
|
||||
});
|
||||
});
|
||||
71
tests/unit/radar-offers-contract.test.ts
Normal file
71
tests/unit/radar-offers-contract.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
RadarOfferSchema,
|
||||
RadarOffersFeedSchema,
|
||||
filterActiveRadarOffers,
|
||||
localizeRadarOfferText,
|
||||
} from "../../src/lib/radar/offersFeedSchema.ts";
|
||||
|
||||
const EXPECTED_FIXTURE_HASH = "f01a4c03a72adbffa944b4bcc8610ad2fec31dc500feaed18bdd9d1af4f06216";
|
||||
|
||||
async function canonicalFixture(): Promise<Buffer> {
|
||||
return readFile(new URL("../fixtures/radar-offers-canonical.json", import.meta.url));
|
||||
}
|
||||
|
||||
test("offers contract fixture is byte-identical to the private server contract", async () => {
|
||||
const bytes = await canonicalFixture();
|
||||
assert.equal(createHash("sha256").update(bytes).digest("hex"), EXPECTED_FIXTURE_HASH);
|
||||
|
||||
const feed = RadarOffersFeedSchema.parse(JSON.parse(bytes.toString("utf8")));
|
||||
assert.equal(feed.count, 2);
|
||||
assert.deepEqual(
|
||||
feed.offers.map(({ id, partner }) => ({ id, partner })),
|
||||
[
|
||||
{ id: "example-official-trial", partner: false },
|
||||
{ id: "example-partner-credit", partner: true },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test("partner offer must be strictly better than a comparable public benefit", async () => {
|
||||
const bytes = await canonicalFixture();
|
||||
const partner = RadarOffersFeedSchema.parse(JSON.parse(bytes.toString("utf8"))).offers[1]!;
|
||||
|
||||
assert.equal(
|
||||
RadarOfferSchema.safeParse({
|
||||
...partner,
|
||||
benefit: { kind: "credit", amountMinor: 500, currency: "USD" },
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
RadarOfferSchema.safeParse({
|
||||
...partner,
|
||||
publicBenefit: { kind: "trial_days", days: 30 },
|
||||
}).success,
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("active projection filters expired offers and localizes with English fallback", async () => {
|
||||
const bytes = await canonicalFixture();
|
||||
const feed = RadarOffersFeedSchema.parse(JSON.parse(bytes.toString("utf8")));
|
||||
const expired = {
|
||||
...feed.offers[0]!,
|
||||
id: "expired",
|
||||
validUntil: "2026-08-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
filterActiveRadarOffers([...feed.offers, expired], new Date("2026-08-09T12:00:00.000Z")).map(
|
||||
({ id }) => id
|
||||
),
|
||||
["example-official-trial", "example-partner-credit"]
|
||||
);
|
||||
assert.equal(localizeRadarOfferText({ en: "English", pt: "Português" }, "pt-BR"), "Português");
|
||||
assert.equal(localizeRadarOfferText({ en: "English" }, "de"), "English");
|
||||
});
|
||||
85
tests/unit/radar-offers-db.test.ts
Normal file
85
tests/unit/radar-offers-db.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
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-offers-db-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.STORAGE_ENCRYPTION_KEY = "test-encryption-key-for-radar-offers-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 });
|
||||
delete process.env.STORAGE_ENCRYPTION_KEY;
|
||||
});
|
||||
|
||||
test("Radar offers cache migration creates a single-row byte-preserving store", () => {
|
||||
const db = core.getDbInstance();
|
||||
assert.equal(radar.getRadarOffersCache(), null);
|
||||
|
||||
radar.setRadarOffersCache({
|
||||
version: "2026.08.09.1",
|
||||
tier: "live",
|
||||
payload: '{"byte":"exact"}\n',
|
||||
signature: "signed",
|
||||
fetchedAt: "2026-08-09T12:05:00.000Z",
|
||||
});
|
||||
radar.setRadarOffersCache({
|
||||
version: "2026.08.09.2",
|
||||
tier: "live",
|
||||
payload: '{"replacement":true}',
|
||||
signature: "signed-again",
|
||||
fetchedAt: "2026-08-09T12:10:00.000Z",
|
||||
});
|
||||
|
||||
assert.deepEqual(radar.getRadarOffersCache(), {
|
||||
version: "2026.08.09.2",
|
||||
tier: "live",
|
||||
payload: '{"replacement":true}',
|
||||
signature: "signed-again",
|
||||
fetchedAt: "2026-08-09T12:10:00.000Z",
|
||||
});
|
||||
const row = db.prepare("SELECT COUNT(*) AS count FROM radar_offers_cache").get() as {
|
||||
count: number;
|
||||
};
|
||||
assert.equal(row.count, 1);
|
||||
});
|
||||
|
||||
test("changing the supporter key atomically invalidates every entitlement-sensitive cache", () => {
|
||||
const db = core.getDbInstance();
|
||||
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.setRadarKey(`omr_${"a".repeat(40)}`);
|
||||
|
||||
assert.equal(radar.getRadarCache(), null);
|
||||
assert.equal(radar.getRadarReferralsCache(), null);
|
||||
assert.equal(radar.getRadarOffersCache(), null);
|
||||
const stored = db
|
||||
.prepare("SELECT supporter_key_encrypted AS key FROM radar_settings WHERE id = 1")
|
||||
.get() as { key: string };
|
||||
assert.ok(!stored.key.includes("omr_"), "supporter key must stay encrypted at rest");
|
||||
});
|
||||
120
tests/unit/radar-offers-routes.test.ts
Normal file
120
tests/unit/radar-offers-routes.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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-offers-routes-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.STORAGE_ENCRYPTION_KEY = "test-encryption-key-for-radar-offers-routes-32b!";
|
||||
process.env.JWT_SECRET = "test-jwt-secret-for-radar-offers-routes";
|
||||
process.env.INITIAL_PASSWORD = "test-bootstrap-password-for-radar-offers-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 resetStorage(): void {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function request(
|
||||
pathname: string,
|
||||
method: "GET" | "POST",
|
||||
headers: Record<string, string> = {},
|
||||
body?: unknown
|
||||
) {
|
||||
return new Request(`http://localhost:20128${pathname}`, {
|
||||
method,
|
||||
headers: { ...headers, ...(body === undefined ? {} : { "content-type": "application/json" }) },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
delete process.env.RADAR_ENABLED;
|
||||
delete process.env.STORAGE_ENCRYPTION_KEY;
|
||||
});
|
||||
|
||||
test("offers routes are inert before auth when the feature flag is off", async () => {
|
||||
resetStorage();
|
||||
delete process.env.RADAR_ENABLED;
|
||||
const { GET } = await import("../../src/app/api/radar/offers/route.ts");
|
||||
const { POST } = await import("../../src/app/api/radar/offers/sync/route.ts");
|
||||
|
||||
assert.equal((await GET(request("/api/radar/offers", "GET"))).status, 404);
|
||||
assert.equal((await POST(request("/api/radar/offers/sync", "POST"))).status, 404);
|
||||
});
|
||||
|
||||
test("offers routes require dashboard or management authentication", async () => {
|
||||
resetStorage();
|
||||
process.env.RADAR_ENABLED = "true";
|
||||
const { GET } = await import("../../src/app/api/radar/offers/route.ts");
|
||||
const { POST } = await import("../../src/app/api/radar/offers/sync/route.ts");
|
||||
|
||||
assert.equal((await GET(request("/api/radar/offers", "GET"))).status, 401);
|
||||
assert.equal((await POST(request("/api/radar/offers/sync", "POST"))).status, 401);
|
||||
});
|
||||
|
||||
test("GET offers returns only the local cache and never exposes supporter key material", async () => {
|
||||
resetStorage();
|
||||
process.env.RADAR_ENABLED = "true";
|
||||
const payload = fs.readFileSync(
|
||||
path.resolve(process.cwd(), "tests/fixtures/radar-offers-canonical.json"),
|
||||
"utf8"
|
||||
);
|
||||
radarDb.setRadarOffersCache({
|
||||
version: "2026.08.09.1",
|
||||
tier: "live",
|
||||
payload,
|
||||
signature: "fixture-signature",
|
||||
fetchedAt: "2026-08-09T12:05:00.000Z",
|
||||
});
|
||||
const { GET } = await import("../../src/app/api/radar/offers/route.ts");
|
||||
const response = await GET(request("/api/radar/offers", "GET", await authHeaders()));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.offers.length, 2);
|
||||
assert.equal(body.meta.tier, "live");
|
||||
assert.ok(!JSON.stringify(body).includes("omr_"));
|
||||
});
|
||||
|
||||
test("POST offers sync validates an empty body and gates a missing key without network", async () => {
|
||||
resetStorage();
|
||||
process.env.RADAR_ENABLED = "true";
|
||||
radarDb.setRadarOptIn(true);
|
||||
const { POST } = await import("../../src/app/api/radar/offers/sync/route.ts");
|
||||
|
||||
const invalid = await POST(
|
||||
request("/api/radar/offers/sync", "POST", await authHeaders(), { provider: "groq" })
|
||||
);
|
||||
assert.equal(invalid.status, 400);
|
||||
|
||||
const response = await POST(request("/api/radar/offers/sync", "POST", await authHeaders()));
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(await response.json(), { status: "no_key" });
|
||||
});
|
||||
|
||||
test("local offer routes never call the private server directly", () => {
|
||||
for (const file of [
|
||||
"src/app/api/radar/offers/route.ts",
|
||||
"src/app/api/radar/offers/sync/route.ts",
|
||||
]) {
|
||||
const source = fs.readFileSync(path.resolve(process.cwd(), file), "utf8");
|
||||
assert.ok(!/fetch\(/.test(source), `${file} must stay local-only`);
|
||||
}
|
||||
});
|
||||
185
tests/unit/radar-offers-sync.test.ts
Normal file
185
tests/unit/radar-offers-sync.test.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
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 offersSync = await import("../../src/lib/radar/offersSync.ts");
|
||||
|
||||
async function fixtureFeed(): Promise<Record<string, unknown>> {
|
||||
const bytes = await readFile(new URL("../fixtures/radar-offers-canonical.json", import.meta.url));
|
||||
return JSON.parse(bytes.toString("utf8")) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function liveSettings(supporterKey: string | null = `omr_${"a".repeat(40)}`) {
|
||||
return { optIn: true, supporterKey };
|
||||
}
|
||||
|
||||
test("offers sync gates flag, opt-in, and missing supporter key before fetch", async () => {
|
||||
for (const expected of ["disabled", "opt_out", "no_key"] as const) {
|
||||
let fetched = false;
|
||||
const result = await offersSync.syncRadarOffers({
|
||||
getFlag: () => expected !== "disabled",
|
||||
getSettings: () =>
|
||||
expected === "opt_out" ? { optIn: false, supporterKey: null } : liveSettings(null),
|
||||
fetch: (async () => {
|
||||
fetched = true;
|
||||
return response(Buffer.from("{}"));
|
||||
}) as typeof fetch,
|
||||
});
|
||||
assert.equal(result.status, expected);
|
||||
assert.equal(fetched, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("valid live offer feed sends Bearer server-side and caches exact signed bytes", async () => {
|
||||
const feed = await fixtureFeed();
|
||||
const bytes = Buffer.from(JSON.stringify(feed));
|
||||
const signature = sign(bytes);
|
||||
const writes: offersSync.RadarOffersCacheEntry[] = [];
|
||||
let requestUrl = "";
|
||||
let authorization = "";
|
||||
|
||||
const result = await offersSync.syncRadarOffers({
|
||||
getFlag: () => true,
|
||||
getSettings: () => liveSettings(),
|
||||
getCache: () => null,
|
||||
setCache: (entry) => writes.push(entry),
|
||||
fetch: (async (input, init) => {
|
||||
requestUrl = String(input);
|
||||
authorization = new Headers(init?.headers).get("authorization") ?? "";
|
||||
return response(bytes, {
|
||||
"x-omniroute-feed-signature": signature,
|
||||
"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(requestUrl, "https://radar.omniroute.online/v1/offers/latest");
|
||||
assert.equal(authorization, `Bearer omr_${"a".repeat(40)}`);
|
||||
assert.equal(writes[0]!.payload, bytes.toString("utf8"));
|
||||
assert.equal(writes[0]!.signature, signature);
|
||||
assert.equal(writes[0]!.tier, "live");
|
||||
});
|
||||
|
||||
test("signature, schema, and live-tier failures preserve the last good cache", async () => {
|
||||
const feed = await fixtureFeed();
|
||||
const validBytes = Buffer.from(JSON.stringify(feed));
|
||||
const cases: Array<{ expected: string; bytes: Buffer; signature: string; tier: string | null }> =
|
||||
[
|
||||
{ expected: "invalid_signature", bytes: validBytes, signature: "invalid", tier: "live" },
|
||||
{
|
||||
expected: "invalid_schema",
|
||||
bytes: Buffer.from('{"feed":"wrong"}'),
|
||||
signature: "valid-for-case",
|
||||
tier: "live",
|
||||
},
|
||||
{ expected: "wrong_tier", bytes: validBytes, signature: "valid-for-case", tier: null },
|
||||
{ expected: "wrong_tier", bytes: validBytes, signature: "valid-for-case", tier: "community" },
|
||||
];
|
||||
|
||||
for (const item of cases) {
|
||||
item.signature = item.expected === "invalid_signature" ? item.signature : sign(item.bytes);
|
||||
let written = false;
|
||||
const result = await offersSync.syncRadarOffers({
|
||||
getFlag: () => true,
|
||||
getSettings: () => liveSettings(),
|
||||
getCache: () => ({
|
||||
version: "2026.08.08.1",
|
||||
tier: "live",
|
||||
payload: "last-good",
|
||||
signature: "old",
|
||||
}),
|
||||
setCache: () => {
|
||||
written = true;
|
||||
},
|
||||
fetch: (async () =>
|
||||
response(item.bytes, {
|
||||
"x-omniroute-feed-signature": item.signature,
|
||||
...(item.tier ? { "x-omniroute-feed-tier": item.tier } : {}),
|
||||
})) as typeof fetch,
|
||||
});
|
||||
assert.equal(result.status, item.expected);
|
||||
assert.equal(written, false);
|
||||
}
|
||||
});
|
||||
|
||||
test("same or older signed offer versions are rejected as stale", async () => {
|
||||
const feed = await fixtureFeed();
|
||||
const bytes = Buffer.from(JSON.stringify(feed));
|
||||
let written = false;
|
||||
const result = await offersSync.syncRadarOffers({
|
||||
getFlag: () => true,
|
||||
getSettings: () => liveSettings(),
|
||||
getCache: () => ({
|
||||
version: "2026.08.09.1",
|
||||
tier: "live",
|
||||
payload: "last-good",
|
||||
signature: "old",
|
||||
}),
|
||||
setCache: () => {
|
||||
written = true;
|
||||
},
|
||||
fetch: (async () =>
|
||||
response(bytes, {
|
||||
"x-omniroute-feed-signature": sign(bytes),
|
||||
"x-omniroute-feed-tier": "live",
|
||||
})) as typeof fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "stale");
|
||||
assert.equal(written, false);
|
||||
});
|
||||
|
||||
test("oversized and sanitized network failures never overwrite the cache or leak the key", async () => {
|
||||
let written = false;
|
||||
const tooLarge = await offersSync.syncRadarOffers({
|
||||
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(tooLarge.status, "too_large");
|
||||
|
||||
const secret = `omr_${"b".repeat(40)}`;
|
||||
const failed = await offersSync.syncRadarOffers({
|
||||
getFlag: () => true,
|
||||
getSettings: () => liveSettings(secret),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
written = true;
|
||||
},
|
||||
fetch: (async () => {
|
||||
throw new Error(`upstream failed for ${secret}\n at /private/path.ts:1:1`);
|
||||
}) as typeof fetch,
|
||||
});
|
||||
assert.equal(failed.status, "error");
|
||||
assert.ok(!("reason" in failed) || !failed.reason.includes(secret));
|
||||
assert.ok(!("reason" in failed) || !failed.reason.includes("/private/path"));
|
||||
assert.equal(written, false);
|
||||
});
|
||||
Reference in New Issue
Block a user