@lattices/cli 0.4.14 → 0.5.0

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 (180) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +2 -2
  3. package/apps/mac/Lattices.app/Contents/Info.plist +4 -12
  4. package/apps/mac/Lattices.app/Contents/MacOS/Lattices +0 -0
  5. package/bin/lattices-app.ts +110 -17
  6. package/bin/lattices-build +125 -0
  7. package/bin/lattices-dev +89 -16
  8. package/bin/lattices.ts +977 -16
  9. package/docs/agents.md +81 -4
  10. package/docs/ai-chat-ux-review.md +416 -0
  11. package/docs/api.md +135 -3
  12. package/docs/app.md +30 -8
  13. package/docs/config.md +4 -0
  14. package/docs/mouse-gestures.md +60 -1
  15. package/docs/proposals/LAT-004-interactive-overlay-actors.md +1 -1
  16. package/docs/proposals/LAT-005-action-runtime-product-spine.md +914 -0
  17. package/docs/proposals/LAT-006-mira-in-lattices.md +553 -0
  18. package/docs/reference/dewey.config.ts +2 -2
  19. package/docs/release.md +171 -0
  20. package/docs/repo-structure.md +4 -5
  21. package/docs/voice.md +11 -27
  22. package/package.json +9 -10
  23. package/apps/mac/Package.swift +0 -27
  24. package/apps/mac/Sources/AppShell/App.swift +0 -26
  25. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  26. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  27. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  28. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  29. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  30. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  31. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  32. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  33. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  34. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  35. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  36. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  37. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  38. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  39. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  41. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  42. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  43. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  44. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  45. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  46. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  47. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  48. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  49. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  50. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  51. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  52. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  53. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  54. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  55. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  56. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  57. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  58. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  59. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  60. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  61. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  62. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  63. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  64. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  65. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  66. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  70. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  71. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  72. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  73. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  74. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  75. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  76. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  77. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  78. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  79. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  80. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  81. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  82. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  83. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  84. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  85. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  86. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  87. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  88. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  90. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  91. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  92. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  93. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  94. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  95. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  98. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  99. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  100. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  101. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  102. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  103. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  104. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  105. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  106. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  107. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  110. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  112. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  113. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  120. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  121. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  122. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  125. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  126. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  129. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  130. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  131. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  132. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  133. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  134. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  135. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  136. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  137. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  138. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  139. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  140. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  141. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  142. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  143. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  144. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  145. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  146. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  147. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  148. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  149. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  150. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  151. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  152. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  153. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  154. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  155. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  156. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  158. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  160. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  161. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  162. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  163. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  164. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  165. package/apps/mac/Sources/UI/Theme.swift +0 -164
  166. package/apps/mac/Tests/StageDragTests.swift +0 -333
  167. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  168. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  169. package/apps/mac/Tests/StageTileTests.swift +0 -353
  170. package/swift/Package.swift +0 -20
  171. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  172. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  173. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  174. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  175. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  176. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  177. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  178. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  179. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  180. package/swift/Tests/DeckKitTests/DeckKitTests.swift +0 -286
