Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions Sources/CodexBarCore/Credentials/CredentialStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import Foundation

public struct ProviderCredentialKey: Hashable, Sendable {
public let provider: UsageProvider

public init(provider: UsageProvider) {
self.provider = provider
}
}

public protocol ProviderCredentialStore: Sendable {
func apiKey(for key: ProviderCredentialKey) -> String?
}

public struct EnvironmentProviderCredentialStore: ProviderCredentialStore {
private let env: [String: String]

public init(env: [String: String] = ProcessInfo.processInfo.environment) {
self.env = env
}

public func apiKey(for key: ProviderCredentialKey) -> String? {
let names = Self.environmentNames(for: key.provider)
for name in names {
if let value = self.env[name]?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize quoted env credentials before returning

This branch only trims whitespace and returns the raw environment value, so credentials like "sk-..." remain quoted and will be sent with quote characters in auth headers when this store is used. Existing provider readers (for example ZaiSettingsReader.cleaned, MiniMaxAPISettingsReader.cleaned, and SyntheticSettingsReader.cleaned) explicitly strip surrounding quotes, so this abstraction introduces behavior drift that can break API authentication in environments where secret injectors or config tooling preserve quotes.

Useful? React with 👍 / 👎.

return value
}
}
return nil
}

private static func environmentNames(for provider: UsageProvider) -> [String] {
switch provider {
case .zai:
[ZaiSettingsReader.apiTokenKey]
case .copilot:
["COPILOT_API_TOKEN"]
case .minimax:
[MiniMaxAPISettingsReader.apiTokenKey]
case .kimik2:
KimiK2SettingsReader.apiKeyEnvironmentKeys
case .synthetic:
[SyntheticSettingsReader.apiKeyKey]
case .warp:
WarpSettingsReader.apiKeyEnvironmentKeys
default:
[]
}
}
}

public struct FileProviderCredentialStore: ProviderCredentialStore {
public let fileURL: URL
private let fileManager: FileManager
private let decoder: JSONDecoder

public init(
fileURL: URL = Self.defaultURL(),
fileManager: FileManager = .default,
decoder: JSONDecoder = JSONDecoder())
{
self.fileURL = fileURL
self.fileManager = fileManager
self.decoder = decoder
}

public func apiKey(for key: ProviderCredentialKey) -> String? {
guard let map = self.loadMap() else { return nil }
guard let value = map[key.provider.rawValue]?.trimmingCharacters(in: .whitespacesAndNewlines) else { return nil }
return value.isEmpty ? nil : value
}

public static func defaultURL(home: URL = FileManager.default.homeDirectoryForCurrentUser) -> URL {
home
.appendingPathComponent(".codexbar", isDirectory: true)
.appendingPathComponent("credentials.json")
}

private func loadMap() -> [String: String]? {
guard self.fileManager.fileExists(atPath: self.fileURL.path) else { return nil }
guard let data = try? Data(contentsOf: self.fileURL) else { return nil }
return try? self.decoder.decode([String: String].self, from: data)
}
}

public struct CompositeProviderCredentialStore: ProviderCredentialStore {
private let stores: [any ProviderCredentialStore]

public init(stores: [any ProviderCredentialStore]) {
self.stores = stores
}

public func apiKey(for key: ProviderCredentialKey) -> String? {
for store in self.stores {
if let value = store.apiKey(for: key), !value.isEmpty {
return value
}
}
return nil
}
}