@lattices/cli 0.4.2 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +3 -0
  2. package/app/Info.plist +2 -2
  3. package/app/Lattices.app/Contents/Info.plist +2 -2
  4. package/app/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/app/Package.swift +6 -0
  6. package/app/Sources/App.swift +10 -0
  7. package/app/Sources/AppDelegate.swift +90 -34
  8. package/app/Sources/AppShellView.swift +2 -0
  9. package/app/Sources/AppTypeClassifier.swift +36 -0
  10. package/app/Sources/AppUpdater.swift +92 -0
  11. package/app/Sources/CheatSheetHUD.swift +1 -0
  12. package/app/Sources/CliActionLauncher.swift +50 -0
  13. package/app/Sources/CommandModeView.swift +4 -24
  14. package/app/Sources/CompanionActivityLog.swift +70 -0
  15. package/app/Sources/CompanionKeyboardController.swift +141 -0
  16. package/app/Sources/DesktopModel.swift +4 -0
  17. package/app/Sources/HandsOffSession.swift +15 -4
  18. package/app/Sources/HomeDashboardView.swift +18 -10
  19. package/app/Sources/HotkeyStore.swift +8 -5
  20. package/app/Sources/IntentEngine.swift +7 -1
  21. package/app/Sources/LatticesApi.swift +125 -4
  22. package/app/Sources/LatticesCompanionBridgeServer.swift +438 -0
  23. package/app/Sources/LatticesCompanionCockpit.swift +555 -0
  24. package/app/Sources/LatticesCompanionSecurityCoordinator.swift +594 -0
  25. package/app/Sources/LatticesCompanionTrackpadController.swift +204 -0
  26. package/app/Sources/LatticesDeckHost.swift +1463 -0
  27. package/app/Sources/LatticesRuntime.swift +61 -0
  28. package/app/Sources/MainView.swift +351 -191
  29. package/app/Sources/MouseFinder.swift +335 -30
  30. package/app/Sources/MouseGestureConfig.swift +364 -0
  31. package/app/Sources/MouseGestureController.swift +1203 -0
  32. package/app/Sources/MouseInputDeviceStore.swift +98 -0
  33. package/app/Sources/MouseInputEventViewer.swift +272 -0
  34. package/app/Sources/MouseShortcutStore.swift +107 -0
  35. package/app/Sources/OmniSearchView.swift +136 -2
  36. package/app/Sources/OmniSearchWindow.swift +65 -5
  37. package/app/Sources/OnboardingView.swift +30 -16
  38. package/app/Sources/PaletteCommand.swift +26 -6
  39. package/app/Sources/PermissionChecker.swift +76 -2
  40. package/app/Sources/PiAuthNextStepCard.swift +148 -0
  41. package/app/Sources/PiAuthPromptCard.swift +90 -0
  42. package/app/Sources/PiChatDock.swift +137 -74
  43. package/app/Sources/PiChatSession.swift +608 -108
  44. package/app/Sources/PiInstallCallout.swift +86 -0
  45. package/app/Sources/PiProviderSetupCallout.swift +99 -0
  46. package/app/Sources/PiWorkspaceView.swift +174 -77
  47. package/app/Sources/Preferences.swift +78 -0
  48. package/app/Sources/ScreenMapState.swift +91 -31
  49. package/app/Sources/ScreenMapView.swift +510 -524
  50. package/app/Sources/ScreenMapWindowController.swift +12 -4
  51. package/app/Sources/SettingsView.swift +869 -152
  52. package/app/Sources/SystemTelemetryMonitor.swift +273 -0
  53. package/app/Sources/VoiceCommandWindow.swift +23 -2
  54. package/app/Sources/WindowDragSnapController.swift +628 -0
  55. package/app/Sources/WindowTiler.swift +328 -65
  56. package/app/Sources/WorkspaceManager.swift +288 -0
  57. package/bin/assistant-intelligence.ts +874 -0
  58. package/bin/handsoff-infer.ts +16 -209
  59. package/bin/handsoff-worker.ts +45 -258
  60. package/bin/lattices-app.ts +62 -0
  61. package/bin/lattices-dev +4 -0
  62. package/bin/lattices.ts +125 -14
  63. package/docs/agents.md +14 -0
  64. package/docs/api.md +55 -0
  65. package/docs/app.md +3 -0
  66. package/docs/companion-deck.md +180 -0
  67. package/docs/config.md +25 -0
  68. package/docs/tiling-reference.md +55 -0
  69. package/docs/voice-error-model.md +73 -0
  70. package/package.json +2 -1
