@lattices/cli 0.4.14 → 0.6.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 (181) hide show
  1. package/README.md +5 -7
  2. package/apps/mac/Info.plist +4 -4
  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/proposals/LAT-007-unified-app-shell.md +128 -0
  19. package/docs/reference/dewey.config.ts +2 -2
  20. package/docs/release.md +171 -0
  21. package/docs/repo-structure.md +5 -5
  22. package/docs/voice.md +11 -27
  23. package/package.json +11 -10
  24. package/apps/mac/Package.swift +0 -27
  25. package/apps/mac/Sources/AppShell/App.swift +0 -26
  26. package/apps/mac/Sources/AppShell/AppActivationCoordinator.swift +0 -27
  27. package/apps/mac/Sources/AppShell/AppDelegate.swift +0 -189
  28. package/apps/mac/Sources/AppShell/AppServicesBootstrap.swift +0 -25
  29. package/apps/mac/Sources/AppShell/AppShellView.swift +0 -171
  30. package/apps/mac/Sources/AppShell/AppUpdater.swift +0 -305
  31. package/apps/mac/Sources/AppShell/CliActionLauncher.swift +0 -50
  32. package/apps/mac/Sources/AppShell/HomeDashboardView.swift +0 -133
  33. package/apps/mac/Sources/AppShell/HotkeyBootstrap.swift +0 -87
  34. package/apps/mac/Sources/AppShell/KeyRecorderView.swift +0 -210
  35. package/apps/mac/Sources/AppShell/LatticesRuntime.swift +0 -104
  36. package/apps/mac/Sources/AppShell/MainView.swift +0 -847
  37. package/apps/mac/Sources/AppShell/MainWindow.swift +0 -83
  38. package/apps/mac/Sources/AppShell/MenuBarController.swift +0 -177
  39. package/apps/mac/Sources/AppShell/OnboardingView.swift +0 -483
  40. package/apps/mac/Sources/AppShell/PermissionsAssistantView.swift +0 -366
  41. package/apps/mac/Sources/AppShell/PermissionsAssistantWindow.swift +0 -70
  42. package/apps/mac/Sources/AppShell/Preferences.swift +0 -297
  43. package/apps/mac/Sources/AppShell/SettingsView.swift +0 -3163
  44. package/apps/mac/Sources/AppShell/SettingsWindow.swift +0 -34
  45. package/apps/mac/Sources/AppShell/WorkspaceInspectorPresenter.swift +0 -13
  46. package/apps/mac/Sources/Core/Actions/HotkeyManager.swift +0 -256
  47. package/apps/mac/Sources/Core/Actions/HotkeyStore.swift +0 -399
  48. package/apps/mac/Sources/Core/Actions/IntentEngine.swift +0 -988
  49. package/apps/mac/Sources/Core/Actions/IntentSchema.swift +0 -94
  50. package/apps/mac/Sources/Core/Actions/Intents/CreateLayerIntent.swift +0 -54
  51. package/apps/mac/Sources/Core/Actions/Intents/DistributeIntent.swift +0 -56
  52. package/apps/mac/Sources/Core/Actions/Intents/FocusIntent.swift +0 -69
  53. package/apps/mac/Sources/Core/Actions/Intents/HelpIntent.swift +0 -41
  54. package/apps/mac/Sources/Core/Actions/Intents/KillIntent.swift +0 -47
  55. package/apps/mac/Sources/Core/Actions/Intents/LatticeIntent.swift +0 -53
  56. package/apps/mac/Sources/Core/Actions/Intents/LaunchIntent.swift +0 -67
  57. package/apps/mac/Sources/Core/Actions/Intents/ListSessionsIntent.swift +0 -32
  58. package/apps/mac/Sources/Core/Actions/Intents/ListWindowsIntent.swift +0 -30
  59. package/apps/mac/Sources/Core/Actions/Intents/ScanIntent.swift +0 -52
  60. package/apps/mac/Sources/Core/Actions/Intents/SearchIntent.swift +0 -190
  61. package/apps/mac/Sources/Core/Actions/Intents/SwitchLayerIntent.swift +0 -50
  62. package/apps/mac/Sources/Core/Actions/Intents/TileIntent.swift +0 -61
  63. package/apps/mac/Sources/Core/Actions/PaletteCommand.swift +0 -439
  64. package/apps/mac/Sources/Core/Actions/VoiceIntentResolver.swift +0 -713
  65. package/apps/mac/Sources/Core/Companion/CompanionActivityLog.swift +0 -70
  66. package/apps/mac/Sources/Core/Companion/CompanionKeyboardController.swift +0 -141
  67. package/apps/mac/Sources/Core/Companion/LatticesCompanionBridgeServer.swift +0 -454
  68. package/apps/mac/Sources/Core/Companion/LatticesCompanionCockpit.swift +0 -555
  69. package/apps/mac/Sources/Core/Companion/LatticesCompanionSecurityCoordinator.swift +0 -629
  70. package/apps/mac/Sources/Core/Companion/LatticesCompanionTrackpadController.swift +0 -204
  71. package/apps/mac/Sources/Core/Companion/LatticesDeckHost.swift +0 -1463
  72. package/apps/mac/Sources/Core/Daemon/DaemonProtocol.swift +0 -114
  73. package/apps/mac/Sources/Core/Daemon/DaemonServer.swift +0 -427
  74. package/apps/mac/Sources/Core/Daemon/LatticesApi.swift +0 -2965
  75. package/apps/mac/Sources/Core/Desktop/AccessibilityTextExtractor.swift +0 -111
  76. package/apps/mac/Sources/Core/Desktop/AppTypeClassifier.swift +0 -106
  77. package/apps/mac/Sources/Core/Desktop/DesktopModel.swift +0 -331
  78. package/apps/mac/Sources/Core/Desktop/DesktopModelTypes.swift +0 -73
  79. package/apps/mac/Sources/Core/Desktop/InventoryManager.swift +0 -35
  80. package/apps/mac/Sources/Core/Desktop/InventoryPath.swift +0 -43
  81. package/apps/mac/Sources/Core/Desktop/MouseFinder.swift +0 -527
  82. package/apps/mac/Sources/Core/Desktop/OcrModel.swift +0 -467
  83. package/apps/mac/Sources/Core/Desktop/OcrStore.swift +0 -329
  84. package/apps/mac/Sources/Core/Desktop/PlacementSpec.swift +0 -195
  85. package/apps/mac/Sources/Core/Desktop/SessionWindowLocator.swift +0 -139
  86. package/apps/mac/Sources/Core/Desktop/TilePickerView.swift +0 -209
  87. package/apps/mac/Sources/Core/Desktop/WindowCapture.swift +0 -33
  88. package/apps/mac/Sources/Core/Desktop/WindowDragSnapController.swift +0 -429
  89. package/apps/mac/Sources/Core/Desktop/WindowPreviewCard.swift +0 -100
  90. package/apps/mac/Sources/Core/Desktop/WindowPreviewStore.swift +0 -112
  91. package/apps/mac/Sources/Core/Desktop/WindowSelectionStore.swift +0 -76
  92. package/apps/mac/Sources/Core/Desktop/WindowTiler.swift +0 -2222
  93. package/apps/mac/Sources/Core/Input/EventTapBreaker.swift +0 -124
  94. package/apps/mac/Sources/Core/Input/EventTapThread.swift +0 -54
  95. package/apps/mac/Sources/Core/Input/InputCaptureResetCenter.swift +0 -20
  96. package/apps/mac/Sources/Core/Input/KeyboardRemapConfig.swift +0 -69
  97. package/apps/mac/Sources/Core/Input/KeyboardRemapController.swift +0 -346
  98. package/apps/mac/Sources/Core/Input/KeyboardRemapStore.swift +0 -141
  99. package/apps/mac/Sources/Core/Input/MouseGestureConfig.swift +0 -499
  100. package/apps/mac/Sources/Core/Input/MouseGestureController.swift +0 -2583
  101. package/apps/mac/Sources/Core/Input/MouseInputDeviceStore.swift +0 -98
  102. package/apps/mac/Sources/Core/Input/MouseInputEventViewer.swift +0 -272
  103. package/apps/mac/Sources/Core/Input/MouseShortcutStore.swift +0 -170
  104. package/apps/mac/Sources/Core/Input/SecureEventInputMonitor.swift +0 -39
  105. package/apps/mac/Sources/Core/Input/ShapeRecognizer.swift +0 -624
  106. package/apps/mac/Sources/Core/Input/TapBudgetMeter.swift +0 -56
  107. package/apps/mac/Sources/Core/Overlays/AppWindowShell.swift +0 -63
  108. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeState.swift +0 -1566
  109. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeView.swift +0 -1927
  110. package/apps/mac/Sources/Core/Overlays/CommandMode/CommandModeWindow.swift +0 -196
  111. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteView.swift +0 -307
  112. package/apps/mac/Sources/Core/Overlays/CommandPalette/CommandPaletteWindow.swift +0 -67
  113. package/apps/mac/Sources/Core/Overlays/HUD/CheatSheetHUD.swift +0 -576
  114. package/apps/mac/Sources/Core/Overlays/HUD/HUDBottomBar.swift +0 -279
  115. package/apps/mac/Sources/Core/Overlays/HUD/HUDController.swift +0 -1158
  116. package/apps/mac/Sources/Core/Overlays/HUD/HUDLeftBar.swift +0 -849
  117. package/apps/mac/Sources/Core/Overlays/HUD/HUDMinimap.swift +0 -179
  118. package/apps/mac/Sources/Core/Overlays/HUD/HUDRightBar.swift +0 -596
  119. package/apps/mac/Sources/Core/Overlays/HUD/HUDState.swift +0 -367
  120. package/apps/mac/Sources/Core/Overlays/HUD/HUDTopBar.swift +0 -243
  121. package/apps/mac/Sources/Core/Overlays/HUD/LauncherHUD.swift +0 -334
  122. package/apps/mac/Sources/Core/Overlays/HUD/LayerBezel.swift +0 -203
  123. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchState.swift +0 -280
  124. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchView.swift +0 -422
  125. package/apps/mac/Sources/Core/Overlays/OmniSearch/OmniSearchWindow.swift +0 -94
  126. package/apps/mac/Sources/Core/Overlays/OverlayPanelShell.swift +0 -241
  127. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapState.swift +0 -3135
  128. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapView.swift +0 -3977
  129. package/apps/mac/Sources/Core/Overlays/ScreenMap/ScreenMapWindowController.swift +0 -119
  130. package/apps/mac/Sources/Core/Overlays/ScreenOverlayCanvasController.swift +0 -1217
  131. package/apps/mac/Sources/Core/Overlays/Voice/VoiceCommandWindow.swift +0 -1575
  132. package/apps/mac/Sources/Core/Pi/PiAuthNextStepCard.swift +0 -148
  133. package/apps/mac/Sources/Core/Pi/PiAuthPromptCard.swift +0 -90
  134. package/apps/mac/Sources/Core/Pi/PiChatDock.swift +0 -564
  135. package/apps/mac/Sources/Core/Pi/PiChatSession.swift +0 -1948
  136. package/apps/mac/Sources/Core/Pi/PiInstallCallout.swift +0 -86
  137. package/apps/mac/Sources/Core/Pi/PiProviderSetupCallout.swift +0 -99
  138. package/apps/mac/Sources/Core/Pi/PiWorkspaceView.swift +0 -510
  139. package/apps/mac/Sources/Core/System/Capability.swift +0 -79
  140. package/apps/mac/Sources/Core/System/DiagnosticLog.swift +0 -373
  141. package/apps/mac/Sources/Core/System/EventBus.swift +0 -31
  142. package/apps/mac/Sources/Core/System/PermissionChecker.swift +0 -224
  143. package/apps/mac/Sources/Core/System/ProcessModel.swift +0 -199
  144. package/apps/mac/Sources/Core/System/ProcessQuery.swift +0 -151
  145. package/apps/mac/Sources/Core/System/SystemTelemetryMonitor.swift +0 -273
  146. package/apps/mac/Sources/Core/Voice/AdvisorLearningStore.swift +0 -90
  147. package/apps/mac/Sources/Core/Voice/AgentSession.swift +0 -377
  148. package/apps/mac/Sources/Core/Voice/AudioProvider.swift +0 -555
  149. package/apps/mac/Sources/Core/Voice/HandsOffSession.swift +0 -839
  150. package/apps/mac/Sources/Core/Voice/VoiceChatView.swift +0 -192
  151. package/apps/mac/Sources/Core/Voice/VoxClient.swift +0 -454
  152. package/apps/mac/Sources/Core/Workspace/Project.swift +0 -28
  153. package/apps/mac/Sources/Core/Workspace/ProjectScanner.swift +0 -141
  154. package/apps/mac/Sources/Core/Workspace/SessionLayerStore.swift +0 -285
  155. package/apps/mac/Sources/Core/Workspace/SessionManager.swift +0 -75
  156. package/apps/mac/Sources/Core/Workspace/Terminal/Terminal.swift +0 -259
  157. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalQuery.swift +0 -156
  158. package/apps/mac/Sources/Core/Workspace/Terminal/TerminalSynthesizer.swift +0 -200
  159. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxModel.swift +0 -60
  160. package/apps/mac/Sources/Core/Workspace/Tmux/TmuxQuery.swift +0 -105
  161. package/apps/mac/Sources/Core/Workspace/WorkspaceManager.swift +0 -1027
  162. package/apps/mac/Sources/UI/ActionRow.swift +0 -78
  163. package/apps/mac/Sources/UI/OrphanRow.swift +0 -129
  164. package/apps/mac/Sources/UI/ProjectRow.swift +0 -368
  165. package/apps/mac/Sources/UI/TabGroupRow.swift +0 -178
  166. package/apps/mac/Sources/UI/Theme.swift +0 -164
  167. package/apps/mac/Tests/StageDragTests.swift +0 -333
  168. package/apps/mac/Tests/StageJoinTests.swift +0 -313
  169. package/apps/mac/Tests/StageManagerTests.swift +0 -280
  170. package/apps/mac/Tests/StageTileTests.swift +0 -353
  171. package/swift/Package.swift +0 -20
  172. package/swift/Sources/DeckKit/DeckAction.swift +0 -51
  173. package/swift/Sources/DeckKit/DeckBridgeSecurity.swift +0 -152
  174. package/swift/Sources/DeckKit/DeckCockpit.swift +0 -82
  175. package/swift/Sources/DeckKit/DeckHost.swift +0 -7
  176. package/swift/Sources/DeckKit/DeckManifest.swift +0 -145
  177. package/swift/Sources/DeckKit/DeckRuntimeSnapshot.swift +0 -533
  178. package/swift/Sources/DeckKit/DeckTrackpad.swift +0 -63
  179. package/swift/Sources/DeckKit/DeckValue.swift +0 -93
  180. package/swift/Sources/DeckKit/DeckVoiceError.swift +0 -88
  181. 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
- }