From 6920d81f964f7ca3b435ab67495e54e3e0fb7a6a Mon Sep 17 00:00:00 2001 From: Hunter B Date: Tue, 29 Sep 2026 04:32:51 -0700 Subject: [PATCH 1/8] fix(web): drop the edge runtime from the telemetry route @opennextjs/cloudflare does not support the edge runtime, and the deployed route answered every method with a 500, so website usage counts never reached the ingest. The route only needs fetch, Request, and TextDecoder. Test: product-telemetry-route.test.ts 2/2 (edge-runtime scan failed without the fix). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks --- web/app/api/product-telemetry/route.ts | 5 ++- web/lib/product-telemetry-route.test.ts | 48 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 web/lib/product-telemetry-route.test.ts diff --git a/web/app/api/product-telemetry/route.ts b/web/app/api/product-telemetry/route.ts index 4e525097c2..1874366a0e 100644 --- a/web/app/api/product-telemetry/route.ts +++ b/web/app/api/product-telemetry/route.ts @@ -19,7 +19,10 @@ import { MAX_ENVELOPE_BYTES, validateEnvelope } from "@/lib/telemetry/product-us * holds it. */ -export const runtime = "edge"; +// No `runtime = "edge"`: @opennextjs/cloudflare does not support the edge +// runtime, and the deployed worker answered every method with a 500 while it +// was declared. The default runtime already has fetch, Request, and +// TextDecoder. export const CANONICAL_INGEST_URL = "https://telemetry.codewhale.net/v1/telemetry"; const FORWARD_TIMEOUT_MS = 1500; diff --git a/web/lib/product-telemetry-route.test.ts b/web/lib/product-telemetry-route.test.ts new file mode 100644 index 0000000000..9045f75c82 --- /dev/null +++ b/web/lib/product-telemetry-route.test.ts @@ -0,0 +1,48 @@ +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { handleProductTelemetry } from "@/app/api/product-telemetry/route"; + +const APP_DIR = fileURLToPath(new URL("../app", import.meta.url)); + +function sourceFiles(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const path = join(dir, entry.name); + if (entry.isDirectory()) return sourceFiles(path); + return /\.(ts|tsx)$/.test(entry.name) ? [path] : []; + }); +} + +describe("/api/product-telemetry", () => { + // @opennextjs/cloudflare does not support the edge runtime; the deployed + // telemetry route answered every request with a 500 while it declared it. + it("no app route or page opts into the edge runtime", () => { + const edge = sourceFiles(APP_DIR).filter((file) => + /export\s+const\s+runtime\s*=\s*["']edge["']/.test(readFileSync(file, "utf8")), + ); + expect(edge).toEqual([]); + }); + + it("answers an empty POST without a server error", async () => { + const post = () => + new Request("https://codewhale.net/api/product-telemetry", { + method: "POST", + headers: { "content-type": "application/json" }, + body: "{}", + }); + + const disabled = await handleProductTelemetry(post(), { ingestUrl: null }); + expect(disabled.status).toBe(200); + expect(await disabled.json()).toEqual({ accepted: false, reason: "disabled" }); + + const enabled = await handleProductTelemetry(post(), { + ingestUrl: "https://telemetry.codewhale.net/v1/telemetry", + forward: async () => { + throw new Error("an invalid envelope must not be forwarded"); + }, + }); + expect(enabled.status).toBe(422); + expect(await enabled.json()).toEqual({ accepted: false, reason: "schema" }); + }); +}); From c37873eb00d9cdab0e03855bba015804f95f0bb4 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Tue, 29 Sep 2026 04:32:51 -0700 Subject: [PATCH 2/8] fix(web): keep cron facts snapshots valid and complete Build the release URL from the tag instead of GitHub's html_url, whose repo casing (Hmbown/Codewhale) failed isRepoFacts and invalidated every KV snapshot. runFactsDrift now refuses to write facts that fail isRepoFacts. Add the missing ModelScope provider to the runtime label map and pin it equal to facts-lib's PROVIDER_LABEL_MAP. Test: facts-drift.test.ts 6/6 (3 new; each failed without its fix). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks --- web/lib/facts-drift.test.ts | 56 ++++++++++++++- web/lib/facts-drift.ts | 132 ++++++++++++++++++++---------------- web/scripts/facts-lib.d.mts | 2 + web/scripts/facts-lib.mjs | 6 +- 4 files changed, 130 insertions(+), 66 deletions(-) create mode 100644 web/scripts/facts-lib.d.mts diff --git a/web/lib/facts-drift.test.ts b/web/lib/facts-drift.test.ts index 7d4edb5b12..31d8a83a46 100644 --- a/web/lib/facts-drift.test.ts +++ b/web/lib/facts-drift.test.ts @@ -1,5 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { deriveFactsFromRemote } from "./facts-drift"; +import { PROVIDER_LABEL_MAP } from "../scripts/facts-lib.mjs"; +import { isRepoFacts } from "./facts"; +import { deriveFactsFromRemote, PROVIDER_LABELS, runFactsDrift } from "./facts-drift"; const REVISION = "b".repeat(40); @@ -7,7 +9,13 @@ function response(body: string, status = 200): Response { return new Response(body, { status }); } -function installGitHubFixture(toolCountSource: string | null): void { +const VALID_GENERATED_FACTS = + 'export const FACTS: RepoFacts = {"toolCount":73,"models":[]};'; + +function installGitHubFixture( + toolCountSource: string | null, + releaseHtmlUrl = "https://github.com/Hmbown/CodeWhale/releases/tag/v0.9.0", +): void { vi.stubGlobal( "fetch", vi.fn(async (input: string | URL | Request) => { @@ -25,7 +33,7 @@ function installGitHubFixture(toolCountSource: string | null): void { JSON.stringify({ tag_name: "v0.9.0", published_at: "2026-07-16T20:05:39Z", - html_url: "https://github.com/Hmbown/CodeWhale/releases/tag/v0.9.0", + html_url: releaseHtmlUrl, }), ); } @@ -105,4 +113,46 @@ describe("deriveFactsFromRemote", () => { await expect(deriveFactsFromRemote()).resolves.toBeNull(); }); + + it("stores a canonical release URL when GitHub answers with the repo's other casing", async () => { + installGitHubFixture( + VALID_GENERATED_FACTS, + "https://github.com/Hmbown/Codewhale/releases/tag/v0.9.0", + ); + + const facts = await deriveFactsFromRemote(); + + expect(facts?.latestPublishedRelease?.url).toBe( + "https://github.com/Hmbown/CodeWhale/releases/tag/v0.9.0", + ); + expect(isRepoFacts(facts)).toBe(true); + }); +}); + +describe("runFactsDrift", () => { + it("writes a KV snapshot that getFacts() accepts", async () => { + installGitHubFixture( + VALID_GENERATED_FACTS, + "https://github.com/Hmbown/Codewhale/releases/tag/v0.9.0", + ); + const store = new Map(); + const kv = { + get: async (key: string) => store.get(key) ?? null, + put: async (key: string, value: string) => { + store.set(key, value); + }, + }; + + const result = await runFactsDrift({ CURATED_KV: kv }); + + expect(result.ok).toBe(true); + expect(isRepoFacts(JSON.parse(store.get("facts:current") ?? "null"))).toBe(true); + }); +}); + +describe("PROVIDER_LABELS", () => { + it("matches the build-time PROVIDER_LABEL_MAP, so cron snapshots keep every provider", () => { + expect(Object.keys(PROVIDER_LABEL_MAP).length).toBeGreaterThan(0); + expect(PROVIDER_LABELS).toEqual(PROVIDER_LABEL_MAP); + }); }); diff --git a/web/lib/facts-drift.ts b/web/lib/facts-drift.ts index 5bdb2610b8..48068ab12f 100644 --- a/web/lib/facts-drift.ts +++ b/web/lib/facts-drift.ts @@ -19,8 +19,10 @@ import type { ModelFact, } from "./facts.generated"; import { FACTS as BUILD_FACTS } from "./facts.generated"; +import { isRepoFacts } from "./facts"; const RAW_ROOT = "https://raw.githubusercontent.com/Hmbown/CodeWhale"; +const RELEASE_TAG_ROOT = "https://github.com/Hmbown/CodeWhale/releases/tag"; const KV_KEY = "facts:current"; const LOG_KEY = "facts:drift-log"; @@ -96,65 +98,72 @@ function deriveCrates(cargo: string): string[] { return [...block[1].matchAll(/"crates\/([^"]+)"/g)].map((m) => m[1]).sort(); } +// Match what the published CLI binary's `--provider` flag accepts +// (ProviderArg in crates/cli/src/lib.rs). DeepseekCN exists in the +// legacy tui ApiProvider enum but is not wired through ProviderKind, +// so the binary rejects it — keep it out of the docs. Issue #1104. +// +// Must equal PROVIDER_LABEL_MAP in web/scripts/facts-lib.mjs; a variant missing +// here is dropped from the cron-written KV snapshot while the build facts keep +// it (facts-drift.test.ts compares the two). +export const PROVIDER_LABELS: Readonly> = { + Deepseek: { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }, + DeepseekAnthropic: { id: "deepseek-anthropic", label: "DeepSeek Anthropic", env: "DEEPSEEK_API_KEY / ANTHROPIC_API_KEY" }, + NvidiaNim: { id: "nvidia-nim", label: "NVIDIA NIM", env: "NVIDIA_API_KEY / NVIDIA_NIM_API_KEY" }, + Openai: { id: "openai", label: "OpenAI-compatible", env: "OPENAI_API_KEY" }, + Atlascloud: { id: "atlascloud", label: "AtlasCloud", env: "ATLASCLOUD_API_KEY" }, + WanjieArk: { id: "wanjie-ark", label: "Wanjie Ark", env: "WANJIE_ARK_API_KEY / WANJIE_API_KEY / WANJIE_MAAS_API_KEY" }, + Volcengine: { id: "volcengine", label: "Volcengine Ark", env: "VOLCENGINE_API_KEY / VOLCENGINE_ARK_API_KEY / ARK_API_KEY" }, + Openrouter: { id: "openrouter", label: "OpenRouter", env: "OPENROUTER_API_KEY" }, + Orcarouter: { id: "orcarouter", label: "OrcaRouter", env: "ORCAROUTER_API_KEY" }, + XiaomiMimo: { id: "xiaomi-mimo", label: "Xiaomi MiMo", env: "XIAOMI_MIMO_TOKEN_PLAN_API_KEY / MIMO_TOKEN_PLAN_API_KEY / XIAOMI_MIMO_API_KEY / XIAOMI_API_KEY / MIMO_API_KEY" }, + Novita: { id: "novita", label: "Novita AI", env: "NOVITA_API_KEY" }, + Fireworks: { id: "fireworks", label: "Fireworks AI", env: "FIREWORKS_API_KEY" }, + Siliconflow: { id: "siliconflow", label: "SiliconFlow", env: "SILICONFLOW_API_KEY" }, + SiliconflowCn: { id: "siliconflow-CN", label: "SiliconFlow CN", env: "SILICONFLOW_API_KEY" }, + Arcee: { id: "arcee", label: "Arcee AI", env: "ARCEE_API_KEY" }, + Moonshot: { id: "moonshot", label: "Moonshot/Kimi", env: "MOONSHOT_API_KEY / KIMI_API_KEY" }, + Sglang: { id: "sglang", label: "SGLang", env: "SGLANG_API_KEY" }, + Vllm: { id: "vllm", label: "vLLM", env: "VLLM_API_KEY" }, + Ollama: { id: "ollama", label: "Ollama", env: "OLLAMA_API_KEY" }, + OllamaCloud: { id: "ollama-cloud", label: "Ollama Cloud", env: "OLLAMA_CLOUD_API_KEY / OLLAMA_API_KEY" }, + Huggingface: { id: "huggingface", label: "Hugging Face", env: "HUGGINGFACE_API_KEY / HF_TOKEN" }, + Modelscope: { id: "modelscope", label: "ModelScope", env: "MODELSCOPE_API_KEY" }, + Deepinfra: { id: "deepinfra", label: "DeepInfra", env: "DEEPINFRA_API_KEY / DEEPINFRA_TOKEN" }, + Together: { id: "together", label: "Together AI", env: "TOGETHER_API_KEY" }, + Qianfan: { id: "qianfan", label: "Baidu Qianfan", env: "QIANFAN_API_KEY / BAIDU_QIANFAN_API_KEY" }, + OpenaiCodex: { id: "openai-codex", label: "OpenAI Codex", env: "ChatGPT OAuth via `codewhale auth chatgpt`; optional consented Codex CLI credentials (OPENAI_CODEX_ACCESS_TOKEN / CODEX_ACCESS_TOKEN override)" }, + OpencodeGo: { id: "opencode-go", label: "OpenCode Go", env: "OPENCODE_GO_API_KEY" }, + OpencodeZen: { id: "opencode-zen", label: "OpenCode Zen", env: "OPENCODE_ZEN_API_KEY / OPENCODE_API_KEY" }, + Anthropic: { id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" }, + Zai: { id: "zai", label: "Z.ai", env: "ZAI_API_KEY / Z_AI_API_KEY" }, + Stepfun: { id: "stepfun", label: "StepFun", env: "STEPFUN_API_KEY / STEP_API_KEY" }, + Minimax: { id: "minimax", label: "MiniMax", env: "MINIMAX_API_KEY" }, + MinimaxAnthropic: { id: "minimax-anthropic", label: "MiniMax (Anthropic-compatible)", env: "MINIMAX_API_KEY" }, + Openmodel: { id: "openmodel", label: "OpenModel", env: "OPENMODEL_API_KEY" }, + Sakana: { id: "sakana", label: "Sakana AI", env: "FUGU_API_KEY / SAKANA_API_KEY" }, + LongCat: { id: "longcat", label: "Meituan LongCat", env: "LONGCAT_API_KEY" }, + Meta: { id: "meta", label: "Meta Model API", env: "META_MODEL_API_KEY / MODEL_API_KEY" }, + Telecomjs: { id: "telecomjs", label: "TelecomJS TokenHub", env: "TELECOMJS_API_KEY" }, + Xai: { id: "xai", label: "xAI", env: "XAI_API_KEY" }, + Mistral: { id: "mistral", label: "Mistral AI", env: "MISTRAL_API_KEY" }, + Google: { id: "google", label: "Google Gemini", env: "GOOGLE_API_KEY / GEMINI_API_KEY" }, + Edenai: { id: "edenai", label: "Eden AI", env: "EDENAI_API_KEY" }, + Concentrate: { id: "concentrate", label: "Concentrate", env: "CONCENTRATE_API_KEY" }, + Codewhale: { id: "codewhale", label: "Codewhale", env: "CODEWHALE_API_KEY" }, + ModelstudioTokenPlan: { id: "modelstudio-token-plan", label: "Model Studio Token Plan", env: "MODELSTUDIO_API_KEY" }, + ModelstudioTokenPlanAnthropic: { id: "modelstudio-token-plan-anthropic", label: "Model Studio Token Plan (Anthropic-compatible)", env: "MODELSTUDIO_API_KEY" }, + ModelstudioCodingPlan: { id: "modelstudio-coding-plan", label: "Model Studio Coding Plan", env: "MODELSTUDIO_API_KEY" }, + ModelstudioCodingPlanAnthropic: { id: "modelstudio-coding-plan-anthropic", label: "Model Studio Coding Plan (Anthropic-compatible)", env: "MODELSTUDIO_API_KEY" }, + Zenmux: { id: "zenmux", label: "ZenMux", env: "ZENMUX_API_KEY" }, + Csdn: { id: "csdn", label: "CSDN 星图 (Starmap)", env: "CSDN_API_KEY" }, +}; + function deriveProvidersFromConfig(cfg: string): ProviderFact[] { const enumBlock = cfg.match(/pub enum ApiProvider \{([\s\S]*?)\}/); if (!enumBlock) return []; const variants = [...enumBlock[1].matchAll(/^\s*(\w+)\s*,\s*$/gm)].map((m) => m[1]); - // Match what the published CLI binary's `--provider` flag accepts - // (ProviderArg in crates/cli/src/lib.rs). DeepseekCN exists in the - // legacy tui ApiProvider enum but is not wired through ProviderKind, - // so the binary rejects it — keep it out of the docs. Issue #1104. - const labelMap: Record = { - Deepseek: { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }, - DeepseekAnthropic: { id: "deepseek-anthropic", label: "DeepSeek Anthropic", env: "DEEPSEEK_API_KEY / ANTHROPIC_API_KEY" }, - NvidiaNim: { id: "nvidia-nim", label: "NVIDIA NIM", env: "NVIDIA_API_KEY / NVIDIA_NIM_API_KEY" }, - Openai: { id: "openai", label: "OpenAI-compatible", env: "OPENAI_API_KEY" }, - Atlascloud: { id: "atlascloud", label: "AtlasCloud", env: "ATLASCLOUD_API_KEY" }, - WanjieArk: { id: "wanjie-ark", label: "Wanjie Ark", env: "WANJIE_ARK_API_KEY / WANJIE_API_KEY / WANJIE_MAAS_API_KEY" }, - Volcengine: { id: "volcengine", label: "Volcengine Ark", env: "VOLCENGINE_API_KEY / VOLCENGINE_ARK_API_KEY / ARK_API_KEY" }, - Openrouter: { id: "openrouter", label: "OpenRouter", env: "OPENROUTER_API_KEY" }, - Orcarouter: { id: "orcarouter", label: "OrcaRouter", env: "ORCAROUTER_API_KEY" }, - XiaomiMimo: { id: "xiaomi-mimo", label: "Xiaomi MiMo", env: "XIAOMI_MIMO_TOKEN_PLAN_API_KEY / MIMO_TOKEN_PLAN_API_KEY / XIAOMI_MIMO_API_KEY / XIAOMI_API_KEY / MIMO_API_KEY" }, - Novita: { id: "novita", label: "Novita AI", env: "NOVITA_API_KEY" }, - Fireworks: { id: "fireworks", label: "Fireworks AI", env: "FIREWORKS_API_KEY" }, - Siliconflow: { id: "siliconflow", label: "SiliconFlow", env: "SILICONFLOW_API_KEY" }, - SiliconflowCn: { id: "siliconflow-CN", label: "SiliconFlow CN", env: "SILICONFLOW_API_KEY" }, - Arcee: { id: "arcee", label: "Arcee AI", env: "ARCEE_API_KEY" }, - Moonshot: { id: "moonshot", label: "Moonshot/Kimi", env: "MOONSHOT_API_KEY / KIMI_API_KEY" }, - Sglang: { id: "sglang", label: "SGLang", env: "SGLANG_API_KEY" }, - Vllm: { id: "vllm", label: "vLLM", env: "VLLM_API_KEY" }, - Ollama: { id: "ollama", label: "Ollama", env: "OLLAMA_API_KEY" }, - OllamaCloud: { id: "ollama-cloud", label: "Ollama Cloud", env: "OLLAMA_CLOUD_API_KEY / OLLAMA_API_KEY" }, - Huggingface: { id: "huggingface", label: "Hugging Face", env: "HUGGINGFACE_API_KEY / HF_TOKEN" }, - Deepinfra: { id: "deepinfra", label: "DeepInfra", env: "DEEPINFRA_API_KEY / DEEPINFRA_TOKEN" }, - Together: { id: "together", label: "Together AI", env: "TOGETHER_API_KEY" }, - Qianfan: { id: "qianfan", label: "Baidu Qianfan", env: "QIANFAN_API_KEY / BAIDU_QIANFAN_API_KEY" }, - OpenaiCodex: { id: "openai-codex", label: "OpenAI Codex", env: "ChatGPT OAuth via `codewhale auth chatgpt`; optional consented Codex CLI credentials (OPENAI_CODEX_ACCESS_TOKEN / CODEX_ACCESS_TOKEN override)" }, - OpencodeGo: { id: "opencode-go", label: "OpenCode Go", env: "OPENCODE_GO_API_KEY" }, - OpencodeZen: { id: "opencode-zen", label: "OpenCode Zen", env: "OPENCODE_ZEN_API_KEY / OPENCODE_API_KEY" }, - Anthropic: { id: "anthropic", label: "Anthropic", env: "ANTHROPIC_API_KEY" }, - Zai: { id: "zai", label: "Z.ai", env: "ZAI_API_KEY / Z_AI_API_KEY" }, - Stepfun: { id: "stepfun", label: "StepFun", env: "STEPFUN_API_KEY / STEP_API_KEY" }, - Minimax: { id: "minimax", label: "MiniMax", env: "MINIMAX_API_KEY" }, - MinimaxAnthropic: { id: "minimax-anthropic", label: "MiniMax (Anthropic-compatible)", env: "MINIMAX_API_KEY" }, - Openmodel: { id: "openmodel", label: "OpenModel", env: "OPENMODEL_API_KEY" }, - Sakana: { id: "sakana", label: "Sakana AI", env: "FUGU_API_KEY / SAKANA_API_KEY" }, - LongCat: { id: "longcat", label: "Meituan LongCat", env: "LONGCAT_API_KEY" }, - Meta: { id: "meta", label: "Meta Model API", env: "META_MODEL_API_KEY / MODEL_API_KEY" }, - Telecomjs: { id: "telecomjs", label: "TelecomJS TokenHub", env: "TELECOMJS_API_KEY" }, - Xai: { id: "xai", label: "xAI", env: "XAI_API_KEY" }, - Mistral: { id: "mistral", label: "Mistral AI", env: "MISTRAL_API_KEY" }, - Google: { id: "google", label: "Google Gemini", env: "GOOGLE_API_KEY / GEMINI_API_KEY" }, - Edenai: { id: "edenai", label: "Eden AI", env: "EDENAI_API_KEY" }, - Concentrate: { id: "concentrate", label: "Concentrate", env: "CONCENTRATE_API_KEY" }, - Codewhale: { id: "codewhale", label: "Codewhale", env: "CODEWHALE_API_KEY" }, - ModelstudioTokenPlan: { id: "modelstudio-token-plan", label: "Model Studio Token Plan", env: "MODELSTUDIO_API_KEY" }, - ModelstudioTokenPlanAnthropic: { id: "modelstudio-token-plan-anthropic", label: "Model Studio Token Plan (Anthropic-compatible)", env: "MODELSTUDIO_API_KEY" }, - ModelstudioCodingPlan: { id: "modelstudio-coding-plan", label: "Model Studio Coding Plan", env: "MODELSTUDIO_API_KEY" }, - ModelstudioCodingPlanAnthropic: { id: "modelstudio-coding-plan-anthropic", label: "Model Studio Coding Plan (Anthropic-compatible)", env: "MODELSTUDIO_API_KEY" }, - Zenmux: { id: "zenmux", label: "ZenMux", env: "ZENMUX_API_KEY" }, - Csdn: { id: "csdn", label: "CSDN 星图 (Starmap)", env: "CSDN_API_KEY" }, - }; + const labelMap = PROVIDER_LABELS; // Log loudly on unmapped variants so a new provider can never be silently // dropped from the drift-derived facts again. DeepseekCN (#1104), the // dynamic Custom meta-provider (#1519, user-defined endpoints), and @@ -165,7 +174,7 @@ function deriveProvidersFromConfig(cfg: string): ProviderFact[] { if (unmapped.length > 0) { console.warn( `[facts-drift] ApiProvider variants missing from labelMap: ${unmapped.join(", ")}. ` + - "Add them to labelMap here AND PROVIDER_LABEL_MAP in web/scripts/facts-lib.mjs (or to EXCLUDED if intentionally hidden).", + "Add them to PROVIDER_LABELS here AND PROVIDER_LABEL_MAP in web/scripts/facts-lib.mjs (or to EXCLUDED if intentionally hidden).", ); } return variants @@ -204,14 +213,12 @@ async function fetchLatestPublishedRelease( const j = (await r.json()) as { tag_name?: string; published_at?: string; - html_url?: string; }; if ( !j.tag_name || !/^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(j.tag_name) || !j.published_at || - !Number.isFinite(Date.parse(j.published_at)) || - !j.html_url + !Number.isFinite(Date.parse(j.published_at)) ) { return null; } @@ -219,7 +226,10 @@ async function fetchLatestPublishedRelease( tag: j.tag_name, version: j.tag_name.slice(1), publishedAt: j.published_at, - url: j.html_url, + // Built from the tag, not `html_url`: GitHub answers with the repo's + // canonical casing (`Hmbown/Codewhale`), which the exact-URL check in + // isRepoFacts rejects, invalidating the whole KV snapshot. + url: `${RELEASE_TAG_ROOT}/${j.tag_name}`, }; } catch { return null; @@ -380,6 +390,8 @@ export async function runFactsDrift(env: { CURATED_KV?: KVNamespace; GITHUB_TOKE const remote = await deriveFactsFromRemote(env.GITHUB_TOKEN); if (!remote) return { ok: false, reason: "remote derivation failed" }; + // getFacts() discards a snapshot isRepoFacts rejects, so never store one. + if (!isRepoFacts(remote)) return { ok: false, reason: "remote facts failed validation" }; const cachedRaw = await env.CURATED_KV.get(KV_KEY); let cached: RepoFacts = BUILD_FACTS; diff --git a/web/scripts/facts-lib.d.mts b/web/scripts/facts-lib.d.mts new file mode 100644 index 0000000000..f013b4652a --- /dev/null +++ b/web/scripts/facts-lib.d.mts @@ -0,0 +1,2 @@ +import type { ProviderFact } from "../lib/facts.generated"; +export const PROVIDER_LABEL_MAP: Readonly>; diff --git a/web/scripts/facts-lib.mjs b/web/scripts/facts-lib.mjs index 9bbb7c362d..ae1a2f8c19 100644 --- a/web/scripts/facts-lib.mjs +++ b/web/scripts/facts-lib.mjs @@ -57,15 +57,15 @@ export function deriveSandboxBackendsFromSource(source) { /** * Provider label map — the single source of truth for provider → website - * display mapping. MUST be kept in sync with the copy in - * web/lib/facts-drift.ts (for the runtime Cloudflare cron path). + * display mapping. MUST equal PROVIDER_LABELS in web/lib/facts-drift.ts (the + * runtime Cloudflare cron path); lib/facts-drift.test.ts fails when they differ. * * Excluded variants: DeepseekCN (not wired through shared ProviderKind, * #1104), Custom (dynamic meta-provider, #1519), and Antigravity * (a non-runnable legacy config tombstone, permanently excluded from public * provider facts). */ -const PROVIDER_LABEL_MAP = { +export const PROVIDER_LABEL_MAP = { Deepseek: { id: "deepseek", label: "DeepSeek", env: "DEEPSEEK_API_KEY" }, DeepseekAnthropic: { id: "deepseek-anthropic", label: "DeepSeek Anthropic", env: "DEEPSEEK_API_KEY / ANTHROPIC_API_KEY" }, NvidiaNim: { id: "nvidia-nim", label: "NVIDIA NIM", env: "NVIDIA_API_KEY / NVIDIA_NIM_API_KEY" }, From 49f11f711a69f719344a19aab6cd762e8ce5fdb9 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Tue, 29 Sep 2026 04:32:51 -0700 Subject: [PATCH 3/8] fix(web): 404 for a first segment that is not a locale Middleware leaves dotted paths alone, so /wp-login.php reached the locale layout and rendered the home page with HTTP 200 and an invalid html lang. Test: locale-layout.test.ts 2/2 (not-found case failed without the fix). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_014ZwqatxgVFxHvovngywnks --- web/app/[locale]/layout.tsx | 11 ++++++++--- web/lib/locale-layout.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 web/lib/locale-layout.test.ts diff --git a/web/app/[locale]/layout.tsx b/web/app/[locale]/layout.tsx index 3a69af7b1a..28f8ca9bb1 100644 --- a/web/app/[locale]/layout.tsx +++ b/web/app/[locale]/layout.tsx @@ -1,10 +1,11 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; +import { notFound } from "next/navigation"; import { Nav } from "@/components/nav"; import { Footer } from "@/components/footer"; import { UsageCounting } from "@/components/usage-counting"; import { BUILD_FACTS } from "@/lib/facts"; -import { localeDirection, locales, type Locale } from "@/lib/i18n/config"; +import { isValidLocale, localeDirection, locales } from "@/lib/i18n/config"; import { getChrome, getHome } from "@/lib/i18n/dictionaries"; import { serializeJsonLd } from "@/lib/json-ld"; import { buildPageMetadata } from "@/lib/page-meta"; @@ -74,6 +75,10 @@ export default async function LocaleLayout({ params: Promise<{ locale: string }>; }) { const { locale } = await params; + // Middleware leaves dotted paths alone, so `/wp-login.php` reaches this + // segment with that "locale". Without this it rendered the home page with + // HTTP 200 and ``. + if (!isValidLocale(locale)) notFound(); const chrome = getChrome(locale); // RTL locales (e.g. ar) set the document direction from the canonical // registry so the browser handles bidirectional layout from the root. @@ -106,9 +111,9 @@ export default async function LocaleLayout({ {chrome.skipToContent} -