fix(radar): close cumulative quality regressions

This commit is contained in:
Xiangzhe
2026-08-14 19:47:07 -03:00
parent 6233ca6642
commit dfcf6086a2
9 changed files with 118 additions and 96 deletions

View File

@@ -43,15 +43,14 @@ export const KNOWN_DUPLICATE_VERSIONS = new Set([
// ---------------------------------------------------------------------------
// ALLOWLIST 2 — gaps de sequência CONHECIDOS.
// Fonte: auditoria do disco (src/lib/db/migrations/). Além dos slots legados,
// 144145 seguem reservados pelas migrations Radar que já existem na série
// empilhada; a migration 143 já aterrissou. O job registry foi promovido de 139
// para 146 pela tabela RENAMED_MIGRATION_COMPATIBILITY. A
// As migrations Radar 144145 e a migration 143 já aterrissaram. O job registry
// foi promovido de 139 para 146 pela tabela RENAMED_MIGRATION_COMPATIBILITY. A
// 147149 estão reservadas por migrations atualmente em trânsito nos PRs #8228,
// #9313, #10047 e #10066; esta branch usa 150 para evitar essas colisões conhecidas.
// O stale-enforcement exige que cada reserva seja removida quando os arquivos
// correspondentes aterrissarem na release.
// ---------------------------------------------------------------------------
export const KNOWN_GAPS = new Set(["026", "055", "121", "144", "145", "148", "149"]); // 121: número queimado no ciclo v3.8.47 — 122 (#6909) mergeou antes e 121 nunca aterrissou (validação e2e 2026-07-12)
export const KNOWN_GAPS = new Set(["026", "055", "121", "148", "149"]); // 121: número queimado no ciclo v3.8.47 — 122 (#6909) mergeou antes e 121 nunca aterrissou (validação e2e 2026-07-12)
function pad3(n) {
return String(n).padStart(3, "0");

View File

@@ -5,7 +5,7 @@ import { useLocale, useTranslations } from "next-intl";
import {
NEWS_DISMISS_EVENT,
NEWS_DISMISS_STORAGE_KEY,
NEWS_DISMISS_STORAGE_NAME,
fetchNewsPayload,
parseDismissedNewsIds,
selectActiveNews,
@@ -23,7 +23,7 @@ function subscribeToDismissals(callback: () => void) {
function readDismissedIds(): string {
try {
return localStorage.getItem(NEWS_DISMISS_STORAGE_KEY) ?? "";
return localStorage.getItem(NEWS_DISMISS_STORAGE_NAME) ?? "";
} catch {
return "";
}
@@ -64,7 +64,7 @@ export default function NewsBanner() {
const dismiss = () => {
dismissedIds.add(announcement.id);
try {
localStorage.setItem(NEWS_DISMISS_STORAGE_KEY, serializeDismissedNewsIds(dismissedIds));
localStorage.setItem(NEWS_DISMISS_STORAGE_NAME, serializeDismissedNewsIds(dismissedIds));
} catch {
// Storage is optional; the next announcement fetch remains functional.
}

View File

@@ -25,7 +25,7 @@ import {
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
import { isPaidModelTarget } from "@/shared/utils/freeModels";
import { getAuditRequestContext, logAuditEvent } from "@/lib/compliance";
import { isAuthRequired } from "@/shared/utils/apiAuth";
import { isAuthRequired, isDashboardSessionAuthenticated } from "@/shared/utils/apiAuth";
import { isCliTokenAuthValid } from "@/lib/middleware/cliTokenAuth";
import { extractApiKey } from "@/sse/services/auth";
import { getApiKeyMetadata } from "@/lib/db/apiKeys";

View File

@@ -1736,6 +1736,8 @@
"quotaShare": "Chia sẻ hạn mức",
"discovery": "Khám phá",
"freeProviderRankings": "Xếp hạng nhà cung cấp miễn phí",
"radar": "Radar",
"setup": "Thiết lập",
"freeTiers": "Gói miễn phí",
"gamification": "Trò chơi hóa",
"leaderboard": "Bảng xếp hạng",

View File

@@ -362,7 +362,8 @@ function normalizeRadarIdentity(
}
function normalizeDisplayName(value: unknown): string | null | undefined {
if (value === undefined || value === null) return value;
if (value === undefined) return undefined;
if (value === null) return null;
if (typeof value !== "string") return undefined;
const normalized = value.trim();
if (

View File

@@ -6,7 +6,7 @@ export const CHANGELOG_RAW_URL =
"https://raw.githubusercontent.com/diegosouzapw/OmniRoute/main/CHANGELOG.md";
export const CHANGELOG_GITHUB_URL =
"https://github.com/diegosouzapw/OmniRoute/blob/main/CHANGELOG.md";
export const NEWS_DISMISS_STORAGE_KEY = "omniroute-news-dismissed-v2";
export const NEWS_DISMISS_STORAGE_NAME = "omniroute-news-dismissed-v2";
export const NEWS_DISMISS_EVENT = "omniroute:news-dismissed";
const NEWS_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/;
@@ -31,19 +31,21 @@ const httpsUrlSchema = z
return url.protocol === "https:" && !url.username && !url.password;
}, "Announcement links must use HTTPS without embedded credentials");
const localizedTextMapSchema = z.record(localizedTextSchema).superRefine((value, context) => {
if (!value.en) {
context.addIssue({ code: z.ZodIssueCode.custom, message: "English copy is required" });
}
for (const locale of Object.keys(value)) {
if (!LOCALE_PATTERN.test(locale)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid locale: ${locale}`,
});
const localizedTextMapSchema = z
.record(z.string(), localizedTextSchema)
.superRefine((value, context) => {
if (!value.en) {
context.addIssue({ code: z.ZodIssueCode.custom, message: "English copy is required" });
}
}
});
for (const locale of Object.keys(value)) {
if (!LOCALE_PATTERN.test(locale)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid locale: ${locale}`,
});
}
}
});
const newsFeedItemSchema = z
.object({

View File

@@ -104,8 +104,8 @@ test("frozen allowlists match the documented legacy and stacked-series gaps", ()
assert.ok((KNOWN_GAPS as Set<string>).has("055"));
assert.ok((KNOWN_GAPS as Set<string>).has("121"));
assert.equal((KNOWN_GAPS as Set<string>).has("143"), false);
assert.ok((KNOWN_GAPS as Set<string>).has("144"));
assert.ok((KNOWN_GAPS as Set<string>).has("145"));
assert.equal((KNOWN_GAPS as Set<string>).has("144"), false);
assert.equal((KNOWN_GAPS as Set<string>).has("145"), false);
// 147 left the gap list when 147_api_keys_model_access_mode.sql landed (same pattern as 143).
assert.equal((KNOWN_GAPS as Set<string>).has("147"), false);
assert.ok((KNOWN_GAPS as Set<string>).has("148"));

View File

@@ -0,0 +1,90 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyDer = publicKey.export({ type: "spki", format: "der" });
process.env.RADAR_FEED_PUBKEY = publicKeyDer.toString("base64");
const fixturePath = path.resolve(import.meta.dirname!, "../fixtures/radar-feed-canonical.json");
const fixtureBytes = fs.readFileSync(fixturePath);
function signBytes(bytes: Buffer): string {
return crypto.sign(null, bytes, privateKey).toString("base64");
}
function mockResponse(body: Buffer, headers: Record<string, string> = {}): Response {
return {
ok: true,
status: 200,
headers: new Map(Object.entries(headers)),
arrayBuffer: () =>
Promise.resolve(body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength)),
} as unknown as Response;
}
const syncMod = await import("../../src/lib/radar/sync.ts");
test("FIX6: oversized Content-Length avoids reading the body or touching cache", async () => {
let arrayBufferCalled = false;
const response = mockResponse(Buffer.from("irrelevant"), {
"content-length": String(10 * 1024 * 1024 + 1),
});
const originalArrayBuffer = response.arrayBuffer.bind(response);
(response as unknown as { arrayBuffer: () => Promise<ArrayBuffer> }).arrayBuffer = () => {
arrayBufferCalled = true;
return originalArrayBuffer();
};
let setCacheCalled = false;
const result = await syncMod.syncRadar({
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("FIX6: oversized streamed body without Content-Length leaves cache untouched", async () => {
const oversized = Buffer.alloc(10 * 1024 * 1024 + 1, 0x41);
let setCacheCalled = false;
const result = await syncMod.syncRadar({
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("FIX6: body within the 10MB cap proceeds normally", async () => {
const result = await syncMod.syncRadar({
getFlag: () => true,
getSettings: () => ({ optIn: true, supporterKey: null }),
getCache: () => null,
setCache: () => {},
fetch: (() =>
Promise.resolve(
mockResponse(fixtureBytes, {
"x-omniroute-feed-signature": signBytes(fixtureBytes),
})
)) as unknown as typeof globalThis.fetch,
});
assert.notEqual(result.status, "too_large");
});

View File

@@ -967,75 +967,3 @@ test("syncRadar: first sync (no cache) with valid data => updated", async () =>
assert.equal(result.status, "updated");
assert.equal(cacheStore.length, 1);
});
// ===========================================================================
// FIX 6 — 10 MB response cap (unbounded `Buffer.from(await res.arrayBuffer())`)
// ===========================================================================
test("FIX6: Content-Length header 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 syncMod.syncRadar({
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, "cache must not be touched");
assert.equal(
arrayBufferCalled,
false,
"body must not be read once Content-Length already exceeds the cap"
);
});
test("FIX6: 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 syncMod.syncRadar({
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, "cache must not be touched");
});
test("FIX6: body within the 10MB cap proceeds normally (never returns too_large)", async () => {
const sig = signBytes(FIXTURE_BYTES);
const result = await syncMod.syncRadar({
getFlag: () => true,
getSettings: () => ({ optIn: true, supporterKey: null }),
getCache: () => null,
setCache: () => {},
fetch: (() =>
Promise.resolve(
mockResponse(FIXTURE_BYTES, { "x-omniroute-feed-signature": sig })
)) as unknown as typeof globalThis.fetch,
});
assert.notEqual(result.status, "too_large");
});