Extracts duplicated platform integration code from ChronoMind + LysnrAI into a single Swift Package. Eliminates ~1,100+ lines of copied code per product app. Components: - BLPlatformConfig — product-specific configuration (productId, baseURL, bundleId) - BLPlatformClient — generic HTTP client with auth injection, x-request-id, timeout - BLKeychain — Keychain CRUD for secure token storage - BLTelemetryClient — telemetry queue + batch flush (matches @bytelyst/telemetry-client) - BLAuthClient — full auth operations (matches @bytelyst/auth-client) - BLFeatureFlagClient — feature flag polling from platform-service /flags/poll - BLSyncEngine — generic offline-first sync with delta pull + batch push Platforms: iOS 17+, watchOS 10+, macOS 14+
55 lines
2.1 KiB
Swift
55 lines
2.1 KiB
Swift
// ── Keychain Helper ───────────────────────────────────────────
|
|
// Generic Keychain CRUD for storing auth tokens securely.
|
|
// Service identifier is configurable per product via BLPlatformConfig.bundleId.
|
|
|
|
import Foundation
|
|
import Security
|
|
|
|
public enum BLKeychain {
|
|
|
|
/// Save a string value to the Keychain.
|
|
@discardableResult
|
|
public static func save(service: String, key: String, value: String) -> Bool {
|
|
guard let data = value.data(using: .utf8) else { return false }
|
|
delete(service: service, key: key)
|
|
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
kSecValueData as String: data,
|
|
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock,
|
|
]
|
|
|
|
return SecItemAdd(query as CFDictionary, nil) == errSecSuccess
|
|
}
|
|
|
|
/// Read a string value from the Keychain.
|
|
public static func read(service: String, key: String) -> String? {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
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 else { return nil }
|
|
return String(data: data, encoding: .utf8)
|
|
}
|
|
|
|
/// Delete a value from the Keychain.
|
|
@discardableResult
|
|
public static func delete(service: String, key: String) -> Bool {
|
|
let query: [String: Any] = [
|
|
kSecClass as String: kSecClassGenericPassword,
|
|
kSecAttrService as String: service,
|
|
kSecAttrAccount as String: key,
|
|
]
|
|
let status = SecItemDelete(query as CFDictionary)
|
|
return status == errSecSuccess || status == errSecItemNotFound
|
|
}
|
|
}
|