import type { JsonRecord } from "./provider-http"; import { isRecord } from "./provider-http"; import { parseStoredOAuth } from "./oauth/stored-oauth"; export type ProviderAuthProfile = | { type: "api_key"; apiKey: string } | { type: "access_token"; accessToken: string } | { type: "account_token"; accessToken: string } | { type: "azure_openai"; baseUrl: string; deployment: string; apiKey: string } | { type: "aws_bedrock"; region: string; accessKeyId: string; secretAccessKey: string; sessionToken: string | null } | { type: "vertex_ai"; projectId: string; region: string; serviceAccountJson: string } | { type: "openai_compat"; baseUrl: string; apiKey: string | null } | { type: "anthropic_compat"; baseUrl: string; apiKey: string | null }; export function parseApiKey(secret: string): ProviderAuthProfile { return { type: "api_key", apiKey: nonEmptySecret(secret, "API key") }; } /** A sign-in provider's parser: it accepts only what a run can use and renew (see parseStoredOAuth). */ export function oauthProfileParser(providerKey: string, type: "access_token" | "account_token"): (secret: string) => ProviderAuthProfile { return (secret) => ({ type, accessToken: parseStoredOAuth(providerKey, secret).access }); } export function parseAzureProfile(secret: string): ProviderAuthProfile { const parsed = parseCloudSecret(secret); const baseUrl = requiredString(parsed.baseUrl, "Azure base URL").replace(/\/+$/, ""); const deployment = requiredString(parsed.deployment, "Azure deployment"); return { type: "azure_openai", baseUrl: normalizedHttpsUrl(baseUrl, "Azure base URL"), deployment, apiKey: requiredString(parsed.secret, "Azure API key") }; } export function parseBedrockProfile(secret: string): ProviderAuthProfile { const parsed = parseCloudSecret(secret); const credential = parseCredentialJson(requiredString(parsed.secret, "AWS credential")); return { type: "aws_bedrock", region: requiredString(parsed.region, "AWS region"), accessKeyId: requiredString(credential.accessKeyId, "AWS access key id"), secretAccessKey: requiredString(credential.secretAccessKey, "AWS secret access key"), sessionToken: typeof credential.sessionToken === "string" && credential.sessionToken.trim() ? credential.sessionToken.trim() : null, }; } export function parseVertexProfile(secret: string): ProviderAuthProfile { const parsed = parseCloudSecret(secret); const serviceAccountJson = requiredString(parsed.secret, "Vertex service account JSON"); parseCredentialJson(serviceAccountJson); return { type: "vertex_ai", projectId: requiredString(parsed.projectId, "Vertex project id"), region: requiredString(parsed.region, "Vertex region"), serviceAccountJson, }; } export function parseOpenAiCompatProfile(secret: string): ProviderAuthProfile { const parsed = parseCloudSecret(secret); return { type: "openai_compat", baseUrl: customEndpointUrl(requiredString(parsed.baseUrl, "Base URL")), apiKey: optionalSecret(parsed.secret) }; } export function parseAnthropicCompatProfile(secret: string): ProviderAuthProfile { const parsed = parseCloudSecret(secret); return { type: "anthropic_compat", baseUrl: customEndpointUrl(requiredString(parsed.baseUrl, "Base URL")), apiKey: optionalSecret(parsed.secret) }; } export function requireOpenAiCompatProfile(profile: ProviderAuthProfile): Extract { if (profile.type !== "openai_compat") throw new Error("OpenAI-compatible profile is required."); return profile; } export function requireAnthropicCompatProfile(profile: ProviderAuthProfile): Extract { if (profile.type !== "anthropic_compat") throw new Error("Anthropic-compatible profile is required."); return profile; } export function requireApiKeyProfile(profile: ProviderAuthProfile, label: string): Extract { if (profile.type !== "api_key") throw new Error(`${label} API key profile is required.`); return profile; } export function requireAccessTokenProfile(profile: ProviderAuthProfile): Extract { if (profile.type !== "access_token") throw new Error("Access token profile is required."); return profile; } export function requireAccountTokenProfile(profile: ProviderAuthProfile): Extract { if (profile.type !== "account_token") throw new Error("Account token profile is required."); return profile; } export function requireAzureProfile(profile: ProviderAuthProfile): Extract { if (profile.type !== "azure_openai") throw new Error("Azure OpenAI profile is required."); return profile; } export function requireBedrockProfile(profile: ProviderAuthProfile): Extract { if (profile.type !== "aws_bedrock") throw new Error("AWS Bedrock profile is required."); return profile; } export function requireVertexProfile(profile: ProviderAuthProfile): Extract { if (profile.type !== "vertex_ai") throw new Error("Vertex AI profile is required."); return profile; } function parseCloudSecret(secret: string): JsonRecord { try { const parsed = JSON.parse(secret) as unknown; if (!isRecord(parsed)) throw new Error("Cloud credential profile is invalid."); return parsed; } catch (error) { if (error instanceof SyntaxError) throw new Error("Cloud credential profile must be JSON."); throw error; } } function parseCredentialJson(value: string): JsonRecord { try { const parsed = JSON.parse(value) as unknown; if (!isRecord(parsed)) throw new Error("Credential JSON is invalid."); return parsed; } catch (error) { if (error instanceof SyntaxError) throw new Error("Credential must be JSON."); throw error; } } function requiredString(value: unknown, label: string): string { if (typeof value !== "string" || !value.trim()) throw new Error(`${label} is required.`); return value.trim(); } function nonEmptySecret(secret: string, label: string): string { const value = secret.trim(); if (!value) throw new Error(`${label} is required.`); return value; } function normalizedHttpsUrl(value: string, label: string): string { const url = new URL(value); if (url.protocol !== "https:") throw new Error(`${label} must use HTTPS.`); return normalizedUrl(url, label); } /** A custom endpoint may use plain HTTP only on this computer or the local network, where no key or prompt crosses the internet. */ function customEndpointUrl(value: string): string { const url = new URL(value); const plainLocal = url.protocol === "http:" && isLocalNetworkHost(url.hostname); if (url.protocol !== "https:" && !plainLocal) throw new Error("Base URL must use HTTPS unless it is on this computer or the local network."); return normalizedUrl(url, "Base URL"); } /** Loopback, private IPv4, IPv6 loopback or unique-local, and mDNS `.local` names. */ function isLocalNetworkHost(hostname: string): boolean { const host = hostname.replace(/^\[|\]$/g, "").toLowerCase(); if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true; if (host === "::1" || /^f[cd][0-9a-f]{2}:/.test(host)) return true; if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return false; const [a, b] = host.split(".").map(Number); return a === 127 || a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168); } /** Local servers often need no key; an empty one means none, anything else must be text. */ function optionalSecret(value: unknown): string | null { if (value === undefined || value === null) return null; if (typeof value !== "string") throw new Error("API key must be text."); return value.trim() || null; } function normalizedUrl(url: URL, label: string): string { 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(/\/+$/, ""); }