mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 20:52:15 +03:00
fix(dashboard): media playground cards stop sending masked API key as Bearer (#10449)
The 9 media *ExampleCard components under media-providers/components used the masked value from useApiKey() (sk-xxxx****yyyy) as an Authorization: Bearer header, which the gateway always rejects (AUTH_002) once REQUIRE_API_KEY is enabled. Mirror the LlmChatCard fix (#3503): authenticate via the dashboard session (credentials: "same-origin") and forward the selected key's id via x-omniroute-playground-key-id instead of its secret. buildCurl now keeps the <your-api-key> placeholder instead of the masked value. Adds tests/unit/bug-9935-masked-bearer.test.ts as the permanent regression guard (asserts none of the 9 cards embed apiKey as a raw Bearer token). Refs #9935 Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
611466b419
commit
dc32732b2a
1
changelog.d/fixes/9935-media-playground-masked-bearer.md
Normal file
1
changelog.d/fixes/9935-media-playground-masked-bearer.md
Normal file
@@ -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)
|
||||
@@ -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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
"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<string, string> = {
|
||||
"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();
|
||||
|
||||
@@ -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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
"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<string, string> = {
|
||||
"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();
|
||||
|
||||
@@ -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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
"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<string, string> = {
|
||||
"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;
|
||||
|
||||
@@ -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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
"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<string, string> = {
|
||||
"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();
|
||||
|
||||
@@ -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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
},
|
||||
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<string, string> = { "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();
|
||||
|
||||
@@ -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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
"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<string, string> = {
|
||||
"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;
|
||||
|
||||
@@ -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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
"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<string, string> = {
|
||||
"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();
|
||||
|
||||
@@ -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<string>("https://example.com");
|
||||
const [format, setFormat] = useState<FetchFormat>("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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
"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<string, string> = {
|
||||
"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();
|
||||
|
||||
@@ -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<string>(() => t("webSearchSample"));
|
||||
const [numResults, setNumResults] = useState<number>(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 || "<your-api-key>"}`,
|
||||
Authorization: "Bearer <your-api-key>",
|
||||
"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<string, string> = {
|
||||
"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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
36
tests/unit/bug-9935-masked-bearer.test.ts
Normal file
36
tests/unit/bug-9935-masked-bearer.test.ts
Normal file
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user