mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 16:22:19 +03:00
Compare commits
5 Commits
feat/9544-
...
feat/radar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
286a5ce283 | ||
|
|
ffa5105a74 | ||
|
|
aa701e14b4 | ||
|
|
0baf08e002 | ||
|
|
5a54a733c1 |
@@ -1 +0,0 @@
|
||||
- feat(providers): add Muse Code CLI provider preset (#9544)
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"open-sse/handlers/search.ts": {
|
||||
"@typescript-eslint/no-explicit-any": {
|
||||
"count": 33
|
||||
"count": 34
|
||||
}
|
||||
},
|
||||
"open-sse/handlers/sseParser.ts": {
|
||||
|
||||
@@ -299,84 +299,36 @@ auth state — only the masked form and a `hasSupporterKey` boolean.
|
||||
|
||||
## Referral links (free credits)
|
||||
|
||||
Referral links are served from a **standalone, always-current** feed —
|
||||
`GET /v1/referrals/latest` — separate from the catalog feed. This is deliberate: the
|
||||
catalog feed on the community tier is a snapshot that can be up to 30 days old, so a
|
||||
referral link extracted from it used to lag the server's real link list by the same
|
||||
amount (a newly-added referral wouldn't reach a free/community user for up to a month).
|
||||
The referrals feed removes that delay by syncing on its own, much shorter cadence.
|
||||
The server-published feed carries a `referrals` section (server-side D28 work, already
|
||||
in production — this section documents the **client** consumption only):
|
||||
|
||||
```ts
|
||||
// GET /v1/referrals/latest response body (Ed25519-signed, same pinned key as
|
||||
// the catalog feed):
|
||||
{
|
||||
feed: "omniroute-radar-referrals",
|
||||
schemaVersion: 1,
|
||||
generatedAt: string, // ISO — deterministic: max(updatedAt) across referral
|
||||
// links, so two identical requests produce the exact
|
||||
// same signed bytes/signature
|
||||
referrals: {
|
||||
fixed: RadarReferral[], // present in EVERY tier, including no-auth/community
|
||||
campaigns: RadarReferral[], // only populated for a valid live (supporter) Bearer
|
||||
// key; no-auth/expired-key requests get []
|
||||
},
|
||||
referrals: {
|
||||
fixed: RadarReferral[], // present in EVERY tier, including community
|
||||
campaigns: RadarReferral[], // only populated on the live (supporter) tier;
|
||||
// the community artifact always publishes []
|
||||
}
|
||||
// RadarReferral = { provider, url, kind: "fixo" | "campanha", validUntil,
|
||||
// requiredAction, isDefault }
|
||||
```
|
||||
|
||||
Unlike the catalog feed, this body carries no `tier` field at all — the server decides
|
||||
what to include per-request based on the `Authorization` key, so the
|
||||
`x-omniroute-feed-tier` response header is the ONLY source for the served tier
|
||||
(`referralsSync.ts::syncRadarReferrals`); an absent/unrecognized header degrades to
|
||||
`"community"`, the least-privileged assumption. `RadarReferralsFeedSchema`
|
||||
(`src/lib/radar/referralsFeedSchema.ts`) validates the whole body, reusing the same
|
||||
per-referral `RadarReferralSchema` exported from `feedSchema.ts` so both feeds validate
|
||||
individual referrals identically. Every `RadarReferral.url` must be `https://` — a
|
||||
`http://` url fails schema validation.
|
||||
|
||||
The OLD catalog-embedded `referrals` field on `RadarFeedSchema` (`feedSchema.ts`) is
|
||||
kept for backward-compat with already-cached catalog feeds, but `getRadarReferrals()`
|
||||
no longer reads it — see [Accessor](#accessor) below.
|
||||
|
||||
### Sync
|
||||
|
||||
`syncRadarReferrals()` (`src/lib/radar/referralsSync.ts`) is the ONLY module that
|
||||
touches the network for referrals, mirroring `syncRadar()`'s contract exactly: flag off
|
||||
→ `disabled`; opt-in false → `opt_out`; downloads `${RADAR_FEED_URL}/v1/referrals/latest`
|
||||
(same `RADAR_FEED_URL`/`RADAR_FEED_PUBKEY` fork overrides as the catalog), verifies the
|
||||
Ed25519 signature over the exact response bytes (`verifyFeedBytes`), validates against
|
||||
`RadarReferralsFeedSchema`, and caches into the `radar_referrals_cache` table
|
||||
(migration `142_radar_referrals_cache.sql`) — a table entirely separate from the
|
||||
catalog's `radar_feed_cache`. A 10 MB response cap and a `generatedAt` floor (an
|
||||
incoming feed with a `generatedAt` no newer than the cached one is treated as `stale`
|
||||
and never overwrites the cache — guards against a replay of an older signed artifact)
|
||||
mirror the catalog sync's own `MAX_FEED_BYTES`/version-floor guards. Never throws —
|
||||
always returns a status object; errors never carry a stack trace in `reason`.
|
||||
|
||||
Two triggers keep the referrals cache warm, both independent of the catalog's own
|
||||
24h cadence:
|
||||
|
||||
- **Sync-on-read** — `GET /api/radar/referrals` itself calls `syncRadarReferrals()`
|
||||
inline whenever the cache is missing or older than `REFERRALS_STALE_MS` (1h,
|
||||
`shouldSyncReferralsOnRead()`), before serving the response. This is what makes fixed
|
||||
links "always current" for the very next dashboard load, without waiting on any
|
||||
background timer.
|
||||
- **Scheduler side-sync** — `radarSchedulerTick()` (`scheduler.ts`) independently
|
||||
evaluates referrals staleness on the same hourly tick used for the catalog, calling
|
||||
`syncRadarReferrals()` when due. This runs regardless of whether the catalog itself
|
||||
was due that tick, and never affects `RadarTickResult`'s shape (best-effort side
|
||||
effect only, swallowed on error).
|
||||
The client never decides which tier it received or which referrals belong in which
|
||||
tier — the server already publishes two artifacts (`live`/`community`) with
|
||||
`campaigns` gated server-side, same principle as the [tiers](#tiers-community-and-live)
|
||||
section above. `RadarFeedSchema` (`src/lib/radar/feedSchema.ts`) validates `referrals`
|
||||
as a whole-object `.default({fixed:[],campaigns:[]})`, and `campaigns` defaults
|
||||
independently inside it — so a feed cached before this section existed on the server
|
||||
still parses cleanly, and `campaigns` alone can also be absent without failing
|
||||
validation. Every `RadarReferral.url` must be `https://` — a `http://` url fails
|
||||
schema validation.
|
||||
|
||||
### Accessor
|
||||
|
||||
`src/lib/radar/index.ts` exports two read-only accessors, both never throwing (same
|
||||
defensive contract as `getRadarCatalog()` — flag off, no cache, or a corrupt cached
|
||||
defensive contract as `getRadarCatalog()` — flag off, no cache, or a corrupt/old cached
|
||||
payload all resolve to the empty shape instead of an error):
|
||||
|
||||
- `getRadarReferrals()` → `{ fixed: RadarReferral[], campaigns: RadarReferral[] }`,
|
||||
reading from `radar_referrals_cache` (via `getRadarReferralsCache()`) and validating
|
||||
through `RadarReferralsFeedSchema` — **not** the catalog cache.
|
||||
- `getRadarReferrals()` → `{ fixed: RadarReferral[], campaigns: RadarReferral[] }`.
|
||||
- `getDefaultReferralFor(provider)` → the `fixed` referral with `isDefault: true` for
|
||||
that provider, or `null`. Only looks at `fixed` — a campaign is never used as a
|
||||
provider's "default" link.
|
||||
@@ -391,13 +343,11 @@ server-only; the providers dashboard imports `referrals.ts` directly instead of
|
||||
### `GET /api/radar/referrals`
|
||||
|
||||
Follows the exact same gate order as every other Radar route: `RADAR_ENABLED` off →
|
||||
`404` (checked first, byte-identical inertia); unauthenticated → `401`; otherwise
|
||||
triggers a sync-on-read (see above) when stale, then `200` with
|
||||
`{ fixed, campaigns, tier }` — `tier` comes straight from the (possibly just-refreshed)
|
||||
cache row and is purely informative (drives the UI's soft upsell copy below). Never
|
||||
proxies the feed server directly — the route's own source contains no `fetch(` call;
|
||||
the network only ever happens inside `syncRadarReferrals()`, same local-cache-only
|
||||
principle as `/api/radar/catalog`.
|
||||
`404` (checked first, byte-identical inertia); unauthenticated → `401`; otherwise `200`
|
||||
with `{ fixed, campaigns, tier }` — `tier` comes straight from the cache row and is
|
||||
purely informative (drives the UI's soft upsell copy below), the route does no
|
||||
gating of its own. Never proxies the feed server — same local-cache-only contract as
|
||||
`/api/radar/catalog`.
|
||||
|
||||
### Dashboard UI — "Free credits" tab on `/dashboard/radar`
|
||||
|
||||
@@ -465,16 +415,6 @@ automatically (`getFeedPublicKeys()` in `src/lib/radar/pinnedKeys.ts`), and vers
|
||||
comparison, schema validation, and the merge rules apply identically to a self-hosted
|
||||
feed.
|
||||
|
||||
Referral links (see [Referral links (free credits)](#referral-links-free-credits)
|
||||
above) are a separate, optional artifact: a fork that only serves `/v1/catalog/latest`
|
||||
still works fully — `syncRadarReferrals()` degrades to `{ status: "error" }` on a `404`
|
||||
from `/v1/referrals/latest` and the cache simply stays empty, so
|
||||
`GET /api/radar/referrals` keeps returning `{ fixed: [], campaigns: [], tier: null }`
|
||||
instead of failing the rest of the page. To also offer referral links, serve
|
||||
`GET /v1/referrals/latest` satisfying `RadarReferralsFeedSchema`
|
||||
(`src/lib/radar/referralsFeedSchema.ts`) and sign it with the same Ed25519 key pair as
|
||||
the catalog feed.
|
||||
|
||||
---
|
||||
|
||||
## Related docs
|
||||
|
||||
@@ -225,7 +225,6 @@ import { digitaloceanProvider } from "./registry/digitalocean/index.ts";
|
||||
import { hcnsecProvider } from "./registry/hcnsec/index.ts";
|
||||
import { promptqlProvider } from "./registry/promptql/index.ts";
|
||||
import { hyperagentProvider } from "./registry/hyperagent/index.ts";
|
||||
import { muse_codeProvider } from "./registry/muse-code/index.ts";
|
||||
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
aimlapi: aimlapiProvider,
|
||||
@@ -452,6 +451,5 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
hcnsec: hcnsecProvider,
|
||||
promptql: promptqlProvider,
|
||||
hyperagent: hyperagentProvider,
|
||||
"muse-code": muse_codeProvider,
|
||||
unorouter: unorouterProvider,
|
||||
};
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
import { buildOpenAiCompatibleRegistryEntry } from "../../shared.ts";
|
||||
|
||||
/**
|
||||
* Muse Code CLI — Meta's agentic coding tool.
|
||||
*
|
||||
* Wire format: OpenAI Responses API (POST /responses).
|
||||
* Auth: Bearer token from META_API_KEY env var.
|
||||
* Reasoning efforts: xhigh/ultra -> high (handled generically).
|
||||
*
|
||||
* @see https://github.com/joymadhu49/muse-openrouter-shim
|
||||
*/
|
||||
export const muse_codeProvider: RegistryEntry = buildOpenAiCompatibleRegistryEntry({
|
||||
id: "muse-code",
|
||||
alias: "mc",
|
||||
passthroughModels: true,
|
||||
defaultContextLength: 200000,
|
||||
models: [
|
||||
{
|
||||
id: "llama-4-maverick",
|
||||
name: "Llama 4 Maverick",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 131072,
|
||||
supportsReasoning: true,
|
||||
supportsXHighEffort: true,
|
||||
toolCalling: true,
|
||||
supportsVision: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs", "logitBias"],
|
||||
},
|
||||
{
|
||||
id: "llama-4-scout",
|
||||
name: "Llama 4 Scout",
|
||||
contextLength: 1048576,
|
||||
maxOutputTokens: 131072,
|
||||
supportsReasoning: true,
|
||||
supportsXHighEffort: true,
|
||||
toolCalling: true,
|
||||
supportsVision: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs", "logitBias"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.3-70b",
|
||||
name: "Llama 3.3 70B",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.1-405b",
|
||||
name: "Llama 3.1 405B",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.1-70b",
|
||||
name: "Llama 3.1 70B",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.1-8b",
|
||||
name: "Llama 3.1 8B",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.2-90b-vision",
|
||||
name: "Llama 3.2 90B Vision",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
supportsVision: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "llama-3.2-11b-vision",
|
||||
name: "Llama 3.2 11B Vision",
|
||||
contextLength: 131072,
|
||||
maxOutputTokens: 32768,
|
||||
supportsReasoning: false,
|
||||
toolCalling: true,
|
||||
supportsVision: true,
|
||||
targetFormat: "openai-responses",
|
||||
unsupportedParams: ["logprobs", "topLogprobs"],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -309,7 +309,6 @@ export function stripStoredItemReferences(body: Record<string, unknown>): void {
|
||||
|
||||
function stripOrphanedCodexFunctionCallOutputs(body: Record<string, unknown>): void {
|
||||
if (!Array.isArray(body.input)) return;
|
||||
const input = body.input;
|
||||
// A previous_response_id delegates history resolution to the upstream
|
||||
// Responses service, so a matching function_call may legitimately live in
|
||||
// that remote response rather than in the local input array.
|
||||
@@ -318,7 +317,7 @@ function stripOrphanedCodexFunctionCallOutputs(body: Record<string, unknown>): v
|
||||
const callIds = new Set<string>();
|
||||
let outputCount = 0;
|
||||
|
||||
for (const item of input) {
|
||||
for (const item of body.input) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||
const record = item as Record<string, unknown>;
|
||||
|
||||
@@ -342,7 +341,9 @@ function stripOrphanedCodexFunctionCallOutputs(body: Record<string, unknown>): v
|
||||
}
|
||||
|
||||
if (outputCount === 0) return;
|
||||
const filteredInput = input.filter((item) => {
|
||||
|
||||
const before = body.input.length;
|
||||
body.input = body.input.filter((item) => {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) return true;
|
||||
const record = item as Record<string, unknown>;
|
||||
if (record.type === "function_call_output" && typeof record.call_id === "string") {
|
||||
@@ -351,8 +352,7 @@ function stripOrphanedCodexFunctionCallOutputs(body: Record<string, unknown>): v
|
||||
return true;
|
||||
});
|
||||
|
||||
const removedCount = input.length - filteredInput.length;
|
||||
body.input = filteredInput;
|
||||
const removedCount = before - body.input.length;
|
||||
if (removedCount > 0) {
|
||||
console.debug(
|
||||
`[Codex] stripOrphanedCodexFunctionCallOutputs: removed ${removedCount} orphaned function_call_output item(s)`
|
||||
|
||||
@@ -61,7 +61,7 @@ type KiroStreamState = {
|
||||
contextUsagePercentage?: number;
|
||||
hasContextUsage?: boolean;
|
||||
hasMeteringEvent?: boolean;
|
||||
usage?: Partial<UsageSummary>;
|
||||
usage?: UsageSummary;
|
||||
hasReasoningContent?: boolean;
|
||||
reasoningChunkCount?: number;
|
||||
// Inline-thinking splitter state (populated only when thinkingExpected=true).
|
||||
@@ -185,7 +185,8 @@ function resolveKiroMaxInputTokens(model: string): number {
|
||||
* inflate `total_tokens`.
|
||||
*/
|
||||
function ensureKiroUsage(state: KiroStreamState, model: string) {
|
||||
if (state.usage?.total_tokens !== undefined) return;
|
||||
if (state.usage) return;
|
||||
|
||||
const estimatedOutputTokens =
|
||||
state.totalContentLength && state.totalContentLength > 0
|
||||
? Math.max(1, Math.floor(state.totalContentLength / 4))
|
||||
@@ -197,11 +198,11 @@ function ensureKiroUsage(state: KiroStreamState, model: string) {
|
||||
: 0;
|
||||
|
||||
if (estimatedTotalTokens <= 0 && estimatedOutputTokens <= 0) return;
|
||||
|
||||
// Without a percentage there is no total to split, so the output estimate is
|
||||
// all that is known and stands on its own.
|
||||
if (estimatedTotalTokens <= 0) {
|
||||
state.usage = {
|
||||
...state.usage,
|
||||
prompt_tokens: 0,
|
||||
completion_tokens: estimatedOutputTokens,
|
||||
total_tokens: estimatedOutputTokens,
|
||||
@@ -212,7 +213,6 @@ function ensureKiroUsage(state: KiroStreamState, model: string) {
|
||||
const promptTokens = Math.max(0, estimatedTotalTokens - estimatedOutputTokens);
|
||||
|
||||
state.usage = {
|
||||
...state.usage,
|
||||
prompt_tokens: promptTokens,
|
||||
completion_tokens: estimatedOutputTokens,
|
||||
total_tokens: promptTokens + estimatedOutputTokens,
|
||||
|
||||
@@ -199,10 +199,10 @@ describe("TierResolver", () => {
|
||||
]);
|
||||
// Observable effect of the cache: the duplicate resolves to the same tier and only
|
||||
// ONE entry is memoized (getTierStats counts cache entries, not classify calls).
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results[0].tier).toBe(results[1].tier);
|
||||
assert.equal(results.length, 2);
|
||||
assert.equal(results[0].tier, results[1].tier);
|
||||
const stats = getTierStats();
|
||||
expect(stats.free + stats.cheap + stats.premium).toBe(1);
|
||||
assert.equal(stats.free + stats.cheap + stats.premium, 1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -372,9 +372,7 @@ export function translateRequest(
|
||||
preserveReasoningContent: isReasoner,
|
||||
// Per-provider/model preserveVideoUrl flag from compat overrides.
|
||||
// Falls back to true for moonshot/kimi when unset (legacy behavior).
|
||||
preserveVideoUrl:
|
||||
getModelPreserveVideoUrl(normalizedProvider, normalizedModel) ??
|
||||
(normalizedProvider === "moonshot" || normalizedProvider === "kimi"),
|
||||
preserveVideoUrl: getModelPreserveVideoUrl(normalizedProvider, options?.model ?? "") ?? (normalizedProvider === "moonshot" || normalizedProvider === "kimi"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
60
package-lock.json
generated
60
package-lock.json
generated
@@ -27,11 +27,12 @@
|
||||
"@xyflow/react": "^12.11.1",
|
||||
"axios": "^1.16.1",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^13.0.2",
|
||||
"bottleneck": "^2.19.5",
|
||||
"clsx": "^2.1.1",
|
||||
"commander": "^15.0.0",
|
||||
"csv-stringify": "^6.7.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "^3.4.12",
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.3",
|
||||
"fflate": "^0.8.3",
|
||||
@@ -103,7 +104,7 @@
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/bun": "latest",
|
||||
"@types/bun": "*",
|
||||
"@types/node": "^26.1.0",
|
||||
"@types/react": "^19.2.15",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
@@ -462,9 +463,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/json-schema-ref-parser/node_modules/js-yaml": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -3075,9 +3076,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/js-yaml": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -12639,9 +12640,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@yarnpkg/parsers/node_modules/js-yaml": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -16979,9 +16980,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
||||
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
||||
"version": "3.4.12",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
|
||||
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
@@ -25196,9 +25197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lockfile-lint/node_modules/js-yaml": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -26005,9 +26006,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mermaid": {
|
||||
"version": "11.16.1",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.1.tgz",
|
||||
"integrity": "sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==",
|
||||
"version": "11.16.0",
|
||||
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz",
|
||||
"integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "^7.1.2",
|
||||
@@ -27528,9 +27529,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -36608,9 +36609,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder2/node_modules/js-yaml": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -36950,7 +36951,12 @@
|
||||
},
|
||||
"open-sse": {
|
||||
"name": "@omniroute/open-sse",
|
||||
"version": "3.8.50"
|
||||
"version": "3.8.50",
|
||||
"dependencies": {
|
||||
"@toon-format/toon": "^4.1.0",
|
||||
"safe-regex": "^2.1.1",
|
||||
"smol-toml": "1.7.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
package.json
25
package.json
@@ -266,7 +266,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"commander": "^15.0.0",
|
||||
"csv-stringify": "^6.7.0",
|
||||
"dompurify": "^3.4.13",
|
||||
"dompurify": "^3.4.12",
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.3",
|
||||
"fflate": "^0.8.3",
|
||||
@@ -415,6 +415,7 @@
|
||||
"unrs-resolver": true
|
||||
},
|
||||
"overrides": {
|
||||
"dompurify": "^3.4.12",
|
||||
"fast-xml-parser": "^5.10.1",
|
||||
"sharp": "^0.35.0",
|
||||
"postcss": "^8.5.18",
|
||||
@@ -430,7 +431,7 @@
|
||||
"fast-uri": "^3.1.5",
|
||||
"body-parser": "^2.3.0",
|
||||
"@yarnpkg/parsers": {
|
||||
"js-yaml": "^4.3.1"
|
||||
"js-yaml": "^4.2.0"
|
||||
},
|
||||
"jsdom": {
|
||||
"undici": "^7.29.0"
|
||||
@@ -445,27 +446,11 @@
|
||||
"promptfoo": {
|
||||
"js-yaml": "^5.2.2",
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"js-yaml": "^4.3.1"
|
||||
"js-yaml": "^4.2.0"
|
||||
},
|
||||
"undici": "^7.29.0"
|
||||
},
|
||||
"socket.io-parser": "^4.2.7",
|
||||
"tar": "^7.5.21",
|
||||
"nanoid": "^3.3.17",
|
||||
"@eslint/eslintrc": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"lockfile-lint": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"xmlbuilder2": {
|
||||
"js-yaml": "^4.3.1"
|
||||
},
|
||||
"monaco-editor": {
|
||||
"dompurify": "^3.4.13"
|
||||
},
|
||||
"@apidevtools/json-schema-ref-parser": {
|
||||
"js-yaml": "^4.3.1"
|
||||
}
|
||||
"tar": "^7.5.21"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
/**
|
||||
* GET /api/radar/referrals — return the referral links section ("Pegue seus
|
||||
* créditos grátis", D28) from the locally cached Radar REFERRALS feed
|
||||
* (`GET /v1/referrals/latest` — a separate, always-current artifact from the
|
||||
* catalog feed; see `src/lib/radar/referralsSync.ts`).
|
||||
* créditos grátis", D28) of the locally cached Radar feed.
|
||||
*
|
||||
* NEVER proxies the private feed server directly — the browser only ever
|
||||
* talks to this local endpoint. Unlike the catalog (whose sync is entirely
|
||||
* client-triggered via `POST /api/radar/sync`), THIS route also triggers a
|
||||
* sync itself, inline, whenever the cached referrals are stale or missing
|
||||
* (`shouldSyncReferralsOnRead`, 1h window) — the whole point of the
|
||||
* standalone referrals feed is that fixed links show up promptly instead of
|
||||
* inheriting the catalog's up-to-30-day community-tier snapshot delay. The
|
||||
* network call still only ever happens inside `syncRadarReferrals()`
|
||||
* (`referralsSync.ts`) — this route itself never talks to the upstream feed
|
||||
* server directly.
|
||||
* NEVER proxies the private feed server. Like GET /api/radar/catalog, the
|
||||
* browser talks only to this local endpoint; sync happens server-side via
|
||||
* POST /api/radar/sync, and this route only reads the cache that sync
|
||||
* already wrote.
|
||||
*
|
||||
* `fixed` referrals are present in every tier (community included, gated
|
||||
* server-side); `campaigns` only comes populated on the `live` (supporter)
|
||||
@@ -32,8 +24,7 @@ import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import { isAuthenticated } from "@/shared/utils/apiAuth";
|
||||
import { getRadarReferrals } from "@/lib/radar";
|
||||
import { getRadarReferralsCache } from "@/lib/db/radar";
|
||||
import { syncRadarReferrals, shouldSyncReferralsOnRead } from "@/lib/radar/referralsSync";
|
||||
import { getRadarCache } from "@/lib/db/radar";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -60,18 +51,8 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Sync-on-read: refresh the cache inline when it is stale or missing.
|
||||
// `syncRadarReferrals()` self-gates on flag/opt-in and never throws, so
|
||||
// this is safe to await unconditionally — a disabled/opted-out operator
|
||||
// just gets an instant no-op here and falls through to serving whatever
|
||||
// (possibly empty) cache already exists.
|
||||
const existingCache = getRadarReferralsCache();
|
||||
if (shouldSyncReferralsOnRead(existingCache?.fetchedAt ?? null, Date.now())) {
|
||||
await syncRadarReferrals();
|
||||
}
|
||||
|
||||
const { fixed, campaigns } = getRadarReferrals();
|
||||
const cache = getRadarReferralsCache();
|
||||
const cache = getRadarCache();
|
||||
return NextResponse.json(
|
||||
{ fixed, campaigns, tier: cache?.tier ?? null },
|
||||
{ headers: { ...CORS_HEADERS, "Cache-Control": "no-store" } },
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Muse Code CLI proprietary model catalog endpoint.
|
||||
*
|
||||
* Muse CLI calls GET /muse-code/models (or --base-url/muse-code/models)
|
||||
* to discover available models. Returns the proprietary Muse format:
|
||||
*
|
||||
* { object: "list", data: [{ id, object, created, owned_by, metadata }] }
|
||||
*
|
||||
* Each model's metadata includes: name, family, reasoning, tool_call,
|
||||
* modalities, limit, cost.
|
||||
*/
|
||||
|
||||
import { muse_codeProvider } from "@omniroute/open-sse/config/providers/registry/muse-code/index.ts";
|
||||
|
||||
const MUSECODE_TIMESTAMP = Math.floor(Date.now() / 1000);
|
||||
|
||||
interface MuseCodeModel {
|
||||
id: string;
|
||||
object: "model";
|
||||
created: number;
|
||||
owned_by: string;
|
||||
metadata: {
|
||||
name: string;
|
||||
family: string;
|
||||
reasoning: boolean;
|
||||
tool_call: boolean;
|
||||
modalities: string[];
|
||||
limit: number;
|
||||
cost: number;
|
||||
};
|
||||
}
|
||||
|
||||
function buildModelCatalog(): MuseCodeModel[] {
|
||||
const data: MuseCodeModel[] = [];
|
||||
|
||||
for (const model of muse_codeProvider.models) {
|
||||
let family = "llama";
|
||||
if (model.id.includes("llama-4")) family = "llama-4";
|
||||
else if (model.id.includes("llama-3.3")) family = "llama-3.3";
|
||||
else if (model.id.includes("llama-3.2")) family = "llama-3.2";
|
||||
else if (model.id.includes("llama-3.1")) family = "llama-3.1";
|
||||
|
||||
const modalities: string[] = ["text"];
|
||||
if (model.supportsVision) modalities.push("image");
|
||||
|
||||
data.push({
|
||||
id: model.id,
|
||||
object: "model",
|
||||
created: MUSECODE_TIMESTAMP,
|
||||
owned_by: "meta",
|
||||
metadata: {
|
||||
name: model.name,
|
||||
family,
|
||||
reasoning: !!model.supportsReasoning,
|
||||
tool_call: !!model.toolCalling,
|
||||
modalities,
|
||||
limit: model.contextLength ?? 200_000,
|
||||
cost: model.id.includes("maverick") || model.id.includes("405b") ? 3 : 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Cache the catalog for the lifetime of the process — model list is static.
|
||||
const CATALOG = buildModelCatalog();
|
||||
const CATALOG_PAYLOAD = JSON.stringify({ object: "list", data: CATALOG }, null, 2);
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
headers: {
|
||||
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
return new Response(CATALOG_PAYLOAD, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"cache-control": "public, max-age=3600",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
-- 142_radar_referrals_cache.sql
|
||||
-- Radar referrals client local cache table.
|
||||
--
|
||||
-- radar_referrals_cache: single-row table holding the last verified
|
||||
-- referrals feed JSON, fetched from GET /v1/referrals/latest — a separate,
|
||||
-- always-current artifact from the catalog feed cached in
|
||||
-- radar_feed_cache (migration 136). Introduced so referral links no
|
||||
-- longer inherit the catalog's up-to-30-day community-tier snapshot
|
||||
-- delay. The payload is the exact byte-identical feed returned by the
|
||||
-- Radar server after Ed25519 signature verification (same pinned key as
|
||||
-- the catalog feed).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS radar_referrals_cache (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
generated_at TEXT,
|
||||
tier TEXT,
|
||||
payload TEXT,
|
||||
signature TEXT,
|
||||
fetched_at TEXT
|
||||
);
|
||||
@@ -17,7 +17,6 @@ export { MODEL_COMPAT_PROTOCOL_KEYS, type ModelCompatProtocolKey };
|
||||
export type ModelCompatPerProtocol = {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
preserveVideoUrl?: boolean;
|
||||
/** Merged into upstream HTTP requests for this model (after default auth headers). */
|
||||
upstreamHeaders?: Record<string, string>;
|
||||
};
|
||||
@@ -77,7 +76,6 @@ export function deepMergeCompatByProtocol(
|
||||
const hasDelta =
|
||||
Object.prototype.hasOwnProperty.call(deltas, "normalizeToolCallId") ||
|
||||
Object.prototype.hasOwnProperty.call(deltas, "preserveOpenAIDeveloperRole") ||
|
||||
Object.prototype.hasOwnProperty.call(deltas, "preserveVideoUrl") ||
|
||||
Object.prototype.hasOwnProperty.call(deltas, "upstreamHeaders");
|
||||
if (!hasDelta) continue;
|
||||
const cur: ModelCompatPerProtocol = { ...(out[key] || {}) };
|
||||
@@ -87,9 +85,6 @@ export function deepMergeCompatByProtocol(
|
||||
if ("preserveOpenAIDeveloperRole" in deltas) {
|
||||
cur.preserveOpenAIDeveloperRole = Boolean(deltas.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
if ("preserveVideoUrl" in deltas) {
|
||||
cur.preserveVideoUrl = Boolean(deltas.preserveVideoUrl);
|
||||
}
|
||||
if ("upstreamHeaders" in deltas) {
|
||||
const uh = deltas.upstreamHeaders;
|
||||
if (uh === undefined) {
|
||||
@@ -110,7 +105,6 @@ export type ModelCompatOverride = {
|
||||
id: string;
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean;
|
||||
preserveVideoUrl?: boolean;
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
upstreamHeaders?: Record<string, string>;
|
||||
isHidden?: boolean;
|
||||
@@ -165,7 +159,6 @@ export function getModelCompatOverrides(providerId: string): ModelCompatOverride
|
||||
export type ModelCompatPatch = {
|
||||
normalizeToolCallId?: boolean;
|
||||
preserveOpenAIDeveloperRole?: boolean | null;
|
||||
preserveVideoUrl?: boolean | null;
|
||||
compatByProtocol?: CompatByProtocolMap;
|
||||
/** Replace top-level extra headers for override-only rows; omit to leave unchanged. */
|
||||
upstreamHeaders?: Record<string, string> | null;
|
||||
@@ -202,13 +195,6 @@ export function mergeModelCompatOverride(
|
||||
next.preserveOpenAIDeveloperRole = Boolean(patch.preserveOpenAIDeveloperRole);
|
||||
}
|
||||
}
|
||||
if ("preserveVideoUrl" in patch) {
|
||||
if (patch.preserveVideoUrl === null) {
|
||||
delete next.preserveVideoUrl;
|
||||
} else {
|
||||
next.preserveVideoUrl = Boolean(patch.preserveVideoUrl);
|
||||
}
|
||||
}
|
||||
if (patch.compatByProtocol && Object.keys(patch.compatByProtocol).length > 0) {
|
||||
const merged = deepMergeCompatByProtocol(next.compatByProtocol, patch.compatByProtocol);
|
||||
if (compatByProtocolHasEntries(merged)) next.compatByProtocol = merged;
|
||||
@@ -225,7 +211,6 @@ export function mergeModelCompatOverride(
|
||||
}
|
||||
const filtered = list.filter((e) => e.id !== modelId);
|
||||
const hasPreserveFlag = Object.prototype.hasOwnProperty.call(next, "preserveOpenAIDeveloperRole");
|
||||
const hasVideoUrlFlag = Object.prototype.hasOwnProperty.call(next, "preserveVideoUrl");
|
||||
const hasTopUpstream = next.upstreamHeaders && Object.keys(next.upstreamHeaders).length > 0;
|
||||
if ("isHidden" in patch) {
|
||||
if (patch.isHidden === null) {
|
||||
@@ -246,7 +231,6 @@ export function mergeModelCompatOverride(
|
||||
if (
|
||||
next.normalizeToolCallId ||
|
||||
hasPreserveFlag ||
|
||||
hasVideoUrlFlag ||
|
||||
hasHiddenFlag ||
|
||||
hasDeletedFlag ||
|
||||
compatByProtocolHasEntries(next.compatByProtocol) ||
|
||||
|
||||
@@ -4,15 +4,10 @@
|
||||
* Provides local cache + settings storage for the OmniRoute Radar client.
|
||||
* Nothing here talks to the network (that's the sync layer).
|
||||
*
|
||||
* Tables (migration 136):
|
||||
* Tables (migration 134):
|
||||
* - radar_feed_cache: single-row signed feed cache
|
||||
* - radar_settings: opt-in + encrypted supporter key
|
||||
*
|
||||
* Tables (migration 142):
|
||||
* - radar_referrals_cache: single-row signed referrals feed cache
|
||||
* (`GET /v1/referrals/latest` — a separate, always-current artifact from
|
||||
* the catalog feed, see `src/lib/radar/referralsSync.ts`).
|
||||
*
|
||||
* 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.
|
||||
@@ -39,14 +34,6 @@ export interface RadarSettings {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface RadarReferralsCache {
|
||||
generatedAt: string;
|
||||
tier: string;
|
||||
payload: string;
|
||||
signature: string;
|
||||
fetchedAt: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// radar_feed_cache
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -137,50 +124,3 @@ export function setRadarKey(key: string | null): void {
|
||||
"UPDATE radar_settings SET supporter_key_encrypted = ?, updated_at = datetime('now') WHERE id = 1"
|
||||
).run(encrypted);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// radar_referrals_cache
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Read the cached Radar referrals feed (`GET /v1/referrals/latest`).
|
||||
* Returns null when no referrals feed has been cached yet — separate from,
|
||||
* and never falling back to, the catalog's `radar_feed_cache`.
|
||||
*/
|
||||
export function getRadarReferralsCache(): RadarReferralsCache | null {
|
||||
const db = getDbInstance();
|
||||
const row = db
|
||||
.prepare(
|
||||
"SELECT generated_at AS generatedAt, tier, payload, signature, fetched_at AS fetchedAt " +
|
||||
"FROM radar_referrals_cache WHERE id = 1"
|
||||
)
|
||||
.get() as RadarReferralsCache | undefined;
|
||||
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert the Radar referrals feed cache (single row). Replaces any existing
|
||||
* entry. If `fetchedAt` is omitted, the current ISO timestamp is used.
|
||||
*/
|
||||
export function setRadarReferralsCache(entry: {
|
||||
generatedAt: string;
|
||||
tier: string;
|
||||
payload: string;
|
||||
signature: string;
|
||||
fetchedAt?: string;
|
||||
}): void {
|
||||
const db = getDbInstance();
|
||||
const fetchedAt = entry.fetchedAt ?? new Date().toISOString();
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO radar_referrals_cache (id, generated_at, tier, payload, signature, fetched_at)
|
||||
VALUES (1, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
generated_at = excluded.generated_at,
|
||||
tier = excluded.tier,
|
||||
payload = excluded.payload,
|
||||
signature = excluded.signature,
|
||||
fetched_at = excluded.fetched_at`
|
||||
).run(entry.generatedAt, entry.tier, entry.payload, entry.signature, fetchedAt);
|
||||
}
|
||||
|
||||
@@ -818,7 +818,5 @@ export {
|
||||
getRadarSettings,
|
||||
setRadarOptIn,
|
||||
setRadarKey,
|
||||
getRadarReferralsCache,
|
||||
setRadarReferralsCache,
|
||||
} from "./db/radar";
|
||||
export type { RadarCache, RadarSettings, RadarReferralsCache } from "./db/radar";
|
||||
export type { RadarCache, RadarSettings } from "./db/radar";
|
||||
|
||||
@@ -96,14 +96,7 @@ const HttpsUrlSchema = z
|
||||
.url()
|
||||
.refine((v) => v.startsWith("https://"), { message: "Referral url must use https://" });
|
||||
|
||||
/**
|
||||
* Exported so `referralsFeedSchema.ts` (the standalone `/v1/referrals/latest`
|
||||
* feed schema) can reuse the exact same per-referral shape instead of
|
||||
* duplicating it — one definition, two feeds (the catalog's legacy embedded
|
||||
* `referrals` section below, kept for backward-compat with old cached
|
||||
* catalog feeds, and the live referrals-only feed).
|
||||
*/
|
||||
export const RadarReferralSchema = z.object({
|
||||
const RadarReferralSchema = z.object({
|
||||
provider: z.string(),
|
||||
url: HttpsUrlSchema,
|
||||
kind: ReferralKindEnum,
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
|
||||
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 { applyFeed, type MergedEntry, type FeedModel } from "./applyFeed";
|
||||
import { findDefaultReferral } from "./referrals";
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import { getRadarCache, getRadarReferralsCache } from "@/lib/db/radar";
|
||||
import { getRadarCache } from "@/lib/db/radar";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -143,30 +142,23 @@ export interface RadarReferralsResult {
|
||||
|
||||
const EMPTY_REFERRALS: RadarReferralsResult = { fixed: [], campaigns: [] };
|
||||
|
||||
/**
|
||||
* Injectable deps for testing. `getCache` now reads the STANDALONE referrals
|
||||
* feed cache (`radar_referrals_cache`, populated by
|
||||
* `syncRadarReferrals()`/`GET /v1/referrals/latest`) — no longer the
|
||||
* catalog's `radar_feed_cache`. This is what removes the up-to-30-day
|
||||
* community-tier delay referral links used to inherit from the catalog
|
||||
* feed: referrals now sync on their own, much shorter cadence
|
||||
* (`REFERRALS_STALE_MS`, see `referralsSync.ts`).
|
||||
*/
|
||||
/** Injectable deps for testing — mirrors GetRadarCatalogDeps. */
|
||||
export interface GetRadarReferralsDeps {
|
||||
getFlag?: (key: string) => boolean;
|
||||
getCache?: () => { generatedAt: string; tier: string; payload: string; fetchedAt: string } | null;
|
||||
getCache?: () => { version: string; tier: string; payload: string; fetchedAt: string } | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the referral links section of the cached Radar REFERRALS feed
|
||||
* (`GET /v1/referrals/latest` — see `referralsSync.ts`).
|
||||
* Return the referral links section of the cached Radar feed.
|
||||
*
|
||||
* Never throws — returns `{fixed:[],campaigns:[]}` when: the flag is off,
|
||||
* there is no cache yet, or the cached payload fails defensive
|
||||
* re-validation (corrupt/garbage payload).
|
||||
* there is no cache yet, the cached payload fails defensive re-validation,
|
||||
* or the cached feed predates the `referrals` section (old-feed compat —
|
||||
* the schema's `.default()` already covers this, this is a second line of
|
||||
* defense for a payload that fails to parse at all).
|
||||
*/
|
||||
export function getRadarReferrals(deps: GetRadarReferralsDeps = {}): RadarReferralsResult {
|
||||
const { getFlag = isFeatureFlagEnabled, getCache: getCacheFn = getRadarReferralsCache } = deps;
|
||||
const { getFlag = isFeatureFlagEnabled, getCache: getCacheFn = getRadarCache } = deps;
|
||||
|
||||
if (!getFlag("RADAR_ENABLED")) {
|
||||
return EMPTY_REFERRALS;
|
||||
@@ -177,10 +169,10 @@ export function getRadarReferrals(deps: GetRadarReferralsDeps = {}): RadarReferr
|
||||
return EMPTY_REFERRALS;
|
||||
}
|
||||
|
||||
let feed: RadarReferralsFeed;
|
||||
let feed: RadarFeed;
|
||||
try {
|
||||
const parsed = JSON.parse(cache.payload);
|
||||
feed = RadarReferralsFeedSchema.parse(parsed);
|
||||
feed = RadarFeedSchema.parse(parsed);
|
||||
} catch {
|
||||
return EMPTY_REFERRALS;
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* referralsFeedSchema.ts — Zod schema for the standalone Radar referrals feed
|
||||
* (`GET /v1/referrals/latest`).
|
||||
*
|
||||
* This is the CLIENT-SIDE mirror of the server's referrals feed schema — a
|
||||
* separate, always-current artifact from the catalog feed (`feedSchema.ts`),
|
||||
* introduced so referral links no longer inherit the catalog's up-to-30-day
|
||||
* community-tier snapshot delay. Reuses `RadarReferralSchema` (the per-link
|
||||
* shape) from `feedSchema.ts` so both feeds validate referrals identically.
|
||||
*
|
||||
* Schema version: 1
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { RadarReferralSchema } from "./feedSchema";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Top-level feed schema
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const RadarReferralsFeedSchema = z.object({
|
||||
feed: z.literal("omniroute-radar-referrals"),
|
||||
schemaVersion: z.literal(1),
|
||||
generatedAt: z.string().datetime(),
|
||||
referrals: z.object({
|
||||
fixed: z.array(RadarReferralSchema),
|
||||
campaigns: z.array(RadarReferralSchema),
|
||||
}),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Inferred types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type RadarReferralsFeed = z.infer<typeof RadarReferralsFeedSchema>;
|
||||
@@ -1,308 +0,0 @@
|
||||
/**
|
||||
* referralsSync.ts — Radar referrals feed sync: download, verify, validate, cache.
|
||||
*
|
||||
* Mirrors `sync.ts` (the catalog feed sync) but targets the standalone,
|
||||
* always-current `GET /v1/referrals/latest` endpoint instead of the
|
||||
* catalog's `/v1/catalog/latest` — the catalog feed is a up-to-30-day-old
|
||||
* snapshot on the community tier, so referral links extracted from it lag
|
||||
* behind the server by up to 30 days. This module removes that delay by
|
||||
* consuming the dedicated referrals endpoint directly.
|
||||
*
|
||||
* This is the ONLY module that touches the network for Radar referrals.
|
||||
* Every step is gated: flag off / opt-out / bad sig / bad schema / oversized
|
||||
* body all bail early without touching the cache.
|
||||
*
|
||||
* Errors never escape `syncRadarReferrals()` — always return a status object.
|
||||
* Stack traces are never included in the `reason` field.
|
||||
*
|
||||
* Deps are injectable for testing.
|
||||
*/
|
||||
|
||||
import {
|
||||
RadarReferralsFeedSchema,
|
||||
type RadarReferralsFeed,
|
||||
} from "./referralsFeedSchema";
|
||||
import { RadarTierSchema, type RadarTier } from "./feedSchema";
|
||||
import { verifyFeedBytes } from "./verify";
|
||||
import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import type { RadarSettingsSnapshot } from "./sync";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Default feed base URL — same default/override convention as `sync.ts`
|
||||
* (`RADAR_FEED_URL` env var points forks/self-hosters at their own server).
|
||||
*/
|
||||
const DEFAULT_FEED_BASE_URL = "https://radar.omniroute.online";
|
||||
|
||||
const SYNC_TIMEOUT_MS = 30_000;
|
||||
|
||||
/**
|
||||
* Hard cap on the referrals feed response body. Same rationale as
|
||||
* `sync.ts::MAX_FEED_BYTES` — this is a small, KB-scale signed JSON document;
|
||||
* anything past this is either a misconfigured `RADAR_FEED_URL` or an
|
||||
* upstream serving garbage. Enforced both via a `Content-Length` preflight
|
||||
* and a running-total check while reading the body, so an absent/lying
|
||||
* `Content-Length` cannot bypass the cap.
|
||||
*/
|
||||
const MAX_FEED_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
/**
|
||||
* How stale the cached referrals must be before a new sync is worth doing
|
||||
* (used by the route's "sync if stale" trigger, see `shouldSyncReferralsOnRead`).
|
||||
* Deliberately much shorter than the catalog's 24h cadence — referrals are
|
||||
* meant to feel "always current", and the fixed links in particular should
|
||||
* surface quickly for a free/community user.
|
||||
*/
|
||||
export const REFERRALS_STALE_MS = 60 * 60 * 1000; // 1h
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type ReferralsSyncStatus =
|
||||
| { status: "disabled" }
|
||||
| { status: "opt_out" }
|
||||
| { status: "invalid_signature" }
|
||||
| { status: "invalid_schema" }
|
||||
| { status: "stale" }
|
||||
| { status: "too_large" }
|
||||
| { status: "updated"; generatedAt: string; tier: string }
|
||||
| { status: "error"; reason: string };
|
||||
|
||||
export interface RadarReferralsCacheEntry {
|
||||
generatedAt: string;
|
||||
tier: string;
|
||||
payload: string;
|
||||
signature: string;
|
||||
fetchedAt?: string;
|
||||
}
|
||||
|
||||
export interface ReferralsSyncDeps {
|
||||
fetch?: typeof globalThis.fetch;
|
||||
now?: () => Date;
|
||||
getFlag?: (key: string) => boolean;
|
||||
getSettings?: () => RadarSettingsSnapshot;
|
||||
getCache?: () => RadarReferralsCacheEntry | null;
|
||||
setCache?: (entry: RadarReferralsCacheEntry) => void;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Served-tier header
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Parse & validate the `x-omniroute-feed-tier` response header.
|
||||
*
|
||||
* Unlike the catalog feed, the referrals feed body carries no `tier` field
|
||||
* at all (there is only ever one signed artifact per `generatedAt`, and the
|
||||
* server decides which referrals to include per-request based on the
|
||||
* `Authorization` key) — so the header is the ONLY source for the served
|
||||
* tier. An absent or unrecognized header degrades to `"community"`, the
|
||||
* least-privileged assumption (matches the server's own no-auth default:
|
||||
* fixed links only, `campaigns: []`).
|
||||
*/
|
||||
function parseServedTierHeader(value: string | null): RadarTier | null {
|
||||
const result = RadarTierSchema.safeParse(value);
|
||||
return result.success ? result.data : null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Staleness helper (exported for the route's "sync if stale" trigger)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Whether the cached referrals feed (fetched at `fetchedAt`) is stale enough
|
||||
* to warrant a fresh sync. Missing/unparseable timestamps count as stale —
|
||||
* mirrors `autoSync.ts::shouldAutoSyncOnOpen`'s conservative default.
|
||||
*/
|
||||
export function shouldSyncReferralsOnRead(
|
||||
fetchedAt: string | null | undefined,
|
||||
nowMs: number,
|
||||
staleMs: number = REFERRALS_STALE_MS
|
||||
): boolean {
|
||||
if (!fetchedAt) return true;
|
||||
const fetchedMs = Date.parse(fetchedAt);
|
||||
if (!Number.isFinite(fetchedMs)) return true;
|
||||
return nowMs - fetchedMs >= staleMs;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// syncRadarReferrals
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Download, verify, validate, and cache the Radar referrals feed.
|
||||
*
|
||||
* Steps:
|
||||
* 1. Feature flag off => `{status:"disabled"}`, no network.
|
||||
* 2. Opt-in false => `{status:"opt_out"}`, no network.
|
||||
* 3. GET `${base}/v1/referrals/latest` with timeout.
|
||||
* 4. Verify Ed25519 signature over exact bytes (same pinned key as the
|
||||
* catalog feed — one key pins both artifacts).
|
||||
* 5. Parse+validate with RadarReferralsFeedSchema.
|
||||
* 6. Replay guard: incoming `generatedAt` must be strictly newer than the
|
||||
* cache (a same/older `generatedAt` is a no-op — nothing changed, or a
|
||||
* stale replay — either way the cache is left untouched).
|
||||
* 7. Cache the result.
|
||||
*
|
||||
* @param deps - Injectable dependencies for testing.
|
||||
*/
|
||||
export async function syncRadarReferrals(
|
||||
deps: ReferralsSyncDeps = {}
|
||||
): Promise<ReferralsSyncStatus> {
|
||||
const {
|
||||
fetch: fetchFn = globalThis.fetch,
|
||||
now = () => new Date(),
|
||||
getFlag = isFeatureFlagEnabled,
|
||||
getSettings: getSettingsFn,
|
||||
getCache: getCacheFn,
|
||||
setCache: setCacheFn,
|
||||
} = deps;
|
||||
|
||||
try {
|
||||
// Step 1: Feature flag gate
|
||||
const flagOn = getFlag("RADAR_ENABLED");
|
||||
if (!flagOn) {
|
||||
return { status: "disabled" };
|
||||
}
|
||||
|
||||
// Step 2: Opt-in gate
|
||||
let settings: RadarSettingsSnapshot;
|
||||
if (getSettingsFn) {
|
||||
settings = getSettingsFn();
|
||||
} else {
|
||||
const mod = await import("@/lib/db/radar");
|
||||
settings = mod.getRadarSettings();
|
||||
}
|
||||
if (!settings.optIn) {
|
||||
return { status: "opt_out" };
|
||||
}
|
||||
|
||||
// Step 3: Download feed
|
||||
const baseUrl = (process.env.RADAR_FEED_URL || DEFAULT_FEED_BASE_URL).replace(/\/+$/, "");
|
||||
const url = `${baseUrl}/v1/referrals/latest`;
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (settings.supporterKey) {
|
||||
headers["Authorization"] = `Bearer ${settings.supporterKey}`;
|
||||
}
|
||||
|
||||
const res = await fetchFn(url, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(SYNC_TIMEOUT_MS),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return { status: "error", reason: `Referrals feed request failed with status ${res.status}` };
|
||||
}
|
||||
|
||||
// Step 3b: Content-Length preflight — skip reading an already-oversized
|
||||
// body entirely (untrusted header, fast-path only; the real enforcement
|
||||
// is the running-total check below).
|
||||
const contentLengthHeader = res.headers.get("content-length");
|
||||
if (contentLengthHeader !== null) {
|
||||
const declaredLength = Number(contentLengthHeader);
|
||||
if (Number.isFinite(declaredLength) && declaredLength > MAX_FEED_BYTES) {
|
||||
return { status: "too_large" };
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Read exact bytes + signature header, enforcing MAX_FEED_BYTES
|
||||
// while reading so an absent/lying Content-Length cannot bypass the cap.
|
||||
let rawBytes: Buffer;
|
||||
const body = res.body as ReadableStream<Uint8Array> | null | undefined;
|
||||
if (body && typeof body.getReader === "function") {
|
||||
const reader = body.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
let total = 0;
|
||||
let tooLarge = false;
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
if (value) {
|
||||
total += value.byteLength;
|
||||
if (total > MAX_FEED_BYTES) {
|
||||
tooLarge = true;
|
||||
await reader.cancel().catch(() => {});
|
||||
break;
|
||||
}
|
||||
chunks.push(value);
|
||||
}
|
||||
}
|
||||
if (tooLarge) {
|
||||
return { status: "too_large" };
|
||||
}
|
||||
rawBytes = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
|
||||
} else {
|
||||
const buffered = Buffer.from(await res.arrayBuffer());
|
||||
if (buffered.byteLength > MAX_FEED_BYTES) {
|
||||
return { status: "too_large" };
|
||||
}
|
||||
rawBytes = buffered;
|
||||
}
|
||||
|
||||
const signature = res.headers.get("x-omniroute-feed-signature") ?? "";
|
||||
|
||||
// Step 5: Verify signature — same pinned Ed25519 key(s) as the catalog feed.
|
||||
const sigValid = verifyFeedBytes(rawBytes, signature);
|
||||
if (!sigValid) {
|
||||
return { status: "invalid_signature" };
|
||||
}
|
||||
|
||||
// Step 6: Parse + validate
|
||||
let feed: RadarReferralsFeed;
|
||||
try {
|
||||
const parsed = JSON.parse(rawBytes.toString("utf-8"));
|
||||
feed = RadarReferralsFeedSchema.parse(parsed);
|
||||
} catch {
|
||||
return { status: "invalid_schema" };
|
||||
}
|
||||
|
||||
// Step 7: generatedAt floor — replay/no-op guard.
|
||||
let existingCache: RadarReferralsCacheEntry | null = null;
|
||||
if (getCacheFn) {
|
||||
existingCache = getCacheFn();
|
||||
} else {
|
||||
const mod = await import("@/lib/db/radar");
|
||||
existingCache = mod.getRadarReferralsCache();
|
||||
}
|
||||
|
||||
if (existingCache) {
|
||||
const existingMs = Date.parse(existingCache.generatedAt);
|
||||
const incomingMs = Date.parse(feed.generatedAt);
|
||||
if (Number.isFinite(existingMs) && Number.isFinite(incomingMs) && incomingMs <= existingMs) {
|
||||
return { status: "stale" };
|
||||
}
|
||||
}
|
||||
|
||||
// Step 8: Resolve served tier — the body carries no `tier` field at all
|
||||
// for this feed, so the header is the only source; absent/garbage header
|
||||
// degrades to the least-privileged "community" default.
|
||||
const servedTier = parseServedTierHeader(res.headers.get("x-omniroute-feed-tier")) ?? "community";
|
||||
|
||||
// Step 9: Cache the result
|
||||
const cacheEntry: RadarReferralsCacheEntry = {
|
||||
generatedAt: feed.generatedAt,
|
||||
tier: servedTier,
|
||||
payload: rawBytes.toString("utf-8"),
|
||||
signature,
|
||||
fetchedAt: now().toISOString(),
|
||||
};
|
||||
|
||||
if (setCacheFn) {
|
||||
setCacheFn(cacheEntry);
|
||||
} else {
|
||||
const mod = await import("@/lib/db/radar");
|
||||
mod.setRadarReferralsCache(cacheEntry);
|
||||
}
|
||||
|
||||
return { status: "updated", generatedAt: feed.generatedAt, tier: servedTier };
|
||||
} catch (err: unknown) {
|
||||
const reason = sanitizeErrorMessage(err) || "Radar referrals sync failed";
|
||||
return { status: "error", reason };
|
||||
}
|
||||
}
|
||||
@@ -12,25 +12,11 @@
|
||||
* network sync when the cache is older than the daily window computed by
|
||||
* `nextSyncTime()`. `syncRadar()` re-checks flag/opt-in internally, so a
|
||||
* mid-flight settings change degrades to a no-op instead of an errant fetch.
|
||||
*
|
||||
* Referrals (`GET /v1/referrals/latest`) piggyback on the SAME hourly tick,
|
||||
* but on their own much shorter staleness window (`REFERRALS_STALE_MS`, 1h —
|
||||
* see `referralsSync.ts`) so they stay close to real-time instead of
|
||||
* inheriting the catalog's daily cadence. This is independent of, and never
|
||||
* gates on, the catalog's own due-ness — the two feeds sync on separate
|
||||
* schedules within the same tick. It is deliberately NOT reflected in
|
||||
* `RadarTickResult` (best-effort, fire-and-await side effect only) so the
|
||||
* existing catalog-sync result shape/assertions stay unchanged.
|
||||
*/
|
||||
|
||||
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
|
||||
import { getRadarCache, getRadarSettings, getRadarReferralsCache } from "@/lib/db/radar";
|
||||
import { getRadarCache, getRadarSettings } from "@/lib/db/radar";
|
||||
import { nextSyncTime, syncRadar, type SyncStatus } from "./sync";
|
||||
import {
|
||||
syncRadarReferrals,
|
||||
shouldSyncReferralsOnRead,
|
||||
type ReferralsSyncStatus,
|
||||
} from "./referralsSync";
|
||||
|
||||
/** How often the scheduler re-evaluates staleness (NOT the sync cadence). */
|
||||
export const RADAR_SCHEDULER_TICK_MS = 60 * 60 * 1000; // hourly
|
||||
@@ -45,10 +31,6 @@ export interface RadarSchedulerDeps {
|
||||
getSettings?: () => { optIn: boolean };
|
||||
getCache?: () => { fetchedAt: string } | null;
|
||||
sync?: () => Promise<SyncStatus>;
|
||||
/** Referrals cache reader — separate from `getCache` (the catalog cache). */
|
||||
getReferralsCache?: () => { fetchedAt: string } | null;
|
||||
/** Referrals sync — separate from `sync` (the catalog sync). */
|
||||
syncReferrals?: () => Promise<ReferralsSyncStatus>;
|
||||
now?: () => number;
|
||||
setIntervalFn?: typeof setInterval;
|
||||
clearIntervalFn?: typeof clearInterval;
|
||||
@@ -56,23 +38,6 @@ export interface RadarSchedulerDeps {
|
||||
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
/**
|
||||
* Best-effort referrals sync, gated on its own (shorter) staleness window.
|
||||
* Never throws — `syncRadarReferrals()` already never throws by contract,
|
||||
* this is defense in depth so a scheduler tick can never fail because of
|
||||
* the referrals side-sync.
|
||||
*/
|
||||
async function maybeSyncReferrals(deps: RadarSchedulerDeps, nowMs: number): Promise<void> {
|
||||
try {
|
||||
const referralsCache = (deps.getReferralsCache ?? getRadarReferralsCache)();
|
||||
if (!shouldSyncReferralsOnRead(referralsCache?.fetchedAt ?? null, nowMs)) return;
|
||||
await (deps.syncReferrals ?? syncRadarReferrals)();
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.warn("[RADAR_SYNC] Referrals side-sync failed (non-fatal):", msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One scheduler evaluation. Exported for tests and for the immediate
|
||||
* post-start tick.
|
||||
@@ -87,13 +52,8 @@ export async function radarSchedulerTick(deps: RadarSchedulerDeps = {}): Promise
|
||||
const settings = (deps.getSettings ?? getRadarSettings)();
|
||||
if (!settings.optIn) return { action: "skipped", reason: "opt_out" };
|
||||
|
||||
const nowMs = (deps.now ?? Date.now)();
|
||||
|
||||
// Referrals sync on their own staleness window — independent of the
|
||||
// catalog's due-ness below, same tick.
|
||||
await maybeSyncReferrals(deps, nowMs);
|
||||
|
||||
const cache = (deps.getCache ?? getRadarCache)();
|
||||
const nowMs = (deps.now ?? Date.now)();
|
||||
if (nowMs < nextSyncTime(cache?.fetchedAt ?? null).getTime()) {
|
||||
return { action: "skipped", reason: "not_due" };
|
||||
}
|
||||
|
||||
@@ -51,13 +51,6 @@ const GEMINI_CLI_PROFILE: ClientIdentityProfile = Object.freeze({
|
||||
"User-Agent": "GeminiCLI/0.1.0 (linux; x64)",
|
||||
}),
|
||||
});
|
||||
const MUSE_CLI_PROFILE: ClientIdentityProfile = Object.freeze({
|
||||
id: "muse-cli",
|
||||
label: "Muse Code CLI",
|
||||
headers: Object.freeze({
|
||||
"User-Agent": "MuseCodeCLI/0.1.0 (linux; x64)",
|
||||
}),
|
||||
});
|
||||
|
||||
/** Ordered so `CLIENT_IDENTITY_PROFILE_OPTIONS` renders "Default" first. */
|
||||
export const CLIENT_IDENTITY_PROFILES: Readonly<Record<string, ClientIdentityProfile>> =
|
||||
@@ -66,7 +59,6 @@ export const CLIENT_IDENTITY_PROFILES: Readonly<Record<string, ClientIdentityPro
|
||||
"claude-cli": CLAUDE_CLI_PROFILE,
|
||||
"codex-cli": CODEX_CLI_PROFILE,
|
||||
"gemini-cli": GEMINI_CLI_PROFILE,
|
||||
"muse-cli": MUSE_CLI_PROFILE,
|
||||
});
|
||||
|
||||
export const CLIENT_IDENTITY_PROFILE_IDS: readonly string[] = Object.keys(CLIENT_IDENTITY_PROFILES);
|
||||
|
||||
@@ -275,19 +275,4 @@ export const APIKEY_PROVIDERS_FRONTIER = {
|
||||
"Writer Palmyra is OpenAI-compatible at https://api.writer.com/v1. palmyra-x5 offers a 1M-token context window.",
|
||||
hasFree: false,
|
||||
},
|
||||
"muse-code": {
|
||||
id: "muse-code",
|
||||
alias: "mc",
|
||||
name: "Muse Code (Meta)",
|
||||
icon: "auto_awesome",
|
||||
color: "#0866FF",
|
||||
textIcon: "MC",
|
||||
website: "https://github.com/meta-llama/llama-stack",
|
||||
authHint:
|
||||
"Use your META_API_KEY env var as a Bearer token. Muse Code CLI uses the OpenAI Responses API wire format (POST /responses).",
|
||||
apiHint:
|
||||
"Muse Code is OpenAI-compatible. OmniRoute routes chat traffic through the Responses API and exposes the proprietary model catalog at /v1/muse-code/models.",
|
||||
passthroughModels: true,
|
||||
hasFree: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3405,26 +3405,6 @@
|
||||
"stream": "https://api.morphllm.com/v1/chat/completions"
|
||||
}
|
||||
},
|
||||
"muse-code": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
"apiKey": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"nonStream": {
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
"oauth": {
|
||||
"Accept": "text/event-stream",
|
||||
"Authorization": "Bearer <TOK>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
},
|
||||
"url": {}
|
||||
},
|
||||
"muse-spark-web": {
|
||||
"format": "openai",
|
||||
"headers": {
|
||||
|
||||
@@ -76,7 +76,6 @@ test("Codex keeps matched outputs and removes orphaned outputs from mixed input"
|
||||
{ type: "function_call_output", call_id: "call_orphan", output: "orphaned" },
|
||||
]);
|
||||
|
||||
assert.equal(result.length, 2);
|
||||
assert.deepEqual(toolOutputs(result), [
|
||||
{ type: "function_call_output", call_id: "call_keep", output: "ok" },
|
||||
]);
|
||||
|
||||
@@ -2318,7 +2318,7 @@ test("handleComboChat returns a 503 when every model is unavailable before execu
|
||||
|
||||
const payload = (await result.json()) as any;
|
||||
assert.equal(result.status, 503);
|
||||
assert.equal(payload.error.code, "ALL_TARGETS_SKIPPED");
|
||||
assert.equal(payload.error.code, "ALL_ACCOUNTS_INACTIVE");
|
||||
});
|
||||
|
||||
test("handleComboChat treats provider circuit breaker responses as ordinary target failures", async () => {
|
||||
@@ -2847,7 +2847,7 @@ test("handleComboChat round-robin resolves nested combos and returns inactive wh
|
||||
|
||||
const payload = (await result.json()) as any;
|
||||
assert.equal(result.status, 503);
|
||||
assert.equal(payload.error.code, "ALL_TARGETS_SKIPPED");
|
||||
assert.equal(payload.error.code, "ALL_ACCOUNTS_INACTIVE");
|
||||
});
|
||||
|
||||
test("handleComboChat round-robin treats provider circuit breaker responses as ordinary target failures", async () => {
|
||||
|
||||
@@ -339,12 +339,8 @@ test("KiroExecutor keeps cache tokens that arrive without input/output totals",
|
||||
const chunks = parseSSEJsonChunks(await transformed.text());
|
||||
const finish = chunks.find((chunk) => chunk.choices?.[0]?.finish_reason);
|
||||
|
||||
assert.deepEqual(finish.usage, {
|
||||
prompt_tokens: 19999,
|
||||
completion_tokens: 1,
|
||||
total_tokens: 20000,
|
||||
cache_read_input_tokens: 900,
|
||||
});
|
||||
assert.equal(finish.usage.cache_read_input_tokens, 900);
|
||||
assert.equal(finish.usage.cache_creation_input_tokens, undefined);
|
||||
});
|
||||
|
||||
// snake_case spellings appear on some Kiro frames; a cache count must not be
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* Tests for Muse Code CLI model catalog endpoint.
|
||||
*
|
||||
* Verifies GET /v1/muse-code/models returns the proprietary Muse format.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { muse_codeProvider } from "../../open-sse/config/providers/registry/muse-code/index.ts";
|
||||
|
||||
// ── Model catalog shape ─────────────────────────────────────────────────────
|
||||
|
||||
test("muse-code provider has at least one model", () => {
|
||||
assert.ok(muse_codeProvider.models.length >= 1);
|
||||
});
|
||||
|
||||
test("muse-code models have unique ids", () => {
|
||||
const ids = muse_codeProvider.models.map((m) => m.id);
|
||||
const unique = new Set(ids);
|
||||
assert.equal(unique.size, ids.length, "model IDs must be unique");
|
||||
});
|
||||
|
||||
test("muse-code models include llama-4-maverick", () => {
|
||||
const ids = muse_codeProvider.models.map((m) => m.id);
|
||||
assert.ok(ids.includes("llama-4-maverick"), "must include llama-4-maverick");
|
||||
});
|
||||
|
||||
test("muse-code models include llama-4-scout", () => {
|
||||
const ids = muse_codeProvider.models.map((m) => m.id);
|
||||
assert.ok(ids.includes("llama-4-scout"), "must include llama-4-scout");
|
||||
});
|
||||
|
||||
test("muse-code models include llama-3.3-70b", () => {
|
||||
const ids = muse_codeProvider.models.map((m) => m.id);
|
||||
assert.ok(ids.includes("llama-3.3-70b"), "must include llama-3.3-70b");
|
||||
});
|
||||
|
||||
test("llama-4 models have supportsXHighEffort", () => {
|
||||
const maverick = muse_codeProvider.models.find((m) => m.id === "llama-4-maverick");
|
||||
assert.ok(maverick, "llama-4-maverick must exist");
|
||||
assert.equal(maverick.supportsXHighEffort, true);
|
||||
|
||||
const scout = muse_codeProvider.models.find((m) => m.id === "llama-4-scout");
|
||||
assert.ok(scout, "llama-4-scout must exist");
|
||||
assert.equal(scout.supportsXHighEffort, true);
|
||||
});
|
||||
|
||||
test("llama-3.3-70b does not support reasoning", () => {
|
||||
const model = muse_codeProvider.models.find((m) => m.id === "llama-3.3-70b");
|
||||
assert.ok(model, "llama-3.3-70b must exist");
|
||||
assert.equal(model.supportsReasoning, false);
|
||||
});
|
||||
|
||||
test("non-reasoning models do not declare supportsXHighEffort", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
if (!model.supportsReasoning) {
|
||||
assert.equal(
|
||||
model.supportsXHighEffort,
|
||||
undefined,
|
||||
`${model.id} is not a reasoning model but has supportsXHighEffort`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Vision models ───────────────────────────────────────────────────────────
|
||||
|
||||
test("vision models have supportsVision: true", () => {
|
||||
const expectedVision = [
|
||||
"llama-4-maverick",
|
||||
"llama-4-scout",
|
||||
"llama-3.2-90b-vision",
|
||||
"llama-3.2-11b-vision",
|
||||
];
|
||||
for (const model of muse_codeProvider.models) {
|
||||
if (expectedVision.includes(model.id)) {
|
||||
assert.equal(model.supportsVision, true, `${model.id} should have supportsVision`);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Tests for Muse Code CLI provider registry entry.
|
||||
*
|
||||
* Verifies the provider entry loads correctly with expected config.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { muse_codeProvider } from "../../open-sse/config/providers/registry/muse-code/index.ts";
|
||||
import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts";
|
||||
|
||||
// ── Registry entry structure ────────────────────────────────────────────────
|
||||
|
||||
test("muse-code provider entry has id", () => {
|
||||
assert.equal(muse_codeProvider.id, "muse-code");
|
||||
});
|
||||
|
||||
test("muse-code provider entry has alias", () => {
|
||||
assert.equal(muse_codeProvider.alias, "mc");
|
||||
});
|
||||
|
||||
test("muse-code provider uses openai format", () => {
|
||||
assert.equal(muse_codeProvider.format, "openai");
|
||||
});
|
||||
|
||||
test("muse-code provider uses apikey auth", () => {
|
||||
assert.equal(muse_codeProvider.authType, "apikey");
|
||||
assert.equal(muse_codeProvider.authHeader, "bearer");
|
||||
});
|
||||
|
||||
test("muse-code provider has passthroughModels enabled", () => {
|
||||
assert.equal(muse_codeProvider.passthroughModels, true);
|
||||
});
|
||||
|
||||
// ── Model entries ───────────────────────────────────────────────────────────
|
||||
|
||||
test("muse-code provider has curated models", () => {
|
||||
assert.ok(muse_codeProvider.models.length > 0);
|
||||
});
|
||||
|
||||
test("all muse-code models have contextLength", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
assert.ok(
|
||||
typeof model.contextLength === "number" && model.contextLength > 0,
|
||||
`${model.id} must have positive contextLength`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("all muse-code models have toolCalling: true", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
assert.equal(model.toolCalling, true, `${model.id} must have toolCalling enabled`);
|
||||
}
|
||||
});
|
||||
|
||||
test("all muse-code models have targetFormat: openai-responses", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
assert.equal(
|
||||
model.targetFormat,
|
||||
"openai-responses",
|
||||
`${model.id} must use openai-responses target format`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("reasoning models have supportsXHighEffort", () => {
|
||||
for (const model of muse_codeProvider.models) {
|
||||
if (model.supportsReasoning) {
|
||||
assert.equal(
|
||||
model.supportsXHighEffort,
|
||||
true,
|
||||
`${model.id} is a reasoning model but missing supportsXHighEffort`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Registry discovery ──────────────────────────────────────────────────────
|
||||
|
||||
test("muse-code is discoverable via getRegistryEntry", () => {
|
||||
const entry = getRegistryEntry("muse-code");
|
||||
assert.ok(entry, "getRegistryEntry must return muse-code entry");
|
||||
assert.equal(entry.id, "muse-code");
|
||||
});
|
||||
|
||||
test("muse-code is discoverable via alias", () => {
|
||||
const entry = getRegistryEntry("mc");
|
||||
assert.ok(entry, "getRegistryEntry must find muse-code by alias mc");
|
||||
assert.equal(entry.id, "muse-code");
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it } from "node:test";
|
||||
import { deepEqual, equal, ok } from "node:assert/strict";
|
||||
import { ok, equal } from "node:assert/strict";
|
||||
|
||||
describe("getModelPreserveVideoUrl", () => {
|
||||
it("exports getModelPreserveVideoUrl as a function", async () => {
|
||||
@@ -8,7 +8,8 @@ describe("getModelPreserveVideoUrl", () => {
|
||||
});
|
||||
|
||||
it("fallback preserves moonshot and kimi legacy behavior", () => {
|
||||
const fallback = (provider: string) => provider === "moonshot" || provider === "kimi";
|
||||
const fallback = (provider: string) =>
|
||||
provider === "moonshot" || provider === "kimi";
|
||||
ok(fallback("moonshot"));
|
||||
ok(fallback("kimi"));
|
||||
equal(fallback("dashscope"), false);
|
||||
@@ -26,67 +27,19 @@ describe("getModelPreserveVideoUrl", () => {
|
||||
});
|
||||
|
||||
it("mergeModelCompatOverride accepts preserveVideoUrl", async () => {
|
||||
const { mergeModelCompatOverride, removeModelCompatOverride } =
|
||||
await import("@/lib/db/models/compat");
|
||||
const { getModelPreserveVideoUrl } = await import("@/lib/db/models/modelPreserveVideoUrl");
|
||||
const { mergeModelCompatOverride, removeModelCompatOverride } = await import("@/lib/db/models/compat");
|
||||
const PROVIDER = "test_provider_9248v3";
|
||||
const MODEL = "test_model_qwen_vl";
|
||||
try {
|
||||
mergeModelCompatOverride(PROVIDER, MODEL, { preserveVideoUrl: true });
|
||||
equal(getModelPreserveVideoUrl(PROVIDER, MODEL), true);
|
||||
} finally {
|
||||
removeModelCompatOverride(PROVIDER, MODEL);
|
||||
}
|
||||
});
|
||||
|
||||
it("translateRequest resolves preserveVideoUrl with the routed model", async () => {
|
||||
const { mergeModelCompatOverride, removeModelCompatOverride } =
|
||||
await import("@/lib/db/models/compat");
|
||||
const { translateRequest } = await import("@omniroute/open-sse/translator/index.ts");
|
||||
const provider = "test-provider-video-override";
|
||||
const model = "test-model-video-override";
|
||||
|
||||
mergeModelCompatOverride(provider, model, { preserveVideoUrl: true });
|
||||
try {
|
||||
const translated = translateRequest(
|
||||
"openai",
|
||||
"openai",
|
||||
model,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{
|
||||
type: "video_url",
|
||||
video_url: { url: "https://cdn.example.com/input.mp4" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
null,
|
||||
provider
|
||||
) as { messages: Array<{ content: Array<{ type: string }> }> };
|
||||
|
||||
deepEqual(
|
||||
translated.messages[0].content.map((part) => part.type),
|
||||
["video_url"]
|
||||
);
|
||||
} finally {
|
||||
removeModelCompatOverride(provider, model);
|
||||
}
|
||||
mergeModelCompatOverride(PROVIDER, MODEL, { preserveVideoUrl: true });
|
||||
removeModelCompatOverride(PROVIDER, MODEL);
|
||||
ok(true, "should accept preserveVideoUrl in ModelCompatPatch");
|
||||
});
|
||||
|
||||
it("deepMergeCompatByProtocol accepts preserveVideoUrl under openai protocol", async () => {
|
||||
const { deepMergeCompatByProtocol } = await import("@/lib/db/models/compat");
|
||||
const result = deepMergeCompatByProtocol(
|
||||
{},
|
||||
{
|
||||
openai: { preserveVideoUrl: true },
|
||||
}
|
||||
);
|
||||
const result = deepMergeCompatByProtocol({}, {
|
||||
openai: { preserveVideoUrl: true },
|
||||
});
|
||||
// Valid protocol keys are 'openai', 'openai-responses', 'claude'
|
||||
equal(result.openai?.preserveVideoUrl, true);
|
||||
});
|
||||
|
||||
@@ -218,101 +218,3 @@ test("setRadarKey uses existing AES-256-GCM encryption from encryption.ts", () =
|
||||
const parts = body.split(":");
|
||||
assert.equal(parts.length, 3, "must have 3 parts (iv:ciphertext:authTag)");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// radar_referrals_cache (migration 142) -- standalone `GET /v1/referrals/latest`
|
||||
// cache, separate from radar_feed_cache (the catalog feed).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("getRadarReferralsCache returns null when no cache exists", () => {
|
||||
const result = radar.getRadarReferralsCache();
|
||||
assert.equal(result, null, "empty referrals cache must return null");
|
||||
});
|
||||
|
||||
test("setRadarReferralsCache then getRadarReferralsCache round-trips exactly", () => {
|
||||
const entry = {
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "live",
|
||||
payload: JSON.stringify({ referrals: { fixed: [], campaigns: [] } }),
|
||||
signature: "ed25519:referrals-sig-abc",
|
||||
};
|
||||
|
||||
radar.setRadarReferralsCache(entry);
|
||||
const result = radar.getRadarReferralsCache();
|
||||
|
||||
assert.ok(result, "referrals cache must not be null after set");
|
||||
assert.equal(result.generatedAt, entry.generatedAt, "generatedAt must round-trip");
|
||||
assert.equal(result.tier, entry.tier, "tier must round-trip");
|
||||
assert.equal(result.payload, entry.payload, "payload must round-trip byte-identically");
|
||||
assert.equal(result.signature, entry.signature, "signature must round-trip");
|
||||
assert.ok(result.fetchedAt, "fetchedAt must be set");
|
||||
});
|
||||
|
||||
test("second setRadarReferralsCache REPLACES the row (still single row)", () => {
|
||||
const db = core.getDbInstance();
|
||||
|
||||
radar.setRadarReferralsCache({
|
||||
generatedAt: "2026-08-07T10:00:00.000Z",
|
||||
tier: "community",
|
||||
payload: '{"old":true}',
|
||||
signature: "sig-old",
|
||||
});
|
||||
|
||||
radar.setRadarReferralsCache({
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "live",
|
||||
payload: '{"new":true}',
|
||||
signature: "sig-new",
|
||||
});
|
||||
|
||||
const result = radar.getRadarReferralsCache();
|
||||
assert.ok(result);
|
||||
assert.equal(result.generatedAt, "2026-08-07T12:00:00.000Z", "must have the second generatedAt");
|
||||
assert.equal(result.tier, "live", "must have the second tier");
|
||||
assert.equal(result.payload, '{"new":true}', "must have the second payload");
|
||||
|
||||
const count = db.prepare("SELECT COUNT(*) AS c FROM radar_referrals_cache").get() as { c: number };
|
||||
assert.equal(count.c, 1, "must have exactly one row");
|
||||
});
|
||||
|
||||
test("setRadarReferralsCache uses fetchedAt when provided", () => {
|
||||
const fixed = "2026-08-07T12:00:00.000Z";
|
||||
|
||||
radar.setRadarReferralsCache({
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "community",
|
||||
payload: "{}",
|
||||
signature: "sig",
|
||||
fetchedAt: fixed,
|
||||
});
|
||||
|
||||
const result = radar.getRadarReferralsCache();
|
||||
assert.ok(result);
|
||||
assert.equal(result.fetchedAt, fixed, "must use the provided fetchedAt");
|
||||
});
|
||||
|
||||
test("radar_referrals_cache is independent of radar_feed_cache (separate tables)", () => {
|
||||
radar.setRadarCache({
|
||||
version: "2026.08.01.1",
|
||||
tier: "community",
|
||||
payload: '{"catalog":true}',
|
||||
signature: "catalog-sig",
|
||||
});
|
||||
radar.setRadarReferralsCache({
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "live",
|
||||
payload: '{"referrals":true}',
|
||||
signature: "referrals-sig",
|
||||
});
|
||||
|
||||
const catalogCache = radar.getRadarCache();
|
||||
const referralsCache = radar.getRadarReferralsCache();
|
||||
|
||||
assert.equal(catalogCache?.payload, '{"catalog":true}');
|
||||
assert.equal(referralsCache?.payload, '{"referrals":true}');
|
||||
assert.notEqual(
|
||||
catalogCache?.payload,
|
||||
referralsCache?.payload,
|
||||
"the two caches must never share storage"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
/**
|
||||
* tests/unit/radar-referrals-route.test.ts
|
||||
*
|
||||
* TDD regression guard for GET /api/radar/referrals (D28 -- referral links /
|
||||
* TDD regression guard for GET /api/radar/referrals (D28 — referral links /
|
||||
* free credits, client side). Mirrors tests/unit/radar-api-routes.test.ts:
|
||||
*
|
||||
* - Flag off => 404, checked BEFORE auth (byte-identical inertia).
|
||||
* - Flag on, no auth => 401.
|
||||
* - Flag on, authenticated, no cache => 200 with { fixed: [], campaigns:
|
||||
* [], tier: null }.
|
||||
* - Flag on, authenticated, cached referrals feed => 200 with the cached
|
||||
* fixed/campaigns + tier.
|
||||
* - Sync-on-read: the route triggers `syncRadarReferrals()` inline when the
|
||||
* cache is stale/missing (opt-in false in every test here, so the
|
||||
* triggered sync always self-gates to a safe `opt_out` no-op -- this
|
||||
* proves the trigger never touches the network in these tests while
|
||||
* still exercising the code path).
|
||||
* - Flag on, authenticated, cached feed with referrals => 200 with the
|
||||
* cached fixed/campaigns + tier.
|
||||
* - Error responses never leak stack traces (Hard Rule #12).
|
||||
*
|
||||
* NEVER proxies the private feed server directly -- this route's own source
|
||||
* contains no `fetch(` call; the network only happens inside
|
||||
* `syncRadarReferrals()` (`src/lib/radar/referralsSync.ts`), which this
|
||||
* route calls but never inlines.
|
||||
* NEVER proxies the private feed server — this route only reads the local
|
||||
* cache written by POST /api/radar/sync.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
@@ -76,12 +69,18 @@ function resetStorage() {
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
function baseReferralsFeed(): Record<string, unknown> {
|
||||
function baseFeed(): Record<string, unknown> {
|
||||
return {
|
||||
feed: "omniroute-radar-referrals",
|
||||
feed: "omniroute-radar",
|
||||
schemaVersion: 1,
|
||||
version: "2026-08-07.1",
|
||||
generatedAt: new Date().toISOString(),
|
||||
referrals: { fixed: [], campaigns: [] },
|
||||
tier: "live",
|
||||
counts: { providers: 0, models: 0 },
|
||||
providers: [],
|
||||
models: [],
|
||||
quirks: [],
|
||||
totals: { dedupedTokensPerMonth: 0, modelCount: 0, poolCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,7 +114,7 @@ test("GET /api/radar/referrals: flag on, no auth => 401", async () => {
|
||||
assert.ok(!JSON.stringify(body).includes("at /"), "Response must not leak stack traces");
|
||||
});
|
||||
|
||||
test("GET /api/radar/referrals: flag on, authenticated, no cache => 200 empty shape (sync-on-read no-ops: opt-in false)", async () => {
|
||||
test("GET /api/radar/referrals: flag on, authenticated, no cache => 200 empty shape", async () => {
|
||||
resetStorage();
|
||||
process.env.RADAR_ENABLED = "true";
|
||||
|
||||
@@ -129,12 +128,12 @@ test("GET /api/radar/referrals: flag on, authenticated, no cache => 200 empty sh
|
||||
assert.equal(body.tier, null);
|
||||
});
|
||||
|
||||
test("GET /api/radar/referrals: flag on, authenticated, cached referrals feed => returns fixed/campaigns/tier", async () => {
|
||||
test("GET /api/radar/referrals: flag on, authenticated, cached feed => returns fixed/campaigns/tier", async () => {
|
||||
resetStorage();
|
||||
process.env.RADAR_ENABLED = "true";
|
||||
|
||||
const feed = {
|
||||
...baseReferralsFeed(),
|
||||
...baseFeed(),
|
||||
referrals: {
|
||||
fixed: [
|
||||
{
|
||||
@@ -149,15 +148,11 @@ test("GET /api/radar/referrals: flag on, authenticated, cached referrals feed =>
|
||||
campaigns: [],
|
||||
},
|
||||
};
|
||||
radarDb.setRadarReferralsCache({
|
||||
generatedAt: feed.generatedAt as string,
|
||||
radarDb.setRadarCache({
|
||||
version: "2026-08-07.1",
|
||||
tier: "live",
|
||||
payload: JSON.stringify(feed),
|
||||
signature: "test-signature",
|
||||
// Fresh timestamp -- inside the 1h staleness window, so sync-on-read
|
||||
// does NOT overwrite this row (opt-in is false anyway, but this also
|
||||
// proves the "not stale" branch is exercised, not just "opt_out").
|
||||
fetchedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const { GET } = await import("../../src/app/api/radar/referrals/route.ts");
|
||||
@@ -171,49 +166,6 @@ test("GET /api/radar/referrals: flag on, authenticated, cached referrals feed =>
|
||||
assert.equal(body.tier, "live");
|
||||
});
|
||||
|
||||
test("GET /api/radar/referrals: stale cached referrals feed still served (sync-on-read triggers but opt-in false => no-op, cache untouched)", async () => {
|
||||
resetStorage();
|
||||
process.env.RADAR_ENABLED = "true";
|
||||
|
||||
const feed = {
|
||||
...baseReferralsFeed(),
|
||||
referrals: {
|
||||
fixed: [
|
||||
{
|
||||
provider: "cerebras",
|
||||
url: "https://cerebras.ai/?ref=omniroute",
|
||||
kind: "fixo",
|
||||
validUntil: null,
|
||||
requiredAction: null,
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
campaigns: [],
|
||||
},
|
||||
};
|
||||
const staleFetchedAt = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(); // 2h ago
|
||||
radarDb.setRadarReferralsCache({
|
||||
generatedAt: feed.generatedAt as string,
|
||||
tier: "community",
|
||||
payload: JSON.stringify(feed),
|
||||
signature: "test-signature",
|
||||
fetchedAt: staleFetchedAt,
|
||||
});
|
||||
|
||||
const { GET } = await import("../../src/app/api/radar/referrals/route.ts");
|
||||
const response = await GET(mockGetRequest(undefined, await authHeaders()));
|
||||
const body = await response.json();
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(body.fixed.length, 1, "stale cache is still served while the sync-on-read no-ops");
|
||||
assert.equal(body.fixed[0].provider, "cerebras");
|
||||
assert.equal(body.tier, "community");
|
||||
|
||||
// The no-op sync must never have overwritten fetchedAt/cache contents.
|
||||
const cacheAfter = radarDb.getRadarReferralsCache();
|
||||
assert.equal(cacheAfter?.fetchedAt, staleFetchedAt);
|
||||
});
|
||||
|
||||
test("GET /api/radar/referrals: never proxies the private feed server (route source has no upstream fetch)", async () => {
|
||||
const routeSrc = fs.readFileSync(
|
||||
path.resolve(process.cwd(), "src/app/api/radar/referrals/route.ts"),
|
||||
|
||||
@@ -1,672 +0,0 @@
|
||||
/**
|
||||
* tests/unit/radar-referrals-sync.test.ts
|
||||
*
|
||||
* TDD regression guard for the standalone Radar referrals feed sync layer
|
||||
* (`GET /v1/referrals/latest`) — the fix that removes the up-to-30-day
|
||||
* community-tier delay referral links used to inherit from the catalog
|
||||
* feed:
|
||||
* - referralsFeedSchema.ts: Zod schema validation
|
||||
* - referralsSync.ts: download/verify/validate/cache pipeline +
|
||||
* `shouldSyncReferralsOnRead` staleness helper
|
||||
*
|
||||
* Mirrors tests/unit/radar-sync.test.ts's structure and conventions (same
|
||||
* ephemeral Ed25519 keypair + `RADAR_FEED_PUBKEY` override, same
|
||||
* mockResponse() shape) — the referrals feed reuses the exact same pinned
|
||||
* key as the catalog feed.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Generate ephemeral Ed25519 keypair for testing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
||||
const PUB_KEY_DER = publicKey.export({ type: "spki", format: "der" });
|
||||
const PUB_KEY_B64 = PUB_KEY_DER.toString("base64");
|
||||
|
||||
// Inject as env override so pinnedKeys.ts picks it up (fork path) — same
|
||||
// pinned key backs both the catalog and the referrals feed.
|
||||
process.env.RADAR_FEED_PUBKEY = PUB_KEY_B64;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function signBytes(bytes: Buffer): string {
|
||||
const sig = crypto.sign(null, bytes, privateKey);
|
||||
return sig.toString("base64");
|
||||
}
|
||||
|
||||
function tamperByte(buf: Buffer): Buffer {
|
||||
const copy = Buffer.from(buf);
|
||||
copy[0] = copy[0] ^ 0xff;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/** Build a minimal Response-like object for fetch mock (matches radar-sync.test.ts). */
|
||||
function mockResponse(body: Buffer, headers: Record<string, string> = {}, status = 200): Response {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
headers: new Map(Object.entries(headers)),
|
||||
arrayBuffer: () =>
|
||||
Promise.resolve(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength)),
|
||||
} as unknown as Response;
|
||||
}
|
||||
|
||||
function baseReferralsFeed(generatedAt = "2026-08-07T12:00:00.000Z"): Record<string, unknown> {
|
||||
return {
|
||||
feed: "omniroute-radar-referrals",
|
||||
schemaVersion: 1,
|
||||
generatedAt,
|
||||
referrals: {
|
||||
fixed: [
|
||||
{
|
||||
provider: "groq",
|
||||
url: "https://groq.com/?ref=omniroute",
|
||||
kind: "fixo",
|
||||
validUntil: null,
|
||||
requiredAction: null,
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
campaigns: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function feedBytes(feed: Record<string, unknown>): Buffer {
|
||||
return Buffer.from(JSON.stringify(feed));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Import modules under test (after env override)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const referralsFeedSchema = await import("../../src/lib/radar/referralsFeedSchema.ts");
|
||||
const referralsSync = await import("../../src/lib/radar/referralsSync.ts");
|
||||
|
||||
// ===========================================================================
|
||||
// referralsFeedSchema.ts
|
||||
// ===========================================================================
|
||||
|
||||
test("RadarReferralsFeedSchema: valid feed parses successfully", () => {
|
||||
const result = referralsFeedSchema.RadarReferralsFeedSchema.safeParse(baseReferralsFeed());
|
||||
assert.equal(
|
||||
result.success,
|
||||
true,
|
||||
"valid feed must parse: " + (result.success ? "" : JSON.stringify(result.error?.issues))
|
||||
);
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: rejects wrong feed literal", () => {
|
||||
const feed = { ...baseReferralsFeed(), feed: "omniroute-radar" };
|
||||
const result = referralsFeedSchema.RadarReferralsFeedSchema.safeParse(feed);
|
||||
assert.equal(result.success, false, "must reject a catalog-feed literal");
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: rejects wrong schemaVersion", () => {
|
||||
const feed = { ...baseReferralsFeed(), schemaVersion: 2 };
|
||||
const result = referralsFeedSchema.RadarReferralsFeedSchema.safeParse(feed);
|
||||
assert.equal(result.success, false);
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: rejects missing referrals section", () => {
|
||||
const feed = baseReferralsFeed();
|
||||
delete (feed as Record<string, unknown>).referrals;
|
||||
const result = referralsFeedSchema.RadarReferralsFeedSchema.safeParse(feed);
|
||||
assert.equal(result.success, false, "referrals section is required (no old-feed compat needed here)");
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: rejects a non-https referral url", () => {
|
||||
const feed = baseReferralsFeed();
|
||||
(feed.referrals as { fixed: Array<Record<string, unknown>> }).fixed[0]!.url =
|
||||
"http://groq.com/?ref=omniroute";
|
||||
const result = referralsFeedSchema.RadarReferralsFeedSchema.safeParse(feed);
|
||||
assert.equal(result.success, false);
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: rejects an invalid generatedAt", () => {
|
||||
const feed = { ...baseReferralsFeed(), generatedAt: "not-a-date" };
|
||||
const result = referralsFeedSchema.RadarReferralsFeedSchema.safeParse(feed);
|
||||
assert.equal(result.success, false);
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// shouldSyncReferralsOnRead
|
||||
// ===========================================================================
|
||||
|
||||
test("shouldSyncReferralsOnRead: null fetchedAt => stale (sync now)", () => {
|
||||
assert.equal(referralsSync.shouldSyncReferralsOnRead(null, Date.now()), true);
|
||||
});
|
||||
|
||||
test("shouldSyncReferralsOnRead: unparseable fetchedAt => stale", () => {
|
||||
assert.equal(referralsSync.shouldSyncReferralsOnRead("garbage", Date.now()), true);
|
||||
});
|
||||
|
||||
test("shouldSyncReferralsOnRead: fresh (< 1h) => not stale", () => {
|
||||
const now = Date.parse("2026-08-07T12:00:00.000Z");
|
||||
const fetchedAt = new Date(now - 30 * 60 * 1000).toISOString(); // 30m ago
|
||||
assert.equal(referralsSync.shouldSyncReferralsOnRead(fetchedAt, now), false);
|
||||
});
|
||||
|
||||
test("shouldSyncReferralsOnRead: exactly at the boundary => stale", () => {
|
||||
const now = Date.parse("2026-08-07T12:00:00.000Z");
|
||||
const fetchedAt = new Date(now - 60 * 60 * 1000).toISOString(); // exactly 1h ago
|
||||
assert.equal(referralsSync.shouldSyncReferralsOnRead(fetchedAt, now), true);
|
||||
});
|
||||
|
||||
test("shouldSyncReferralsOnRead: old (> 1h) => stale", () => {
|
||||
const now = Date.parse("2026-08-07T12:00:00.000Z");
|
||||
const fetchedAt = new Date(now - 2 * 60 * 60 * 1000).toISOString(); // 2h ago
|
||||
assert.equal(referralsSync.shouldSyncReferralsOnRead(fetchedAt, now), true);
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// syncRadarReferrals — gating (flag/opt-in), never touching the network
|
||||
// ===========================================================================
|
||||
|
||||
test("syncRadarReferrals: flag off => disabled, no fetch call", async () => {
|
||||
let fetchCalled = false;
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => false,
|
||||
fetch: (() => {
|
||||
fetchCalled = true;
|
||||
return Promise.resolve(mockResponse(Buffer.from("{}")));
|
||||
}) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
assert.deepEqual(result, { status: "disabled" });
|
||||
assert.equal(fetchCalled, false);
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: opt-in false => opt_out, no fetch call", async () => {
|
||||
let fetchCalled = false;
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: false, supporterKey: null }),
|
||||
fetch: (() => {
|
||||
fetchCalled = true;
|
||||
return Promise.resolve(mockResponse(Buffer.from("{}")));
|
||||
}) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
assert.deepEqual(result, { status: "opt_out" });
|
||||
assert.equal(fetchCalled, false);
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// syncRadarReferrals — signature verification over exact bytes
|
||||
// ===========================================================================
|
||||
|
||||
test("syncRadarReferrals: valid signature => cache updated, payload byte-identical", async () => {
|
||||
const feed = baseReferralsFeed();
|
||||
const bytes = feedBytes(feed);
|
||||
const sig = signBytes(bytes);
|
||||
const cacheStore: referralsSync.RadarReferralsCacheEntry[] = [];
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: (entry) => {
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig, "x-omniroute-feed-tier": "community" })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
now: () => new Date("2026-08-07T12:05:00.000Z"),
|
||||
});
|
||||
|
||||
assert.equal(result.status, "updated");
|
||||
assert.equal(cacheStore.length, 1);
|
||||
assert.equal(cacheStore[0]!.payload, bytes.toString("utf-8"));
|
||||
assert.equal(cacheStore[0]!.generatedAt, feed.generatedAt);
|
||||
assert.equal(cacheStore[0]!.tier, "community");
|
||||
assert.equal(cacheStore[0]!.signature, sig);
|
||||
assert.equal(cacheStore[0]!.fetchedAt, "2026-08-07T12:05:00.000Z");
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: tampered bytes => invalid_signature, cache untouched", async () => {
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
const sig = signBytes(bytes);
|
||||
const tampered = tamperByte(bytes);
|
||||
let cacheWritten = false;
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
cacheWritten = true;
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(tampered, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "invalid_signature");
|
||||
assert.equal(cacheWritten, false);
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: missing signature header => invalid_signature", async () => {
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
let cacheWritten = false;
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
cacheWritten = true;
|
||||
},
|
||||
fetch: (() => Promise.resolve(mockResponse(bytes, {}))) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "invalid_signature");
|
||||
assert.equal(cacheWritten, false);
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: valid sig over garbage JSON => invalid_schema, cache untouched", async () => {
|
||||
const garbageBytes = Buffer.from('{"not":"a-valid-referrals-feed"}');
|
||||
const sig = signBytes(garbageBytes);
|
||||
let cacheWritten = false;
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
cacheWritten = true;
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(garbageBytes, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "invalid_schema");
|
||||
assert.equal(cacheWritten, false);
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// syncRadarReferrals — generatedAt floor (replay/no-op guard)
|
||||
// ===========================================================================
|
||||
|
||||
test("syncRadarReferrals: same generatedAt as cache => stale, cache untouched", async () => {
|
||||
const feed = baseReferralsFeed("2026-08-07T12:00:00.000Z");
|
||||
const bytes = feedBytes(feed);
|
||||
const sig = signBytes(bytes);
|
||||
let cacheWritten = false;
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => ({
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "community",
|
||||
payload: "{}",
|
||||
signature: "old-sig",
|
||||
}),
|
||||
setCache: () => {
|
||||
cacheWritten = true;
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "stale");
|
||||
assert.equal(cacheWritten, false);
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: older generatedAt than cache => stale (replay rejected)", async () => {
|
||||
const feed = baseReferralsFeed("2026-08-07T10:00:00.000Z"); // older
|
||||
const bytes = feedBytes(feed);
|
||||
const sig = signBytes(bytes);
|
||||
let cacheWritten = false;
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => ({
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "community",
|
||||
payload: "{}",
|
||||
signature: "old-sig",
|
||||
}),
|
||||
setCache: () => {
|
||||
cacheWritten = true;
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "stale");
|
||||
assert.equal(cacheWritten, false);
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: newer generatedAt than cache => updated", async () => {
|
||||
const feed = baseReferralsFeed("2026-08-07T13:00:00.000Z"); // newer
|
||||
const bytes = feedBytes(feed);
|
||||
const sig = signBytes(bytes);
|
||||
const cacheStore: referralsSync.RadarReferralsCacheEntry[] = [];
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => ({
|
||||
generatedAt: "2026-08-07T12:00:00.000Z",
|
||||
tier: "community",
|
||||
payload: "{}",
|
||||
signature: "old-sig",
|
||||
}),
|
||||
setCache: (entry) => {
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
now: () => new Date("2026-08-07T13:05:00.000Z"),
|
||||
});
|
||||
|
||||
assert.equal(result.status, "updated");
|
||||
assert.equal(cacheStore.length, 1);
|
||||
assert.equal(cacheStore[0]!.generatedAt, "2026-08-07T13:00:00.000Z");
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// syncRadarReferrals — 2 identical requests => same signature => same cache
|
||||
// (determinism contract from the server: generatedAt is the max updatedAt
|
||||
// across referral links, so unchanged data re-signs identically)
|
||||
// ===========================================================================
|
||||
|
||||
test("syncRadarReferrals: two identical fetches (no cache between) produce identical cache entries modulo fetchedAt", async () => {
|
||||
const feed = baseReferralsFeed();
|
||||
const bytes = feedBytes(feed);
|
||||
const sig = signBytes(bytes);
|
||||
const cacheStore: referralsSync.RadarReferralsCacheEntry[] = [];
|
||||
|
||||
const run = () =>
|
||||
referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null, // simulate two independent "first sync" calls
|
||||
setCache: (entry) => {
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
now: () => new Date("2026-08-07T12:05:00.000Z"),
|
||||
});
|
||||
|
||||
await run();
|
||||
await run();
|
||||
|
||||
assert.equal(cacheStore.length, 2);
|
||||
assert.equal(cacheStore[0]!.signature, cacheStore[1]!.signature);
|
||||
assert.equal(cacheStore[0]!.payload, cacheStore[1]!.payload);
|
||||
assert.equal(cacheStore[0]!.generatedAt, cacheStore[1]!.generatedAt);
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// syncRadarReferrals — served-tier header
|
||||
// ===========================================================================
|
||||
|
||||
test("syncRadarReferrals: header 'community' => cache + result use community", async () => {
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
const sig = signBytes(bytes);
|
||||
const cacheStore: referralsSync.RadarReferralsCacheEntry[] = [];
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: (entry) => {
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, {
|
||||
"x-omniroute-feed-signature": sig,
|
||||
"x-omniroute-feed-tier": "community",
|
||||
})
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "updated");
|
||||
if (result.status === "updated") assert.equal(result.tier, "community");
|
||||
assert.equal(cacheStore[0]!.tier, "community");
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: header 'live' (supporter key) => cache + result use live", async () => {
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
const sig = signBytes(bytes);
|
||||
const cacheStore: referralsSync.RadarReferralsCacheEntry[] = [];
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: "omr_supporter-key" }),
|
||||
getCache: () => null,
|
||||
setCache: (entry) => {
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, {
|
||||
"x-omniroute-feed-signature": sig,
|
||||
"x-omniroute-feed-tier": "live",
|
||||
})
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "updated");
|
||||
if (result.status === "updated") assert.equal(result.tier, "live");
|
||||
assert.equal(cacheStore[0]!.tier, "live");
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: header absent => falls back to 'community' (no body tier field to fall back to)", async () => {
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
const sig = signBytes(bytes);
|
||||
const cacheStore: referralsSync.RadarReferralsCacheEntry[] = [];
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: (entry) => {
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(mockResponse(bytes, { "x-omniroute-feed-signature": sig }))) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "updated");
|
||||
if (result.status === "updated") assert.equal(result.tier, "community");
|
||||
assert.equal(cacheStore[0]!.tier, "community");
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: garbage tier header => never trusted, falls back to 'community'", async () => {
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
const sig = signBytes(bytes);
|
||||
const cacheStore: referralsSync.RadarReferralsCacheEntry[] = [];
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: (entry) => {
|
||||
cacheStore.push(entry);
|
||||
},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, {
|
||||
"x-omniroute-feed-signature": sig,
|
||||
"x-omniroute-feed-tier": "premium",
|
||||
})
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "updated");
|
||||
if (result.status === "updated") {
|
||||
assert.equal(result.tier, "community");
|
||||
assert.notEqual(result.tier as string, "premium");
|
||||
}
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// syncRadarReferrals — Authorization header (supporter key)
|
||||
// ===========================================================================
|
||||
|
||||
test("syncRadarReferrals: sends Authorization header when supporter key exists", async () => {
|
||||
let capturedHeaders: Record<string, string> = {};
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
const sig = signBytes(bytes);
|
||||
|
||||
await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: "omr_test-key-123" }),
|
||||
getCache: () => null,
|
||||
setCache: () => {},
|
||||
fetch: ((url: string, init: RequestInit) => {
|
||||
capturedHeaders = Object.fromEntries(
|
||||
(init.headers as Record<string, string> | undefined)
|
||||
? Object.entries(init.headers as Record<string, string>)
|
||||
: []
|
||||
);
|
||||
return Promise.resolve(mockResponse(bytes, { "x-omniroute-feed-signature": sig }));
|
||||
}) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(capturedHeaders["Authorization"], "Bearer omr_test-key-123");
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: no Authorization header when no supporter key", async () => {
|
||||
let capturedHeaders: Record<string, string> = {};
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
const sig = signBytes(bytes);
|
||||
|
||||
await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {},
|
||||
fetch: ((url: string, init: RequestInit) => {
|
||||
capturedHeaders = Object.fromEntries(
|
||||
(init.headers as Record<string, string> | undefined)
|
||||
? Object.entries(init.headers as Record<string, string>)
|
||||
: []
|
||||
);
|
||||
return Promise.resolve(mockResponse(bytes, { "x-omniroute-feed-signature": sig }));
|
||||
}) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(capturedHeaders["Authorization"], undefined);
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// syncRadarReferrals — errors, never leaking a stack
|
||||
// ===========================================================================
|
||||
|
||||
test("syncRadarReferrals: network error => error with no stack in reason", async () => {
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
fetch: (() => Promise.reject(new Error("ECONNREFUSED"))) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "error");
|
||||
if (result.status === "error") {
|
||||
assert.ok(result.reason.length > 0);
|
||||
assert.ok(!result.reason.includes("at ") && !result.reason.includes(".ts:"));
|
||||
}
|
||||
});
|
||||
|
||||
test("syncRadarReferrals: HTTP non-200 => error mentioning the status code", async () => {
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
fetch: (() =>
|
||||
Promise.resolve(mockResponse(Buffer.from("Internal Server Error"), {}, 500))) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.equal(result.status, "error");
|
||||
if (result.status === "error") assert.ok(result.reason.includes("500"));
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// syncRadarReferrals — 10 MB response cap
|
||||
// ===========================================================================
|
||||
|
||||
test("FIX: Content-Length exceeding the 10MB cap => too_large, cache untouched, body never read", async () => {
|
||||
let arrayBufferCalled = false;
|
||||
const oversizedContentLength = String(10 * 1024 * 1024 + 1);
|
||||
const response = mockResponse(Buffer.from("irrelevant"), {
|
||||
"content-length": oversizedContentLength,
|
||||
});
|
||||
const originalArrayBuffer = response.arrayBuffer.bind(response);
|
||||
(response as unknown as { arrayBuffer: () => Promise<ArrayBuffer> }).arrayBuffer = () => {
|
||||
arrayBufferCalled = true;
|
||||
return originalArrayBuffer();
|
||||
};
|
||||
|
||||
let setCacheCalled = false;
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
setCacheCalled = true;
|
||||
},
|
||||
fetch: (() => Promise.resolve(response)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "too_large" });
|
||||
assert.equal(setCacheCalled, false);
|
||||
assert.equal(arrayBufferCalled, false);
|
||||
});
|
||||
|
||||
test("FIX: oversized body without a trustworthy Content-Length header => too_large, cache untouched", async () => {
|
||||
const oversized = Buffer.alloc(10 * 1024 * 1024 + 1, 0x41);
|
||||
let setCacheCalled = false;
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {
|
||||
setCacheCalled = true;
|
||||
},
|
||||
fetch: (() => Promise.resolve(mockResponse(oversized, {}))) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "too_large" });
|
||||
assert.equal(setCacheCalled, false);
|
||||
});
|
||||
|
||||
test("FIX: body within the 10MB cap proceeds normally (never returns too_large)", async () => {
|
||||
const bytes = feedBytes(baseReferralsFeed());
|
||||
const sig = signBytes(bytes);
|
||||
|
||||
const result = await referralsSync.syncRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getSettings: () => ({ optIn: true, supporterKey: null }),
|
||||
getCache: () => null,
|
||||
setCache: () => {},
|
||||
fetch: (() =>
|
||||
Promise.resolve(
|
||||
mockResponse(bytes, { "x-omniroute-feed-signature": sig })
|
||||
)) as unknown as typeof globalThis.fetch,
|
||||
});
|
||||
|
||||
assert.notEqual(result.status, "too_large");
|
||||
});
|
||||
@@ -2,33 +2,29 @@
|
||||
* tests/unit/radar-referrals.test.ts
|
||||
*
|
||||
* TDD regression guard for the client-side "referral links / free credits"
|
||||
* feature (D28). Referral links now come from the STANDALONE, always-current
|
||||
* `GET /v1/referrals/latest` feed (`radar_referrals_cache` table /
|
||||
* `referralsSync.ts`) instead of being extracted from the catalog feed's
|
||||
* cached snapshot -- the catalog feed on the community tier can be up to 30
|
||||
* days stale, so referral links extracted from it used to lag the server by
|
||||
* the same amount. This suite covers the CLIENT side only:
|
||||
* feature (D28). The server already publishes a `referrals` section on the
|
||||
* signed feed (`{ fixed: RadarReferral[], campaigns: RadarReferral[] }`) —
|
||||
* this suite covers the CLIENT side only:
|
||||
*
|
||||
* - RadarReferralsFeedSchema (`referralsFeedSchema.ts`): valid feed parses;
|
||||
* an invalid referral (non-https url) is rejected. (Schema-level
|
||||
* coverage for the referrals feed's error/replay/tier paths lives in
|
||||
* `tests/unit/radar-referrals-sync.test.ts`.)
|
||||
* - RadarFeedSchema: a feed WITHOUT `referrals` stays valid (compat with
|
||||
* old cached feeds); a feed WITH an invalid referral (non-https url) is
|
||||
* rejected.
|
||||
* - getRadarReferrals(): flag off => {fixed:[],campaigns:[]}; no cache =>
|
||||
* same; corrupt cache => same (never throws).
|
||||
* same; corrupt cache => same; feed without the section => same
|
||||
* (never throws).
|
||||
* - getDefaultReferralFor(): returns the fixed+isDefault referral for a
|
||||
* provider, ignores campaigns, returns null when none.
|
||||
* - findDefaultReferral() (pure helper, DB-free -- must be importable from
|
||||
* a client bundle without pulling in @/lib/db/*) -- same contract as
|
||||
* above, operating directly on a `fixed` array.
|
||||
* - findDefaultReferral() (pure helper, DB-free — must be importable from a
|
||||
* client bundle without pulling in @/lib/db/*): same contract as above,
|
||||
* operating directly on a `fixed` array.
|
||||
*
|
||||
* No DB is touched here -- all DB access is injected via `deps`, matching
|
||||
* No DB is touched here — all DB access is injected via `deps`, matching
|
||||
* the existing tests/unit/radar-apply-feed.test.ts convention.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { RadarReferralsFeedSchema } from "../../src/lib/radar/referralsFeedSchema.ts";
|
||||
import type { RadarReferral } from "../../src/lib/radar/feedSchema.ts";
|
||||
import { RadarFeedSchema, type RadarFeed, type RadarReferral } from "../../src/lib/radar/feedSchema.ts";
|
||||
import { findDefaultReferral } from "../../src/lib/radar/referrals.ts";
|
||||
import { getRadarReferrals, getDefaultReferralFor } from "../../src/lib/radar/index.ts";
|
||||
|
||||
@@ -36,13 +32,18 @@ import { getRadarReferrals, getDefaultReferralFor } from "../../src/lib/radar/in
|
||||
// Fixtures
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** A standalone referrals feed (`GET /v1/referrals/latest` shape). */
|
||||
function baseReferralsFeed(): Record<string, unknown> {
|
||||
function baseFeed(): Record<string, unknown> {
|
||||
return {
|
||||
feed: "omniroute-radar-referrals",
|
||||
feed: "omniroute-radar",
|
||||
schemaVersion: 1,
|
||||
version: "2026-08-07.1",
|
||||
generatedAt: new Date().toISOString(),
|
||||
referrals: { fixed: [], campaigns: [] },
|
||||
tier: "live",
|
||||
counts: { providers: 0, models: 0 },
|
||||
providers: [],
|
||||
models: [],
|
||||
quirks: [],
|
||||
totals: { dedupedTokensPerMonth: 0, modelCount: 0, poolCount: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,29 +59,25 @@ function makeReferral(overrides: Partial<RadarReferral> = {}): RadarReferral {
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a `getRadarReferralsCache()`-shaped row from a referrals feed object. */
|
||||
function cacheRowFor(feed: Record<string, unknown>, overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
generatedAt: feed.generatedAt as string,
|
||||
tier: "live",
|
||||
payload: JSON.stringify(feed),
|
||||
fetchedAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RadarReferralsFeedSchema -- validation
|
||||
// RadarFeedSchema — compat + validation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("RadarReferralsFeedSchema: minimal empty-referrals feed parses successfully", () => {
|
||||
const parsed = RadarReferralsFeedSchema.parse(baseReferralsFeed());
|
||||
test("RadarFeedSchema: feed without `referrals` stays valid (old-feed compat)", () => {
|
||||
const parsed = RadarFeedSchema.parse(baseFeed());
|
||||
assert.deepEqual(parsed.referrals, { fixed: [], campaigns: [] });
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: full referrals section round-trips", () => {
|
||||
test("RadarFeedSchema: feed with `referrals.fixed` but no `campaigns` defaults campaigns to []", () => {
|
||||
const feed = { ...baseFeed(), referrals: { fixed: [makeReferral()] } };
|
||||
const parsed = RadarFeedSchema.parse(feed);
|
||||
assert.equal(parsed.referrals.fixed.length, 1);
|
||||
assert.deepEqual(parsed.referrals.campaigns, []);
|
||||
});
|
||||
|
||||
test("RadarFeedSchema: full referrals section round-trips", () => {
|
||||
const feed = {
|
||||
...baseReferralsFeed(),
|
||||
...baseFeed(),
|
||||
referrals: {
|
||||
fixed: [makeReferral()],
|
||||
campaigns: [
|
||||
@@ -94,30 +91,30 @@ test("RadarReferralsFeedSchema: full referrals section round-trips", () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
const parsed = RadarReferralsFeedSchema.parse(feed);
|
||||
const parsed = RadarFeedSchema.parse(feed);
|
||||
assert.equal(parsed.referrals.fixed.length, 1);
|
||||
assert.equal(parsed.referrals.campaigns.length, 1);
|
||||
assert.equal(parsed.referrals.campaigns[0]!.kind, "campanha");
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: rejects a referral with a non-https url", () => {
|
||||
test("RadarFeedSchema: rejects a referral with a non-https url", () => {
|
||||
const feed = {
|
||||
...baseReferralsFeed(),
|
||||
...baseFeed(),
|
||||
referrals: { fixed: [makeReferral({ url: "http://groq.com/?ref=omniroute" })], campaigns: [] },
|
||||
};
|
||||
assert.throws(() => RadarReferralsFeedSchema.parse(feed));
|
||||
assert.throws(() => RadarFeedSchema.parse(feed));
|
||||
});
|
||||
|
||||
test("RadarReferralsFeedSchema: rejects an invalid `kind`", () => {
|
||||
test("RadarFeedSchema: rejects an invalid `kind`", () => {
|
||||
const feed = {
|
||||
...baseReferralsFeed(),
|
||||
...baseFeed(),
|
||||
referrals: { fixed: [{ ...makeReferral(), kind: "bogus" }], campaigns: [] },
|
||||
};
|
||||
assert.throws(() => RadarReferralsFeedSchema.parse(feed));
|
||||
assert.throws(() => RadarFeedSchema.parse(feed));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// findDefaultReferral -- pure, DB-free helper (client-safe)
|
||||
// findDefaultReferral — pure, DB-free helper (client-safe)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("findDefaultReferral: returns the fixed+isDefault referral for the provider", () => {
|
||||
@@ -144,7 +141,7 @@ test("findDefaultReferral: empty array => null", () => {
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getRadarReferrals() -- flag/cache gating, never throws
|
||||
// getRadarReferrals() — flag/cache gating, never throws
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("getRadarReferrals: flag off => empty, cache never read", () => {
|
||||
@@ -169,7 +166,7 @@ test("getRadarReferrals: flag on, corrupt cache payload => empty (defensive, nev
|
||||
const result = getRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getCache: () => ({
|
||||
generatedAt: "x",
|
||||
version: "x",
|
||||
tier: "live",
|
||||
payload: "{not-json",
|
||||
fetchedAt: new Date().toISOString(),
|
||||
@@ -178,41 +175,37 @@ test("getRadarReferrals: flag on, corrupt cache payload => empty (defensive, nev
|
||||
assert.deepEqual(result, { fixed: [], campaigns: [] });
|
||||
});
|
||||
|
||||
test("getRadarReferrals: flag on, cached payload fails schema validation => empty (defensive)", () => {
|
||||
test("getRadarReferrals: flag on, cached feed has no `referrals` section => empty", () => {
|
||||
const result = getRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getCache: () => ({
|
||||
generatedAt: "x",
|
||||
version: "x",
|
||||
tier: "live",
|
||||
// Wrong `feed` literal -- fails RadarReferralsFeedSchema.
|
||||
payload: JSON.stringify({ ...baseReferralsFeed(), feed: "omniroute-radar" }),
|
||||
payload: JSON.stringify(baseFeed()),
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
assert.deepEqual(result, { fixed: [], campaigns: [] });
|
||||
});
|
||||
|
||||
test("getRadarReferrals: flag on, cached referrals feed => returns them", () => {
|
||||
test("getRadarReferrals: flag on, cached feed has referrals => returns them", () => {
|
||||
const feed = {
|
||||
...baseReferralsFeed(),
|
||||
...baseFeed(),
|
||||
referrals: { fixed: [makeReferral()], campaigns: [] },
|
||||
};
|
||||
const result = getRadarReferrals({
|
||||
getFlag: () => true,
|
||||
getCache: () => cacheRowFor(feed),
|
||||
getCache: () => ({
|
||||
version: "x",
|
||||
tier: "live",
|
||||
payload: JSON.stringify(feed),
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
assert.equal(result.fixed.length, 1);
|
||||
assert.equal(result.fixed[0]!.provider, "groq");
|
||||
});
|
||||
|
||||
test("getRadarReferrals: default getCache reads from getRadarReferralsCache (module wiring)", async () => {
|
||||
// Confirms the accessor's default dep is the NEW referrals cache reader,
|
||||
// not the old catalog cache -- exercised via the flag-off short-circuit
|
||||
// (no DB touch needed) so this stays a pure unit test.
|
||||
const result = getRadarReferrals({ getFlag: () => false });
|
||||
assert.deepEqual(result, { fixed: [], campaigns: [] });
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// getDefaultReferralFor()
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -224,7 +217,7 @@ test("getDefaultReferralFor: flag off => null", () => {
|
||||
|
||||
test("getDefaultReferralFor: returns the fixed default referral, ignoring campaigns", () => {
|
||||
const feed = {
|
||||
...baseReferralsFeed(),
|
||||
...baseFeed(),
|
||||
referrals: {
|
||||
fixed: [makeReferral({ provider: "groq", isDefault: true })],
|
||||
campaigns: [makeReferral({ provider: "groq", kind: "campanha", isDefault: true })],
|
||||
@@ -232,16 +225,26 @@ test("getDefaultReferralFor: returns the fixed default referral, ignoring campai
|
||||
};
|
||||
const result = getDefaultReferralFor("groq", {
|
||||
getFlag: () => true,
|
||||
getCache: () => cacheRowFor(feed),
|
||||
getCache: () => ({
|
||||
version: "x",
|
||||
tier: "live",
|
||||
payload: JSON.stringify(feed),
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
assert.equal(result?.kind, "fixo");
|
||||
});
|
||||
|
||||
test("getDefaultReferralFor: provider with no default referral => null", () => {
|
||||
const feed = { ...baseReferralsFeed(), referrals: { fixed: [], campaigns: [] } };
|
||||
const feed = { ...baseFeed(), referrals: { fixed: [], campaigns: [] } };
|
||||
const result = getDefaultReferralFor("groq", {
|
||||
getFlag: () => true,
|
||||
getCache: () => cacheRowFor(feed),
|
||||
getCache: () => ({
|
||||
version: "x",
|
||||
tier: "live",
|
||||
payload: JSON.stringify(feed),
|
||||
fetchedAt: new Date().toISOString(),
|
||||
}),
|
||||
});
|
||||
assert.equal(result, null);
|
||||
});
|
||||
|
||||
@@ -20,11 +20,6 @@ const {
|
||||
const NOW = Date.parse("2026-08-06T12:00:00.000Z");
|
||||
const FRESH = new Date(NOW - 60 * 60 * 1000).toISOString(); // 1h ago — inside the daily window
|
||||
const STALE = new Date(NOW - 25 * 60 * 60 * 1000).toISOString(); // 25h ago — due
|
||||
// Referrals staleness window is much shorter (1h, see REFERRALS_STALE_MS) —
|
||||
// this default must sit well inside it so existing catalog-only subtests
|
||||
// never trigger a referrals sync as an unasserted side effect.
|
||||
const REFERRALS_FRESH = new Date(NOW - 5 * 60 * 1000).toISOString(); // 5m ago
|
||||
const REFERRALS_STALE = new Date(NOW - 2 * 60 * 60 * 1000).toISOString(); // 2h ago — due
|
||||
|
||||
/** Fake interval registry so no real timer ever exists in these tests. */
|
||||
function fakeTimers() {
|
||||
@@ -45,11 +40,9 @@ function fakeTimers() {
|
||||
|
||||
function deps(overrides: Record<string, unknown> = {}) {
|
||||
const syncCalls: number[] = [];
|
||||
const referralsSyncCalls: number[] = [];
|
||||
const timers = fakeTimers();
|
||||
return {
|
||||
syncCalls,
|
||||
referralsSyncCalls,
|
||||
timers,
|
||||
d: {
|
||||
getFlag: () => true,
|
||||
@@ -59,15 +52,6 @@ function deps(overrides: Record<string, unknown> = {}) {
|
||||
syncCalls.push(1);
|
||||
return { status: "updated", version: "2026.08.06.1", tier: "live" } as const;
|
||||
},
|
||||
// Referrals side-sync — separate cache/sync from the catalog above.
|
||||
// Defaults to a FRESH referrals cache so existing subtests (which
|
||||
// don't care about referrals at all) never trigger a referrals sync
|
||||
// as an unasserted side effect.
|
||||
getReferralsCache: () => ({ fetchedAt: REFERRALS_FRESH }),
|
||||
syncReferrals: async () => {
|
||||
referralsSyncCalls.push(1);
|
||||
return { status: "updated", generatedAt: "2026-08-06T12:00:00.000Z", tier: "live" } as const;
|
||||
},
|
||||
now: () => NOW,
|
||||
setIntervalFn: timers.setIntervalFn,
|
||||
clearIntervalFn: timers.clearIntervalFn,
|
||||
@@ -166,69 +150,4 @@ test("radar sync scheduler", async (t) => {
|
||||
assert.equal(initRadarSyncScheduler(d), false);
|
||||
assert.equal(timers.registered.length, 0);
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Referrals side-sync — piggybacks on the same hourly tick but its own
|
||||
// (much shorter, 1h) staleness window, independent of the catalog's
|
||||
// due-ness. Never surfaces in RadarTickResult (fire-and-await side effect
|
||||
// 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 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({
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user