macos-ai-menubar-usage/AIMenubarUsage/Services/KeychainService.swift
Felipe M. 00c4e33171
Initial commit: AI Menubar Usage — macOS menu bar app for Cursor and Claude.
Includes provider integrations, settings, NSStatusItem UI, tests, Makefile, and README with screenshot.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 20:41:40 +02:00

70 lines
2.1 KiB
Swift

import Foundation
import Security
enum KeychainService {
private static let serviceName = "com.aiusage.app"
static func save(_ value: String, account: String) throws {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName,
kSecAttrAccount as String: account,
]
SecItemDelete(query as CFDictionary)
var addQuery = query
addQuery[kSecValueData as String] = data
addQuery[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
let status = SecItemAdd(addQuery as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unhandled(status)
}
}
static func load(account: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne,
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let string = String(data: data, encoding: .utf8)
else {
return nil
}
return string
}
static func delete(account: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName,
kSecAttrAccount as String: account,
]
SecItemDelete(query as CFDictionary)
}
static func keychainAccount(provider: ProviderID, authMode: AuthMode) -> String {
"\(provider.rawValue).\(authMode.rawValue)"
}
}
enum KeychainError: Error, LocalizedError {
case unhandled(OSStatus)
var errorDescription: String? {
switch self {
case .unhandled(let status):
"Keychain error (\(status))"
}
}
}