@@ -1,629 +0,0 @@
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
- case insufficientCapability(String)
16
-
17
- var errorDescription: String? {
18
- switch self {
19
- case .missingHeader(let name):
20
- return "Missing bridge security header: \(name)."
21
- case .untrustedDevice:
22
- return "This device is not trusted by the Mac bridge yet."
23
- case .staleRequest:
24
- return "This bridge request expired before it reached the Mac."
25
- case .replayedRequest:
26
- return "This bridge request was already used."
27
- case .invalidSignature:
28
- return "The bridge request signature could not be verified."
29
- case .invalidEnvelope:
30
- return "The bridge payload could not be decrypted."
31
- case .invalidDeviceKey:
32
- return "The device pairing key is invalid."
33
- case .insufficientCapability(let capability):
34
- return "This trusted device is missing the required bridge capability: \(capability)."
35
- }
36
- }
37
- }
38
-
39
- struct LatticesCompanionTrustedDeviceRecord: Codable, Equatable, Identifiable, Sendable {
40
- var id: String
41
- var name: String
42
- var publicKey: String
43
- var fingerprint: String
44
- var platform: String
45
- var appVersion: String?
46
- var capabilities: [String]?
47
- var pairedAt: Date
48
- var lastSeenAt: Date
49
-
50
- var summary: DeckTrustedDeviceSummary {
51
- DeckTrustedDeviceSummary(
52
- id: id,
53
- name: name,
54
- fingerprint: fingerprint,
55
- capabilities: effectiveCapabilities,
56
- pairedAt: pairedAt,
57
- lastSeenAt: lastSeenAt
58
- )
59
- }
60
-
61
- var effectiveCapabilities: [String] {
62
- capabilities ?? DeckBridgeCapability.defaultCompanionCapabilities
63
- }
64
- }
65
-
66
- struct AuthorizedBridgeRequest {
67
- let device: LatticesCompanionTrustedDeviceRecord
68
- let requestNonce: String
69
- let requestTimestamp: String
70
- }
71
-
72
- final class LatticesCompanionSecurityCoordinator {
73
- static let shared = LatticesCompanionSecurityCoordinator()
74
-
75
- private enum DefaultsKey {
76
- static let trustedDevices = "companion.security.trustedDevices"
77
- }
78
-
79
- private enum KeychainKey {
80
- static let service = "com.arach.lattices.companion.bridge"
81
- static let account = "bridge.keyagreement.private"
82
- }
83
-
84
- private enum Header {
85
- static let deviceID = "x-lattices-device-id"
86
- static let timestamp = "x-lattices-timestamp"
87
- static let nonce = "x-lattices-nonce"
88
- static let signature = "x-lattices-signature"
89
- }
90
-
91
- private let encoder = JSONEncoder()
92
- private let decoder = JSONDecoder()
93
- private let bridgePrivateKey: Curve25519.KeyAgreement.PrivateKey
94
- private let timeSkewAllowance: TimeInterval = 120
95
- private let replayWindow: TimeInterval = 600
96
-
97
- private var trustedDevices: [String: LatticesCompanionTrustedDeviceRecord]
98
- private var seenNonces: [String: Date] = [:]
99
-
100
- private init() {
101
- self.bridgePrivateKey = Self.loadOrCreateBridgeKey()
102
- self.trustedDevices = Self.loadTrustedDevices()
103
- }
104
-
105
- var bridgePublicKeyBase64: String {
106
- Data(bridgePrivateKey.publicKey.rawRepresentation).base64EncodedString()
107
- }
108
-
109
- var bridgeFingerprint: String {
110
- Self.fingerprint(forPublicKeyBase64: bridgePublicKeyBase64)
111
- }
112
-
113
- func trustedDeviceSummaries() -> [DeckTrustedDeviceSummary] {
114
- trustedDevices.values
115
- .map(\.summary)
116
- .sorted { lhs, rhs in
117
- if lhs.lastSeenAt == rhs.lastSeenAt {
118
- return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
119
- }
120
- return lhs.lastSeenAt > rhs.lastSeenAt
121
- }
122
- }
123
-
124
- func clearTrustedDevices() {
125
- trustedDevices.removeAll()
126
- persistTrustedDevices()
127
- }
128
-
129
- func revokeTrustedDevice(id: String) {
130
- trustedDevices.removeValue(forKey: id)
131
- persistTrustedDevices()
132
- }
133
-
134
- func handlePairingRequest(_ request: DeckPairingRequest) -> DeckPairingResponse {
135
- let diag = DiagnosticLog.shared
136
- diag.info("CompanionPairing: request device=\(request.deviceName) id=\(request.deviceID)")
137
- let grantedCapabilities = Self.grantedCapabilities(for: request.requestedCapabilities)
138
-
139
- guard
140
- request.deviceID.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false,
141
- request.deviceName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false,
142
- decodePublicKey(base64: request.devicePublicKey) != nil
143
- else {
144
- diag.warn("CompanionPairing: invalid key material for device id=\(request.deviceID)")
145
- return DeckPairingResponse(
146
- disposition: .denied,
147
- bridgeName: Host.current().localizedName ?? "Lattices Companion",
148
- bridgePublicKey: bridgePublicKeyBase64,
149
- bridgeFingerprint: bridgeFingerprint,
150
- requestSigningRequired: true,
151
- payloadEncryptionRequired: true,
152
- grantedCapabilities: [],
153
- detail: LatticesCompanionSecurityError.invalidDeviceKey.localizedDescription
154
- )
155
- }
156
-
157
- if var existing = trustedDevices[request.deviceID], existing.publicKey == request.devicePublicKey {
158
- existing.lastSeenAt = Date()
159
- existing.capabilities = grantedCapabilities.isEmpty ? existing.effectiveCapabilities : grantedCapabilities
160
- trustedDevices[request.deviceID] = existing
161
- persistTrustedDevices()
162
- diag.success("CompanionPairing: device already trusted id=\(request.deviceID)")
163
- return DeckPairingResponse(
164
- disposition: .alreadyTrusted,
165
- bridgeName: Host.current().localizedName ?? "Lattices Companion",
166
- bridgePublicKey: bridgePublicKeyBase64,
167
- bridgeFingerprint: bridgeFingerprint,
168
- requestSigningRequired: true,
169
- payloadEncryptionRequired: true,
170
- grantedCapabilities: existing.effectiveCapabilities,
171
- detail: "This device is already trusted on the Mac."
172
- )
173
- }
174
-
175
- let approved = promptForPairingApproval(request)
176
- guard approved else {
177
- diag.warn("CompanionPairing: denied device id=\(request.deviceID)")
178
- return DeckPairingResponse(
179
- disposition: .denied,
180
- bridgeName: Host.current().localizedName ?? "Lattices Companion",
181
- bridgePublicKey: bridgePublicKeyBase64,
182
- bridgeFingerprint: bridgeFingerprint,
183
- requestSigningRequired: true,
184
- payloadEncryptionRequired: true,
185
- grantedCapabilities: [],
186
- detail: "Pairing was denied on the Mac."
187
- )
188
- }
189
-
190
- let now = Date()
191
- trustedDevices[request.deviceID] = LatticesCompanionTrustedDeviceRecord(
192
- id: request.deviceID,
193
- name: request.deviceName,
194
- publicKey: request.devicePublicKey,
195
- fingerprint: Self.fingerprint(forPublicKeyBase64: request.devicePublicKey),
196
- platform: request.platform,
197
- appVersion: request.appVersion,
198
- capabilities: grantedCapabilities,
199
- pairedAt: now,
200
- lastSeenAt: now
201
- )
202
- persistTrustedDevices()
203
- diag.success("CompanionPairing: approved device id=\(request.deviceID)")
204
-
205
- return DeckPairingResponse(
206
- disposition: .approved,
207
- bridgeName: Host.current().localizedName ?? "Lattices Companion",
208
- bridgePublicKey: bridgePublicKeyBase64,
209
- bridgeFingerprint: bridgeFingerprint,
210
- requestSigningRequired: true,
211
- payloadEncryptionRequired: true,
212
- grantedCapabilities: grantedCapabilities,
213
- detail: "Trusted and ready for encrypted bridge requests."
214
- )
215
- }
216
-
217
- func authorize(
218
- method: String,
219
- path: String,
220
- headers: [String: String],
221
- body: Data
222
- ) throws -> AuthorizedBridgeRequest {
223
- let deviceID = try requiredHeader(Header.deviceID, from: headers)
224
- let timestamp = try requiredHeader(Header.timestamp, from: headers)
225
- let requestNonce = try requiredHeader(Header.nonce, from: headers)
226
- let signature = try requiredHeader(Header.signature, from: headers)
227
-
228
- guard let device = trustedDevices[deviceID] else {
229
- throw LatticesCompanionSecurityError.untrustedDevice
230
- }
231
-
232
- let requestDate = try parseRequestDate(timestamp)
233
- let now = Date()
234
- guard abs(requestDate.timeIntervalSince(now)) <= timeSkewAllowance else {
235
- throw LatticesCompanionSecurityError.staleRequest
236
- }
237
-
238
- pruneSeenNonces(now: now)
239
- let replayKey = "\(deviceID):\(requestNonce)"
240
- guard seenNonces[replayKey] == nil else {
241
- throw LatticesCompanionSecurityError.replayedRequest
242
- }
243
-
244
- let expectedSignature = try requestSignature(
245
- method: method,
246
- path: path,
247
- device: device,
248
- timestamp: timestamp,
249
- requestNonce: requestNonce,
250
- body: body
251
- )
252
- guard Self.constantTimeEquals(signature, expectedSignature) else {
253
- throw LatticesCompanionSecurityError.invalidSignature
254
- }
255
-
256
- seenNonces[replayKey] = now
257
- touchDevice(deviceID: deviceID, at: now)
258
- return AuthorizedBridgeRequest(
259
- device: trustedDevices[deviceID] ?? device,
260
- requestNonce: requestNonce,
261
- requestTimestamp: timestamp
262
- )
263
- }
264
-
265
- func requireCapability(_ capability: String, for auth: AuthorizedBridgeRequest) throws {
266
- guard auth.device.effectiveCapabilities.contains(capability) else {
267
- throw LatticesCompanionSecurityError.insufficientCapability(capability)
268
- }
269
- }
270
-
271
- func decodeProtectedBody<T: Decodable>(
272
- _ type: T.Type,
273
- body: Data,
274
- auth: AuthorizedBridgeRequest,
275
- method: String,
276
- path: String
277
- ) throws -> T {
278
- let envelope = try decoder.decode(DeckEncryptedEnvelope.self, from: body)
279
- let plaintext = try openEnvelope(
280
- envelope,
281
- device: auth.device,
282
- aad: requestAAD(
283
- method: method,
284
- path: path,
285
- deviceID: auth.device.id,
286
- timestamp: auth.requestTimestamp,
287
- requestNonce: auth.requestNonce
288
- )
289
- )
290
- return try decoder.decode(type, from: plaintext)
291
- }
292
-
293
- func encodeProtectedResponse<T: Encodable>(
294
- _ value: T,
295
- auth: AuthorizedBridgeRequest,
296
- status: Int,
297
- path: String
298
- ) throws -> DeckEncryptedEnvelope {
299
- let plaintext = try encoder.encode(value)
300
- return try sealEnvelope(
301
- plaintext,
302
- device: auth.device,
303
- aad: responseAAD(
304
- status: status,
305
- path: path,
306
- deviceID: auth.device.id,
307
- requestNonce: auth.requestNonce
308
- )
309
- )
310
- }
311
- }
312
-
313
- private extension LatticesCompanionSecurityCoordinator {
314
- static func loadTrustedDevices() -> [String: LatticesCompanionTrustedDeviceRecord] {
315
- guard
316
- let data = UserDefaults.standard.data(forKey: DefaultsKey.trustedDevices),
317
- let devices = try? JSONDecoder().decode([LatticesCompanionTrustedDeviceRecord].self, from: data)
318
- else {
319
- return [:]
320
- }
321
- return Dictionary(uniqueKeysWithValues: devices.map { ($0.id, $0) })
322
- }
323
-
324
- static func loadOrCreateBridgeKey() -> Curve25519.KeyAgreement.PrivateKey {
325
- if
326
- let stored = KeychainBridge.load(service: KeychainKey.service, account: KeychainKey.account),
327
- let key = try? Curve25519.KeyAgreement.PrivateKey(rawRepresentation: stored)
328
- {
329
- return key
330
- }
331
-
332
- let key = Curve25519.KeyAgreement.PrivateKey()
333
- let data = key.rawRepresentation
334
- _ = KeychainBridge.save(data, service: KeychainKey.service, account: KeychainKey.account)
335
- return key
336
- }
337
-
338
- static func fingerprint(forPublicKeyBase64 value: String) -> String {
339
- let digest = SHA256.hash(data: Data(value.utf8))
340
- let hex = digest.map { String(format: "%02x", $0) }.joined()
341
- let compact = String(hex.prefix(12)).uppercased()
342
- return compact.chunked(into: 4).joined(separator: "-")
343
- }
344
-
345
- static func grantedCapabilities(for requested: [String]) -> [String] {
346
- let supported = Set(DeckBridgeCapability.defaultCompanionCapabilities)
347
- let requested = requested.isEmpty ? supported : Set(requested)
348
- return requested
349
- .intersection(supported)
350
- .sorted()
351
- }
352
-
353
- func persistTrustedDevices() {
354
- let sorted = trustedDevices.values.sorted { lhs, rhs in
355
- lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
356
- }
357
- guard let data = try? encoder.encode(sorted) else { return }
358
- UserDefaults.standard.set(data, forKey: DefaultsKey.trustedDevices)
359
- }
360
-
361
- func requiredHeader(_ name: String, from headers: [String: String]) throws -> String {
362
- guard let value = headers[name], value.isEmpty == false else {
363
- throw LatticesCompanionSecurityError.missingHeader(name)
364
- }
365
- return value
366
- }
367
-
368
- func parseRequestDate(_ timestamp: String) throws -> Date {
369
- guard let value = ISO8601DateFormatter.latticesBridge.date(from: timestamp) else {
370
- throw LatticesCompanionSecurityError.staleRequest
371
- }
372
- return value
373
- }
374
-
375
- func requestSignature(
376
- method: String,
377
- path: String,
378
- device: LatticesCompanionTrustedDeviceRecord,
379
- timestamp: String,
380
- requestNonce: String,
381
- body: Data
382
- ) throws -> String {
383
- let key = try signingKey(for: device)
384
- let canonical = requestCanonicalData(
385
- method: method,
386
- path: path,
387
- deviceID: device.id,
388
- timestamp: timestamp,
389
- requestNonce: requestNonce,
390
- body: body
391
- )
392
- let mac = HMAC<SHA256>.authenticationCode(for: canonical, using: key)
393
- return Data(mac).base64EncodedString()
394
- }
395
-
396
- func requestCanonicalData(
397
- method: String,
398
- path: String,
399
- deviceID: String,
400
- timestamp: String,
401
- requestNonce: String,
402
- body: Data
403
- ) -> Data {
404
- let digest = SHA256.hash(data: body)
405
- let hex = digest.map { String(format: "%02x", $0) }.joined()
406
- let canonical = [
407
- method.uppercased(),
408
- path,
409
- deviceID,
410
- timestamp,
411
- requestNonce,
412
- hex
413
- ].joined(separator: "\n")
414
- return Data(canonical.utf8)
415
- }
416
-
417
- func requestAAD(
418
- method: String,
419
- path: String,
420
- deviceID: String,
421
- timestamp: String,
422
- requestNonce: String
423
- ) -> Data {
424
- let value = [
425
- "request",
426
- method.uppercased(),
427
- path,
428
- deviceID,
429
- timestamp,
430
- requestNonce
431
- ].joined(separator: "\n")
432
- return Data(value.utf8)
433
- }
434
-
435
- func responseAAD(
436
- status: Int,
437
- path: String,
438
- deviceID: String,
439
- requestNonce: String
440
- ) -> Data {
441
- let value = [
442
- "response",
443
- String(status),
444
- path,
445
- deviceID,
446
- requestNonce
447
- ].joined(separator: "\n")
448
- return Data(value.utf8)
449
- }
450
-
451
- func signingKey(for device: LatticesCompanionTrustedDeviceRecord) throws -> SymmetricKey {
452
- let publicKey = try trustedPublicKey(for: device)
453
- let sharedSecret = try bridgePrivateKey.sharedSecretFromKeyAgreement(with: publicKey)
454
- return sharedSecret.hkdfDerivedSymmetricKey(
455
- using: SHA256.self,
456
- salt: Data("lattices-bridge-v1".utf8),
457
- sharedInfo: Data("signing".utf8),
458
- outputByteCount: 32
459
- )
460
- }
461
-
462
- func encryptionKey(for device: LatticesCompanionTrustedDeviceRecord) throws -> SymmetricKey {
463
- let publicKey = try trustedPublicKey(for: device)
464
- let sharedSecret = try bridgePrivateKey.sharedSecretFromKeyAgreement(with: publicKey)
465
- return sharedSecret.hkdfDerivedSymmetricKey(
466
- using: SHA256.self,
467
- salt: Data("lattices-bridge-v1".utf8),
468
- sharedInfo: Data("encryption".utf8),
469
- outputByteCount: 32
470
- )
471
- }
472
-
473
- func trustedPublicKey(for device: LatticesCompanionTrustedDeviceRecord) throws -> Curve25519.KeyAgreement.PublicKey {
474
- guard let key = decodePublicKey(base64: device.publicKey) else {
475
- throw LatticesCompanionSecurityError.invalidDeviceKey
476
- }
477
- return key
478
- }
479
-
480
- func decodePublicKey(base64: String) -> Curve25519.KeyAgreement.PublicKey? {
481
- guard let data = Data(base64Encoded: base64) else { return nil }
482
- return try? Curve25519.KeyAgreement.PublicKey(rawRepresentation: data)
483
- }
484
-
485
- func openEnvelope(
486
- _ envelope: DeckEncryptedEnvelope,
487
- device: LatticesCompanionTrustedDeviceRecord,
488
- aad: Data
489
- ) throws -> Data {
490
- guard let data = Data(base64Encoded: envelope.sealedBox),
491
- let sealed = try? ChaChaPoly.SealedBox(combined: data) else {
492
- throw LatticesCompanionSecurityError.invalidEnvelope
493
- }
494
- let key = try encryptionKey(for: device)
495
- guard let plaintext = try? ChaChaPoly.open(sealed, using: key, authenticating: aad) else {
496
- throw LatticesCompanionSecurityError.invalidEnvelope
497
- }
498
- return plaintext
499
- }
500
-
501
- func sealEnvelope(
502
- _ plaintext: Data,
503
- device: LatticesCompanionTrustedDeviceRecord,
504
- aad: Data
505
- ) throws -> DeckEncryptedEnvelope {
506
- let key = try encryptionKey(for: device)
507
- let sealed = try ChaChaPoly.seal(plaintext, using: key, authenticating: aad)
508
- return DeckEncryptedEnvelope(sealedBox: Data(sealed.combined).base64EncodedString())
509
- }
510
-
511
- func pruneSeenNonces(now: Date) {
512
- seenNonces = seenNonces.filter { now.timeIntervalSince($0.value) < replayWindow }
513
- }
514
-
515
- func touchDevice(deviceID: String, at date: Date) {
516
- guard var device = trustedDevices[deviceID] else { return }
517
- guard date.timeIntervalSince(device.lastSeenAt) >= 30 else { return }
518
- device.lastSeenAt = date
519
- trustedDevices[deviceID] = device
520
- persistTrustedDevices()
521
- }
522
-
523
- func promptForPairingApproval(_ request: DeckPairingRequest) -> Bool {
524
- if Thread.isMainThread {
525
- return runPairingAlert(request)
526
- }
527
-
528
- let semaphore = DispatchSemaphore(value: 0)
529
- var approved = false
530
-
531
- DispatchQueue.main.async {
532
- approved = self.runPairingAlert(request)
533
- semaphore.signal()
534
- }
535
-
536
- semaphore.wait()
537
- return approved
538
- }
539
-
540
- func runPairingAlert(_ request: DeckPairingRequest) -> Bool {
541
- NSApp.activate(ignoringOtherApps: true)
542
-
543
- let alert = NSAlert()
544
- alert.alertStyle = .warning
545
- alert.messageText = "Allow \(request.deviceName) to pair with Lattices?"
546
- alert.informativeText = """
547
- This device is asking for encrypted local-network control of your Mac.
548
-
549
- Device ID: \(request.deviceID)
550
- Device Fingerprint: \(Self.fingerprint(forPublicKeyBase64: request.devicePublicKey))
551
- Platform: \(request.platform)
552
- """
553
- alert.addButton(withTitle: "Allow Pairing")
554
- alert.addButton(withTitle: "Deny")
555
- return alert.runModal() == .alertFirstButtonReturn
556
- }
557
-
558
- static func constantTimeEquals(_ lhs: String, _ rhs: String) -> Bool {
559
- let lhsData = Array(lhs.utf8)
560
- let rhsData = Array(rhs.utf8)
561
- guard lhsData.count == rhsData.count else { return false }
562
- var diff: UInt8 = 0
563
- for index in lhsData.indices {
564
- diff |= lhsData[index] ^ rhsData[index]
565
- }
566
- return diff == 0
567
- }
568
- }
569
-
570
- private enum KeychainBridge {
571
- static func load(service: String, account: String) -> Data? {
572
- let query: [String: Any] = [
573
- kSecClass as String: kSecClassGenericPassword,
574
- kSecAttrService as String: service,
575
- kSecAttrAccount as String: account,
576
- kSecReturnData as String: true,
577
- kSecMatchLimit as String: kSecMatchLimitOne,
578
- ]
579
-
580
- var result: CFTypeRef?
581
- let status = SecItemCopyMatching(query as CFDictionary, &result)
582
- guard status == errSecSuccess else { return nil }
583
- return result as? Data
584
- }
585
-
586
- static func save(_ data: Data, service: String, account: String) -> Bool {
587
- let query: [String: Any] = [
588
- kSecClass as String: kSecClassGenericPassword,
589
- kSecAttrService as String: service,
590
- kSecAttrAccount as String: account,
591
- ]
592
-
593
- let attributes: [String: Any] = [
594
- kSecValueData as String: data,
595
- ]
596
-
597
- let updateStatus = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
598
- if updateStatus == errSecSuccess {
599
- return true
600
- }
601
-
602
- var insert = query
603
- insert[kSecValueData as String] = data
604
- let addStatus = SecItemAdd(insert as CFDictionary, nil)
605
- return addStatus == errSecSuccess
606
- }
607
- }
608
-
609
- private extension ISO8601DateFormatter {
610
- static let latticesBridge: ISO8601DateFormatter = {
611
- let formatter = ISO8601DateFormatter()
612
- formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
613
- return formatter
614
- }()
615
- }
616
-
617
- private extension String {
618
- func chunked(into size: Int) -> [String] {
619
- guard size > 0, isEmpty == false else { return [self] }
620
- var chunks: [String] = []
621
- var index = startIndex
622
- while index < endIndex {
623
- let next = self.index(index, offsetBy: size, limitedBy: endIndex) ?? endIndex
624
- chunks.append(String(self[index..<next]))
625
- index = next
626
- }
627
- return chunks
628
- }
629
- }