- Add AppDiscovery provider for running app enumeration - Implement AppDropdownView with auto-launch functionality - Create SignalAction models for 40+ yabai commands - Build ActionBuilderView with nested parameter controls - Add LiveShellPreview for real-time shell command generation - Implement ActionValidator for conflict detection - Add migration parser for existing raw action strings - Include feature flag for safe rollout - Maintain full backward compatibility
103 lines
2.7 KiB
Swift
103 lines
2.7 KiB
Swift
//
|
|
// RemoteAuthManager.swift
|
|
// YabaiPro
|
|
//
|
|
// Lightweight pairing manager for local network devices
|
|
//
|
|
|
|
import Foundation
|
|
|
|
final class RemoteAuthManager {
|
|
static let shared = RemoteAuthManager()
|
|
|
|
struct PairedDevice: Codable {
|
|
var name: String
|
|
var token: String
|
|
var pairedAt: Date
|
|
}
|
|
|
|
private let storageURL: URL
|
|
private var pairedDevices: [PairedDevice] = []
|
|
|
|
// ephemeral PIN -> token mapping (in-memory)
|
|
private var pendingPins: [String: String] = [:]
|
|
private let pinLifetimeSeconds: TimeInterval = 120
|
|
|
|
private init() {
|
|
let fm = FileManager.default
|
|
let appSupport = try? fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
|
let dir = appSupport ?? URL(fileURLWithPath: NSTemporaryDirectory())
|
|
storageURL = dir.appendingPathComponent("yabaipro_paired_devices.json")
|
|
load()
|
|
}
|
|
|
|
private func load() {
|
|
do {
|
|
let data = try Data(contentsOf: storageURL)
|
|
pairedDevices = try JSONDecoder().decode([PairedDevice].self, from: data)
|
|
} catch {
|
|
pairedDevices = []
|
|
}
|
|
}
|
|
|
|
private func save() {
|
|
do {
|
|
let data = try JSONEncoder().encode(pairedDevices)
|
|
try FileManager.default.createDirectory(at: storageURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
try data.write(to: storageURL, options: [.atomic])
|
|
} catch {
|
|
print("RemoteAuthManager: failed to save paired devices: \(error)")
|
|
}
|
|
}
|
|
|
|
func generatePIN() -> String {
|
|
// generate 6-digit pin
|
|
let pin = String(format: "%06d", Int.random(in: 0...999999))
|
|
let token = UUID().uuidString
|
|
pendingPins[pin] = token
|
|
|
|
// schedule expiration
|
|
DispatchQueue.global().asyncAfter(deadline: .now() + pinLifetimeSeconds) { [weak self] in
|
|
self?.pendingPins[pin] = nil
|
|
}
|
|
return pin
|
|
}
|
|
|
|
func confirmPIN(_ pin: String, deviceName: String?) -> String? {
|
|
guard let token = pendingPins[pin] else { return nil }
|
|
pendingPins[pin] = nil
|
|
|
|
let name = deviceName ?? "iPhone-\(Int.random(in: 1000...9999))"
|
|
let device = PairedDevice(name: name, token: token, pairedAt: Date())
|
|
pairedDevices.append(device)
|
|
save()
|
|
return token
|
|
}
|
|
|
|
func revokeDevice(named name: String) {
|
|
pairedDevices.removeAll { $0.name == name }
|
|
save()
|
|
}
|
|
|
|
func isTokenPaired(_ token: String) -> Bool {
|
|
pairedDevices.contains { $0.token == token }
|
|
}
|
|
|
|
func listPairedDevices() -> [PairedDevice] {
|
|
pairedDevices
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|