fix(ui): write playground key settings only through the encoded storage path

Key source, virtual key and custom headers now go through their own write loop, separate from the plain settings, so a key value can never take the plain sessionStorage branch
This commit is contained in:
Yuneng Jiang 2026-10-16 10:55:55 -07:00
parent e547be51a5
commit ca4e742115
No known key found for this signature in database
4 changed files with 54 additions and 24 deletions

View file

@ -6,7 +6,7 @@ import { makeOpenAIChatCompletionRequest } from "@/components/llm_calls/chat_com
import { fetchAvailableModels } from "@/components/llm_calls/fetch_models";
import { makeOpenAIResponsesRequest } from "@/components/llm_calls/responses_api";
import { callMCPTool, fetchMCPServers, fetchMCPToolsets, listMCPTools } from "@/components/networking";
import { setSecureItem } from "@/utils/secureStorage";
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
import { PlaygroundChat } from "./PlaygroundChat";
vi.mock("@/app/(dashboard)/hooks/useCan", () => ({ default: () => true }));
@ -487,4 +487,17 @@ describe("PlaygroundChat", () => {
expect(screen.queryByText("Python Code Executed")).not.toBeInTheDocument();
});
it("keeps the virtual key encoded in session storage", async () => {
setSecureItem("apiKeySource", JSON.stringify("custom"));
setSecureItem("apiKey", "sk-virtual-123");
preload({ selectedModel: "gpt-fast" });
renderChat();
await waitFor(() => expect(fetchAvailableModels).toHaveBeenCalledWith("sk-virtual-123"));
expect(getSecureItem("apiKey")).toBe("sk-virtual-123");
const stored = Object.keys(sessionStorage).map((key) => sessionStorage.getItem(key) ?? "");
expect(stored.length).toBeGreaterThan(0);
expect(stored.filter((value) => value.includes("sk-virtual-123"))).toEqual([]);
});
});

View file

@ -6,7 +6,8 @@ import {
allowedTools,
defaultSettings,
loadSettings,
storedValues,
plainValues,
secureValues,
toggleMcpItem,
toggleToolLimit,
withEndpoint,
@ -29,7 +30,7 @@ const TOOLS = ["search", "fetch", "summarize"] as const;
const SCOPE = { server: "s", tools: TOOLS };
describe("loadSettings", () => {
it("restores every persisted field from what storedValues wrote", () => {
it("restores every persisted field from what was written", () => {
const persisted: Partial<PlaygroundSettings> = {
keySource: "custom",
virtualKey: "sk-test",
@ -48,8 +49,8 @@ describe("loadSettings", () => {
};
const original = settingsWith(persisted);
const written = Object.fromEntries(
storedValues(original)
.filter((entry): entry is { key: string; value: string; secure: boolean } => entry.value !== null)
[...secureValues(original), ...plainValues(original)]
.filter((entry): entry is { key: string; value: string } => entry.value !== null)
.map(({ key, value }) => [key, value]),
);

View file

@ -29,7 +29,7 @@ export type PlaygroundSettings = Readonly<{
maxTokens: number | null;
}>;
export type StoredValue = Readonly<{ key: string; value: string | null; secure: boolean }>;
export type StoredValue = Readonly<{ key: string; value: string | null }>;
export type SettingsStorage = Readonly<{
read: (key: string) => string | null;
@ -122,21 +122,24 @@ export const loadSettings = (storage: SettingsStorage, keysDisabled: boolean): P
stream: storage.read("streamingEnabled") !== "false",
});
export const storedValues = (settings: PlaygroundSettings): readonly StoredValue[] => [
{ key: "apiKeySource", value: JSON.stringify(settings.keySource), secure: true },
{ key: "apiKey", value: settings.virtualKey, secure: true },
{ key: "customHeaders", value: JSON.stringify(settings.headerPairs), secure: true },
{ key: "customProxyBaseUrl", value: settings.proxyBaseUrl || null, secure: false },
{ key: "endpointType", value: settings.endpoint, secure: false },
{ key: "selectedModel", value: settings.model, secure: false },
{ key: "selectedVoice", value: settings.voice, secure: false },
{ key: "selectedTags", value: JSON.stringify(settings.tags), secure: false },
{ key: "selectedVectorStores", value: JSON.stringify(settings.vectorStores), secure: false },
{ key: "selectedGuardrails", value: JSON.stringify(settings.guardrails), secure: false },
{ key: "selectedPolicies", value: JSON.stringify(settings.policies), secure: false },
{ key: "selectedMCPServers", value: JSON.stringify(settings.mcpSelection), secure: false },
{ key: "mcpServerToolRestrictions", value: JSON.stringify(settings.toolLimits), secure: false },
{ key: "streamingEnabled", value: String(settings.stream), secure: false },
export const secureValues = (settings: PlaygroundSettings): readonly StoredValue[] => [
{ key: "apiKeySource", value: JSON.stringify(settings.keySource) },
{ key: "apiKey", value: settings.virtualKey },
{ key: "customHeaders", value: JSON.stringify(settings.headerPairs) },
];
export const plainValues = (settings: PlaygroundSettings): readonly StoredValue[] => [
{ key: "customProxyBaseUrl", value: settings.proxyBaseUrl || null },
{ key: "endpointType", value: settings.endpoint },
{ key: "selectedModel", value: settings.model },
{ key: "selectedVoice", value: settings.voice },
{ key: "selectedTags", value: JSON.stringify(settings.tags) },
{ key: "selectedVectorStores", value: JSON.stringify(settings.vectorStores) },
{ key: "selectedGuardrails", value: JSON.stringify(settings.guardrails) },
{ key: "selectedPolicies", value: JSON.stringify(settings.policies) },
{ key: "selectedMCPServers", value: JSON.stringify(settings.mcpSelection) },
{ key: "mcpServerToolRestrictions", value: JSON.stringify(settings.toolLimits) },
{ key: "streamingEnabled", value: String(settings.stream) },
];
const singleServer = (selection: readonly string[]): readonly string[] =>

View file

@ -1,6 +1,12 @@
import { useCallback, useEffect, useState } from "react";
import { getSecureItem, setSecureItem } from "@/utils/secureStorage";
import { defaultSettings, loadSettings, storedValues, type PlaygroundSettings } from "./playgroundSettings";
import {
defaultSettings,
loadSettings,
plainValues,
secureValues,
type PlaygroundSettings,
} from "./playgroundSettings";
export type SettingsUpdate = Partial<PlaygroundSettings> | ((current: PlaygroundSettings) => PlaygroundSettings);
@ -10,10 +16,17 @@ const browserStorage = {
};
const persist = (settings: PlaygroundSettings): void => {
storedValues(settings).forEach(({ key, value, secure }) => {
secureValues(settings).forEach(({ key, value }) => {
try {
if (value === null) window.sessionStorage.removeItem(key);
else setSecureItem(key, value);
} catch {
return;
}
});
plainValues(settings).forEach(({ key, value }) => {
try {
if (value === null) window.sessionStorage.removeItem(key);
else if (secure) setSecureItem(key, value);
else window.sessionStorage.setItem(key, value);
} catch {
return;