From 9b8a325dd118d40aa19d98f647952937be4b24b9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 11 Jun 2026 02:26:08 -0700 Subject: [PATCH 1/3] fix: validate provider endpoint overrides Co-authored-by: hinotoi-agent --- CHANGELOG.md | 1 + .../ProviderEndpointOverrideValidator.swift | 118 ++++++++++++ .../AlibabaCodingPlanProviderDescriptor.swift | 2 +- .../AlibabaCodingPlanSettingsReader.swift | 47 ++++- .../AlibabaCodingPlanUsageFetcher.swift | 88 +++++---- .../MiniMax/MiniMaxProviderDescriptor.swift | 2 +- .../MiniMax/MiniMaxSettingsReader.swift | 55 ++++-- .../MiniMax/MiniMaxSubscriptionMetadata.swift | 8 +- .../Providers/MiniMax/MiniMaxUsageError.swift | 5 + .../MiniMax/MiniMaxUsageFetcher.swift | 13 +- .../Providers/ProviderDiagnosticExport.swift | 10 + .../AlibabaCodingPlanProviderTests.swift | 178 ++++++++++++++++-- .../CodexBarTests/MiniMaxProviderTests.swift | 147 +++++++++++++++ .../ProviderDiagnosticExportTests.swift | 9 + docs/alibaba-coding-plan.md | 4 +- docs/minimax.md | 2 + 16 files changed, 606 insertions(+), 83 deletions(-) create mode 100644 Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e8279555..2c0fc9aac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Cursor: show the Safari Full Disk Access recovery hint before the long browser login list so permission guidance remains visible when menu errors truncate (#1419, fixes #1417). Thanks @hhh2210! - 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! ## 0.33.0 — 2026-06-11 diff --git a/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift b/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift new file mode 100644 index 0000000000..0635c173d8 --- /dev/null +++ b/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift @@ -0,0 +1,118 @@ +import Foundation + +struct ProviderEndpointOverrideValidator: Sendable { + enum HostPolicy: Sendable { + case allowAnyHTTPSHost + case providerOwnedOnly + } + + private let allowedHosts: Set + private let allowedDomainSuffixes: Set + + init(allowedHosts: [String] = [], allowedDomainSuffixes: [String] = []) { + self.allowedHosts = Set(allowedHosts.map { $0.lowercased() }) + self.allowedDomainSuffixes = Set(allowedDomainSuffixes.map { $0.lowercased() }) + } + + func validatedHost(_ raw: String?, policy: HostPolicy = .allowAnyHTTPSHost) -> String? { + guard let raw, + let url = self.url(from: raw), + let host = self.validatedDecodedHost(for: url, policy: policy) + else { return nil } + return self.hostAuthority(host: host, port: url.port) + } + + func validatedURL(_ raw: String?, policy: HostPolicy = .allowAnyHTTPSHost) -> URL? { + guard let raw, + let url = self.url(from: raw), + self.validatedDecodedHost(for: url, policy: policy) != nil + else { return nil } + return url + } + + static func normalizedHTTPSURL(from raw: String) -> URL? { + let url = if Self.hasExplicitURLScheme(raw) { + URL(string: raw) + } else { + URL(string: "https://\(raw)") + } + 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 } + return url + } + + private func url(from raw: String) -> URL? { + Self.normalizedHTTPSURL(from: raw) + } + + private static func hasExplicitURLScheme(_ raw: String) -> Bool { + guard let colonIndex = raw.firstIndex(of: ":") else { return false } + if raw[colonIndex...].hasPrefix("://") { return true } + + if let authorityEnd = raw.firstIndex(where: { ["/", "?", "#"].contains($0) }), + colonIndex > authorityEnd + { + return false + } + + let afterColon = raw.index(after: colonIndex) + guard afterColon < raw.endIndex else { return true } + let portEnd = raw[afterColon...].firstIndex { Set(["/", "?", "#"]).contains($0) } ?? raw.endIndex + let suffix = raw[afterColon.. String { + let authorityHost = host.contains(":") ? "[\(host)]" : host + guard let port else { return authorityHost } + return "\(authorityHost):\(port)" + } + + private func validatedDecodedHost(for url: URL, policy: HostPolicy) -> String? { + guard let decodedHost = url.host(percentEncoded: false)?.lowercased(), + !decodedHost.isEmpty, + !decodedHost.contains("%"), + let encodedHost = url.host(percentEncoded: true)?.lowercased(), + self.hostHasNoEncodedDelimiters(encodedHost, decodedHost: decodedHost, url: url) + else { return nil } + + switch policy { + case .allowAnyHTTPSHost: + return decodedHost + case .providerOwnedOnly: + let isAllowedHost = self.allowedHosts.contains(decodedHost) + let isAllowedSuffix = self.allowedDomainSuffixes.contains { suffix in + decodedHost == suffix || decodedHost.hasSuffix(".\(suffix)") + } + guard isAllowedHost || isAllowedSuffix else { return nil } + return decodedHost + } + } + + private func hostHasNoEncodedDelimiters(_ encodedHost: String, decodedHost: String, url: URL) -> Bool { + if decodedHost.contains(":") { + guard encodedHost == decodedHost, + let componentHost = URLComponents(url: url, resolvingAgainstBaseURL: false)?.host, + componentHost.hasPrefix("["), + componentHost.hasSuffix("]") + else { return false } + + let address = componentHost.dropFirst().dropLast() + return !address.isEmpty && address.allSatisfy { $0.isHexDigit || $0 == ":" || $0 == "." } + } + + let decodedDelimiters = CharacterSet(charactersIn: "/\\?#@:") + guard decodedHost.rangeOfCharacter(from: decodedDelimiters) == nil else { return false } + + let encodedDelimiters = ["%2f", "%5c", "%3f", "%23", "%40", "%3a"] + return !encodedDelimiters.contains { encodedHost.contains($0) } + } +} diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift index b30863bd05..8aadc06e24 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift @@ -190,7 +190,7 @@ struct AlibabaCodingPlanWebFetchStrategy: ProviderFetchStrategy { return message.contains("HTTP 404") || message.contains("HTTP 403") case .networkError: return true - case .parseFailed: + case .parseFailed, .invalidEndpointOverride: return false } } diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanSettingsReader.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanSettingsReader.swift index 54d2493aa6..c0824774eb 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanSettingsReader.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanSettingsReader.swift @@ -1,6 +1,15 @@ import Foundation public struct AlibabaCodingPlanSettingsReader: Sendable { + private static let endpointValidator = ProviderEndpointOverrideValidator( + allowedHosts: [ + "modelstudio.console.alibabacloud.com", + "bailian-singapore-cs.alibabacloud.com", + "bailian.console.aliyun.com", + "bailian-cs.console.aliyun.com", + "bailian-beijing-cs.aliyuncs.com", + ]) + public static let apiTokenKey = "ALIBABA_CODING_PLAN_API_KEY" public static let qwenAPITokenKey = "ALIBABA_QWEN_API_KEY" public static let dashScopeAPITokenKey = "DASHSCOPE_API_KEY" @@ -12,6 +21,11 @@ public struct AlibabaCodingPlanSettingsReader: Sendable { public static let cookieHeaderKey = "ALIBABA_CODING_PLAN_COOKIE" public static let hostKey = "ALIBABA_CODING_PLAN_HOST" public static let quotaURLKey = "ALIBABA_CODING_PLAN_QUOTA_URL" + public static let requireProviderEndpointOverridesKey = "ALIBABA_CODING_PLAN_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES" + private static let endpointOverrideKeys = [ + Self.hostKey, + Self.quotaURLKey, + ] public static func apiToken( environment: [String: String] = ProcessInfo.processInfo.environment) -> String? @@ -25,7 +39,22 @@ public struct AlibabaCodingPlanSettingsReader: Sendable { public static func hostOverride( environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { - self.cleaned(environment[self.hostKey]) + self.endpointValidator.validatedHost( + self.cleaned(environment[self.hostKey]), + policy: self.endpointOverrideHostPolicy(environment: environment)) + } + + public static func rejectedEndpointOverrideKey( + environment: [String: String] = ProcessInfo.processInfo.environment) -> String? + { + let policy = self.endpointOverrideHostPolicy(environment: environment) + return self.endpointOverrideKeys.first { key in + guard let value = self.cleaned(environment[key]) else { return false } + if key == Self.hostKey { + return self.endpointValidator.validatedHost(value, policy: policy) == nil + } + return self.endpointValidator.validatedURL(value, policy: policy) == nil + } } public static func cookieHeader( @@ -37,11 +66,17 @@ public struct AlibabaCodingPlanSettingsReader: Sendable { public static func quotaURL( environment: [String: String] = ProcessInfo.processInfo.environment) -> URL? { - guard let raw = self.cleaned(environment[self.quotaURLKey]) else { return nil } - if let url = URL(string: raw), url.scheme != nil { - return url - } - return URL(string: "https://\(raw)") + self.endpointValidator.validatedURL( + self.cleaned(environment[self.quotaURLKey]), + policy: self.endpointOverrideHostPolicy(environment: environment)) + } + + static func endpointOverrideHostPolicy(environment: [String: String]) -> ProviderEndpointOverrideValidator + .HostPolicy { + guard let value = self.cleaned(environment[self.requireProviderEndpointOverridesKey])?.lowercased(), + ["1", "true", "yes", "on"].contains(value) + else { return .allowAnyHTTPSHost } + return .providerOwnedOnly } static func cleaned(_ raw: String?) -> String? { diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift index 4eeaa2e5ab..4de56c41bf 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift @@ -22,19 +22,24 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { apiKey: String, region: AlibabaCodingPlanAPIRegion = .international, environment: [String: String] = ProcessInfo.processInfo.environment, - now: Date = Date()) async throws -> AlibabaCodingPlanUsageSnapshot + now: Date = Date(), + transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> AlibabaCodingPlanUsageSnapshot { let cleanedKey = apiKey.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleanedKey.isEmpty else { throw AlibabaCodingPlanUsageError.invalidCredentials } + if let rejectedKey = AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: environment) { + throw AlibabaCodingPlanUsageError.invalidEndpointOverride(rejectedKey) + } if region != .international { return try await self.fetchUsageOnce( apiKey: cleanedKey, region: region, environment: environment, - now: now) + now: now, + transport: transport) } do { @@ -42,7 +47,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { apiKey: cleanedKey, region: .international, environment: environment, - now: now) + now: now, + transport: transport) } catch let error as AlibabaCodingPlanUsageError { guard error.shouldRetryOnAlternateRegion else { throw error } Self.log.debug("Alibaba Coding Plan request failed on intl host; retrying cn host") @@ -50,7 +56,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { apiKey: cleanedKey, region: .chinaMainland, environment: environment, - now: now) + now: now, + transport: transport) } } @@ -58,18 +65,23 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { cookieHeader: String, region: AlibabaCodingPlanAPIRegion = .international, environment: [String: String] = ProcessInfo.processInfo.environment, - now: Date = Date()) async throws -> AlibabaCodingPlanUsageSnapshot + now: Date = Date(), + transport: any ProviderHTTPTransport = ProviderHTTPClient.shared) async throws -> AlibabaCodingPlanUsageSnapshot { guard let normalizedCookie = CookieHeaderNormalizer.normalize(cookieHeader) else { throw AlibabaCodingPlanSettingsError.invalidCookie } + if let rejectedKey = AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: environment) { + throw AlibabaCodingPlanUsageError.invalidEndpointOverride(rejectedKey) + } if region != .international { return try await self.fetchUsageOnce( cookieHeader: normalizedCookie, region: region, environment: environment, - now: now) + now: now, + transport: transport) } do { @@ -77,7 +89,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { cookieHeader: normalizedCookie, region: .international, environment: environment, - now: now) + now: now, + transport: transport) } catch let error as AlibabaCodingPlanUsageError { guard error.shouldRetryOnAlternateRegion else { throw error } Self.log.debug("Alibaba Coding Plan cookie request failed on intl host; retrying cn host") @@ -85,7 +98,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { cookieHeader: normalizedCookie, region: .chinaMainland, environment: environment, - now: now) + now: now, + transport: transport) } } @@ -93,7 +107,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { apiKey: String, region: AlibabaCodingPlanAPIRegion, environment: [String: String], - now: Date) async throws -> AlibabaCodingPlanUsageSnapshot + now: Date, + transport: any ProviderHTTPTransport) async throws -> AlibabaCodingPlanUsageSnapshot { let url = self.resolveQuotaURL(region: region, environment: environment) var request = URLRequest(url: url) @@ -108,7 +123,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { request.setValue(region.gatewayBaseURLString, forHTTPHeaderField: "Origin") request.setValue(region.dashboardURL.absoluteString, forHTTPHeaderField: "Referer") - let response = try await ProviderHTTPClient.shared.response(for: request) + let response = try await transport.response(for: request) let data = response.data guard response.statusCode == 200 else { if response.statusCode == 401 || response.statusCode == 403 { @@ -126,13 +141,15 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { cookieHeader: String, region: AlibabaCodingPlanAPIRegion, environment: [String: String], - now: Date) async throws -> AlibabaCodingPlanUsageSnapshot + now: Date, + transport: any ProviderHTTPTransport) async throws -> AlibabaCodingPlanUsageSnapshot { let url = self.resolveConsoleQuotaURL(region: region, environment: environment) let secToken = try await self.resolveConsoleSECToken( cookieHeader: cookieHeader, region: region, - environment: environment) + environment: environment, + transport: transport) let anonymousID = self.extractCookieValue(name: "cna", from: cookieHeader) var request = URLRequest(url: url) @@ -155,7 +172,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { request.setValue(region.gatewayBaseURLString, forHTTPHeaderField: "Origin") request.setValue(region.consoleRefererURL.absoluteString, forHTTPHeaderField: "Referer") - let response = try await ProviderHTTPClient.shared.response(for: request) + let response = try await transport.response(for: request) let data = response.data guard response.statusCode == 200 else { if response.statusCode == 401 || response.statusCode == 403 { @@ -273,11 +290,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { let cleaned = AlibabaCodingPlanSettingsReader.cleaned(rawHost) guard let cleaned else { return nil } - let base: URL? = if let url = URL(string: cleaned), url.scheme != nil { - url - } else { - URL(string: "https://\(cleaned)") - } + let base = ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: cleaned) guard let base else { return nil } var components = URLComponents(url: base, resolvingAgainstBaseURL: false) @@ -297,11 +310,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { let cleaned = AlibabaCodingPlanSettingsReader.cleaned(rawHost) guard let cleaned else { return nil } - let base: URL? = if let url = URL(string: cleaned), url.scheme != nil { - url - } else { - URL(string: "https://\(cleaned)") - } + let base = ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: cleaned) guard let base else { return nil } var components = URLComponents(url: base, resolvingAgainstBaseURL: false) @@ -318,7 +327,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { private static func resolveConsoleSECToken( cookieHeader: String, region: AlibabaCodingPlanAPIRegion, - environment: [String: String]) async throws -> String + environment: [String: String], + transport: any ProviderHTTPTransport) async throws -> String { let cookieSECToken = self.extractCookieValue(name: "sec_token", from: cookieHeader) @@ -332,7 +342,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", forHTTPHeaderField: "Accept") - if let response = try? await ProviderHTTPClient.shared.response(for: request), + if let response = try? await transport.response(for: request), response.statusCode == 200, let html = String(data: response.data, encoding: .utf8), let token = self.extractConsoleSECToken(from: html), @@ -344,7 +354,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { if let token = try? await self.fetchSECTokenFromUserInfo( cookieHeader: cookieHeader, region: region, - environment: environment) + environment: environment, + transport: transport) { return token } @@ -360,11 +371,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { let cleaned = AlibabaCodingPlanSettingsReader.cleaned(rawHost) guard let cleaned else { return nil } - let base: URL? = if let url = URL(string: cleaned), url.scheme != nil { - url - } else { - URL(string: "https://\(cleaned)") - } + let base = ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: cleaned) guard let base else { return nil } guard var components = URLComponents(url: base, resolvingAgainstBaseURL: false), @@ -384,7 +391,8 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { private static func fetchSECTokenFromUserInfo( cookieHeader: String, region: AlibabaCodingPlanAPIRegion, - environment: [String: String]) async throws -> String? + environment: [String: String], + transport: any ProviderHTTPTransport) async throws -> String? { let gatewayBaseURL = self.resolveConsoleGatewayBaseURL(region: region, environment: environment) let userInfoURL = gatewayBaseURL.appendingPathComponent("tool/user/info.json") @@ -397,7 +405,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { .absoluteString + "/" request.setValue(referer, forHTTPHeaderField: "Referer") - let response = try await ProviderHTTPClient.shared.response(for: request) + let response = try await transport.response(for: request) guard response.statusCode == 200 else { return nil } @@ -423,11 +431,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { let cleaned = AlibabaCodingPlanSettingsReader.cleaned(rawHost) guard let cleaned else { return nil } - let base: URL? = if let url = URL(string: cleaned), url.scheme != nil { - url - } else { - URL(string: "https://\(cleaned)") - } + let base = ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: cleaned) guard let base else { return nil } guard var components = URLComponents(url: base, resolvingAgainstBaseURL: false) else { @@ -1075,6 +1079,7 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable { case apiError(String) case parseFailed(String) case apiKeyUnavailableInRegion + case invalidEndpointOverride(String) var shouldRetryOnAlternateRegion: Bool { switch self { @@ -1090,6 +1095,8 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable { message.contains("Missing coding plan quota data") || message.contains("No quota windows found") case .networkError: false + case .invalidEndpointOverride: + false } } @@ -1110,6 +1117,11 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable { "Alibaba Coding Plan API error: \(message)" case let .parseFailed(message): "Failed to parse Alibaba Coding Plan response: \(message)" + case let .invalidEndpointOverride(key): + "Alibaba Coding Plan endpoint override \(key) is not allowed. " + + "Use an HTTPS endpoint without user info or encoded host tricks. " + + "If ALIBABA_CODING_PLAN_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true is set, " + + "the endpoint must also be Alibaba-owned." } } } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift index 703c9d5e22..90d79c1063 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift @@ -100,7 +100,7 @@ struct MiniMaxAPIFetchStrategy: ProviderFetchStrategy { return true case let .apiError(message): return message.contains("HTTP 404") - case .networkError, .parseFailed: + case .networkError, .parseFailed, .invalidEndpointOverride: return false } } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSettingsReader.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSettingsReader.swift index 0e0684fdcc..0bbe3a6217 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSettingsReader.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSettingsReader.swift @@ -1,6 +1,9 @@ import Foundation public struct MiniMaxSettingsReader: Sendable { + private static let endpointValidator = ProviderEndpointOverrideValidator( + allowedDomainSuffixes: ["minimax.io", "minimaxi.com"]) + public static let cookieHeaderKeys = [ "MINIMAX_COOKIE", "MINIMAX_COOKIE_HEADER", @@ -9,6 +12,13 @@ public struct MiniMaxSettingsReader: Sendable { public static let codingPlanURLKey = "MINIMAX_CODING_PLAN_URL" public static let remainsURLKey = "MINIMAX_REMAINS_URL" public static let billingHistoryURLKey = "MINIMAX_BILLING_HISTORY_URL" + public static let requireProviderEndpointOverridesKey = "MINIMAX_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES" + private static let endpointOverrideKeys = [ + Self.hostKey, + Self.codingPlanURLKey, + Self.remainsURLKey, + Self.billingHistoryURLKey, + ] public static func cookieHeader( environment: [String: String] = ProcessInfo.processInfo.environment) -> String? @@ -27,25 +37,54 @@ public struct MiniMaxSettingsReader: Sendable { } public static func hostOverride(environment: [String: String] = ProcessInfo.processInfo.environment) -> String? { - self.cleaned(environment[self.hostKey]) + self.endpointValidator.validatedHost( + self.cleaned(environment[self.hostKey]), + policy: self.endpointOverrideHostPolicy(environment: environment)) + } + + public static func rejectedEndpointOverrideKey( + environment: [String: String] = ProcessInfo.processInfo.environment) -> String? + { + let policy = self.endpointOverrideHostPolicy(environment: environment) + return self.endpointOverrideKeys.first { key in + guard let value = self.cleaned(environment[key]) else { return false } + if key == Self.hostKey { + return self.endpointValidator.validatedHost(value, policy: policy) == nil + } + return self.endpointValidator.validatedURL(value, policy: policy) == nil + } } public static func codingPlanURL( environment: [String: String] = ProcessInfo.processInfo.environment) -> URL? { - self.url(from: environment[self.codingPlanURLKey]) + self.endpointValidator.validatedURL( + self.cleaned(environment[self.codingPlanURLKey]), + policy: self.endpointOverrideHostPolicy(environment: environment)) } public static func remainsURL( environment: [String: String] = ProcessInfo.processInfo.environment) -> URL? { - self.url(from: environment[self.remainsURLKey]) + self.endpointValidator.validatedURL( + self.cleaned(environment[self.remainsURLKey]), + policy: self.endpointOverrideHostPolicy(environment: environment)) } public static func billingHistoryURL( environment: [String: String] = ProcessInfo.processInfo.environment) -> URL? { - self.url(from: environment[self.billingHistoryURLKey]) + self.endpointValidator.validatedURL( + self.cleaned(environment[self.billingHistoryURLKey]), + policy: self.endpointOverrideHostPolicy(environment: environment)) + } + + static func endpointOverrideHostPolicy(environment: [String: String]) -> ProviderEndpointOverrideValidator + .HostPolicy { + guard let value = self.cleaned(environment[self.requireProviderEndpointOverridesKey])?.lowercased(), + ["1", "true", "yes", "on"].contains(value) + else { return .allowAnyHTTPSHost } + return .providerOwnedOnly } static func cleaned(_ raw: String?) -> String? { @@ -62,14 +101,6 @@ public struct MiniMaxSettingsReader: Sendable { value = value.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? nil : value } - - private static func url(from raw: String?) -> URL? { - guard let cleaned = self.cleaned(raw) else { return nil } - if let url = URL(string: cleaned), url.scheme != nil { - return url - } - return URL(string: "https://\(cleaned)") - } } public enum MiniMaxSettingsError: LocalizedError, Sendable { diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSubscriptionMetadata.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSubscriptionMetadata.swift index 37f62f7556..33f9cc1dd9 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSubscriptionMetadata.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSubscriptionMetadata.swift @@ -59,8 +59,12 @@ enum MiniMaxSubscriptionMetadataFetcher { } static func resolveComboURL(region: MiniMaxAPIRegion, environment: [String: String]) throws -> URL { - let host = MiniMaxSettingsReader.hostOverride(environment: environment) ?? self.defaultWebHost(region: region) - guard var components = URLComponents(string: host.hasPrefix("http") ? host : "https://\(host)"), + if let rejectedKey = MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: environment) { + throw MiniMaxUsageError.invalidEndpointOverride(rejectedKey) + } + let baseURL = MiniMaxSettingsReader.hostOverride(environment: environment) + .map { "https://\($0)" } ?? self.defaultWebHost(region: region) + guard var components = URLComponents(string: baseURL), components.host?.isEmpty == false else { throw MiniMaxUsageError.apiError("MiniMax combo metadata host is invalid.") diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageError.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageError.swift index 4cd856816e..c5d6c13f72 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageError.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageError.swift @@ -5,6 +5,7 @@ public enum MiniMaxUsageError: LocalizedError, Sendable, Equatable { case networkError(String) case apiError(String) case parseFailed(String) + case invalidEndpointOverride(String) public var errorDescription: String? { switch self { @@ -16,6 +17,10 @@ public enum MiniMaxUsageError: LocalizedError, Sendable, Equatable { "MiniMax API error: \(message)" case let .parseFailed(message): "Failed to parse MiniMax coding plan: \(message)" + case let .invalidEndpointOverride(key): + "MiniMax endpoint override \(key) is not allowed. " + + "Use an HTTPS endpoint without user info or encoded host tricks. " + + "If MINIMAX_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true is set, the endpoint must also be MiniMax-owned." } } } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift index 3f67e9a5b4..9292a729ce 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift @@ -37,6 +37,9 @@ public struct MiniMaxUsageFetcher: Sendable { guard let cookie = MiniMaxCookieHeader.normalized(from: cookieHeader) else { throw MiniMaxUsageError.invalidCredentials } + if let rejectedKey = MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: environment) { + throw MiniMaxUsageError.invalidEndpointOverride(rejectedKey) + } let context = WebFetchContext( cookie: cookie, @@ -192,6 +195,8 @@ public struct MiniMaxUsageFetcher: Sendable { message.contains("HTTP 404") || message.contains("HTTP 405") case .networkError, .parseFailed: true + case .invalidEndpointOverride: + false } } @@ -360,6 +365,8 @@ public struct MiniMaxUsageFetcher: Sendable { message.contains("HTTP 404") || message.contains("HTTP 405") case .networkError, .parseFailed: true + case .invalidEndpointOverride: + false } } @@ -621,11 +628,7 @@ public struct MiniMaxUsageFetcher: Sendable { return components.url } - if let url = URL(string: cleaned), url.scheme != nil { - if let composed = compose(url) { return composed } - return url - } - guard let base = URL(string: "https://\(cleaned)") else { return nil } + guard let base = ProviderEndpointOverrideValidator.normalizedHTTPSURL(from: cleaned) else { return nil } return compose(base) } diff --git a/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift b/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift index 4ce2fad2f2..c82d8b30dc 100644 --- a/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift +++ b/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift @@ -222,6 +222,16 @@ public struct ProviderDiagnosticError: Codable, Sendable { case .invalidCredentials: return "auth" case .apiError: return "api" case .parseFailed: return "parse" + case .invalidEndpointOverride: return "configuration" + } + } + if let alibabaError = error as? AlibabaCodingPlanUsageError { + switch alibabaError { + case .networkError: return "network" + case .loginRequired, .invalidCredentials: return "auth" + case .apiError, .apiKeyUnavailableInRegion: return "api" + case .parseFailed: return "parse" + case .invalidEndpointOverride: return "configuration" } } if error is MiniMaxSettingsError || error is MiniMaxAPISettingsError { return "auth" } diff --git a/Tests/CodexBarTests/AlibabaCodingPlanProviderTests.swift b/Tests/CodexBarTests/AlibabaCodingPlanProviderTests.swift index 25dc0537b4..2c4154868e 100644 --- a/Tests/CodexBarTests/AlibabaCodingPlanProviderTests.swift +++ b/Tests/CodexBarTests/AlibabaCodingPlanProviderTests.swift @@ -46,6 +46,108 @@ struct AlibabaCodingPlanSettingsReaderTests { #expect(url?.absoluteString == "https://modelstudio.console.alibabacloud.com/data/api.json") } + @Test + func `endpoint overrides allow custom https hosts by default`() { + let env = [ + AlibabaCodingPlanSettingsReader.hostKey: "https://attacker.example", + AlibabaCodingPlanSettingsReader.quotaURLKey: "https://attacker.example/data/api.json", + ] + + #expect(AlibabaCodingPlanSettingsReader.hostOverride(environment: env) == "attacker.example") + #expect(AlibabaCodingPlanSettingsReader.quotaURL(environment: env)?.host == "attacker.example") + #expect(AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: env) == nil) + } + + @Test + func `host endpoint overrides preserve explicit port`() { + let env = [AlibabaCodingPlanSettingsReader.hostKey: "proxy.example.test:8443"] + + #expect(AlibabaCodingPlanSettingsReader.hostOverride(environment: env) == "proxy.example.test:8443") + #expect( + AlibabaCodingPlanUsageFetcher.resolveQuotaURL(region: .international, environment: env).absoluteString == + "https://proxy.example.test:8443/data/api.json?action=zeldaEasy.broadscope-bailian.codingPlan.queryCodingPlanInstanceInfoV2&product=broadscope-bailian&api=queryCodingPlanInstanceInfoV2¤tRegionId=ap-southeast-1") + #expect( + AlibabaCodingPlanUsageFetcher.resolveConsoleDashboardURL(region: .international, environment: env) + .absoluteString + .hasPrefix("https://proxy.example.test:8443/") == true) + } + + @Test + func `endpoint overrides reject encoded host delimiters before suffix matching`() { + let encodedSlash = "https://attacker.example%2f.modelstudio.console.alibabacloud.com" + let doubleEncodedSlash = "https://attacker.example%252f.modelstudio.console.alibabacloud.com" + let env = [ + AlibabaCodingPlanSettingsReader.hostKey: encodedSlash, + AlibabaCodingPlanSettingsReader.quotaURLKey: "\(encodedSlash)/data/api.json", + ] + + #expect(AlibabaCodingPlanSettingsReader.hostOverride(environment: env) == nil) + #expect(AlibabaCodingPlanSettingsReader.quotaURL(environment: env) == nil) + #expect(AlibabaCodingPlanSettingsReader.hostOverride(environment: [ + AlibabaCodingPlanSettingsReader.hostKey: doubleEncodedSlash, + ]) == nil) + } + + @Test + func `endpoint overrides require https and no userinfo`() { + #expect(AlibabaCodingPlanSettingsReader.hostOverride(environment: [ + AlibabaCodingPlanSettingsReader.hostKey: "http://modelstudio.console.alibabacloud.com", + ]) == nil) + #expect(AlibabaCodingPlanSettingsReader.quotaURL(environment: [ + AlibabaCodingPlanSettingsReader.quotaURLKey: + "https://user:pass@modelstudio.console.alibabacloud.com/data/api.json", + ]) == nil) + } + + @Test + func `strict provider endpoint mode rejects custom hosts`() { + let env = [ + AlibabaCodingPlanSettingsReader.requireProviderEndpointOverridesKey: "true", + AlibabaCodingPlanSettingsReader.hostKey: "proxy.example.test", + AlibabaCodingPlanSettingsReader.quotaURLKey: "https://proxy.example.test/data/api.json", + ] + + #expect(AlibabaCodingPlanSettingsReader.hostOverride(environment: env) == nil) + #expect(AlibabaCodingPlanSettingsReader.quotaURL(environment: env) == nil) + #expect(AlibabaCodingPlanSettingsReader + .rejectedEndpointOverrideKey(environment: env) == AlibabaCodingPlanSettingsReader.hostKey) + } + + @Test + func `strict provider endpoint mode rejects customer controlled Alibaba Cloud hosts`() { + let env = [ + AlibabaCodingPlanSettingsReader.requireProviderEndpointOverridesKey: "true", + AlibabaCodingPlanSettingsReader.hostKey: "tenant.cn-beijing.fc.aliyuncs.com", + ] + + #expect(AlibabaCodingPlanSettingsReader.hostOverride(environment: env) == nil) + #expect(AlibabaCodingPlanSettingsReader + .rejectedEndpointOverrideKey(environment: env) == AlibabaCodingPlanSettingsReader.hostKey) + } + + @Test + func `strict provider endpoint mode accepts known Coding Plan hosts`() { + let env = [ + AlibabaCodingPlanSettingsReader.requireProviderEndpointOverridesKey: "true", + AlibabaCodingPlanSettingsReader.hostKey: "bailian-beijing-cs.aliyuncs.com", + ] + + #expect( + AlibabaCodingPlanSettingsReader.hostOverride(environment: env) == + "bailian-beijing-cs.aliyuncs.com") + #expect(AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: env) == nil) + } + + @Test + func `custom https compatibility mode still rejects http and userinfo`() { + #expect(AlibabaCodingPlanSettingsReader.hostOverride(environment: [ + AlibabaCodingPlanSettingsReader.hostKey: "http://proxy.example.test", + ]) == nil) + #expect(AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: [ + AlibabaCodingPlanSettingsReader.quotaURLKey: "https://user:pass@proxy.example.test/data/api.json", + ]) == AlibabaCodingPlanSettingsReader.quotaURLKey) + } + @Test func `missing cookie error includes access hint when present`() { let error = AlibabaCodingPlanSettingsError @@ -657,9 +759,53 @@ struct AlibabaCodingPlanRegionTests { @Test func `quota url override beats host`() { - let env = [AlibabaCodingPlanSettingsReader.quotaURLKey: "https://example.com/custom/quota"] + let env = [ + AlibabaCodingPlanSettingsReader.quotaURLKey: + "https://modelstudio.console.alibabacloud.com/custom/quota", + ] + let url = AlibabaCodingPlanUsageFetcher.resolveQuotaURL(region: .international, environment: env) + #expect(url.absoluteString == "https://modelstudio.console.alibabacloud.com/custom/quota") + } + + @Test + func `custom quota url override is preserved by default`() { + let env = [AlibabaCodingPlanSettingsReader.quotaURLKey: "https://attacker.example/custom/quota"] + let url = AlibabaCodingPlanUsageFetcher.resolveQuotaURL(region: .international, environment: env) + #expect(url.host == "attacker.example") + } + + @Test + func `strict provider endpoint mode falls back to provider endpoint`() { + let env = [ + AlibabaCodingPlanSettingsReader.requireProviderEndpointOverridesKey: "true", + AlibabaCodingPlanSettingsReader.quotaURLKey: "https://attacker.example/custom/quota", + ] let url = AlibabaCodingPlanUsageFetcher.resolveQuotaURL(region: .international, environment: env) - #expect(url.absoluteString == "https://example.com/custom/quota") + #expect(url.host == AlibabaCodingPlanAPIRegion.international.quotaURL.host) + } + + @Test + func `explicit endpoint override rejects invalid api scheme before network`() async { + await #expect(throws: AlibabaCodingPlanUsageError.invalidEndpointOverride( + AlibabaCodingPlanSettingsReader.quotaURLKey)) + { + _ = try await AlibabaCodingPlanUsageFetcher.fetchUsage( + apiKey: "cpk-test", + environment: [AlibabaCodingPlanSettingsReader + .quotaURLKey: "http://modelstudio.console.alibabacloud.com/custom/quota"]) + } + } + + @Test + func `explicit endpoint override rejects invalid cookie scheme before network`() async { + await #expect(throws: AlibabaCodingPlanUsageError.invalidEndpointOverride( + AlibabaCodingPlanSettingsReader.quotaURLKey)) + { + _ = try await AlibabaCodingPlanUsageFetcher.fetchUsage( + cookieHeader: "login_aliyunid_ticket=ticket; login_aliyunid_pk=user", + environment: [AlibabaCodingPlanSettingsReader + .quotaURLKey: "http://modelstudio.console.alibabacloud.com/custom/quota"]) + } } } @@ -667,24 +813,23 @@ struct AlibabaCodingPlanRegionTests { struct AlibabaCodingPlanUsageFetcherRequestTests { @Test func `api401 maps to invalid credentials`() async throws { - let registered = URLProtocol.registerClass(AlibabaUsageFetcherStubURLProtocol.self) - defer { - if registered { - URLProtocol.unregisterClass(AlibabaUsageFetcherStubURLProtocol.self) - } - AlibabaUsageFetcherStubURLProtocol.handler = nil - } - - AlibabaUsageFetcherStubURLProtocol.handler = { request in + let transport = ProviderHTTPTransportHandler { request in guard let url = request.url else { throw URLError(.badURL) } - return Self.makeResponse(url: url, body: #"{"message":"unauthorized"}"#, statusCode: 401) + let (response, data) = Self.makeResponse( + url: url, + body: #"{"message":"unauthorized"}"#, + statusCode: 401) + return (data, response) } await #expect(throws: AlibabaCodingPlanUsageError.invalidCredentials) { _ = try await AlibabaCodingPlanUsageFetcher.fetchUsage( apiKey: "cpk-test", region: .chinaMainland, - environment: [AlibabaCodingPlanSettingsReader.quotaURLKey: "https://alibaba-api.test/data/api.json"]) + environment: [ + AlibabaCodingPlanSettingsReader.quotaURLKey: "https://bailian.console.aliyun.com/data/api.json", + ], + transport: transport) } } @@ -756,7 +901,7 @@ struct AlibabaCodingPlanUsageFetcherRequestTests { AlibabaConsoleSECTokenStubURLProtocol.handler = { request in guard let url = request.url else { throw URLError(.badURL) } - #expect(url.host == "alibaba-proxy.test") + #expect(url.host == "modelstudio.console.alibabacloud.com") if request.httpMethod == "GET", url.path == AlibabaCodingPlanAPIRegion.international.dashboardURL.path { return Self.makeResponse(url: url, body: "", statusCode: 200) @@ -796,7 +941,7 @@ struct AlibabaCodingPlanUsageFetcherRequestTests { let snapshot = try await AlibabaCodingPlanUsageFetcher.fetchUsage( cookieHeader: "sec_token=cookie-sec-token; login_aliyunid_ticket=ticket; login_aliyunid_pk=user", region: .international, - environment: [AlibabaCodingPlanSettingsReader.hostKey: "https://alibaba-proxy.test"], + environment: [AlibabaCodingPlanSettingsReader.hostKey: "https://modelstudio.console.alibabacloud.com"], now: Date(timeIntervalSince1970: 1_700_000_000)) #expect(snapshot.planName == "Alibaba Coding Plan Pro") @@ -922,7 +1067,7 @@ final class AlibabaUsageFetcherStubURLProtocol: URLProtocol { nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))? override static func canInit(with request: URLRequest) -> Bool { - request.url?.host == "alibaba-api.test" + request.url?.host == "bailian.console.aliyun.com" } override static func canonicalRequest(for request: URLRequest) -> URLRequest { @@ -954,7 +1099,6 @@ final class AlibabaConsoleSECTokenStubURLProtocol: URLProtocol { override static func canInit(with request: URLRequest) -> Bool { guard let host = request.url?.host else { return false } return [ - "alibaba-proxy.test", "modelstudio.console.alibabacloud.com", "bailian-singapore-cs.alibabacloud.com", "bailian.console.aliyun.com", diff --git a/Tests/CodexBarTests/MiniMaxProviderTests.swift b/Tests/CodexBarTests/MiniMaxProviderTests.swift index be79cc1dd7..cb8a4b9ec0 100644 --- a/Tests/CodexBarTests/MiniMaxProviderTests.swift +++ b/Tests/CodexBarTests/MiniMaxProviderTests.swift @@ -25,6 +25,153 @@ struct MiniMaxAPISettingsReaderTests { } } +struct MiniMaxEndpointOverrideSettingsTests { + @Test + func `strict endpoint overrides reject non MiniMax hosts`() { + let env = [ + MiniMaxSettingsReader.requireProviderEndpointOverridesKey: "true", + MiniMaxSettingsReader.hostKey: "https://attacker.example", + MiniMaxSettingsReader.codingPlanURLKey: "https://attacker.example/coding-plan", + MiniMaxSettingsReader.remainsURLKey: "https://attacker.example/remains", + MiniMaxSettingsReader.billingHistoryURLKey: "https://attacker.example/account/amount", + ] + + #expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil) + #expect(MiniMaxSettingsReader.codingPlanURL(environment: env) == nil) + #expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil) + #expect(MiniMaxSettingsReader.billingHistoryURL(environment: env) == nil) + } + + @Test + func `endpoint overrides reject encoded host delimiters before suffix matching`() { + let encodedSlash = "https://attacker.example%2f.platform.minimax.io" + let doubleEncodedSlash = "https://attacker.example%252f.platform.minimax.io" + let env = [ + MiniMaxSettingsReader.hostKey: encodedSlash, + MiniMaxSettingsReader.codingPlanURLKey: "\(encodedSlash)/coding-plan", + MiniMaxSettingsReader.remainsURLKey: "\(encodedSlash)/remains", + MiniMaxSettingsReader.billingHistoryURLKey: "\(encodedSlash)/account/amount", + ] + + #expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil) + #expect(MiniMaxSettingsReader.codingPlanURL(environment: env) == nil) + #expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil) + #expect(MiniMaxSettingsReader.billingHistoryURL(environment: env) == nil) + #expect(MiniMaxSettingsReader.hostOverride(environment: [ + MiniMaxSettingsReader.hostKey: doubleEncodedSlash, + ]) == nil) + } + + @Test + func `endpoint overrides require https and no userinfo`() { + #expect(MiniMaxSettingsReader.hostOverride(environment: [ + MiniMaxSettingsReader.hostKey: "http://platform.minimax.io", + ]) == nil) + #expect(MiniMaxSettingsReader.remainsURL(environment: [ + MiniMaxSettingsReader.remainsURLKey: "https://user:pass@platform.minimax.io/remains", + ]) == nil) + #expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: [ + MiniMaxSettingsReader.hostKey: ":443", + ]) == MiniMaxSettingsReader.hostKey) + } + + @Test + func `endpoint overrides allow MiniMax and custom https hosts`() { + let env = [ + MiniMaxSettingsReader.hostKey: "platform.minimaxi.com", + MiniMaxSettingsReader.remainsURLKey: "https://platform.minimax.io/custom/remains", + ] + + #expect(MiniMaxSettingsReader.hostOverride(environment: env) == "platform.minimaxi.com") + #expect(MiniMaxSettingsReader.remainsURL(environment: env)?.host == "platform.minimax.io") + + let customEnv = [ + MiniMaxSettingsReader.hostKey: "proxy.example.test", + MiniMaxSettingsReader.remainsURLKey: "https://proxy.example.test/custom/remains", + ] + #expect(MiniMaxSettingsReader.hostOverride(environment: customEnv) == "proxy.example.test") + #expect(MiniMaxSettingsReader.remainsURL(environment: customEnv)?.host == "proxy.example.test") + #expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: customEnv) == nil) + } + + @Test + func `host endpoint overrides preserve explicit port`() { + let env = [MiniMaxSettingsReader.hostKey: "proxy.example.test:8443"] + + #expect(MiniMaxSettingsReader.hostOverride(environment: env) == "proxy.example.test:8443") + #expect( + MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env).absoluteString == + "https://proxy.example.test:8443/user-center/payment/coding-plan?cycle_type=3") + #expect( + MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: env).absoluteString == + "https://proxy.example.test:8443/v1/api/openplatform/coding_plan/remains") + } + + @Test + func `subscription metadata accepts host names beginning with http`() throws { + let url = try MiniMaxSubscriptionMetadataFetcher.resolveComboURL( + region: .global, + environment: [MiniMaxSettingsReader.hostKey: "https://http-proxy.example.test"]) + + #expect(url.host == "http-proxy.example.test") + #expect(url.scheme == "https") + } + + @Test + func `scheme less endpoint preserves colon in path`() { + let env = [MiniMaxSettingsReader.remainsURLKey: "proxy.example.test/api:v1"] + + #expect( + MiniMaxSettingsReader.remainsURL(environment: env)?.absoluteString == + "https://proxy.example.test/api:v1") + } + + @Test + func `custom https endpoints allow bracketed IPv6 literals`() { + let env = [ + MiniMaxSettingsReader.hostKey: "[::1]:8443", + MiniMaxSettingsReader.remainsURLKey: "https://[::1]:8443/remains", + ] + + #expect(MiniMaxSettingsReader.hostOverride(environment: env) == "[::1]:8443") + #expect(MiniMaxSettingsReader.remainsURL(environment: env)?.host == "::1") + #expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: env) == nil) + } + + @Test + func `strict provider endpoint mode rejects custom hosts`() { + let env = [ + MiniMaxSettingsReader.requireProviderEndpointOverridesKey: "true", + MiniMaxSettingsReader.hostKey: "proxy.example.test", + MiniMaxSettingsReader.remainsURLKey: "https://proxy.example.test/custom/remains", + ] + + #expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil) + #expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil) + #expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: env) == MiniMaxSettingsReader.hostKey) + } + + @Test + func `custom https compatibility mode still rejects http and userinfo`() { + #expect(MiniMaxSettingsReader.hostOverride(environment: [ + MiniMaxSettingsReader.hostKey: "http://proxy.example.test", + ]) == nil) + #expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: [ + MiniMaxSettingsReader.remainsURLKey: "https://user:pass@proxy.example.test/remains", + ]) == MiniMaxSettingsReader.remainsURLKey) + } + + @Test + func `explicit endpoint override rejects invalid scheme before network`() async { + await #expect(throws: MiniMaxUsageError.invalidEndpointOverride(MiniMaxSettingsReader.codingPlanURLKey)) { + _ = try await MiniMaxUsageFetcher.fetchUsage( + cookieHeader: "session=abc123", + environment: [MiniMaxSettingsReader.codingPlanURLKey: "http://platform.minimax.io/coding-plan"], + includeBillingHistory: false) + } + } +} + struct MiniMaxProviderStrategyTests { private struct StubClaudeFetcher: ClaudeUsageFetching { func loadLatestUsage(model _: String) async throws -> ClaudeUsageSnapshot { diff --git a/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift b/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift index 71065bdffe..2ed6057a16 100644 --- a/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift @@ -100,6 +100,15 @@ struct ProviderDiagnosticExportTests { #expect(diagParse.category == "parse") } + @Test + func `diagnostic error maps Alibaba invalid endpoint override to configuration`() { + let error = AlibabaCodingPlanUsageError.invalidEndpointOverride("ALIBABA_CODING_PLAN_QUOTA_URL") + let diag = ProviderDiagnosticError(from: error, authConfigured: true) + + #expect(diag.category == "configuration") + #expect(diag.safeDescription == "Configuration issue - check provider source and settings") + } + @Test func `no available strategy maps missing auth to auth category`() { let error = ProviderFetchError.noAvailableStrategy(.minimax) diff --git a/docs/alibaba-coding-plan.md b/docs/alibaba-coding-plan.md index 480204803a..834e62e385 100644 --- a/docs/alibaba-coding-plan.md +++ b/docs/alibaba-coding-plan.md @@ -42,7 +42,9 @@ When the RPC endpoint returns `ConsoleNeedLogin`, CodexBar treats that as a cons - Override host base: `ALIBABA_CODING_PLAN_HOST` - Example: `ALIBABA_CODING_PLAN_HOST=modelstudio.console.alibabacloud.com` - Override full quota URL: `ALIBABA_CODING_PLAN_QUOTA_URL` - - Example: `ALIBABA_CODING_PLAN_QUOTA_URL=https://example.com/data/api.json?action=...` + - Example: `ALIBABA_CODING_PLAN_QUOTA_URL=https://modelstudio.console.alibabacloud.com/data/api.json?action=...` +- Security policy: endpoint overrides are only accepted when they use `https://`, omit userinfo, and do not contain encoded host delimiters. Custom HTTPS proxy/test domains continue to work for compatibility, but `http://` endpoints are rejected so cookies and API credentials are not sent in cleartext. +- Strict provider-host mode: set `ALIBABA_CODING_PLAN_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true` to additionally reject custom proxy/test domains and only accept the known Alibaba Coding Plan console and RPC hosts. ## Request headers - `Authorization: Bearer ` diff --git a/docs/minimax.md b/docs/minimax.md index 15bb3ecbc8..c8ecd8ed6b 100644 --- a/docs/minimax.md +++ b/docs/minimax.md @@ -39,6 +39,8 @@ falls back across the provider's supported web requests when needed. - `MINIMAX_HOST=platform.minimaxi.com` - `MINIMAX_CODING_PLAN_URL=...` (full URL override) - `MINIMAX_REMAINS_URL=...` (full URL override) +- Security policy: endpoint overrides are only accepted when they use `https://`, omit userinfo, and do not contain encoded host delimiters. Custom HTTPS proxy/test domains continue to work for compatibility, but `http://` endpoints are rejected so cookies and authorization headers are not sent in cleartext. +- Strict provider-host mode: set `MINIMAX_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true` to additionally reject custom proxy/test domains and only accept MiniMax-owned hosts under `minimax.io` or `minimaxi.com`. ## Cookie capture (optional override) - Open the Coding Plan page and DevTools → Network. From 542018f084543dc8d931d6564afa626f795af3ec Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 11 Jun 2026 04:12:35 -0700 Subject: [PATCH 2/3] fix: classify endpoint override diagnostics --- .../Providers/ProviderDiagnosticExport.swift | 3 +++ .../ProviderDiagnosticExportTests.swift | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift b/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift index c82d8b30dc..e9ac388cf6 100644 --- a/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift +++ b/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift @@ -177,6 +177,9 @@ public struct ProviderDiagnosticFetchAttempt: Codable, Sendable { public static func errorCategoryLabel(_ description: String?) -> String { guard let desc = description?.lowercased() else { return "unknown" } + if desc.contains("endpoint override") { + return "configuration" + } if desc.contains("network") || desc.contains("timeout") || desc.contains("connection") { return "network" } diff --git a/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift b/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift index 2ed6057a16..5161517064 100644 --- a/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift @@ -109,6 +109,22 @@ struct ProviderDiagnosticExportTests { #expect(diag.safeDescription == "Configuration issue - check provider source and settings") } + @Test + func `endpoint override fetch attempt stays in configuration category`() { + let error = MiniMaxUsageError.invalidEndpointOverride("MINIMAX_HOST") + let attempt = ProviderFetchAttempt( + strategyID: "minimax.web", + kind: .web, + wasAvailable: true, + errorDescription: error.localizedDescription) + + let diagError = ProviderDiagnosticError(from: error, authConfigured: true) + let diagAttempt = ProviderDiagnosticFetchAttempt(from: attempt) + + #expect(diagError.category == "configuration") + #expect(diagAttempt.errorCategory == "configuration") + } + @Test func `no available strategy maps missing auth to auth category`() { let error = ProviderFetchError.noAvailableStrategy(.minimax) From 5bdfad9cc849858d21046c9c78646789ff5539f5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 11 Jun 2026 04:19:47 -0700 Subject: [PATCH 3/3] fix: preserve provider error API compatibility --- .../ProviderEndpointOverrideValidator.swift | 19 +++++++++++++++++++ .../AlibabaCodingPlanProviderDescriptor.swift | 2 +- .../AlibabaCodingPlanUsageFetcher.swift | 12 ++---------- .../MiniMax/MiniMaxProviderDescriptor.swift | 2 +- .../MiniMax/MiniMaxSubscriptionMetadata.swift | 2 +- .../Providers/MiniMax/MiniMaxUsageError.swift | 5 ----- .../MiniMax/MiniMaxUsageFetcher.swift | 6 +----- .../Providers/ProviderDiagnosticExport.swift | 5 +++-- .../AlibabaCodingPlanProviderTests.swift | 4 ++-- .../CodexBarTests/MiniMaxProviderTests.swift | 2 +- .../MiniMaxTokenPlanChangeTests.swift | 4 ++-- .../ProviderDiagnosticExportTests.swift | 4 ++-- 12 files changed, 35 insertions(+), 32 deletions(-) diff --git a/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift b/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift index 0635c173d8..ec1d39a8d7 100644 --- a/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift +++ b/Sources/CodexBarCore/ProviderEndpointOverrideValidator.swift @@ -1,5 +1,24 @@ import Foundation +enum ProviderEndpointOverrideError: LocalizedError, Sendable, Equatable { + case minimax(String) + case alibabaCodingPlan(String) + + var errorDescription: String? { + switch self { + case let .minimax(key): + "MiniMax endpoint override \(key) is not allowed. " + + "Use an HTTPS endpoint without user info or encoded host tricks. " + + "If MINIMAX_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true is set, the endpoint must also be MiniMax-owned." + case let .alibabaCodingPlan(key): + "Alibaba Coding Plan endpoint override \(key) is not allowed. " + + "Use an HTTPS endpoint without user info or encoded host tricks. " + + "If ALIBABA_CODING_PLAN_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true is set, " + + "the endpoint must also be Alibaba-owned." + } + } +} + struct ProviderEndpointOverrideValidator: Sendable { enum HostPolicy: Sendable { case allowAnyHTTPSHost diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift index 8aadc06e24..b30863bd05 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift @@ -190,7 +190,7 @@ struct AlibabaCodingPlanWebFetchStrategy: ProviderFetchStrategy { return message.contains("HTTP 404") || message.contains("HTTP 403") case .networkError: return true - case .parseFailed, .invalidEndpointOverride: + case .parseFailed: return false } } diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift index 4de56c41bf..2eb7918ee9 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanUsageFetcher.swift @@ -30,7 +30,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { throw AlibabaCodingPlanUsageError.invalidCredentials } if let rejectedKey = AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: environment) { - throw AlibabaCodingPlanUsageError.invalidEndpointOverride(rejectedKey) + throw ProviderEndpointOverrideError.alibabaCodingPlan(rejectedKey) } if region != .international { @@ -72,7 +72,7 @@ public struct AlibabaCodingPlanUsageFetcher: Sendable { throw AlibabaCodingPlanSettingsError.invalidCookie } if let rejectedKey = AlibabaCodingPlanSettingsReader.rejectedEndpointOverrideKey(environment: environment) { - throw AlibabaCodingPlanUsageError.invalidEndpointOverride(rejectedKey) + throw ProviderEndpointOverrideError.alibabaCodingPlan(rejectedKey) } if region != .international { @@ -1079,7 +1079,6 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable { case apiError(String) case parseFailed(String) case apiKeyUnavailableInRegion - case invalidEndpointOverride(String) var shouldRetryOnAlternateRegion: Bool { switch self { @@ -1095,8 +1094,6 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable { message.contains("Missing coding plan quota data") || message.contains("No quota windows found") case .networkError: false - case .invalidEndpointOverride: - false } } @@ -1117,11 +1114,6 @@ public enum AlibabaCodingPlanUsageError: LocalizedError, Sendable, Equatable { "Alibaba Coding Plan API error: \(message)" case let .parseFailed(message): "Failed to parse Alibaba Coding Plan response: \(message)" - case let .invalidEndpointOverride(key): - "Alibaba Coding Plan endpoint override \(key) is not allowed. " + - "Use an HTTPS endpoint without user info or encoded host tricks. " + - "If ALIBABA_CODING_PLAN_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true is set, " + - "the endpoint must also be Alibaba-owned." } } } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift index 90d79c1063..703c9d5e22 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift @@ -100,7 +100,7 @@ struct MiniMaxAPIFetchStrategy: ProviderFetchStrategy { return true case let .apiError(message): return message.contains("HTTP 404") - case .networkError, .parseFailed, .invalidEndpointOverride: + case .networkError, .parseFailed: return false } } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSubscriptionMetadata.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSubscriptionMetadata.swift index 33f9cc1dd9..de2b787979 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSubscriptionMetadata.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxSubscriptionMetadata.swift @@ -60,7 +60,7 @@ enum MiniMaxSubscriptionMetadataFetcher { static func resolveComboURL(region: MiniMaxAPIRegion, environment: [String: String]) throws -> URL { if let rejectedKey = MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: environment) { - throw MiniMaxUsageError.invalidEndpointOverride(rejectedKey) + throw ProviderEndpointOverrideError.minimax(rejectedKey) } let baseURL = MiniMaxSettingsReader.hostOverride(environment: environment) .map { "https://\($0)" } ?? self.defaultWebHost(region: region) diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageError.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageError.swift index c5d6c13f72..4cd856816e 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageError.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageError.swift @@ -5,7 +5,6 @@ public enum MiniMaxUsageError: LocalizedError, Sendable, Equatable { case networkError(String) case apiError(String) case parseFailed(String) - case invalidEndpointOverride(String) public var errorDescription: String? { switch self { @@ -17,10 +16,6 @@ public enum MiniMaxUsageError: LocalizedError, Sendable, Equatable { "MiniMax API error: \(message)" case let .parseFailed(message): "Failed to parse MiniMax coding plan: \(message)" - case let .invalidEndpointOverride(key): - "MiniMax endpoint override \(key) is not allowed. " + - "Use an HTTPS endpoint without user info or encoded host tricks. " + - "If MINIMAX_REQUIRE_PROVIDER_ENDPOINT_OVERRIDES=true is set, the endpoint must also be MiniMax-owned." } } } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift index 9292a729ce..0b0ad445ea 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift @@ -38,7 +38,7 @@ public struct MiniMaxUsageFetcher: Sendable { throw MiniMaxUsageError.invalidCredentials } if let rejectedKey = MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: environment) { - throw MiniMaxUsageError.invalidEndpointOverride(rejectedKey) + throw ProviderEndpointOverrideError.minimax(rejectedKey) } let context = WebFetchContext( @@ -195,8 +195,6 @@ public struct MiniMaxUsageFetcher: Sendable { message.contains("HTTP 404") || message.contains("HTTP 405") case .networkError, .parseFailed: true - case .invalidEndpointOverride: - false } } @@ -365,8 +363,6 @@ public struct MiniMaxUsageFetcher: Sendable { message.contains("HTTP 404") || message.contains("HTTP 405") case .networkError, .parseFailed: true - case .invalidEndpointOverride: - false } } diff --git a/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift b/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift index e9ac388cf6..1a3da675a2 100644 --- a/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift +++ b/Sources/CodexBarCore/Providers/ProviderDiagnosticExport.swift @@ -219,13 +219,15 @@ public struct ProviderDiagnosticError: Codable, Sendable { if case ProviderFetchError.noAvailableStrategy = error { return authConfigured ? "configuration" : "auth" } + if error is ProviderEndpointOverrideError { + return "configuration" + } if let minimaxError = error as? MiniMaxUsageError { switch minimaxError { case .networkError: return "network" case .invalidCredentials: return "auth" case .apiError: return "api" case .parseFailed: return "parse" - case .invalidEndpointOverride: return "configuration" } } if let alibabaError = error as? AlibabaCodingPlanUsageError { @@ -234,7 +236,6 @@ public struct ProviderDiagnosticError: Codable, Sendable { case .loginRequired, .invalidCredentials: return "auth" case .apiError, .apiKeyUnavailableInRegion: return "api" case .parseFailed: return "parse" - case .invalidEndpointOverride: return "configuration" } } if error is MiniMaxSettingsError || error is MiniMaxAPISettingsError { return "auth" } diff --git a/Tests/CodexBarTests/AlibabaCodingPlanProviderTests.swift b/Tests/CodexBarTests/AlibabaCodingPlanProviderTests.swift index 2c4154868e..e6f5563eaa 100644 --- a/Tests/CodexBarTests/AlibabaCodingPlanProviderTests.swift +++ b/Tests/CodexBarTests/AlibabaCodingPlanProviderTests.swift @@ -786,7 +786,7 @@ struct AlibabaCodingPlanRegionTests { @Test func `explicit endpoint override rejects invalid api scheme before network`() async { - await #expect(throws: AlibabaCodingPlanUsageError.invalidEndpointOverride( + await #expect(throws: ProviderEndpointOverrideError.alibabaCodingPlan( AlibabaCodingPlanSettingsReader.quotaURLKey)) { _ = try await AlibabaCodingPlanUsageFetcher.fetchUsage( @@ -798,7 +798,7 @@ struct AlibabaCodingPlanRegionTests { @Test func `explicit endpoint override rejects invalid cookie scheme before network`() async { - await #expect(throws: AlibabaCodingPlanUsageError.invalidEndpointOverride( + await #expect(throws: ProviderEndpointOverrideError.alibabaCodingPlan( AlibabaCodingPlanSettingsReader.quotaURLKey)) { _ = try await AlibabaCodingPlanUsageFetcher.fetchUsage( diff --git a/Tests/CodexBarTests/MiniMaxProviderTests.swift b/Tests/CodexBarTests/MiniMaxProviderTests.swift index cb8a4b9ec0..ebee142d0b 100644 --- a/Tests/CodexBarTests/MiniMaxProviderTests.swift +++ b/Tests/CodexBarTests/MiniMaxProviderTests.swift @@ -163,7 +163,7 @@ struct MiniMaxEndpointOverrideSettingsTests { @Test func `explicit endpoint override rejects invalid scheme before network`() async { - await #expect(throws: MiniMaxUsageError.invalidEndpointOverride(MiniMaxSettingsReader.codingPlanURLKey)) { + await #expect(throws: ProviderEndpointOverrideError.minimax(MiniMaxSettingsReader.codingPlanURLKey)) { _ = try await MiniMaxUsageFetcher.fetchUsage( cookieHeader: "session=abc123", environment: [MiniMaxSettingsReader.codingPlanURLKey: "http://platform.minimax.io/coding-plan"], diff --git a/Tests/CodexBarTests/MiniMaxTokenPlanChangeTests.swift b/Tests/CodexBarTests/MiniMaxTokenPlanChangeTests.swift index 899d60c0b6..42eb21967c 100644 --- a/Tests/CodexBarTests/MiniMaxTokenPlanChangeTests.swift +++ b/Tests/CodexBarTests/MiniMaxTokenPlanChangeTests.swift @@ -581,7 +581,7 @@ struct MiniMaxTokenPlanChangeTests { contentType: "application/json") } - await #expect(throws: MiniMaxUsageError.self) { + await #expect(throws: ProviderEndpointOverrideError.minimax(MiniMaxSettingsReader.hostKey)) { try await MiniMaxSubscriptionMetadataFetcher.fetch( cookieHeader: "_token=secret", groupID: "2013894056999916075", @@ -604,7 +604,7 @@ struct MiniMaxTokenPlanChangeTests { contentType: "application/json") } - await #expect(throws: MiniMaxUsageError.self) { + await #expect(throws: ProviderEndpointOverrideError.minimax(MiniMaxSettingsReader.hostKey)) { try await MiniMaxSubscriptionMetadataFetcher.fetch( cookieHeader: "_token=secret", groupID: "2013894056999916075", diff --git a/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift b/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift index 5161517064..ca42351d9d 100644 --- a/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/ProviderDiagnosticExportTests.swift @@ -102,7 +102,7 @@ struct ProviderDiagnosticExportTests { @Test func `diagnostic error maps Alibaba invalid endpoint override to configuration`() { - let error = AlibabaCodingPlanUsageError.invalidEndpointOverride("ALIBABA_CODING_PLAN_QUOTA_URL") + let error = ProviderEndpointOverrideError.alibabaCodingPlan("ALIBABA_CODING_PLAN_QUOTA_URL") let diag = ProviderDiagnosticError(from: error, authConfigured: true) #expect(diag.category == "configuration") @@ -111,7 +111,7 @@ struct ProviderDiagnosticExportTests { @Test func `endpoint override fetch attempt stays in configuration category`() { - let error = MiniMaxUsageError.invalidEndpointOverride("MINIMAX_HOST") + let error = ProviderEndpointOverrideError.minimax("MINIMAX_HOST") let attempt = ProviderFetchAttempt( strategyID: "minimax.web", kind: .web,