diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0fc9aac9..6fd961ffee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Cursor: present legacy request-based plans as one Requests quota with the raw used/limit count instead of unrelated token-based Auto/API bars (#1420, fixes #1418). Thanks @hhh2210! - Cost usage: memoize Codex priority-turn trace metadata incrementally so warm refreshes scan only appended rows instead of rescanning large trace databases (#1404). Thanks @ProspectOre! - Security: reject insecure or malformed MiniMax and Alibaba endpoint overrides while preserving valid custom HTTPS deployments (#1269). Thanks @Hinotoi-agent! +- Security: reject insecure or malformed OpenRouter, Codebuff, Groq, and ElevenLabs endpoint overrides before sending provider credentials (#1256). Thanks @Hinotoi-agent! ## 0.33.0 — 2026-06-11 diff --git a/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift b/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift index 0691c363d9..dc24735070 100644 --- a/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift +++ b/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift @@ -58,7 +58,14 @@ struct ProviderEndpointOverrideValidator: Sendable { guard let url else { return nil } guard let scheme = url.scheme?.lowercased(), scheme == "https" else { return nil } guard url.user == nil, url.password == nil else { return nil } - guard url.host(percentEncoded: false) != nil else { return nil } + guard let decodedHost = url.host(percentEncoded: false)?.lowercased(), + !decodedHost.isEmpty, + !decodedHost.contains("%"), + decodedHost.rangeOfCharacter(from: .whitespacesAndNewlines) == nil, + decodedHost.rangeOfCharacter(from: .controlCharacters) == nil, + let encodedHost = url.host(percentEncoded: true)?.lowercased(), + Self.hostHasNoEncodedDelimiters(encodedHost, decodedHost: decodedHost, url: url) + else { return nil } return url } @@ -102,7 +109,7 @@ struct ProviderEndpointOverrideValidator: Sendable { decodedHost.rangeOfCharacter(from: .whitespacesAndNewlines) == nil, decodedHost.rangeOfCharacter(from: .controlCharacters) == nil, let encodedHost = url.host(percentEncoded: true)?.lowercased(), - self.hostHasNoEncodedDelimiters(encodedHost, decodedHost: decodedHost, url: url) + Self.hostHasNoEncodedDelimiters(encodedHost, decodedHost: decodedHost, url: url) else { return nil } switch policy { @@ -118,7 +125,7 @@ struct ProviderEndpointOverrideValidator: Sendable { } } - private func hostHasNoEncodedDelimiters(_ encodedHost: String, decodedHost: String, url: URL) -> Bool { + private static func hostHasNoEncodedDelimiters(_ encodedHost: String, decodedHost: String, url: URL) -> Bool { if decodedHost.contains(":") { guard encodedHost == decodedHost, let componentHost = URLComponents(url: url, resolvingAgainstBaseURL: false)?.host, diff --git a/Sources/CodexBarCore/Providers/Codebuff/CodebuffProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Codebuff/CodebuffProviderDescriptor.swift index 8ea43102f9..293bb970b8 100644 --- a/Sources/CodexBarCore/Providers/Codebuff/CodebuffProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Codebuff/CodebuffProviderDescriptor.swift @@ -80,14 +80,17 @@ struct CodebuffAPIFetchStrategy: ProviderFetchStrategy { } /// Errors related to Codebuff settings. -public enum CodebuffSettingsError: LocalizedError, Sendable { +public enum CodebuffSettingsError: LocalizedError, Sendable, Equatable { case missingToken + case invalidEndpointOverride(String) public var errorDescription: String? { switch self { case .missingToken: "Codebuff API token not configured. Set CODEBUFF_API_KEY or run `codebuff login` to " + "populate ~/.config/manicode/credentials.json." + case let .invalidEndpointOverride(key): + "Codebuff endpoint override \(key) must use HTTPS or a bare host." } } } diff --git a/Sources/CodexBarCore/Providers/Codebuff/CodebuffSettingsReader.swift b/Sources/CodexBarCore/Providers/Codebuff/CodebuffSettingsReader.swift index a046b43cae..633bf00d94 100644 --- a/Sources/CodexBarCore/Providers/Codebuff/CodebuffSettingsReader.swift +++ b/Sources/CodexBarCore/Providers/Codebuff/CodebuffSettingsReader.swift @@ -13,14 +13,20 @@ public enum CodebuffSettingsReader { /// Returns the API base URL, defaulting to the production endpoint. public static func apiURL(environment: [String: String] = ProcessInfo.processInfo.environment) -> URL { - if let override = environment["CODEBUFF_API_URL"], - let url = URL(string: cleaned(override) ?? "") - { - return url + if let override = self.validAPIURL(environment: environment) { + return override } return URL(string: "https://www.codebuff.com")! } + public static func validateEndpointOverrides( + environment: [String: String] = ProcessInfo.processInfo.environment) throws + { + guard let raw = self.cleaned(environment["CODEBUFF_API_URL"]) else { return } + guard ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) == nil else { return } + throw CodebuffSettingsError.invalidEndpointOverride("CODEBUFF_API_URL") + } + /// Returns the auth token from the local credentials file if present. public static func authToken( authFileURL: URL? = nil, @@ -60,6 +66,11 @@ public enum CodebuffSettingsReader { value = value.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? nil : value } + + private static func validAPIURL(environment: [String: String]) -> URL? { + guard let raw = self.cleaned(environment["CODEBUFF_API_URL"]) else { return nil } + return ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) + } } private struct CredentialsFile: Decodable { diff --git a/Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageFetcher.swift b/Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageFetcher.swift index bd8062ad65..c5f3dcbc37 100644 --- a/Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Codebuff/CodebuffUsageFetcher.swift @@ -23,6 +23,7 @@ public enum CodebuffUsageFetcher { guard !trimmed.isEmpty else { throw CodebuffUsageError.missingCredentials } + try CodebuffSettingsReader.validateEndpointOverrides(environment: environment) let baseURL = CodebuffSettingsReader.apiURL(environment: environment) diff --git a/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsSettingsReader.swift b/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsSettingsReader.swift index bd3cdf4caa..2ffececd1c 100644 --- a/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsSettingsReader.swift +++ b/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsSettingsReader.swift @@ -17,14 +17,20 @@ public enum ElevenLabsSettingsReader { } public static func apiURL(environment: [String: String] = ProcessInfo.processInfo.environment) -> URL { - if let override = self.cleaned(environment[self.apiURLEnvironmentKey]), - let url = URL(string: override) - { - return url + if let override = self.validAPIURL(environment: environment) { + return override } return URL(string: "https://api.elevenlabs.io")! } + public static func validateEndpointOverrides( + environment: [String: String] = ProcessInfo.processInfo.environment) throws + { + guard let raw = self.cleaned(environment[self.apiURLEnvironmentKey]) else { return } + guard ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) == nil else { return } + throw ElevenLabsSettingsError.invalidEndpointOverride(self.apiURLEnvironmentKey) + } + static func cleaned(_ raw: String?) -> String? { guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return nil @@ -37,4 +43,20 @@ public enum ElevenLabsSettingsReader { value = value.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? nil : value } + + private static func validAPIURL(environment: [String: String]) -> URL? { + guard let raw = self.cleaned(environment[self.apiURLEnvironmentKey]) else { return nil } + return ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) + } +} + +public enum ElevenLabsSettingsError: LocalizedError, Sendable, Equatable { + case invalidEndpointOverride(String) + + public var errorDescription: String? { + switch self { + case let .invalidEndpointOverride(key): + "ElevenLabs endpoint override \(key) must use HTTPS or a bare host." + } + } } diff --git a/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsUsageFetcher.swift b/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsUsageFetcher.swift index bdf2931327..9befb3cc15 100644 --- a/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/ElevenLabs/ElevenLabsUsageFetcher.swift @@ -188,6 +188,7 @@ public struct ElevenLabsUsageFetcher: Sendable { guard !trimmed.isEmpty else { throw ElevenLabsUsageError.missingCredentials } + try ElevenLabsSettingsReader.validateEndpointOverrides(environment: environment) let url = Self.subscriptionURL(baseURL: ElevenLabsSettingsReader.apiURL(environment: environment)) var request = URLRequest(url: url) diff --git a/Sources/CodexBarCore/Providers/Groq/GroqSettingsReader.swift b/Sources/CodexBarCore/Providers/Groq/GroqSettingsReader.swift index 4b4f20804a..65c8a4a3df 100644 --- a/Sources/CodexBarCore/Providers/Groq/GroqSettingsReader.swift +++ b/Sources/CodexBarCore/Providers/Groq/GroqSettingsReader.swift @@ -13,14 +13,20 @@ public enum GroqSettingsReader { public static func apiURL( environment: [String: String] = ProcessInfo.processInfo.environment) -> URL { - if let raw = self.cleaned(environment[self.apiURLEnvironmentKey]), - let url = URL(string: raw) - { - return url + if let override = self.validAPIURL(environment: environment) { + return override } return URL(string: "https://api.groq.com/v1")! } + public static func validateEndpointOverrides( + environment: [String: String] = ProcessInfo.processInfo.environment) throws + { + guard let raw = self.cleaned(environment[self.apiURLEnvironmentKey]) else { return } + guard ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) == nil else { return } + throw GroqSettingsError.invalidEndpointOverride(self.apiURLEnvironmentKey) + } + static func cleaned(_ raw: String?) -> String? { guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return nil @@ -33,4 +39,20 @@ public enum GroqSettingsReader { value = value.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? nil : value } + + private static func validAPIURL(environment: [String: String]) -> URL? { + guard let raw = self.cleaned(environment[self.apiURLEnvironmentKey]) else { return nil } + return ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) + } +} + +public enum GroqSettingsError: LocalizedError, Sendable, Equatable { + case invalidEndpointOverride(String) + + public var errorDescription: String? { + switch self { + case let .invalidEndpointOverride(key): + "Groq endpoint override \(key) must use HTTPS or a bare host." + } + } } diff --git a/Sources/CodexBarCore/Providers/Groq/GroqUsageFetcher.swift b/Sources/CodexBarCore/Providers/Groq/GroqUsageFetcher.swift index c375c515ae..bff9a1b0a3 100644 --- a/Sources/CodexBarCore/Providers/Groq/GroqUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Groq/GroqUsageFetcher.swift @@ -144,6 +144,7 @@ public struct GroqUsageFetcher: Sendable { guard !apiKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw GroqUsageError.missingCredentials } + try GroqSettingsReader.validateEndpointOverrides(environment: environment) let baseURL = GroqSettingsReader.apiURL(environment: environment) .appendingPathComponent("metrics") .appendingPathComponent("prometheus") diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift index 7119541985..3b4a8b340d 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift @@ -71,13 +71,16 @@ struct OpenRouterAPIFetchStrategy: ProviderFetchStrategy { } /// Errors related to OpenRouter settings -public enum OpenRouterSettingsError: LocalizedError, Sendable { +public enum OpenRouterSettingsError: LocalizedError, Sendable, Equatable { case missingToken + case invalidEndpointOverride(String) public var errorDescription: String? { switch self { case .missingToken: "OpenRouter API token not configured. Set OPENROUTER_API_KEY environment variable or configure in Settings." + case let .invalidEndpointOverride(key): + "OpenRouter endpoint override \(key) must use HTTPS or a bare host." } } } diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterSettingsReader.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterSettingsReader.swift index 1072ebe088..3219e56c0d 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterSettingsReader.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterSettingsReader.swift @@ -12,14 +12,20 @@ public enum OpenRouterSettingsReader { /// Returns the API URL, defaulting to production endpoint public static func apiURL(environment: [String: String] = ProcessInfo.processInfo.environment) -> URL { - if let override = environment["OPENROUTER_API_URL"], - let url = URL(string: cleaned(override) ?? "") - { - return url + if let override = self.validAPIURL(environment: environment) { + return override } return URL(string: "https://openrouter.ai/api/v1")! } + public static func validateEndpointOverrides( + environment: [String: String] = ProcessInfo.processInfo.environment) throws + { + guard let raw = self.cleaned(environment["OPENROUTER_API_URL"]) else { return } + guard ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) == nil else { return } + throw OpenRouterSettingsError.invalidEndpointOverride("OPENROUTER_API_URL") + } + static func cleaned(_ raw: String?) -> String? { guard var value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty else { return nil @@ -34,4 +40,9 @@ public enum OpenRouterSettingsReader { value = value.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? nil : value } + + private static func validAPIURL(environment: [String: String]) -> URL? { + guard let raw = self.cleaned(environment["OPENROUTER_API_URL"]) else { return nil } + return ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: raw) + } } diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterUsageStats.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterUsageStats.swift index b37544dccc..21c51f46bb 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterUsageStats.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterUsageStats.swift @@ -225,6 +225,7 @@ public struct OpenRouterUsageFetcher: Sendable { guard !apiKey.isEmpty else { throw OpenRouterUsageError.invalidCredentials } + try OpenRouterSettingsReader.validateEndpointOverrides(environment: environment) let baseURL = OpenRouterSettingsReader.apiURL(environment: environment) let creditsURL = baseURL.appendingPathComponent("credits") diff --git a/Tests/CodexBarTests/ProviderEndpointOverrideSecurityTests.swift b/Tests/CodexBarTests/ProviderEndpointOverrideSecurityTests.swift new file mode 100644 index 0000000000..aec2741f4c --- /dev/null +++ b/Tests/CodexBarTests/ProviderEndpointOverrideSecurityTests.swift @@ -0,0 +1,232 @@ +import CodexBarCore +import Foundation +import Testing + +struct ProviderEndpointOverrideSecurityTests { + @Test + func `sibling endpoint overrides allow bracketed IPv6 literals`() throws { + let endpoint = "https://[::1]:8443/v1" + + try OpenRouterSettingsReader.validateEndpointOverrides( + environment: ["OPENROUTER_API_URL": endpoint]) + #expect(OpenRouterSettingsReader.apiURL( + environment: ["OPENROUTER_API_URL": endpoint]).absoluteString == endpoint) + + try CodebuffSettingsReader.validateEndpointOverrides( + environment: ["CODEBUFF_API_URL": endpoint]) + #expect(CodebuffSettingsReader.apiURL( + environment: ["CODEBUFF_API_URL": endpoint]).absoluteString == endpoint) + + try GroqSettingsReader.validateEndpointOverrides( + environment: [GroqSettingsReader.apiURLEnvironmentKey: endpoint]) + #expect(GroqSettingsReader.apiURL( + environment: [GroqSettingsReader.apiURLEnvironmentKey: endpoint]).absoluteString == endpoint) + + try ElevenLabsSettingsReader.validateEndpointOverrides( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: endpoint]) + #expect(ElevenLabsSettingsReader.apiURL( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: endpoint]).absoluteString == endpoint) + } + + @Test + func `sibling endpoint overrides reject userinfo and encoded host delimiters`() { + let userInfoURL = "https://user:pass@proxy.test/v1" + let malformedHostURLs = [ + "https://proxy.test%2f.attacker.test/v1", + "https://bad host/v1", + "https://bad%20host/v1", + "https://bad%09host/v1", + ] + + #expect(OpenRouterSettingsReader.apiURL( + environment: ["OPENROUTER_API_URL": userInfoURL]).host == "openrouter.ai") + for malformedHostURL in malformedHostURLs { + #expect(throws: OpenRouterSettingsError.invalidEndpointOverride("OPENROUTER_API_URL")) { + try OpenRouterSettingsReader.validateEndpointOverrides( + environment: ["OPENROUTER_API_URL": malformedHostURL]) + } + } + + #expect(CodebuffSettingsReader.apiURL( + environment: ["CODEBUFF_API_URL": userInfoURL]).host == "www.codebuff.com") + for malformedHostURL in malformedHostURLs { + #expect(throws: CodebuffSettingsError.invalidEndpointOverride("CODEBUFF_API_URL")) { + try CodebuffSettingsReader.validateEndpointOverrides( + environment: ["CODEBUFF_API_URL": malformedHostURL]) + } + } + + #expect(GroqSettingsReader.apiURL( + environment: [GroqSettingsReader.apiURLEnvironmentKey: userInfoURL]).host == "api.groq.com") + for malformedHostURL in malformedHostURLs { + #expect(throws: GroqSettingsError.invalidEndpointOverride(GroqSettingsReader.apiURLEnvironmentKey)) { + try GroqSettingsReader.validateEndpointOverrides( + environment: [GroqSettingsReader.apiURLEnvironmentKey: malformedHostURL]) + } + } + + #expect(ElevenLabsSettingsReader.apiURL( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: userInfoURL]).host == "api.elevenlabs.io") + for malformedHostURL in malformedHostURLs { + #expect(throws: ElevenLabsSettingsError.invalidEndpointOverride( + ElevenLabsSettingsReader.apiURLEnvironmentKey)) + { + try ElevenLabsSettingsReader.validateEndpointOverrides( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: malformedHostURL]) + } + } + } + + @Test + func `credentialed fetchers reject insecure overrides before sending requests`() async { + let insecureURL = "http://attacker.test/v1" + + do { + _ = try await OpenRouterUsageFetcher.fetchUsage( + apiKey: "openrouter-test", + environment: ["OPENROUTER_API_URL": insecureURL]) + Issue.record("Expected OpenRouterSettingsError.invalidEndpointOverride") + } catch { + #expect(error as? OpenRouterSettingsError == .invalidEndpointOverride("OPENROUTER_API_URL")) + } + + do { + _ = try await CodebuffUsageFetcher.fetchUsage( + apiKey: "codebuff-test", + environment: ["CODEBUFF_API_URL": insecureURL]) + Issue.record("Expected CodebuffSettingsError.invalidEndpointOverride") + } catch { + #expect(error as? CodebuffSettingsError == .invalidEndpointOverride("CODEBUFF_API_URL")) + } + + do { + _ = try await GroqUsageFetcher.fetchUsage( + apiKey: "groq-test", + environment: [GroqSettingsReader.apiURLEnvironmentKey: insecureURL]) + Issue.record("Expected GroqSettingsError.invalidEndpointOverride") + } catch { + #expect(error as? GroqSettingsError == .invalidEndpointOverride(GroqSettingsReader.apiURLEnvironmentKey)) + } + + do { + _ = try await ElevenLabsUsageFetcher.fetchUsage( + apiKey: "elevenlabs-test", + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: insecureURL]) + Issue.record("Expected ElevenLabsSettingsError.invalidEndpointOverride") + } catch { + #expect(error as? ElevenLabsSettingsError == .invalidEndpointOverride( + ElevenLabsSettingsReader.apiURLEnvironmentKey)) + } + } + + @Test + func `OpenRouter endpoint override must be HTTPS or a bare host`() throws { + let httpsURL = OpenRouterSettingsReader.apiURL( + environment: ["OPENROUTER_API_URL": "https://router.test/v1"]) + #expect(httpsURL.absoluteString == "https://router.test/v1") + + let bareURL = OpenRouterSettingsReader.apiURL(environment: ["OPENROUTER_API_URL": "router.test/v1"]) + #expect(bareURL.absoluteString == "https://router.test/v1") + + let hostPortURL = OpenRouterSettingsReader.apiURL( + environment: ["OPENROUTER_API_URL": "localhost:8080/v1"]) + #expect(hostPortURL.absoluteString == "https://localhost:8080/v1") + + let httpURL = OpenRouterSettingsReader.apiURL( + environment: ["OPENROUTER_API_URL": "http://attacker.test/v1"]) + #expect(httpURL.absoluteString == "https://openrouter.ai/api/v1") + + do { + try OpenRouterSettingsReader.validateEndpointOverrides( + environment: ["OPENROUTER_API_URL": "http://attacker.test/v1"]) + Issue.record("Expected OpenRouterSettingsError.invalidEndpointOverride") + } catch OpenRouterSettingsError.invalidEndpointOverride("OPENROUTER_API_URL") { + // Expected. + } catch { + Issue.record("Expected OpenRouterSettingsError.invalidEndpointOverride, got \(error)") + } + } + + @Test + func `Codebuff endpoint override must be HTTPS or a bare host`() throws { + let httpsURL = CodebuffSettingsReader.apiURL(environment: ["CODEBUFF_API_URL": "https://codebuff.test"]) + #expect(httpsURL.absoluteString == "https://codebuff.test") + + let bareURL = CodebuffSettingsReader.apiURL(environment: ["CODEBUFF_API_URL": "codebuff.test"]) + #expect(bareURL.absoluteString == "https://codebuff.test") + + let hostPortURL = CodebuffSettingsReader.apiURL(environment: ["CODEBUFF_API_URL": "localhost:8080"]) + #expect(hostPortURL.absoluteString == "https://localhost:8080") + + let httpURL = CodebuffSettingsReader.apiURL(environment: ["CODEBUFF_API_URL": "http://attacker.test"]) + #expect(httpURL.absoluteString == "https://www.codebuff.com") + + do { + try CodebuffSettingsReader.validateEndpointOverrides( + environment: ["CODEBUFF_API_URL": "http://attacker.test"]) + Issue.record("Expected CodebuffSettingsError.invalidEndpointOverride") + } catch CodebuffSettingsError.invalidEndpointOverride("CODEBUFF_API_URL") { + // Expected. + } catch { + Issue.record("Expected CodebuffSettingsError.invalidEndpointOverride, got \(error)") + } + } + + @Test + func `Groq endpoint override must be HTTPS or a bare host`() throws { + let httpsURL = GroqSettingsReader.apiURL( + environment: [GroqSettingsReader.apiURLEnvironmentKey: "https://groq.test/v1"]) + #expect(httpsURL.absoluteString == "https://groq.test/v1") + + let bareURL = GroqSettingsReader.apiURL( + environment: [GroqSettingsReader.apiURLEnvironmentKey: "groq.test/v1"]) + #expect(bareURL.absoluteString == "https://groq.test/v1") + + let hostPortURL = GroqSettingsReader.apiURL( + environment: [GroqSettingsReader.apiURLEnvironmentKey: "localhost:8080/v1"]) + #expect(hostPortURL.absoluteString == "https://localhost:8080/v1") + + let httpURL = GroqSettingsReader.apiURL( + environment: [GroqSettingsReader.apiURLEnvironmentKey: "http://attacker.test/v1"]) + #expect(httpURL.absoluteString == "https://api.groq.com/v1") + + do { + try GroqSettingsReader.validateEndpointOverrides( + environment: [GroqSettingsReader.apiURLEnvironmentKey: "http://attacker.test/v1"]) + Issue.record("Expected GroqSettingsError.invalidEndpointOverride") + } catch GroqSettingsError.invalidEndpointOverride(GroqSettingsReader.apiURLEnvironmentKey) { + // Expected. + } catch { + Issue.record("Expected GroqSettingsError.invalidEndpointOverride, got \(error)") + } + } + + @Test + func `ElevenLabs endpoint override must be HTTPS or a bare host`() throws { + let httpsURL = ElevenLabsSettingsReader.apiURL( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: "https://eleven.test"]) + #expect(httpsURL.absoluteString == "https://eleven.test") + + let bareURL = ElevenLabsSettingsReader.apiURL( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: "eleven.test"]) + #expect(bareURL.absoluteString == "https://eleven.test") + + let hostPortURL = ElevenLabsSettingsReader.apiURL( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: "localhost:8080"]) + #expect(hostPortURL.absoluteString == "https://localhost:8080") + + let httpURL = ElevenLabsSettingsReader.apiURL( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: "http://attacker.test"]) + #expect(httpURL.absoluteString == "https://api.elevenlabs.io") + + do { + try ElevenLabsSettingsReader.validateEndpointOverrides( + environment: [ElevenLabsSettingsReader.apiURLEnvironmentKey: "http://attacker.test"]) + Issue.record("Expected ElevenLabsSettingsError.invalidEndpointOverride") + } catch ElevenLabsSettingsError.invalidEndpointOverride(ElevenLabsSettingsReader.apiURLEnvironmentKey) { + // Expected. + } catch { + Issue.record("Expected ElevenLabsSettingsError.invalidEndpointOverride, got \(error)") + } + } +}