@hasna/recordings 0.2.10 → 0.2.13

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 (167) hide show
  1. package/README.md +350 -35
  2. package/bun.lock +3 -0
  3. package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
  4. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
  5. package/dist/cli/index.js +1728 -320
  6. package/dist/cli/macos-permissions.d.ts +13 -0
  7. package/dist/cli/macos-permissions.d.ts.map +1 -0
  8. package/dist/cli/options.d.ts +12 -0
  9. package/dist/cli/options.d.ts.map +1 -1
  10. package/dist/db/pg-migrations.d.ts.map +1 -1
  11. package/dist/db/recordings.d.ts +2 -1
  12. package/dist/db/recordings.d.ts.map +1 -1
  13. package/dist/db/remote-storage.d.ts +5 -1
  14. package/dist/db/remote-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +628 -147
  18. package/dist/lib/bun-runtime.d.ts +11 -0
  19. package/dist/lib/bun-runtime.d.ts.map +1 -0
  20. package/dist/lib/config.d.ts +8 -1
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/enhancer.d.ts +10 -2
  23. package/dist/lib/enhancer.d.ts.map +1 -1
  24. package/dist/lib/install-maintenance.d.ts +24 -0
  25. package/dist/lib/install-maintenance.d.ts.map +1 -0
  26. package/dist/lib/machine.d.ts +2 -0
  27. package/dist/lib/machine.d.ts.map +1 -0
  28. package/dist/lib/recorder.d.ts +2 -1
  29. package/dist/lib/recorder.d.ts.map +1 -1
  30. package/dist/lib/recording-create-identity.d.ts +13 -0
  31. package/dist/lib/recording-create-identity.d.ts.map +1 -0
  32. package/dist/lib/release-install-policy.d.ts +31 -0
  33. package/dist/lib/release-install-policy.d.ts.map +1 -0
  34. package/dist/lib/transcriber.d.ts.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +977 -194
  37. package/dist/sdk/index.js +5 -1
  38. package/dist/sdk/v1.generated.d.ts +4 -0
  39. package/dist/sdk/v1.generated.d.ts.map +1 -1
  40. package/dist/server/cloud-config.d.ts +9 -0
  41. package/dist/server/cloud-config.d.ts.map +1 -0
  42. package/dist/server/cloud-readiness.d.ts +10 -0
  43. package/dist/server/cloud-readiness.d.ts.map +1 -0
  44. package/dist/server/cloud.d.ts +9 -18
  45. package/dist/server/cloud.d.ts.map +1 -1
  46. package/dist/server/index.js +1654 -304
  47. package/dist/server/migrate-command.d.ts +11 -0
  48. package/dist/server/migrate-command.d.ts.map +1 -0
  49. package/dist/server/openapi.d.ts +22 -0
  50. package/dist/server/openapi.d.ts.map +1 -1
  51. package/dist/server/repo.d.ts +8 -1
  52. package/dist/server/repo.d.ts.map +1 -1
  53. package/dist/server/serve.d.ts +6 -1
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage.js +515 -80
  57. package/dist/store.d.ts +3 -1
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +7 -4
  63. package/packaging/macos/Empty.entitlements +5 -0
  64. package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
  65. package/packaging/macos/Verifier.entitlements +10 -0
  66. package/packaging/macos/artifact-verifier.sb +33 -0
  67. package/packaging/macos/build_release_pkg.sh +872 -0
  68. package/packaging/macos/managed_bootstrap.sh +623 -0
  69. package/packaging/macos/pkgutil_fingerprint.awk +37 -0
  70. package/packaging/macos/release_lifecycle.ts +463 -0
  71. package/packaging/macos/scripts/postinstall +565 -0
  72. package/packaging/macos/scripts/preinstall +297 -0
  73. package/scripts/build_companion_cli.sh +337 -0
  74. package/scripts/build_native_fs_guard.sh +59 -0
  75. package/scripts/generate-sdk.ts +19 -1
  76. package/scripts/install_macos_app.sh +1922 -77
  77. package/scripts/macos_artifact.ts +5316 -0
  78. package/scripts/migrate.ts +38 -10
  79. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  80. package/scripts/native/recordings_fs_guard.c +1167 -0
  81. package/scripts/native_fs_guard.ts +158 -0
  82. package/scripts/release-guard.ts +20 -1
  83. package/scripts/resolve_tailscale_cli.sh +389 -0
  84. package/scripts/smoke_macos_app.sh +573 -0
  85. package/src/native/Recordings/App/ContentView.swift +83 -0
  86. package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
  87. package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
  88. package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
  89. package/src/native/Recordings/App/RecordingsApp.swift +367 -44
  90. package/src/native/Recordings/App/RecordingsListView.swift +130 -0
  91. package/src/native/Recordings/App/RecordingsStore.swift +206 -0
  92. package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
  93. package/src/native/Recordings/App/SidebarView.swift +212 -0
  94. package/src/native/Recordings/App/Theme.swift +87 -0
  95. package/src/native/Recordings/Package.resolved +5 -5
  96. package/src/native/Recordings/Package.swift +62 -2
  97. package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
  98. package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
  99. package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
  101. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
  102. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
  103. package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
  104. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
  105. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
  106. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
  107. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
  108. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
  109. package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
  110. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
  111. package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
  112. package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
  113. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
  114. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
  115. package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
  116. package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
  117. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
  118. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
  119. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
  120. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
  121. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
  122. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
  123. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
  124. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
  125. package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
  126. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
  127. package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
  128. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
  129. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
  130. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
  131. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
  132. package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
  133. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
  134. package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
  135. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
  136. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
  137. package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
  138. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
  139. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
  140. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
  141. package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
  142. package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
  143. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
  144. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
  145. package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
  146. package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
  147. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
  148. package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
  149. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
  150. package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
  151. package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
  152. package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
  153. package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
  154. package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
  155. package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
  156. package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
  157. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
  158. package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
  159. package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
  160. package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
  161. package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
  162. package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
  163. package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
  164. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
  165. package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
  166. package/src/native/Recordings/build.sh +1990 -29
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -0,0 +1,362 @@
1
+ import Darwin
2
+ import Foundation
3
+ import RecordingsUpdateProtocol
4
+
5
+ enum InstallJournalOperation: String, Codable, Sendable {
6
+ case applicationActivation = "application-activation"
7
+ case bootstrapCommit = "bootstrap-commit"
8
+ }
9
+
10
+ struct BrokerInstallJournal: Codable, Sendable {
11
+ let schemaVersion: Int
12
+ let transactionID: String
13
+ var phase: String
14
+ let releaseID: String
15
+ let releaseSequence: UInt64
16
+ let keyEpoch: UInt64
17
+ let envelopePayloadSHA256: String
18
+ let artifactSHA256: String
19
+ let manifestSHA256: String
20
+ let cohortPackageSHA256: String
21
+ let candidateTreeSHA256: String
22
+ let minimumOSVersion: String?
23
+ let candidateApplicationMode: UInt16
24
+ let candidateExecutableModes: [String: UInt16]
25
+ let previousTreeSHA256: String?
26
+ let previousApplicationMode: UInt16?
27
+ let previousExecutableModes: [String: UInt16]?
28
+ let previousApplicationPath: String?
29
+ let candidateApplicationPath: String
30
+ let applicationPath: String
31
+ /// Optional for schema-v1 compatibility. Every journal written before this
32
+ /// discriminator existed represented an ordinary application activation.
33
+ let operation: InstallJournalOperation?
34
+ /// Bootstrap journals bind the exact protected transaction path rather than
35
+ /// relying only on a UUID-derived reconstruction during recovery.
36
+ let transactionDirectory: String?
37
+ /// A bootstrap advance is legal only from an absent monotonic state. Recording
38
+ /// that fact before `seen` makes the one-time transition explicit and recoverable.
39
+ let bootstrapPriorMonotonicState: String?
40
+
41
+ var resolvedOperation: InstallJournalOperation {
42
+ journalOperation
43
+ }
44
+
45
+ var expectedPurpose: String {
46
+ journalOperation == .bootstrapCommit ? "bootstrap" : "update"
47
+ }
48
+
49
+ private var journalOperation: InstallJournalOperation {
50
+ operation ?? .applicationActivation
51
+ }
52
+
53
+ init(
54
+ schemaVersion: Int,
55
+ transactionID: String,
56
+ phase: String,
57
+ releaseID: String,
58
+ releaseSequence: UInt64,
59
+ keyEpoch: UInt64,
60
+ envelopePayloadSHA256: String,
61
+ artifactSHA256: String,
62
+ manifestSHA256: String,
63
+ cohortPackageSHA256: String,
64
+ candidateTreeSHA256: String,
65
+ minimumOSVersion: String? = nil,
66
+ candidateApplicationMode: UInt16,
67
+ candidateExecutableModes: [String: UInt16],
68
+ previousTreeSHA256: String?,
69
+ previousApplicationMode: UInt16?,
70
+ previousExecutableModes: [String: UInt16]?,
71
+ previousApplicationPath: String?,
72
+ candidateApplicationPath: String,
73
+ applicationPath: String,
74
+ operation: InstallJournalOperation? = nil,
75
+ transactionDirectory: String? = nil,
76
+ bootstrapPriorMonotonicState: String? = nil
77
+ ) {
78
+ self.schemaVersion = schemaVersion
79
+ self.transactionID = transactionID
80
+ self.phase = phase
81
+ self.releaseID = releaseID
82
+ self.releaseSequence = releaseSequence
83
+ self.keyEpoch = keyEpoch
84
+ self.envelopePayloadSHA256 = envelopePayloadSHA256
85
+ self.artifactSHA256 = artifactSHA256
86
+ self.manifestSHA256 = manifestSHA256
87
+ self.cohortPackageSHA256 = cohortPackageSHA256
88
+ self.candidateTreeSHA256 = candidateTreeSHA256
89
+ self.minimumOSVersion = minimumOSVersion
90
+ self.candidateApplicationMode = candidateApplicationMode
91
+ self.candidateExecutableModes = candidateExecutableModes
92
+ self.previousTreeSHA256 = previousTreeSHA256
93
+ self.previousApplicationMode = previousApplicationMode
94
+ self.previousExecutableModes = previousExecutableModes
95
+ self.previousApplicationPath = previousApplicationPath
96
+ self.candidateApplicationPath = candidateApplicationPath
97
+ self.applicationPath = applicationPath
98
+ self.operation = operation
99
+ self.transactionDirectory = transactionDirectory
100
+ self.bootstrapPriorMonotonicState = bootstrapPriorMonotonicState
101
+ }
102
+
103
+ enum CodingKeys: String, CodingKey {
104
+ case schemaVersion = "schema_version"
105
+ case transactionID = "transaction_id"
106
+ case phase
107
+ case releaseID = "release_id"
108
+ case releaseSequence = "release_sequence"
109
+ case keyEpoch = "key_epoch"
110
+ case envelopePayloadSHA256 = "envelope_payload_sha256"
111
+ case artifactSHA256 = "artifact_sha256"
112
+ case manifestSHA256 = "manifest_sha256"
113
+ case cohortPackageSHA256 = "cohort_package_sha256"
114
+ case candidateTreeSHA256 = "candidate_tree_sha256"
115
+ case minimumOSVersion = "minimum_os_version"
116
+ case candidateApplicationMode = "candidate_application_mode"
117
+ case candidateExecutableModes = "candidate_executable_modes"
118
+ case previousTreeSHA256 = "previous_tree_sha256"
119
+ case previousApplicationMode = "previous_application_mode"
120
+ case previousExecutableModes = "previous_executable_modes"
121
+ case previousApplicationPath = "previous_application_path"
122
+ case candidateApplicationPath = "candidate_application_path"
123
+ case applicationPath = "application_path"
124
+ case operation
125
+ case transactionDirectory = "transaction_directory"
126
+ case bootstrapPriorMonotonicState = "bootstrap_prior_monotonic_state"
127
+ }
128
+ }
129
+
130
+ struct DurableInstallJournal {
131
+ let transactionDirectory: String
132
+
133
+ func read() throws -> BrokerInstallJournal? {
134
+ let directory = try openValidatedDirectory()
135
+ defer { Darwin.close(directory) }
136
+ let input = openat(directory, "install-journal.json", O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
137
+ if input < 0, errno == ENOENT { return nil }
138
+ guard input >= 0 else { throw InstallJournalError.invalidJournal }
139
+ defer { Darwin.close(input) }
140
+ var metadata = stat()
141
+ guard fstat(input, &metadata) == 0,
142
+ (metadata.st_mode & S_IFMT) == S_IFREG,
143
+ metadata.st_uid == 0,
144
+ (metadata.st_mode & 0o077) == 0,
145
+ metadata.st_nlink == 1,
146
+ DarwinACLValidator.descriptorHasNoExtendedACL(input),
147
+ metadata.st_size > 0,
148
+ metadata.st_size <= 64 * 1024
149
+ else {
150
+ throw InstallJournalError.invalidJournal
151
+ }
152
+ var data = Data(count: Int(metadata.st_size))
153
+ var offset: off_t = 0
154
+ try data.withUnsafeMutableBytes { bytes in
155
+ guard let base = bytes.baseAddress else { return }
156
+ while offset < metadata.st_size {
157
+ let count = pread(
158
+ input,
159
+ base.advanced(by: Int(offset)),
160
+ Int(metadata.st_size - offset),
161
+ offset
162
+ )
163
+ if count < 0, errno == EINTR { continue }
164
+ guard count > 0 else { throw InstallJournalError.invalidJournal }
165
+ offset += off_t(count)
166
+ }
167
+ }
168
+ var after = stat()
169
+ guard fstat(input, &after) == 0,
170
+ metadata.st_dev == after.st_dev,
171
+ metadata.st_ino == after.st_ino,
172
+ metadata.st_size == after.st_size,
173
+ metadata.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
174
+ metadata.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
175
+ metadata.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
176
+ metadata.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec,
177
+ DarwinACLValidator.descriptorHasNoExtendedACL(input),
178
+ let journal = try? JSONDecoder().decode(BrokerInstallJournal.self, from: data)
179
+ else {
180
+ throw InstallJournalError.invalidJournal
181
+ }
182
+ try validate(journal)
183
+ return journal
184
+ }
185
+
186
+ func write(_ journal: BrokerInstallJournal) throws {
187
+ try validate(journal)
188
+ let directory = try openValidatedDirectory()
189
+ defer { Darwin.close(directory) }
190
+ let encoder = JSONEncoder()
191
+ encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
192
+ let data = try encoder.encode(journal) + Data([0x0a])
193
+ let temporary = ".install-journal.\(UUID().uuidString.lowercased()).tmp"
194
+ let output = openat(
195
+ directory,
196
+ temporary,
197
+ O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
198
+ 0o600
199
+ )
200
+ guard output >= 0 else { throw InstallJournalError.couldNotPersist }
201
+ var removeTemporary = true
202
+ defer {
203
+ Darwin.close(output)
204
+ if removeTemporary { unlinkat(directory, temporary, 0) }
205
+ }
206
+ guard DarwinACLValidator.descriptorHasNoExtendedACL(output) else {
207
+ throw InstallJournalError.couldNotPersist
208
+ }
209
+ try data.withUnsafeBytes { bytes in
210
+ guard let base = bytes.baseAddress else { return }
211
+ var offset = 0
212
+ while offset < bytes.count {
213
+ let count = Darwin.write(output, base.advanced(by: offset), bytes.count - offset)
214
+ if count < 0, errno == EINTR { continue }
215
+ guard count > 0 else { throw InstallJournalError.couldNotPersist }
216
+ offset += count
217
+ }
218
+ }
219
+ guard fsync(output) == 0,
220
+ renameat(directory, temporary, directory, "install-journal.json") == 0,
221
+ fsync(directory) == 0
222
+ else {
223
+ throw InstallJournalError.couldNotPersist
224
+ }
225
+ removeTemporary = false
226
+ }
227
+
228
+ private func openValidatedDirectory() throws -> Int32 {
229
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
230
+ to: transactionDirectory
231
+ ) else {
232
+ throw InstallJournalError.unsafeDirectory
233
+ }
234
+ let directory = Darwin.open(
235
+ transactionDirectory,
236
+ O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW
237
+ )
238
+ guard directory >= 0 else { throw InstallJournalError.unsafeDirectory }
239
+ var metadata = stat()
240
+ guard fstat(directory, &metadata) == 0,
241
+ (metadata.st_mode & S_IFMT) == S_IFDIR,
242
+ metadata.st_uid == 0,
243
+ (metadata.st_mode & 0o077) == 0,
244
+ DarwinACLValidator.descriptorHasNoExtendedACL(directory)
245
+ else {
246
+ Darwin.close(directory)
247
+ throw InstallJournalError.unsafeDirectory
248
+ }
249
+ return directory
250
+ }
251
+
252
+ private func validate(_ journal: BrokerInstallJournal) throws {
253
+ let leaf = URL(fileURLWithPath: transactionDirectory).lastPathComponent
254
+ guard leaf.hasPrefix("transaction-") else { throw InstallJournalError.invalidJournal }
255
+ let transactionID = String(leaf.dropFirst("transaction-".count))
256
+ let expectedCandidate = transactionDirectory + "/candidate/Recordings.app"
257
+ let activationPhases: Set<String> = [
258
+ "prepared", "launch-barrier-pending", "launch-barrier-held",
259
+ "swap-pending", "swapped", "previous-retaining",
260
+ "previous-retained", "first-install-pending", "first-installed",
261
+ "launch-barrier-releasing", "launch-barrier-released",
262
+ "activated", "rollback-started", "rolled-back", "committed",
263
+ ]
264
+ let bootstrapPhases: Set<String> = [
265
+ "bootstrap-prepared", "bootstrap-commit-pending",
266
+ "bootstrap-aborted", "bootstrap-committed",
267
+ ]
268
+ guard journal.schemaVersion == RecordingsUpdateConstants.protocolVersion,
269
+ journal.transactionID == transactionID,
270
+ UUID(uuidString: journal.transactionID)?.uuidString.lowercased() == journal.transactionID,
271
+ UUID(uuidString: journal.releaseID) != nil,
272
+ journal.releaseSequence > 0,
273
+ journal.keyEpoch > 0,
274
+ Self.isSHA256(journal.envelopePayloadSHA256),
275
+ Self.isSHA256(journal.artifactSHA256),
276
+ Self.isSHA256(journal.manifestSHA256),
277
+ Self.isSHA256(journal.cohortPackageSHA256),
278
+ Self.isSHA256(journal.candidateTreeSHA256),
279
+ Self.isValidMinimumOSVersion(journal.minimumOSVersion),
280
+ Self.isSafeApplicationMode(journal.candidateApplicationMode),
281
+ Self.areSafeExecutableModes(journal.candidateExecutableModes),
282
+ journal.applicationPath == RecordingsUpdateConstants.applicationPath
283
+ else {
284
+ throw InstallJournalError.invalidJournal
285
+ }
286
+
287
+ switch journal.operation ?? .applicationActivation {
288
+ case .applicationActivation:
289
+ // `operation == nil` is the only accepted legacy migration: schema-v1
290
+ // journals predating the discriminator were all application activations.
291
+ guard activationPhases.contains(journal.phase),
292
+ journal.transactionDirectory == nil ||
293
+ journal.transactionDirectory == transactionDirectory,
294
+ journal.bootstrapPriorMonotonicState == nil,
295
+ journal.candidateApplicationPath == expectedCandidate,
296
+ (journal.previousTreeSHA256 == nil) == (journal.previousApplicationPath == nil),
297
+ (journal.previousTreeSHA256 == nil) == (journal.previousApplicationMode == nil),
298
+ (journal.previousTreeSHA256 == nil) == (journal.previousExecutableModes == nil)
299
+ else {
300
+ throw InstallJournalError.invalidJournal
301
+ }
302
+ if let previousDigest = journal.previousTreeSHA256,
303
+ (!Self.isSHA256(previousDigest) ||
304
+ !Self.isSafeApplicationMode(journal.previousApplicationMode ?? 0) ||
305
+ !Self.areSafeExecutableModes(journal.previousExecutableModes ?? [:]) ||
306
+ journal.previousApplicationPath != transactionDirectory + "/previous.app") {
307
+ throw InstallJournalError.invalidJournal
308
+ }
309
+ case .bootstrapCommit:
310
+ guard bootstrapPhases.contains(journal.phase),
311
+ journal.transactionDirectory == transactionDirectory,
312
+ journal.bootstrapPriorMonotonicState == "absent",
313
+ journal.candidateApplicationPath == RecordingsUpdateConstants.applicationPath,
314
+ journal.previousTreeSHA256 == nil,
315
+ journal.previousApplicationMode == nil,
316
+ journal.previousExecutableModes == nil,
317
+ journal.previousApplicationPath == nil
318
+ else {
319
+ throw InstallJournalError.invalidJournal
320
+ }
321
+ }
322
+ }
323
+
324
+ private static func isSHA256(_ value: String) -> Bool {
325
+ value.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil
326
+ }
327
+
328
+ private static func isValidMinimumOSVersion(_ value: String?) -> Bool {
329
+ guard let value else { return true }
330
+ return HostOSVersionPolicy.isValidNumericVersion(value)
331
+ }
332
+
333
+ private static func isSafeApplicationMode(_ value: UInt16) -> Bool {
334
+ value <= 0o777 && (value & 0o500) == 0o500 && (value & 0o022) == 0
335
+ }
336
+
337
+ private static func isSafeExecutableMode(_ value: UInt16) -> Bool {
338
+ value <= 0o777 && (value & 0o500) == 0o500 && (value & 0o022) == 0
339
+ }
340
+
341
+ private static func areSafeExecutableModes(_ values: [String: UInt16]) -> Bool {
342
+ Set(values.keys) == Set([
343
+ "Contents/MacOS/Recordings",
344
+ "Contents/Helpers/recordings",
345
+ "Contents/Helpers/recordings-update-client",
346
+ ]) && values.values.allSatisfy { Self.isSafeExecutableMode($0) }
347
+ }
348
+ }
349
+
350
+ enum InstallJournalError: Error, CustomStringConvertible {
351
+ case unsafeDirectory
352
+ case invalidJournal
353
+ case couldNotPersist
354
+
355
+ var description: String {
356
+ switch self {
357
+ case .unsafeDirectory: "The install-journal directory is unsafe"
358
+ case .invalidJournal: "The install journal is malformed, unsafe, or ambiguously bound"
359
+ case .couldNotPersist: "Could not durably persist the install journal"
360
+ }
361
+ }
362
+ }