Phase 0: scaffold

Two SPM packages (VNCCore, VNCUI) + thin iOS app target wired via
xcodegen. Builds for iPhone 17 simulator, unit tests pass.

- VNCCore: SessionState, SessionController stub, Transport protocol
  with DirectTransport (NWConnection), DiscoveryService (Bonjour on
  _rfb._tcp and _workstation._tcp), SavedConnection @Model,
  ConnectionStore, KeychainService, ClipboardBridge
- VNCUI: ConnectionListView, AddConnectionView, SessionView,
  FramebufferView/FramebufferUIView (UIKit CALayer), InputMapper,
  SettingsView; UIKit bits guarded with #if canImport(UIKit) so
  swift test runs on macOS
- App: @main VNCApp, AppStateController state machine, RootView
- RoyalVNCKit dependency pinned to main (transitive CryptoSwift
  constraint blocks tagged releases)
- xcodegen Project.yml + README + .gitignore

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-04-16 19:29:47 -05:00
commit 2cff17fa0d
28 changed files with 1161 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
import Foundation
import Security
public protocol KeychainServicing: Sendable {
func storePassword(_ password: String, account: String) throws
func loadPassword(account: String) throws -> String?
func deletePassword(account: String) throws
}
public struct KeychainService: KeychainServicing {
private let service: String
public init(service: String = "com.screens.vnc") {
self.service = service
}
public func storePassword(_ password: String, account: String) throws {
guard let data = password.data(using: .utf8) else { throw KeychainError.encoding }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
SecItemDelete(query as CFDictionary)
var attributes = query
attributes[kSecValueData as String] = data
attributes[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
let status = SecItemAdd(attributes as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.unhandled(status) }
}
public func loadPassword(account: String) throws -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecItemNotFound { return nil }
guard status == errSecSuccess, let data = result as? Data else {
throw KeychainError.unhandled(status)
}
return String(data: data, encoding: .utf8)
}
public func deletePassword(account: String) throws {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: account
]
let status = SecItemDelete(query as CFDictionary)
if status != errSecSuccess && status != errSecItemNotFound {
throw KeychainError.unhandled(status)
}
}
}
public enum KeychainError: Error, Sendable {
case encoding
case unhandled(OSStatus)
}