Auto-derive room ID from iCloud account or device hardware

RoomIdentity.derive() in TetherKit: ubiquityIdentityToken (iCloud account,
portable across devices) → IOKit/identifierForVendor (hardware, device-tied)
→ Keychain UUID (last resort). No user config needed. Both apps auto-connect
on relaunch if server URL was previously saved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 15:00:12 -05:00
parent 6c2dcdd603
commit c4334143b0
6 changed files with 100 additions and 17 deletions

View File

@@ -4,7 +4,7 @@ import SwiftUI
struct MacTetherApp: App {
@State private var store = MacTetherStore()
var body: some Scene {
MenuBarExtra("tether", systemImage: store.connected ? "paperclip.circle.fill" : "paperclip.circle") {
MenuBarExtra("tether", systemImage: store.connected ? "link.circle.fill" : "link.circle") {
MenuView(store: store)
.frame(width: 320)
}

View File

@@ -7,10 +7,11 @@ import TetherKit
@MainActor @Observable
final class MacTetherStore {
var serverURL: String = UserDefaults.standard.string(forKey: "serverURL") ?? "http://tether.lan:8765"
var room: String = UserDefaults.standard.string(forKey: "room") ?? ""
var room: String = RoomIdentity.derive()
var feed: [Message] = []
var connected = false
var lastError: String?
var hasAttemptedConnect = false
private let deviceID = Host.current().localizedName ?? UUID().uuidString
private var client: TetherClient?
@@ -18,14 +19,19 @@ final class MacTetherStore {
private var pollTask: Task<Void, Never>?
private var lastChangeCount = NSPasteboard.general.changeCount
func connectIfSaved() {
guard UserDefaults.standard.string(forKey: "serverURL") != nil else { return }
connect()
}
func connect() {
guard let base = URL(string: serverURL.trimmingCharacters(in: .whitespaces)),
!room.isEmpty else {
lastError = "Set a server URL and room id."
guard let base = URL(string: serverURL.trimmingCharacters(in: .whitespaces)) else {
hasAttemptedConnect = true
lastError = "Invalid server URL."
return
}
hasAttemptedConnect = true
UserDefaults.standard.set(serverURL, forKey: "serverURL")
UserDefaults.standard.set(room, forKey: "room")
disconnect()
let source = "macos-\(ProcessInfo.processInfo.hostName)"

View File

@@ -14,6 +14,7 @@ struct MenuView: View {
composer
}
.padding(8)
.task { store.connectIfSaved() }
}
private var header: some View {
@@ -32,14 +33,11 @@ struct MenuView: View {
if !store.connected {
TextField("http://tether.lan:8765", text: $store.serverURL)
.textFieldStyle(.roundedBorder).font(.caption)
HStack {
TextField("room id (8 hex)", text: $store.room)
.textFieldStyle(.roundedBorder).font(.caption)
Button("Go") { store.connect() }
.buttonStyle(.borderedProminent).controlSize(.small)
}
Button("Connect") { store.connect() }
.buttonStyle(.borderedProminent).controlSize(.small)
.frame(maxWidth: .infinity)
}
if let err = store.lastError {
if store.hasAttemptedConnect, let err = store.lastError {
Text(err).font(.caption2).foregroundStyle(.red)
}
}

View File

@@ -8,7 +8,7 @@ import TetherKit
@MainActor @Observable
final class TetherStore {
var serverURL: String = UserDefaults.standard.string(forKey: "serverURL") ?? "http://tether.lan:8765"
var room: String = UserDefaults.standard.string(forKey: "room") ?? ""
var room: String = RoomIdentity.derive()
var feed: [Message] = []
var connected = false
var lastError: String?
@@ -17,14 +17,18 @@ final class TetherStore {
private var client: TetherClient?
private var receiveTask: Task<Void, Never>?
func connectIfSaved() {
guard UserDefaults.standard.string(forKey: "serverURL") != nil else { return }
connect()
}
func connect() {
guard let base = URL(string: serverURL.trimmingCharacters(in: .whitespaces)),
!room.isEmpty else {
lastError = "Set a server URL and room id first."
lastError = "Set a server URL first."
return
}
UserDefaults.standard.set(serverURL, forKey: "serverURL")
UserDefaults.standard.set(room, forKey: "room")
disconnect()
let cfg = TetherClient.Config(baseURL: base, room: room, from: deviceID)

View File

@@ -8,7 +8,9 @@ let package = Package(
.library(name: "TetherKit", targets: ["TetherKit"]),
],
targets: [
.target(name: "TetherKit"),
.target(name: "TetherKit", linkerSettings: [
.linkedFramework("IOKit", .when(platforms: [.macOS])),
]),
.testTarget(name: "TetherKitTests", dependencies: ["TetherKit"]),
]
)

View File

@@ -0,0 +1,73 @@
import CryptoKit
import Foundation
import Security
#if os(macOS)
import IOKit
#elseif os(iOS)
import UIKit
#endif
/// Derives a stable 8-hex room ID with no user configuration.
/// Priority:
/// 1. iCloud account token (`ubiquityIdentityToken`) portable across the user's devices
/// 2. Hardware UUID durable to this device even without an iCloud account
/// 3. Keychain-stored UUID last resort, survives app reinstalls
public enum RoomIdentity {
public static func derive() -> String {
iCloudDerived() ?? hardwareDerived() ?? keychainOrNew()
}
/// Non-nil when an iCloud account is signed in. Same value on every device
/// signed into the same Apple ID. Changes if the user signs out.
private static func iCloudDerived() -> String? {
guard let token = FileManager.default.ubiquityIdentityToken,
let data = try? NSKeyedArchiver.archivedData(withRootObject: token,
requiringSecureCoding: false)
else { return nil }
return sha8(data)
}
/// Machine/device hardware UUID no account needed, no entitlements.
private static func hardwareDerived() -> String? {
#if os(macOS)
let service = IOServiceGetMatchingService(kIOMainPortDefault,
IOServiceMatching("IOPlatformExpertDevice"))
guard service != 0 else { return nil }
defer { IOObjectRelease(service) }
guard let raw = IORegistryEntryCreateCFProperty(
service, "IOPlatformUUID" as CFString, kCFAllocatorDefault, 0),
let uuid = raw.takeRetainedValue() as? String
else { return nil }
return sha8(Data(uuid.utf8))
#elseif os(iOS)
guard let uuid = UIDevice.current.identifierForVendor?.uuidString else { return nil }
return sha8(Data(uuid.utf8))
#else
return nil
#endif
}
/// Generate once, store in Keychain so it survives reinstalls.
private static func keychainOrNew() -> String {
let account = "io.pecord.tether.roomID"
let query: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrAccount: account,
kSecReturnData: true]
var ref: AnyObject?
if SecItemCopyMatching(query as CFDictionary, &ref) == errSecSuccess,
let data = ref as? Data, let stored = String(data: data, encoding: .utf8) {
return stored
}
let new = String(UUID().uuidString.lowercased().filter(\.isHexDigit).prefix(8))
let add: [CFString: Any] = [kSecClass: kSecClassGenericPassword,
kSecAttrAccount: account,
kSecValueData: Data(new.utf8)]
SecItemAdd(add as CFDictionary, nil)
return new
}
private static func sha8(_ data: Data) -> String {
SHA256.hash(data: data).prefix(4).map { String(format: "%02x", $0) }.joined()
}
}