diff --git a/changelog.d/fixes/9935-media-playground-masked-bearer.md b/changelog.d/fixes/9935-media-playground-masked-bearer.md new file mode 100644 index 0000000000..2fd123d801 --- /dev/null +++ b/changelog.d/fixes/9935-media-playground-masked-bearer.md @@ -0,0 +1 @@ +- fix(dashboard): media mini-playgrounds authenticate via session instead of sending the masked API key as Bearer, fixing 401s under REQUIRE_API_KEY (#9935) diff --git a/src/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard.tsx index 2f8983ba8a..724ad8422e 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "next-intl"; import { useApiKey } from "../../providers/hooks/useApiKey"; import { useProviderModels } from "../../providers/hooks/useProviderModels"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; import { PlaygroundCard } from "./PlaygroundCard"; interface Props { @@ -24,7 +25,7 @@ function extractError(data: unknown): string | null { export function EmbeddingExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const { models } = useProviderModels(providerId); const firstModel = models[0]?.id ?? ""; @@ -43,7 +44,7 @@ export function EmbeddingExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", "Content-Type": "application/json", }, body: buildBody(), @@ -55,13 +56,18 @@ export function EmbeddingExampleCard({ providerId }: Props) { setResult(undefined); const t0 = performance.now(); try { + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { + "Content-Type": "application/json", + "x-connection-id": providerId, + }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: JSON.stringify(buildBody()), }); const data: unknown = await res.json(); diff --git a/src/app/(dashboard)/dashboard/media-providers/components/ImageExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/ImageExampleCard.tsx index 4ebc8ab815..8b77fb865b 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/ImageExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/ImageExampleCard.tsx @@ -6,6 +6,7 @@ import { useTranslations } from "next-intl"; import { useApiKey } from "../../providers/hooks/useApiKey"; import { useProviderModels } from "../../providers/hooks/useProviderModels"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; import { PlaygroundCard } from "./PlaygroundCard"; interface SuggestedHfModel { @@ -101,7 +102,7 @@ function ImageResultRenderer(data: unknown, altText: string) { export function ImageExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); const tMedia = useTranslations("media"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const { models } = useProviderModels(providerId); const suggestedModels = useHfSuggestedImageModels(providerId); @@ -121,7 +122,7 @@ export function ImageExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", "Content-Type": "application/json", }, body: buildBody(), @@ -133,13 +134,18 @@ export function ImageExampleCard({ providerId }: Props) { setResult(undefined); const t0 = performance.now(); try { + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { + "Content-Type": "application/json", + "x-connection-id": providerId, + }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: JSON.stringify(buildBody()), }); const data: unknown = await res.json(); diff --git a/src/app/(dashboard)/dashboard/media-providers/components/MusicExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/MusicExampleCard.tsx index ceef366d2e..cd9418a779 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/MusicExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/MusicExampleCard.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "next-intl"; import { useApiKey } from "../../providers/hooks/useApiKey"; import { useProviderModels } from "../../providers/hooks/useProviderModels"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; import { PlaygroundCard } from "./PlaygroundCard"; interface Props { @@ -24,7 +25,7 @@ function extractError(data: unknown): string | null { export function MusicExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const { models } = useProviderModels(providerId); const firstModel = models[0]?.id ?? ""; @@ -45,7 +46,7 @@ export function MusicExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", "Content-Type": "application/json", }, body: buildBody(), @@ -58,13 +59,18 @@ export function MusicExampleCard({ providerId }: Props) { setAudioUrl(null); const t0 = performance.now(); try { + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { + "Content-Type": "application/json", + "x-connection-id": providerId, + }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: JSON.stringify(buildBody()), }); const elapsed = performance.now() - t0; diff --git a/src/app/(dashboard)/dashboard/media-providers/components/OcrExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/OcrExampleCard.tsx index 4c2e0dab95..d623943984 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/OcrExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/OcrExampleCard.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "next-intl"; import { useApiKey } from "../../providers/hooks/useApiKey"; import { useProviderModels } from "../../providers/hooks/useProviderModels"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; import { PlaygroundCard } from "./PlaygroundCard"; interface Props { @@ -25,7 +26,7 @@ function extractError(data: unknown): string | null { export function OcrExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const { models } = useProviderModels(providerId); const firstModel = models[0]?.id ?? ""; @@ -47,7 +48,7 @@ export function OcrExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", "Content-Type": "application/json", }, body: buildBody(), @@ -59,13 +60,18 @@ export function OcrExampleCard({ providerId }: Props) { setResult(undefined); const t0 = performance.now(); try { + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { + "Content-Type": "application/json", + "x-connection-id": providerId, + }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: JSON.stringify(buildBody()), }); const data: unknown = await res.json(); diff --git a/src/app/(dashboard)/dashboard/media-providers/components/SttExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/SttExampleCard.tsx index 188af4b0e5..57016199da 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/SttExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/SttExampleCard.tsx @@ -6,6 +6,7 @@ import { useApiKey } from "../../providers/hooks/useApiKey"; import { useProviderModels } from "../../providers/hooks/useProviderModels"; import { PlaygroundCard } from "./PlaygroundCard"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; interface Props { providerId: string; @@ -39,7 +40,7 @@ function SttResultRenderer(data: unknown) { export function SttExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const { models } = useProviderModels(providerId); // Show only speech-to-text models. Providers like OpenRouter expose a large @@ -72,7 +73,7 @@ export function SttExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", }, body: { model: qualify(effectiveModel), @@ -105,12 +106,15 @@ export function SttExampleCard({ providerId }: Props) { formData.append("model", qualify(effectiveModel)); formData.append("file", file); + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { "x-connection-id": providerId }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: formData, }); const data: unknown = await res.json(); diff --git a/src/app/(dashboard)/dashboard/media-providers/components/TtsExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/TtsExampleCard.tsx index 17875795df..4efd624ec8 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/TtsExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/TtsExampleCard.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "next-intl"; import { useApiKey } from "../../providers/hooks/useApiKey"; import { useProviderModels } from "../../providers/hooks/useProviderModels"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; import { PlaygroundCard } from "./PlaygroundCard"; interface Props { @@ -26,7 +27,7 @@ function extractError(data: unknown): string | null { export function TtsExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const { models } = useProviderModels(providerId); const firstModel = models[0]?.id ?? "tts-1"; @@ -54,7 +55,7 @@ export function TtsExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", "Content-Type": "application/json", }, body: buildBody(), @@ -69,13 +70,18 @@ export function TtsExampleCard({ providerId }: Props) { } const t0 = performance.now(); try { + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { + "Content-Type": "application/json", + "x-connection-id": providerId, + }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: JSON.stringify(buildBody()), }); const elapsed = performance.now() - t0; diff --git a/src/app/(dashboard)/dashboard/media-providers/components/VideoExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/VideoExampleCard.tsx index 1a4d872d3d..3a815544e8 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/VideoExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/VideoExampleCard.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "next-intl"; import { useApiKey } from "../../providers/hooks/useApiKey"; import { useProviderModels } from "../../providers/hooks/useProviderModels"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; import { PlaygroundCard } from "./PlaygroundCard"; interface Props { @@ -48,7 +49,7 @@ function VideoResultRenderer(data: unknown, unsupportedText: string) { export function VideoExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const { models } = useProviderModels(providerId); const firstModel = models[0]?.id ?? ""; @@ -66,7 +67,7 @@ export function VideoExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", "Content-Type": "application/json", }, body: buildBody(), @@ -78,13 +79,18 @@ export function VideoExampleCard({ providerId }: Props) { setResult(undefined); const t0 = performance.now(); try { + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { + "Content-Type": "application/json", + "x-connection-id": providerId, + }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: JSON.stringify(buildBody()), }); const data: unknown = await res.json(); diff --git a/src/app/(dashboard)/dashboard/media-providers/components/WebFetchExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/WebFetchExampleCard.tsx index 4c746e4216..dc91cbcda0 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/WebFetchExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/WebFetchExampleCard.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { useTranslations } from "next-intl"; import { useApiKey } from "../../providers/hooks/useApiKey"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; import { PlaygroundCard } from "./PlaygroundCard"; interface Props { @@ -34,7 +35,7 @@ function extractError(data: unknown): string | null { export function WebFetchExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const [url, setUrl] = useState("https://example.com"); const [format, setFormat] = useState("markdown"); @@ -50,7 +51,7 @@ export function WebFetchExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", "Content-Type": "application/json", }, body: buildBody(), @@ -62,13 +63,18 @@ export function WebFetchExampleCard({ providerId }: Props) { setResult(undefined); const t0 = performance.now(); try { + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { + "Content-Type": "application/json", + "x-connection-id": providerId, + }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: JSON.stringify(buildBody()), }); const data: unknown = await res.json(); diff --git a/src/app/(dashboard)/dashboard/media-providers/components/WebSearchExampleCard.tsx b/src/app/(dashboard)/dashboard/media-providers/components/WebSearchExampleCard.tsx index 901b938d37..111559aae1 100644 --- a/src/app/(dashboard)/dashboard/media-providers/components/WebSearchExampleCard.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/components/WebSearchExampleCard.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import { useTranslations } from "next-intl"; import { useApiKey } from "../../providers/hooks/useApiKey"; import { buildCurl } from "../../providers/utils/buildCurl"; +import { PLAYGROUND_KEY_ID_HEADER, resolvePlaygroundKeyId } from "../../providers/utils/playgroundAuth"; import { PlaygroundCard } from "./PlaygroundCard"; interface Props { @@ -64,7 +65,7 @@ function SearchResultRenderer(data: unknown, fallbackTitle: (number: number) => export function WebSearchExampleCard({ providerId }: Props) { const t = useTranslations("miniPlayground"); - const { apiKey } = useApiKey(); + const { apiKey, keys } = useApiKey(); const [query, setQuery] = useState(() => t("webSearchSample")); const [numResults, setNumResults] = useState(5); @@ -79,7 +80,7 @@ export function WebSearchExampleCard({ providerId }: Props) { (typeof window !== "undefined" ? window.location.origin : "http://localhost:20128") + ENDPOINT_PATH, headers: { - Authorization: `Bearer ${apiKey || ""}`, + Authorization: "Bearer ", "Content-Type": "application/json", }, body: buildBody(), @@ -91,13 +92,18 @@ export function WebSearchExampleCard({ providerId }: Props) { setResult(undefined); const t0 = performance.now(); try { + // Authenticate via the dashboard session cookie — never send the masked + // apiKey as a Bearer token (it is not a real credential; see #9935). + const headers: Record = { + "Content-Type": "application/json", + "x-connection-id": providerId, + }; + const playgroundKeyId = resolvePlaygroundKeyId(apiKey, keys); + if (playgroundKeyId) headers[PLAYGROUND_KEY_ID_HEADER] = playgroundKeyId; const res = await fetch(ENDPOINT_PATH, { method: "POST", - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - "x-connection-id": providerId, - }, + credentials: "same-origin", + headers, body: JSON.stringify(buildBody()), }); const data: unknown = await res.json(); diff --git a/src/app/(dashboard)/dashboard/providers/utils/playgroundAuth.ts b/src/app/(dashboard)/dashboard/providers/utils/playgroundAuth.ts new file mode 100644 index 0000000000..559c3b0c9b --- /dev/null +++ b/src/app/(dashboard)/dashboard/providers/utils/playgroundAuth.ts @@ -0,0 +1,34 @@ +/** + * playgroundAuth — shared helpers so the dashboard mini-playgrounds authenticate + * upstream requests via the dashboard session instead of a raw Bearer token. + * + * `/api/keys` only ever exposes MASKED key values (sk-xxxx****yyyy — see + * `maskStoredApiKey` in `src/lib/apiKeyExposure.ts`). Sending that masked + * string as `Authorization: Bearer` is never a valid credential and 401s + * under `REQUIRE_API_KEY` (#9935). The fix mirrors `LlmChatCard.tsx` (#3503): + * requests go out with `credentials: "same-origin"` and no Bearer header: the + * gateway falls through to the dashboard session. When a specific key is + * selected we forward only its id via `PLAYGROUND_KEY_ID_HEADER` so the + * gateway can still apply that key's policy (allowed_models, etc.) + * server-side — the secret itself never reaches the browser. + */ + +/** Header used to test a specific API key's policy from the dashboard playground + * without exposing the key secret to the browser — the gateway resolves the key + * by id server-side (see enforceApiKeyPolicy). */ +export const PLAYGROUND_KEY_ID_HEADER = "x-omniroute-playground-key-id"; + +/** + * Map the playground's masked key selection (sk-xxxx****yyyy, as returned by + * `/api/keys`) back to its key id. The id — never the secret — is sent to the + * gateway so it can apply that key's policy (allowed_models, etc.) server-side. + * Returns null when there is no match, which falls through to the dashboard + * session (full access, any model). + */ +export function resolvePlaygroundKeyId( + selectedMaskedKey: string, + keys: { id: string; key: string }[] +): string | null { + if (!selectedMaskedKey) return null; + return keys.find((k) => k.key === selectedMaskedKey)?.id ?? null; +} diff --git a/tests/unit/bug-9935-masked-bearer.test.ts b/tests/unit/bug-9935-masked-bearer.test.ts new file mode 100644 index 0000000000..e1587d64d1 --- /dev/null +++ b/tests/unit/bug-9935-masked-bearer.test.ts @@ -0,0 +1,36 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { maskStoredApiKey } from "../../src/lib/apiKeyExposure"; + +const COMPONENT_DIR = resolve("src/app/(dashboard)/dashboard/media-providers/components"); +const EXAMPLE_CARDS = [ + "WebSearchExampleCard.tsx", + "WebFetchExampleCard.tsx", + "ImageExampleCard.tsx", + "TtsExampleCard.tsx", + "SttExampleCard.tsx", + "OcrExampleCard.tsx", + "MusicExampleCard.tsx", + "EmbeddingExampleCard.tsx", + "VideoExampleCard.tsx", +]; +const FIXED_REFERENCE = "LlmChatCard.tsx"; +const MASKED_BEARER = /\bBearer\s*\$?\{?\s*apiKey/; + +test("every media ExampleCard avoids sending a masked apiKey as Bearer (#9935)", () => { + for (const file of [...EXAMPLE_CARDS, FIXED_REFERENCE]) { + const src = readFileSync(resolve(COMPONENT_DIR, file), "utf8"); + assert.ok( + !MASKED_BEARER.test(src), + `${file} still sends the (masked) apiKey as an Authorization: Bearer token — will 401 under REQUIRE_API_KEY` + ); + } +}); + +test("masked value is never a real API key (repro of the 401 trigger)", () => { + const real = "sk-abcdef0123456789wxyz"; + const masked = maskStoredApiKey(real); + assert.notEqual(masked, real); +});