New SDK components extracted from product apps: - BLBlobClient — Azure Blob Storage upload via SAS tokens (from LysnrAI BlobService) - BLKillSwitchClient — Kill switch check from platform-service (from LysnrAI KillSwitchService) - BLLicenseClient — License key activation + status (from LysnrAI LicenseService) - BLBiometricAuth — Face ID / Touch ID wrapper (from LysnrAI BiometricAuth) - BLCrashReporter — MetricKit crash reporting (from ChronoMind CrashReporter) - BLAuditLogger — Local rotating JSON audit log (from LysnrAI AuditLogger) SDK now has 13 source files. Updated README with full component table and migration status (3 apps fully migrated, 18 wrappers total).
84 lines
2.7 KiB
Swift
84 lines
2.7 KiB
Swift
// ── Audit Logger ────────────────────────────────────────────
|
|
// Generic local audit logger that tracks user actions for debugging.
|
|
// Stores events in a rotating JSON file (configurable max entries).
|
|
// Product apps configure with a product-specific file name.
|
|
|
|
import Foundation
|
|
|
|
/// Audit event stored locally.
|
|
public struct BLAuditEvent: Codable, Sendable {
|
|
public let id: String
|
|
public let action: String
|
|
public let details: String?
|
|
public let timestamp: Date
|
|
|
|
public init(action: String, details: String? = nil) {
|
|
self.id = UUID().uuidString
|
|
self.action = action
|
|
self.details = details
|
|
self.timestamp = Date()
|
|
}
|
|
}
|
|
|
|
/// Generic local audit logger for all ByteLyst iOS apps.
|
|
/// Stores events in a rotating JSON file in the Documents directory.
|
|
public enum BLAuditLogger {
|
|
|
|
private static var maxEvents = 1000
|
|
private static var fileName = "audit_log.json"
|
|
|
|
/// Configure the logger with a product-specific file name and max events.
|
|
public static func configure(fileName: String = "audit_log.json", maxEvents: Int = 1000) {
|
|
self.fileName = fileName
|
|
self.maxEvents = maxEvents
|
|
}
|
|
|
|
private static var fileURL: URL {
|
|
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
return docs.appendingPathComponent(fileName)
|
|
}
|
|
|
|
/// Log a user action.
|
|
public static func log(_ action: String, details: String? = nil) {
|
|
let event = BLAuditEvent(action: action, details: details)
|
|
|
|
var events = loadEvents()
|
|
events.append(event)
|
|
|
|
// Rotate: keep only the most recent maxEvents
|
|
if events.count > maxEvents {
|
|
events = Array(events.suffix(maxEvents))
|
|
}
|
|
|
|
saveEvents(events)
|
|
}
|
|
|
|
/// Get all logged events (newest first).
|
|
public static func getEvents(limit: Int = 100) -> [BLAuditEvent] {
|
|
let events = loadEvents()
|
|
return Array(events.suffix(limit).reversed())
|
|
}
|
|
|
|
/// Clear all audit logs.
|
|
public static func clear() {
|
|
try? FileManager.default.removeItem(at: fileURL)
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
private static func loadEvents() -> [BLAuditEvent] {
|
|
guard let data = try? Data(contentsOf: fileURL),
|
|
let events = try? JSONDecoder().decode([BLAuditEvent].self, from: data) else {
|
|
return []
|
|
}
|
|
return events
|
|
}
|
|
|
|
private static func saveEvents(_ events: [BLAuditEvent]) {
|
|
let encoder = JSONEncoder()
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
guard let data = try? encoder.encode(events) else { return }
|
|
try? data.write(to: fileURL, options: .atomic)
|
|
}
|
|
}
|