Fix masked API key echoed back when editing custom providers
The custom provider edit form seeded the API key input with the masked value (e.g. "abc•••••wxyz") returned by the sanitized GET. Saving or running "Detect Models" without retyping echoed that mask back — the probe endpoint rejects it and a persisted mask broke HTTP header encoding. The field now starts blank with a "leave blank to keep" hint; an empty field omits apiKey so the stored credential is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,4 +2,6 @@
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Fix custom provider message sends failing with a `ByteString` error (`character ... value 8226`). The settings UI displays the saved API key masked with `•` characters; saving the provider without retyping the key persisted that mask as the real credential, which then broke HTTP header encoding. Masked values echoed back on update are now treated as "unchanged" and the stored key is preserved; masked values on create/probe are rejected. Re-enter the real API key once to clear any already-corrupted key.
|
||||
Fix custom provider message sends failing with a `ByteString` error (`character ... value 8226`). The settings UI displays the saved API key masked with `•` characters; saving the provider without retyping the key persisted that mask as the real credential, which then broke HTTP header encoding. Masked values echoed back on update are now treated as "unchanged" and the stored key is preserved; masked values on create/probe are rejected.
|
||||
|
||||
The edit form no longer seeds the API key field with the masked value at all — it starts blank (with a "Leave blank to keep current key" hint) so the mask can never be echoed back to save or "Detect Models". Existing keys are preserved when the field is left empty.
|
||||
|
||||
@@ -143,7 +143,11 @@ export function CustomProvidersSection({ embedded = false, onProviderChange }: C
|
||||
setName(provider.name);
|
||||
setApiType(provider.apiType);
|
||||
setBaseUrl(provider.baseUrl);
|
||||
setApiKey(provider.apiKey ?? "");
|
||||
// The loaded provider's apiKey is masked (e.g. "abc•••••wxyz") for display.
|
||||
// Never seed the editable field with the mask — echoing it back would send a
|
||||
// masked value to save/probe (which the server rejects). Start empty; an
|
||||
// unchanged blank field leaves the stored key untouched on save.
|
||||
setApiKey("");
|
||||
setModels((provider.models ?? []).map((model) => model.id).join(", "));
|
||||
setFormError(null);
|
||||
setDetectError(null);
|
||||
@@ -366,6 +370,9 @@ export function CustomProvidersSection({ embedded = false, onProviderChange }: C
|
||||
id="custom-provider-api-key"
|
||||
type="password"
|
||||
className="input"
|
||||
placeholder={editingProvider?.apiKey
|
||||
? t("providers.apiKeyKeepPlaceholder", "Leave blank to keep current key")
|
||||
: undefined}
|
||||
value={apiKey}
|
||||
onChange={(event) => setApiKey(event.target.value)}
|
||||
disabled={saving}
|
||||
|
||||
@@ -244,6 +244,49 @@ describe("CustomProvidersSection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not echo the masked key back when editing without retyping", async () => {
|
||||
mockFetchCustomProviders
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: "test-id",
|
||||
name: "Keyed Provider",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://api.example.com",
|
||||
// Server returns the key masked for display.
|
||||
apiKey: "abc•••••wxyz",
|
||||
},
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
id: "test-id",
|
||||
name: "Keyed Provider",
|
||||
apiType: "openai-compatible",
|
||||
baseUrl: "https://api.example.com",
|
||||
},
|
||||
]);
|
||||
|
||||
render(<CustomProvidersSection embedded />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Edit Keyed Provider")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByLabelText("Edit Keyed Provider"));
|
||||
|
||||
// The API key field must start empty, never seeded with the mask.
|
||||
const apiKeyInput = screen.getByLabelText("API key") as HTMLInputElement;
|
||||
expect(apiKeyInput.value).toBe("");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save Changes" }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateCustomProvider).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
// apiKey is omitted entirely so the stored credential is preserved.
|
||||
const [, payload] = mockUpdateCustomProvider.mock.calls[0];
|
||||
expect(payload).not.toHaveProperty("apiKey");
|
||||
});
|
||||
|
||||
it("deletes provider after confirmation", async () => {
|
||||
mockFetchCustomProviders
|
||||
.mockResolvedValueOnce([
|
||||
|
||||
@@ -4299,6 +4299,7 @@
|
||||
"saving": "Saving..."
|
||||
},
|
||||
"addCustom": "Add Custom Provider",
|
||||
"apiKeyKeepPlaceholder": "Leave blank to keep current key",
|
||||
"apiKeyLabel": "API key",
|
||||
"apiTypeAnthropic": "Anthropic-compatible",
|
||||
"apiTypeInvalid": "API type is invalid.",
|
||||
|
||||
Reference in New Issue
Block a user