@@ -0,0 +1,594 @@
1
+ import AppKit
2
+ import CryptoKit
3
+ import DeckKit
4
+ import Foundation
5
+ import Security
6
+
7
+ enum LatticesCompanionSecurityError: LocalizedError {
8
+ case missingHeader(String)
9
+ case untrustedDevice
10
+ case staleRequest
11
+ case replayedRequest
12
+ case invalidSignature
13
+ case invalidEnvelope
14
+ case invalidDeviceKey
15
+
16
+ var errorDescription: String? {
17
+ switch self {
18
+ case .missingHeader(let name):
19
+ return "Missing bridge security header: \(name)."
20
+ case .untrustedDevice:
21
+ return "This device is not trusted by the Mac bridge yet."
22
+ case .staleRequest:
23
+ return "This bridge request expired before it reached the Mac."
24
+ case .replayedRequest:
25
+ return "This bridge request was already used."
26
+ case .invalidSignature:
27
+ return "The bridge request signature could not be verified."
28
+ case .invalidEnvelope:
29
+ return "The bridge payload could not be decrypted."
30
+ case .invalidDeviceKey:
31
+ return "The device pairing key is invalid."
32
+ }
33
+ }
34
+ }
35
+
36
+ struct LatticesCompanionTrustedDeviceRecord: Codable, Equatable, Identifiable, Sendable {
37
+ var id: String
38
+ var name: String
39
+ var publicKey: String
40
+ var fingerprint: String
41
+ var platform: String
42
+ var appVersion: String?
43
+ var pairedAt: Date
44
+ var lastSeenAt: Date
45
+
46
+ var summary: DeckTrustedDeviceSummary {
47
+ DeckTrustedDeviceSummary(
48
+ id: id,
49
+ name: name,
50
+ fingerprint: fingerprint,
51
+ pairedAt: pairedAt,
52
+ lastSeenAt: lastSeenAt
53
+ )
54
+ }
55
+ }
56
+
57
+ struct AuthorizedBridgeRequest {
58
+ let device: LatticesCompanionTrustedDeviceRecord
59
+ let requestNonce: String
60
+ let requestTimestamp: String
61
+ }
62
+
63
+ final class LatticesCompanionSecurityCoordinator {
64
+ static let shared = LatticesCompanionSecurityCoordinator()
65
+
66
+ private enum DefaultsKey {
67
+ static let trustedDevices = "companion.security.trustedDevices"
68
+ }
69
+
70
+ private enum KeychainKey {
71
+ static let service = "com.arach.lattices.companion.bridge"
72
+ static let account = "bridge.keyagreement.private"
73
+ }
74
+
75
+ private enum Header {
76
+ static let deviceID = "x-lattices-device-id"
77
+ static let timestamp = "x-lattices-timestamp"
78
+ static let nonce = "x-lattices-nonce"
79
+ static let signature = "x-lattices-signature"
80
+ }
81
+
82
+ private let encoder = JSONEncoder()
83
+ private let decoder = JSONDecoder()
84
+ private let bridgePrivateKey: Curve25519.KeyAgreement.PrivateKey
85
+ private let timeSkewAllowance: TimeInterval = 120
86
+ private let replayWindow: TimeInterval = 600
87
+
88
+ private var trustedDevices: [String: LatticesCompanionTrustedDeviceRecord]
89
+ private var seenNonces: [String: Date] = [:]
90
+
91
+ private init() {
92
+ self.bridgePrivateKey = Self.loadOrCreateBridgeKey()
93
+ self.trustedDevices = Self.loadTrustedDevices()
94
+ }
95
+
96
+ var bridgePublicKeyBase64: String {
97
+ Data(bridgePrivateKey.publicKey.rawRepresentation).base64EncodedString()
98
+ }
99
+
100
+ var bridgeFingerprint: String {
101
+ Self.fingerprint(forPublicKeyBase64: bridgePublicKeyBase64)
102
+ }
103
+
104
+ func trustedDeviceSummaries() -> [DeckTrustedDeviceSummary] {
105
+ trustedDevices.values
106
+ .map(\.summary)
107
+ .sorted { lhs, rhs in
108
+ if lhs.lastSeenAt == rhs.lastSeenAt {
109
+ return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
110
+ }
111
+ return lhs.lastSeenAt > rhs.lastSeenAt
112
+ }
113
+ }
114
+
115
+ func clearTrustedDevices() {
116
+ trustedDevices.removeAll()
117
+ persistTrustedDevices()
118
+ }
119
+
120
+ func handlePairingRequest(_ request: DeckPairingRequest) -> DeckPairingResponse {
121
+ let diag = DiagnosticLog.shared
122
+ diag.info("CompanionPairing: request device=\(request.deviceName) id=\(request.deviceID)")
123
+
124
+ guard
125
+ request.deviceID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false,
126
+ request.deviceName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false,
127
+ decodePublicKey(base64: request.devicePublicKey) != nil
128
+ else {
129
+ diag.warn("CompanionPairing: invalid key material for device id=\(request.deviceID)")
130
+ return DeckPairingResponse(
131
+ disposition: .denied,
132
+ bridgeName: Host.current().localizedName ?? "Lattices Companion",
133
+ bridgePublicKey: bridgePublicKeyBase64,
134
+ bridgeFingerprint: bridgeFingerprint,
135
+ requestSigningRequired: true,
136
+ payloadEncryptionRequired: true,
137
+ detail: LatticesCompanionSecurityError.invalidDeviceKey.localizedDescription
138
+ )
139
+ }
140
+
141
+ if var existing = trustedDevices[request.deviceID], existing.publicKey == request.devicePublicKey {
142
+ existing.lastSeenAt = Date()
143
+ trustedDevices[request.deviceID] = existing
144
+ persistTrustedDevices()
145
+ diag.success("CompanionPairing: device already trusted id=\(request.deviceID)")
146
+ return DeckPairingResponse(
147
+ disposition: .alreadyTrusted,
148
+ bridgeName: Host.current().localizedName ?? "Lattices Companion",
149
+ bridgePublicKey: bridgePublicKeyBase64,
150
+ bridgeFingerprint: bridgeFingerprint,
151
+ requestSigningRequired: true,
152
+ payloadEncryptionRequired: true,
153
+ detail: "This device is already trusted on the Mac."
154
+ )
155
+ }
156
+
157
+ let approved = promptForPairingApproval(request)
158
+ guard approved else {
159
+ diag.warn("CompanionPairing: denied device id=\(request.deviceID)")
160
+ return DeckPairingResponse(
161
+ disposition: .denied,
162
+ bridgeName: Host.current().localizedName ?? "Lattices Companion",
163
+ bridgePublicKey: bridgePublicKeyBase64,
164
+ bridgeFingerprint: bridgeFingerprint,
165
+ requestSigningRequired: true,
166
+ payloadEncryptionRequired: true,
167
+ detail: "Pairing was denied on the Mac."
168
+ )
169
+ }
170
+
171
+ let now = Date()
172
+ trustedDevices[request.deviceID] = LatticesCompanionTrustedDeviceRecord(
173
+ id: request.deviceID,
174
+ name: request.deviceName,
175
+ publicKey: request.devicePublicKey,
176
+ fingerprint: Self.fingerprint(forPublicKeyBase64: request.devicePublicKey),
177
+ platform: request.platform,
178
+ appVersion: request.appVersion,
179
+ pairedAt: now,
180
+ lastSeenAt: now
181
+ )
182
+ persistTrustedDevices()
183
+ diag.success("CompanionPairing: approved device id=\(request.deviceID)")
184
+
185
+ return DeckPairingResponse(
186
+ disposition: .approved,
187
+ bridgeName: Host.current().localizedName ?? "Lattices Companion",
188
+ bridgePublicKey: bridgePublicKeyBase64,
189
+ bridgeFingerprint: bridgeFingerprint,
190
+ requestSigningRequired: true,
191
+ payloadEncryptionRequired: true,
192
+ detail: "Trusted and ready for encrypted bridge requests."
193
+ )
194
+ }
195
+
196
+ func authorize(
197
+ method: String,
198
+ path: String,
199
+ headers: [String: String],
200
+ body: Data
201
+ ) throws -> AuthorizedBridgeRequest {
202
+ let deviceID = try requiredHeader(Header.deviceID, from: headers)
203
+ let timestamp = try requiredHeader(Header.timestamp, from: headers)
204
+ let requestNonce = try requiredHeader(Header.nonce, from: headers)
205
+ let signature = try requiredHeader(Header.signature, from: headers)
206
+
207
+ guard let device = trustedDevices[deviceID] else {
208
+ throw LatticesCompanionSecurityError.untrustedDevice
209
+ }
210
+
211
+ let requestDate = try parseRequestDate(timestamp)
212
+ let now = Date()
213
+ guard abs(requestDate.timeIntervalSince(now)) <= timeSkewAllowance else {
214
+ throw LatticesCompanionSecurityError.staleRequest
215
+ }
216
+
217
+ pruneSeenNonces(now: now)
218
+ let replayKey = "\(deviceID):\(requestNonce)"
219
+ guard seenNonces[replayKey] == nil else {
220
+ throw LatticesCompanionSecurityError.replayedRequest
221
+ }
222
+
223
+ let expectedSignature = try requestSignature(
224
+ method: method,
225
+ path: path,
226
+ device: device,
227
+ timestamp: timestamp,
228
+ requestNonce: requestNonce,
229
+ body: body
230
+ )
231
+ guard Self.constantTimeEquals(signature, expectedSignature) else {
232
+ throw LatticesCompanionSecurityError.invalidSignature
233
+ }
234
+
235
+ seenNonces[replayKey] = now
236
+ touchDevice(deviceID: deviceID, at: now)
237
+ return AuthorizedBridgeRequest(
238
+ device: trustedDevices[deviceID] ?? device,
239
+ requestNonce: requestNonce,
240
+ requestTimestamp: timestamp
241
+ )
242
+ }
243
+
244
+ func decodeProtectedBody<T: Decodable>(
245
+ _ type: T.Type,
246
+ body: Data,
247
+ auth: AuthorizedBridgeRequest,
248
+ method: String,
249
+ path: String
250
+ ) throws -> T {
251
+ let envelope = try decoder.decode(DeckEncryptedEnvelope.self, from: body)
252
+ let plaintext = try openEnvelope(
253
+ envelope,
254
+ device: auth.device,
255
+ aad: requestAAD(
256
+ method: method,
257
+ path: path,
258
+ deviceID: auth.device.id,
259
+ timestamp: auth.requestTimestamp,
260
+ requestNonce: auth.requestNonce
261
+ )
262
+ )
263
+ return try decoder.decode(type, from: plaintext)
264
+ }
265
+
266
+ func encodeProtectedResponse<T: Encodable>(
267
+ _ value: T,
268
+ auth: AuthorizedBridgeRequest,
269
+ status: Int,
270
+ path: String
271
+ ) throws -> DeckEncryptedEnvelope {
272
+ let plaintext = try encoder.encode(value)
273
+ return try sealEnvelope(
274
+ plaintext,
275
+ device: auth.device,
276
+ aad: responseAAD(
277
+ status: status,
278
+ path: path,
279
+ deviceID: auth.device.id,
280
+ requestNonce: auth.requestNonce
281
+ )
282
+ )
283
+ }
284
+ }
285
+
286
+ private extension LatticesCompanionSecurityCoordinator {
287
+ static func loadTrustedDevices() -> [String: LatticesCompanionTrustedDeviceRecord] {
288
+ guard
289
+ let data = UserDefaults.standard.data(forKey: DefaultsKey.trustedDevices),
290
+ let devices = try? JSONDecoder().decode([LatticesCompanionTrustedDeviceRecord].self, from: data)
291
+ else {
292
+ return [:]
293
+ }
294
+ return Dictionary(uniqueKeysWithValues: devices.map { ($0.id, $0) })
295
+ }
296
+
297
+ static func loadOrCreateBridgeKey() -> Curve25519.KeyAgreement.PrivateKey {
298
+ if
299
+ let stored = KeychainBridge.load(service: KeychainKey.service, account: KeychainKey.account),
300
+ let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: stored)
301
+ {
302
+ return key
303
+ }
304
+
305
+ let key = Curve25519.KeyAgreement.PrivateKey()
306
+ let data = key.rawRepresentation
307
+ _ = KeychainBridge.save(data, service: KeychainKey.service, account: KeychainKey.account)
308
+ return key
309
+ }
310
+
311
+ static func fingerprint(forPublicKeyBase64 value: String) -> String {
312
+ let digest = SHA256.hash(data: Data(value.utf8))
313
+ let hex = digest.map { String(format: "%02x", $0) }.joined()
314
+ let compact = String(hex.prefix(12)).uppercased()
315
+ return compact.chunked(into: 4).joined(separator: "-")
316
+ }
317
+
318
+ func persistTrustedDevices() {
319
+ let sorted = trustedDevices.values.sorted { lhs, rhs in
320
+ lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
321
+ }
322
+ guard let data = try? encoder.encode(sorted) else { return }
323
+ UserDefaults.standard.set(data, forKey: DefaultsKey.trustedDevices)
324
+ }
325
+
326
+ func requiredHeader(_ name: String, from headers: [String: String]) throws -> String {
327
+ guard let value = headers[name], value.isEmpty == false else {
328
+ throw LatticesCompanionSecurityError.missingHeader(name)
329
+ }
330
+ return value
331
+ }
332
+
333
+ func parseRequestDate(_ timestamp: String) throws -> Date {
334
+ guard let value = ISO8601DateFormatter.latticesBridge.date(from: timestamp) else {
335
+ throw LatticesCompanionSecurityError.staleRequest
336
+ }
337
+ return value
338
+ }
339
+
340
+ func requestSignature(
341
+ method: String,
342
+ path: String,
343
+ device: LatticesCompanionTrustedDeviceRecord,
344
+ timestamp: String,
345
+ requestNonce: String,
346
+ body: Data
347
+ ) throws -> String {
348
+ let key = try signingKey(for: device)
349
+ let canonical = requestCanonicalData(
350
+ method: method,
351
+ path: path,
352
+ deviceID: device.id,
353
+ timestamp: timestamp,
354
+ requestNonce: requestNonce,
355
+ body: body
356
+ )
357
+ let mac = HMAC<SHA256>.authenticationCode(for: canonical, using: key)
358
+ return Data(mac).base64EncodedString()
359
+ }
360
+
361
+ func requestCanonicalData(
362
+ method: String,
363
+ path: String,
364
+ deviceID: String,
365
+ timestamp: String,
366
+ requestNonce: String,
367
+ body: Data
368
+ ) -> Data {
369
+ let digest = SHA256.hash(data: body)
370
+ let hex = digest.map { String(format: "%02x", $0) }.joined()
371
+ let canonical = [
372
+ method.uppercased(),
373
+ path,
374
+ deviceID,
375
+ timestamp,
376
+ requestNonce,
377
+ hex
378
+ ].joined(separator: "\n")
379
+ return Data(canonical.utf8)
380
+ }
381
+
382
+ func requestAAD(
383
+ method: String,
384
+ path: String,
385
+ deviceID: String,
386
+ timestamp: String,
387
+ requestNonce: String
388
+ ) -> Data {
389
+ let value = [
390
+ "request",
391
+ method.uppercased(),
392
+ path,
393
+ deviceID,
394
+ timestamp,
395
+ requestNonce
396
+ ].joined(separator: "\n")
397
+ return Data(value.utf8)
398
+ }
399
+
400
+ func responseAAD(
401
+ status: Int,
402
+ path: String,
403
+ deviceID: String,
404
+ requestNonce: String
405
+ ) -> Data {
406
+ let value = [
407
+ "response",
408
+ String(status),
409
+ path,
410
+ deviceID,
411
+ requestNonce
412
+ ].joined(separator: "\n")
413
+ return Data(value.utf8)
414
+ }
415
+
416
+ func signingKey(for device: LatticesCompanionTrustedDeviceRecord) throws -> SymmetricKey {
417
+ let publicKey = try trustedPublicKey(for: device)
418
+ let sharedSecret = try bridgePrivateKey.sharedSecretFromKeyAgreement(with: publicKey)
419
+ return sharedSecret.hkdfDerivedSymmetricKey(
420
+ using: SHA256.self,
421
+ salt: Data("lattices-bridge-v1".utf8),
422
+ sharedInfo: Data("signing".utf8),
423
+ outputByteCount: 32
424
+ )
425
+ }
426
+
427
+ func encryptionKey(for device: LatticesCompanionTrustedDeviceRecord) throws -> SymmetricKey {
428
+ let publicKey = try trustedPublicKey(for: device)
429
+ let sharedSecret = try bridgePrivateKey.sharedSecretFromKeyAgreement(with: publicKey)
430
+ return sharedSecret.hkdfDerivedSymmetricKey(
431
+ using: SHA256.self,
432
+ salt: Data("lattices-bridge-v1".utf8),
433
+ sharedInfo: Data("encryption".utf8),
434
+ outputByteCount: 32
435
+ )
436
+ }
437
+
438
+ func trustedPublicKey(for device: LatticesCompanionTrustedDeviceRecord) throws -> Curve25519.KeyAgreement.PublicKey {
439
+ guard let key = decodePublicKey(base64: device.publicKey) else {
440
+ throw LatticesCompanionSecurityError.invalidDeviceKey
441
+ }
442
+ return key
443
+ }
444
+
445
+ func decodePublicKey(base64: String) -> Curve25519.KeyAgreement.PublicKey? {
446
+ guard let data = Data(base64Encoded: base64) else { return nil }
447
+ return try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: data)
448
+ }
449
+
450
+ func openEnvelope(
451
+ _ envelope: DeckEncryptedEnvelope,
452
+ device: LatticesCompanionTrustedDeviceRecord,
453
+ aad: Data
454
+ ) throws -> Data {
455
+ guard let data = Data(base64Encoded: envelope.sealedBox),
456
+ let sealed = try? ChaChaPoly.SealedBox(combined: data) else {
457
+ throw LatticesCompanionSecurityError.invalidEnvelope
458
+ }
459
+ let key = try encryptionKey(for: device)
460
+ guard let plaintext = try? ChaChaPoly.open(sealed, using: key, authenticating: aad) else {
461
+ throw LatticesCompanionSecurityError.invalidEnvelope
462
+ }
463
+ return plaintext
464
+ }
465
+
466
+ func sealEnvelope(
467
+ _ plaintext: Data,
468
+ device: LatticesCompanionTrustedDeviceRecord,
469
+ aad: Data
470
+ ) throws -> DeckEncryptedEnvelope {
471
+ let key = try encryptionKey(for: device)
472
+ let sealed = try ChaChaPoly.seal(plaintext, using: key, authenticating: aad)
473
+ return DeckEncryptedEnvelope(sealedBox: Data(sealed.combined).base64EncodedString())
474
+ }
475
+
476
+ func pruneSeenNonces(now: Date) {
477
+ seenNonces = seenNonces.filter { now.timeIntervalSince($0.value) < replayWindow }
478
+ }
479
+
480
+ func touchDevice(deviceID: String, at date: Date) {
481
+ guard var device = trustedDevices[deviceID] else { return }
482
+ guard date.timeIntervalSince(device.lastSeenAt) >= 30 else { return }
483
+ device.lastSeenAt = date
484
+ trustedDevices[deviceID] = device
485
+ persistTrustedDevices()
486
+ }
487
+
488
+ func promptForPairingApproval(_ request: DeckPairingRequest) -> Bool {
489
+ if Thread.isMainThread {
490
+ return runPairingAlert(request)
491
+ }
492
+
493
+ let semaphore = DispatchSemaphore(value: 0)
494
+ var approved = false
495
+
496
+ DispatchQueue.main.async {
497
+ approved = self.runPairingAlert(request)
498
+ semaphore.signal()
499
+ }
500
+
501
+ semaphore.wait()
502
+ return approved
503
+ }
504
+
505
+ func runPairingAlert(_ request: DeckPairingRequest) -> Bool {
506
+ NSApp.activate(ignoringOtherApps: true)
507
+
508
+ let alert = NSAlert()
509
+ alert.alertStyle = .warning
510
+ alert.messageText = "Allow \(request.deviceName) to pair with Lattices?"
511
+ alert.informativeText = """
512
+ This device is asking for encrypted local-network control of your Mac.
513
+
514
+ Device ID: \(request.deviceID)
515
+ Device Fingerprint: \(Self.fingerprint(forPublicKeyBase64: request.devicePublicKey))
516
+ Platform: \(request.platform)
517
+ """
518
+ alert.addButton(withTitle: "Allow Pairing")
519
+ alert.addButton(withTitle: "Deny")
520
+ return alert.runModal() == .alertFirstButtonReturn
521
+ }
522
+
523
+ static func constantTimeEquals(_ lhs: String, _ rhs: String) -> Bool {
524
+ let lhsData = Array(lhs.utf8)
525
+ let rhsData = Array(rhs.utf8)
526
+ guard lhsData.count == rhsData.count else { return false }
527
+ var diff: UInt8 = 0
528
+ for index in lhsData.indices {
529
+ diff |= lhsData[index] ^ rhsData[index]
530
+ }
531
+ return diff == 0
532
+ }
533
+ }
534
+
535
+ private enum KeychainBridge {
536
+ static func load(service: String, account: String) -> Data? {
537
+ let query: [String: Any] = [
538
+ kSecClass as String: kSecClassGenericPassword,
539
+ kSecAttrService as String: service,
540
+ kSecAttrAccount as String: account,
541
+ kSecReturnData as String: true,
542
+ kSecMatchLimit as String: kSecMatchLimitOne,
543
+ ]
544
+
545
+ var result: CFTypeRef?
546
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
547
+ guard status == errSecSuccess else { return nil }
548
+ return result as? Data
549
+ }
550
+
551
+ static func save(_ data: Data, service: String, account: String) -> Bool {
552
+ let query: [String: Any] = [
553
+ kSecClass as String: kSecClassGenericPassword,
554
+ kSecAttrService as String: service,
555
+ kSecAttrAccount as String: account,
556
+ ]
557
+
558
+ let attributes: [String: Any] = [
559
+ kSecValueData as String: data,
560
+ ]
561
+
562
+ let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
563
+ if updateStatus == errSecSuccess {
564
+ return true
565
+ }
566
+
567
+ var insert = query
568
+ insert[kSecValueData as String] = data
569
+ let addStatus = SecItemAdd(insert as CFDictionary, nil)
570
+ return addStatus == errSecSuccess
571
+ }
572
+ }
573
+
574
+ private extension ISO8601DateFormatter {
575
+ static let latticesBridge: ISO8601DateFormatter = {
576
+ let formatter = ISO8601DateFormatter()
577
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
578
+ return formatter
579
+ }()
580
+ }
581
+
582
+ private extension String {
583
+ func chunked(into size: Int) -> [String] {
584
+ guard size > 0, isEmpty == false else { return [self] }
585
+ var chunks: [String] = []
586
+ var index = startIndex
587
+ while index < endIndex {
588
+ let next = self.index(index, offsetBy: size, limitedBy: endIndex) ?? endIndex
589
+ chunks.append(String(self[index..<next]))
590
+ index = next
591
+ }
592
+ return chunks
593
+ }
594
+ }