Harden custom provider endpoint validation

This commit is contained in:
ZacharyZhang-NY committed 2026-06-26 19:31:32 -04:00
1 parent 745c22ec77
commit d85a47c40c
3 files changed
+26 -8

No files matched your search

@@ -75,12 +75,12 @@ export function parseVertexProfile(secret: string): ProviderAuthProfile {
export function parseOpenAiCompatProfile(secret: string): ProviderAuthProfile {
const parsed = parseCloudSecret(secret);
return { type: "openai_compat", baseUrl: normalizedEndpointUrl(requiredString(parsed.baseUrl, "Base URL"), "Base URL"), apiKey: requiredString(parsed.secret, "API key") };
return { type: "openai_compat", baseUrl: normalizedHttpsUrl(requiredString(parsed.baseUrl, "Base URL"), "Base URL"), apiKey: requiredString(parsed.secret, "API key") };
}
export function parseAnthropicCompatProfile(secret: string): ProviderAuthProfile {
const parsed = parseCloudSecret(secret);
return { type: "anthropic_compat", baseUrl: normalizedEndpointUrl(requiredString(parsed.baseUrl, "Base URL"), "Base URL"), apiKey: requiredString(parsed.secret, "API key") };
return { type: "anthropic_compat", baseUrl: normalizedHttpsUrl(requiredString(parsed.baseUrl, "Base URL"), "Base URL"), apiKey: requiredString(parsed.secret, "API key") };
}
export function requireOpenAiCompatProfile(profile: ProviderAuthProfile): Extract<ProviderAuthProfile, { type: "openai_compat" }> {
@@ -159,11 +159,7 @@ function nonEmptySecret(secret: string, label: string): string {
function normalizedHttpsUrl(value: string, label: string): string {
const url = new URL(value);
if (url.protocol !== "https:") throw new Error(`${label} must use HTTPS.`);
return url.toString().replace(/\/+$/, "");
}
function normalizedEndpointUrl(value: string, label: string): string {
const url = new URL(value);
if (url.protocol !== "https:" && url.protocol !== "http:") throw new Error(`${label} must be an http(s) URL.`);
if (url.username || url.password) throw new Error(`${label} must not include credentials.`);
if (url.search || url.hash) throw new Error(`${label} must not include query strings or fragments.`);
return url.toString().replace(/\/+$/, "");
}
@@ -85,6 +85,20 @@ describe("provider contracts", () => {
deployment: "gpt-5.4",
secret: "azure-key",
}))).toThrow("Azure base URL must use HTTPS.");
for (const providerKey of ["custom-openai", "custom-anthropic"]) {
expect(() => providerAdapters[providerKey].parseAuthProfile(JSON.stringify({
baseUrl: "http://api.example.com/v1",
secret: "sk-custom",
}))).toThrow("Base URL must use HTTPS.");
expect(() => providerAdapters[providerKey].parseAuthProfile(JSON.stringify({
baseUrl: "https://user:token@api.example.com/v1",
secret: "sk-custom",
}))).toThrow("Base URL must not include credentials.");
expect(() => providerAdapters[providerKey].parseAuthProfile(JSON.stringify({
baseUrl: "https://api.example.com/v1?tenant=acme",
secret: "sk-custom",
}))).toThrow("Base URL must not include query strings or fragments.");
}
});
// Providers with no enumerable models endpoint keep returning the curated
@@ -604,10 +604,12 @@ describe("provider service", () => {
const providers = new ProviderService(connection.db);
let orgId = "";
let azureProviderId = "";
let customOpenAiProviderId = "";
connection.transaction(() => {
orgId = createOrganization(organization);
azureProviderId = providerId(connection, orgId, "azure-openai");
customOpenAiProviderId = providerId(connection, orgId, "custom-openai");
});
expect(() => connection.transaction(() => providers.saveCredential({
@@ -616,6 +618,12 @@ describe("provider service", () => {
secret: JSON.stringify({ baseUrl: "http://example.com", deployment: "gpt-5", secret: "key" }),
model: "gpt-5",
}))).toThrow("Azure base URL must use HTTPS.");
expect(() => connection.transaction(() => providers.saveCredential({
orgId,
providerId: customOpenAiProviderId,
secret: JSON.stringify({ baseUrl: "http://api.example.com/v1", secret: "key" }),
model: "gpt-5",
}))).toThrow("Base URL must use HTTPS.");
connection.close();
});
});