@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,341 @@
1
+ import CryptoKit
2
+ import Darwin
3
+ import Foundation
4
+ import RecordingsUpdateProtocol
5
+ import RecordingsVerifierLauncher
6
+
7
+ struct StagedFile: Sendable {
8
+ let path: String
9
+ let sha256: String
10
+ let byteCount: Int64
11
+ let data: Data?
12
+ }
13
+
14
+ struct StagedUpdate: Sendable {
15
+ let transactionID: UUID
16
+ let directory: String
17
+ let archive: StagedFile
18
+ let manifest: StagedFile
19
+ let envelope: StagedFile
20
+ }
21
+
22
+ final class RootUpdateState {
23
+ let rootPath: String
24
+ private let rootDescriptor: Int32
25
+
26
+ init(path: String = RecordingsUpdateConstants.stateRoot) throws {
27
+ guard geteuid() == 0 else { throw IngestError.brokerNotRoot }
28
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(to: path) else {
29
+ throw IngestError.unsafeStateRoot
30
+ }
31
+ var metadata = stat()
32
+ guard lstat(path, &metadata) == 0,
33
+ (metadata.st_mode & S_IFMT) == S_IFDIR,
34
+ metadata.st_uid == 0,
35
+ (metadata.st_mode & 0o077) == 0
36
+ else {
37
+ throw IngestError.unsafeStateRoot
38
+ }
39
+ rootDescriptor = Darwin.open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
40
+ guard rootDescriptor >= 0 else { throw IngestError.unsafeStateRoot }
41
+ var openedMetadata = stat()
42
+ guard fstat(rootDescriptor, &openedMetadata) == 0,
43
+ openedMetadata.st_dev == metadata.st_dev,
44
+ openedMetadata.st_ino == metadata.st_ino,
45
+ DarwinACLValidator.descriptorHasNoExtendedACL(rootDescriptor)
46
+ else {
47
+ Darwin.close(rootDescriptor)
48
+ throw IngestError.unsafeStateRoot
49
+ }
50
+ rootPath = path
51
+ }
52
+
53
+ deinit { Darwin.close(rootDescriptor) }
54
+
55
+ func withExclusiveTransactionLock<T>(_ operation: () throws -> T) throws -> T {
56
+ let lock = openat(
57
+ rootDescriptor,
58
+ ".transactions.lock",
59
+ O_RDWR | O_CREAT | O_CLOEXEC | O_NOFOLLOW,
60
+ 0o600
61
+ )
62
+ guard lock >= 0 else { throw IngestError.transactionLockUnavailable }
63
+ defer { Darwin.close(lock) }
64
+ var metadata = stat()
65
+ guard fstat(lock, &metadata) == 0,
66
+ (metadata.st_mode & S_IFMT) == S_IFREG,
67
+ metadata.st_uid == 0,
68
+ (metadata.st_mode & 0o077) == 0,
69
+ DarwinACLValidator.descriptorHasNoExtendedACL(lock),
70
+ flock(lock, LOCK_EX) == 0
71
+ else {
72
+ throw IngestError.transactionLockUnavailable
73
+ }
74
+ defer { flock(lock, LOCK_UN) }
75
+ return try operation()
76
+ }
77
+
78
+ func ensureFreeSpace(requiredBytes: UInt64) throws {
79
+ var filesystem = statfs()
80
+ guard fstatfs(rootDescriptor, &filesystem) == 0,
81
+ filesystem.f_bavail > 0,
82
+ filesystem.f_bsize > 0
83
+ else {
84
+ throw IngestError.insufficientStagingCapacity
85
+ }
86
+ let (available, overflow) = UInt64(filesystem.f_bavail)
87
+ .multipliedReportingOverflow(by: UInt64(filesystem.f_bsize))
88
+ let safetyReserve: UInt64 = 2 * 1024 * 1024 * 1024
89
+ guard !overflow,
90
+ requiredBytes <= UInt64.max - safetyReserve,
91
+ available >= requiredBytes + safetyReserve
92
+ else {
93
+ throw IngestError.insufficientStagingCapacity
94
+ }
95
+ }
96
+
97
+ func ensureTransactionQuota() throws {
98
+ let names = try FileManager.default.contentsOfDirectory(atPath: rootPath)
99
+ for name in names where name.hasPrefix("transaction-") {
100
+ guard let identifier = UUID(uuidString: String(name.dropFirst("transaction-".count))),
101
+ "transaction-" + identifier.uuidString.lowercased() == name
102
+ else {
103
+ throw IngestError.unsafeRetainedTransaction
104
+ }
105
+ // Startup recovery must resolve and prune every previous transaction
106
+ // before the listener opens. One broker process admits one transaction.
107
+ throw IngestError.transactionQuotaExceeded
108
+ }
109
+ }
110
+
111
+ func removeTransactionDirectory(id: UUID) throws {
112
+ let leaf = "transaction-\(id.uuidString.lowercased())"
113
+ let status = leaf.withCString {
114
+ recordings_remove_directory_tree_at(rootDescriptor, $0)
115
+ }
116
+ guard status == 0 || status == ENOENT else { throw IngestError.couldNotCleanup }
117
+ }
118
+
119
+ func createTransactionDirectory(id: UUID) throws -> String {
120
+ let leaf = "transaction-\(id.uuidString.lowercased())"
121
+ guard mkdirat(rootDescriptor, leaf, 0o700) == 0 else {
122
+ throw IngestError.couldNotCreateTransaction
123
+ }
124
+ guard fsync(rootDescriptor) == 0 else { throw IngestError.couldNotSync }
125
+ let descriptor = openat(rootDescriptor, leaf, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
126
+ guard descriptor >= 0 else { throw IngestError.couldNotCreateTransaction }
127
+ defer { Darwin.close(descriptor) }
128
+ var metadata = stat()
129
+ guard fstat(descriptor, &metadata) == 0,
130
+ (metadata.st_mode & S_IFMT) == S_IFDIR,
131
+ metadata.st_uid == 0,
132
+ (metadata.st_mode & 0o077) == 0,
133
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
134
+ else {
135
+ throw IngestError.couldNotCreateTransaction
136
+ }
137
+ return rootPath + "/" + leaf
138
+ }
139
+ }
140
+
141
+ struct ArtifactIngestor {
142
+ static let maximumArchiveBytes: Int64 = 256 * 1024 * 1024
143
+ static let maximumManifestBytes: Int64 = 16 * 1024 * 1024
144
+ static let maximumEnvelopeBytes: Int64 = 1024 * 1024
145
+ static let maximumTransactionFootprintBytes: UInt64 = 2 * 1024 * 1024 * 1024
146
+
147
+ let state: RootUpdateState
148
+
149
+ func stage(archive: FileHandle, manifest: FileHandle, envelope: FileHandle) throws -> StagedUpdate {
150
+ try state.ensureFreeSpace(
151
+ requiredBytes: Self.maximumTransactionFootprintBytes
152
+ )
153
+ let retainedDescriptors = try [archive, manifest, envelope].map { handle -> Int32 in
154
+ let retained = fcntl(handle.fileDescriptor, F_DUPFD_CLOEXEC, 10)
155
+ guard retained >= 0, lseek(retained, 0, SEEK_CUR) >= 0 else {
156
+ if retained >= 0 { Darwin.close(retained) }
157
+ throw IngestError.invalidSourceDescriptor
158
+ }
159
+ return retained
160
+ }
161
+ defer { retainedDescriptors.forEach { Darwin.close($0) } }
162
+ let transactionID = UUID()
163
+ let directory = try state.createTransactionDirectory(id: transactionID)
164
+ do {
165
+ let stagedArchive = try copyDescriptor(
166
+ retainedDescriptors[0],
167
+ to: directory + "/artifact.pkg",
168
+ maximumBytes: Self.maximumArchiveBytes,
169
+ retainData: false
170
+ )
171
+ let stagedManifest = try copyDescriptor(
172
+ retainedDescriptors[1],
173
+ to: directory + "/manifest.json",
174
+ maximumBytes: Self.maximumManifestBytes,
175
+ retainData: true
176
+ )
177
+ let stagedEnvelope = try copyDescriptor(
178
+ retainedDescriptors[2],
179
+ to: directory + "/envelope.json",
180
+ maximumBytes: Self.maximumEnvelopeBytes,
181
+ retainData: true
182
+ )
183
+ return StagedUpdate(
184
+ transactionID: transactionID,
185
+ directory: directory,
186
+ archive: stagedArchive,
187
+ manifest: stagedManifest,
188
+ envelope: stagedEnvelope
189
+ )
190
+ } catch {
191
+ // Nothing in this transaction has executed. Remove only the exact
192
+ // broker-generated UUID using the descriptor-relative root primitive.
193
+ do {
194
+ try state.removeTransactionDirectory(id: transactionID)
195
+ } catch {
196
+ throw error
197
+ }
198
+ throw error
199
+ }
200
+ }
201
+
202
+ private func copyDescriptor(
203
+ _ sourceDescriptor: Int32,
204
+ to destination: String,
205
+ maximumBytes: Int64,
206
+ retainData: Bool
207
+ ) throws -> StagedFile {
208
+ var before = stat()
209
+ guard fstat(sourceDescriptor, &before) == 0,
210
+ (before.st_mode & S_IFMT) == S_IFREG,
211
+ before.st_size > 0
212
+ else {
213
+ throw IngestError.invalidSourceDescriptor
214
+ }
215
+ guard before.st_size <= maximumBytes else { throw IngestError.sourceTooLarge }
216
+
217
+ let output = Darwin.open(
218
+ destination,
219
+ O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
220
+ 0o600
221
+ )
222
+ guard output >= 0 else { throw IngestError.couldNotCreateStagingFile }
223
+ var shouldUnlink = true
224
+ defer {
225
+ Darwin.close(output)
226
+ if shouldUnlink { Darwin.unlink(destination) }
227
+ }
228
+ guard DarwinACLValidator.descriptorHasNoExtendedACL(output) else {
229
+ throw IngestError.couldNotCreateStagingFile
230
+ }
231
+
232
+ var hasher = SHA256()
233
+ var captured = retainData ? Data() : nil
234
+ if retainData { captured?.reserveCapacity(Int(before.st_size)) }
235
+ var offset: off_t = 0
236
+ var buffer = [UInt8](repeating: 0, count: 1024 * 1024)
237
+ while offset < before.st_size {
238
+ let requestCount = min(buffer.count, Int(before.st_size - offset))
239
+ let count = buffer.withUnsafeMutableBytes {
240
+ pread(sourceDescriptor, $0.baseAddress, requestCount, offset)
241
+ }
242
+ guard count > 0 else { throw IngestError.sourceChangedDuringCopy }
243
+ let data = Data(buffer.prefix(count))
244
+ hasher.update(data: data)
245
+ captured?.append(data)
246
+ try writeAll(data, to: output)
247
+ offset += off_t(count)
248
+ }
249
+
250
+ var trailingByte: UInt8 = 0
251
+ guard pread(sourceDescriptor, &trailingByte, 1, offset) == 0 else {
252
+ throw IngestError.sourceChangedDuringCopy
253
+ }
254
+ var after = stat()
255
+ guard fstat(sourceDescriptor, &after) == 0,
256
+ before.st_dev == after.st_dev,
257
+ before.st_ino == after.st_ino,
258
+ before.st_size == after.st_size,
259
+ before.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
260
+ before.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
261
+ before.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
262
+ before.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec
263
+ else {
264
+ throw IngestError.sourceChangedDuringCopy
265
+ }
266
+ guard fsync(output) == 0,
267
+ fchmod(output, 0o400) == 0,
268
+ DarwinACLValidator.descriptorHasNoExtendedACL(output),
269
+ fsync(output) == 0
270
+ else {
271
+ throw IngestError.couldNotSync
272
+ }
273
+ let parent = Darwin.open(
274
+ URL(fileURLWithPath: destination).deletingLastPathComponent().path,
275
+ O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW
276
+ )
277
+ guard parent >= 0 else { throw IngestError.couldNotSync }
278
+ defer { Darwin.close(parent) }
279
+ guard DarwinACLValidator.descriptorHasNoExtendedACL(parent),
280
+ fsync(parent) == 0
281
+ else {
282
+ throw IngestError.couldNotSync
283
+ }
284
+ shouldUnlink = false
285
+ return StagedFile(
286
+ path: destination,
287
+ sha256: hasher.finalize().map { String(format: "%02x", $0) }.joined(),
288
+ byteCount: before.st_size,
289
+ data: captured
290
+ )
291
+ }
292
+
293
+ private func writeAll(_ data: Data, to descriptor: Int32) throws {
294
+ try data.withUnsafeBytes { bytes in
295
+ guard let base = bytes.baseAddress else { return }
296
+ var written = 0
297
+ while written < bytes.count {
298
+ let count = Darwin.write(descriptor, base.advanced(by: written), bytes.count - written)
299
+ if count < 0, errno == EINTR { continue }
300
+ guard count > 0 else { throw IngestError.couldNotWrite }
301
+ written += count
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ enum IngestError: Error, CustomStringConvertible {
308
+ case brokerNotRoot
309
+ case unsafeStateRoot
310
+ case couldNotCreateTransaction
311
+ case invalidSourceDescriptor
312
+ case sourceTooLarge
313
+ case sourceChangedDuringCopy
314
+ case couldNotCreateStagingFile
315
+ case couldNotWrite
316
+ case couldNotSync
317
+ case transactionLockUnavailable
318
+ case insufficientStagingCapacity
319
+ case couldNotCleanup
320
+ case unsafeRetainedTransaction
321
+ case transactionQuotaExceeded
322
+
323
+ var description: String {
324
+ switch self {
325
+ case .brokerNotRoot: "The update broker is not running as root"
326
+ case .unsafeStateRoot: "The root-owned update state directory is missing or unsafe"
327
+ case .couldNotCreateTransaction: "Could not create a protected update transaction"
328
+ case .invalidSourceDescriptor: "The update input is not a regular file descriptor"
329
+ case .sourceTooLarge: "The update input exceeds its size limit"
330
+ case .sourceChangedDuringCopy: "The update input changed while it was being copied"
331
+ case .couldNotCreateStagingFile: "Could not create a protected staging file"
332
+ case .couldNotWrite: "Could not write the protected staging file"
333
+ case .couldNotSync: "Could not durably synchronize update state"
334
+ case .transactionLockUnavailable: "The protected update transaction lock is unavailable"
335
+ case .insufficientStagingCapacity: "The protected update volume lacks the required safety reserve"
336
+ case .couldNotCleanup: "Could not remove a protected update transaction"
337
+ case .unsafeRetainedTransaction: "A retained update transaction name is unsafe"
338
+ case .transactionQuotaExceeded: "A protected update transaction is already retained"
339
+ }
340
+ }
341
+ }