@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,739 @@
1
+ import CryptoKit
2
+ import Darwin
3
+ import Foundation
4
+ import RecordingsUpdateProtocol
5
+ import Security
6
+
7
+ @main
8
+ enum RecordingsBootstrapPreflight {
9
+ static func main() {
10
+ do {
11
+ let arguments = try Arguments.parse(CommandLine.arguments)
12
+ try verify(arguments)
13
+ } catch {
14
+ let message = "recordings-bootstrap-preflight: \(sanitized(error))\n"
15
+ FileHandle.standardError.write(Data(message.utf8))
16
+ Darwin.exit(1)
17
+ }
18
+ }
19
+
20
+ private static func verify(_ arguments: Arguments) throws {
21
+ guard geteuid() == 0 else { throw PreflightError.notRoot }
22
+ let packageDigest = try sha256RegularFile(arguments.packagePath, maximumBytes: 2 * 1024 * 1024 * 1024)
23
+ let packageSize = try regularFileSize(arguments.packagePath, maximumBytes: 2 * 1024 * 1024 * 1024)
24
+ let manifestData = try readRegularFile(arguments.manifestPath, maximumBytes: 16 * 1024 * 1024)
25
+ let envelopeData = try readRegularFile(arguments.envelopePath, maximumBytes: 1024 * 1024)
26
+ let envelope = try JSONDecoder().decode(SignedReleaseEnvelope.self, from: envelopeData)
27
+
28
+ let key = try findSolePublicKey(payloadRoot: arguments.payloadRoot)
29
+ let payload = try envelope.verify(publicKeyData: key.data)
30
+ guard payload.purpose == "bootstrap",
31
+ payload.keyEpoch == key.epoch,
32
+ payload.signingTeamIdentifier == arguments.expectedTeamIdentifier,
33
+ payload.installerCertificateSHA256 == arguments.installerCertificateSHA256,
34
+ payload.packageSHA256 == packageDigest,
35
+ payload.artifactSHA256 == packageDigest,
36
+ payload.artifactByteCount == UInt64(packageSize),
37
+ payload.manifestSHA256 == sha256(manifestData),
38
+ payload.manifestByteCount == UInt64(manifestData.count)
39
+ else {
40
+ throw PreflightError.envelopeBindingMismatch
41
+ }
42
+ do {
43
+ try HostOSVersionPolicy.validate(
44
+ candidateMinimumOSVersion: payload.minimumOSVersion,
45
+ hostProductVersion: try hostProductVersion()
46
+ )
47
+ } catch {
48
+ throw PreflightError.hostCompatibilityMismatch
49
+ }
50
+
51
+ let manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestData)
52
+ try validateManifest(manifest, payload: payload)
53
+
54
+ let app = arguments.payloadRoot + "/Applications/Recordings.app"
55
+ let client = app + "/" + RecordingsUpdateConstants.updateClientRelativePath
56
+ let broker = arguments.payloadRoot + "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
57
+ let artifactVerifier = arguments.payloadRoot
58
+ + "/Library/PrivilegedHelperTools/com.hasna.recordings.artifact-verifier"
59
+ let marker = arguments.payloadRoot
60
+ + "/Library/Application Support/Hasna/Recordings/Trust/bootstrap-marker.json"
61
+
62
+ try requireDigest(client, expected: payload.updateClientSHA256)
63
+ try requireDigest(broker, expected: payload.updateBrokerSHA256)
64
+ try requireDigest(artifactVerifier, expected: payload.artifactVerifierSHA256)
65
+ try requireDigest(marker, expected: payload.bootstrapMarkerSHA256)
66
+ try validateStaticCode(
67
+ path: app,
68
+ requirementText: payload.applicationDesignatedRequirement,
69
+ expectedTeam: payload.signingTeamIdentifier,
70
+ checkNestedCode: true
71
+ )
72
+ try validateStaticCode(
73
+ path: client,
74
+ requirementText: payload.updateClientDesignatedRequirement,
75
+ expectedTeam: payload.signingTeamIdentifier
76
+ )
77
+ try requireArchitectures(client, expected: payload.architectures)
78
+ try validateStaticCode(
79
+ path: broker,
80
+ requirementText: payload.updateBrokerDesignatedRequirement,
81
+ expectedTeam: payload.signingTeamIdentifier
82
+ )
83
+ try validateStaticCode(
84
+ path: artifactVerifier,
85
+ requirementText: payload.artifactVerifierDesignatedRequirement,
86
+ expectedTeam: payload.signingTeamIdentifier
87
+ )
88
+ try validateApplicationMetadata(app: app, manifest: manifest, payload: payload)
89
+ let actualTreeDigest = try treeDigest(using: artifactVerifier, app: app)
90
+ guard actualTreeDigest == payload.candidateTreeSHA256,
91
+ actualTreeDigest == manifest.binding.bundleTreeSHA256
92
+ else {
93
+ throw PreflightError.applicationBindingMismatch
94
+ }
95
+ try validateBootstrapMarker(path: marker, payload: payload)
96
+
97
+ if let statePath = arguments.releaseStatePath {
98
+ try validateReleaseState(
99
+ path: statePath,
100
+ payload: payload,
101
+ requireCommitted: arguments.requireCommittedState
102
+ )
103
+ } else if arguments.requireCommittedState {
104
+ throw PreflightError.releaseStateMismatch
105
+ }
106
+ }
107
+
108
+ private static func findSolePublicKey(payloadRoot: String) throws -> (epoch: UInt64, data: Data) {
109
+ let directory = payloadRoot
110
+ + "/Library/Application Support/Hasna/Recordings/Trust/envelope-keys"
111
+ var metadata = stat()
112
+ guard lstat(directory, &metadata) == 0,
113
+ (metadata.st_mode & S_IFMT) == S_IFDIR,
114
+ metadata.st_uid == 0,
115
+ (metadata.st_mode & 0o022) == 0
116
+ else {
117
+ throw PreflightError.unsafeInput
118
+ }
119
+ let entries = try FileManager.default.contentsOfDirectory(atPath: directory)
120
+ guard entries.count == 1,
121
+ entries[0].hasSuffix(".raw"),
122
+ let epoch = UInt64(entries[0].dropLast(4)),
123
+ epoch > 0
124
+ else {
125
+ throw PreflightError.publicKeyLayoutMismatch
126
+ }
127
+ let data = try readRegularFile(directory + "/" + entries[0], maximumBytes: 32)
128
+ guard data.count == 32 else { throw PreflightError.publicKeyLayoutMismatch }
129
+ return (epoch, data)
130
+ }
131
+
132
+ private static func validateManifest(
133
+ _ manifest: ReleaseManifest,
134
+ payload: ReleaseEnvelopePayload
135
+ ) throws {
136
+ guard manifest.schemaVersion == 4,
137
+ manifest.artifactType == "recordings-macos-app",
138
+ manifest.bundleIdentifier == "com.hasna.recordings",
139
+ manifest.bundleVersion == payload.version,
140
+ manifest.bundleBuildVersion == payload.build,
141
+ manifest.sourceCommit == payload.sourceCommit,
142
+ manifest.teamIdentifier == payload.signingTeamIdentifier,
143
+ manifest.signing.teamIdentifier == payload.signingTeamIdentifier,
144
+ manifest.signing.helperTeamIdentifier == payload.signingTeamIdentifier,
145
+ manifest.architectures == payload.architectures,
146
+ manifest.companion.architectures == payload.architectures,
147
+ manifest.minimumMacOS == payload.minimumOSVersion,
148
+ manifest.binding.bundleTreeSHA256 == payload.candidateTreeSHA256,
149
+ sha256(Data(payload.applicationDesignatedRequirement.utf8))
150
+ == manifest.signing.designatedRequirementSHA256,
151
+ isLowerSHA256(manifest.applicationSHA256),
152
+ isLowerSHA256(manifest.provenanceSHA256),
153
+ isLowerSHA256(manifest.companion.sha256),
154
+ isLowerSHA256(manifest.archive.sha256)
155
+ else {
156
+ throw PreflightError.manifestBindingMismatch
157
+ }
158
+ }
159
+
160
+ private static func validateApplicationMetadata(
161
+ app: String,
162
+ manifest: ReleaseManifest,
163
+ payload: ReleaseEnvelopePayload
164
+ ) throws {
165
+ let informationData = try readRegularFile(app + "/Contents/Info.plist", maximumBytes: 1024 * 1024)
166
+ guard let information = try PropertyListSerialization.propertyList(
167
+ from: informationData,
168
+ options: [],
169
+ format: nil
170
+ ) as? [String: Any],
171
+ information["CFBundleIdentifier"] as? String == manifest.bundleIdentifier,
172
+ information["CFBundleShortVersionString"] as? String == payload.version,
173
+ information["CFBundleVersion"] as? String == payload.build,
174
+ information["LSMinimumSystemVersion"] as? String == payload.minimumOSVersion,
175
+ let executableName = information["CFBundleExecutable"] as? String,
176
+ executableName.range(
177
+ of: #"^[A-Za-z0-9._-]{1,128}$"#,
178
+ options: .regularExpression
179
+ ) != nil
180
+ else {
181
+ throw PreflightError.applicationBindingMismatch
182
+ }
183
+ let executable = app + "/Contents/MacOS/" + executableName
184
+ try requireDigest(executable, expected: manifest.applicationSHA256)
185
+ try requireArchitectures(executable, expected: payload.architectures)
186
+ let companion = app + "/Contents/Helpers/recordings"
187
+ try requireDigest(companion, expected: manifest.companion.sha256)
188
+ try requireArchitectures(companion, expected: payload.architectures)
189
+ let provenance = app + "/Contents/Resources/recordings-build-provenance.json"
190
+ try requireDigest(provenance, expected: manifest.provenanceSHA256)
191
+ }
192
+
193
+ private static func validateBootstrapMarker(
194
+ path: String,
195
+ payload: ReleaseEnvelopePayload
196
+ ) throws {
197
+ let data = try readRegularFile(path, maximumBytes: 1024 * 1024)
198
+ let marker = try JSONDecoder().decode(BootstrapMarker.self, from: data)
199
+ guard marker.schemaVersion == RecordingsUpdateConstants.protocolVersion,
200
+ marker.keyEpoch == payload.keyEpoch,
201
+ marker.releaseSequence == payload.releaseSequence,
202
+ marker.releaseID == payload.releaseID,
203
+ marker.version == payload.version,
204
+ marker.sourceCommit == payload.sourceCommit,
205
+ marker.signingTeamIdentifier == payload.signingTeamIdentifier,
206
+ marker.appTreeSHA256 == payload.candidateTreeSHA256,
207
+ marker.updateClientSHA256 == payload.updateClientSHA256,
208
+ marker.updateBrokerSHA256 == payload.updateBrokerSHA256,
209
+ marker.artifactVerifierSHA256 == payload.artifactVerifierSHA256,
210
+ marker.lifecycle == RecordingsUpdateConstants.lifecycle,
211
+ marker.rootMaintenanceSupported == RecordingsUpdateConstants.rootMaintenanceSupported,
212
+ marker.keyRotationSupported == RecordingsUpdateConstants.keyRotationSupported
213
+ else {
214
+ throw PreflightError.bootstrapMarkerMismatch
215
+ }
216
+ }
217
+
218
+ private static func validateReleaseState(
219
+ path: String,
220
+ payload: ReleaseEnvelopePayload,
221
+ requireCommitted: Bool
222
+ ) throws {
223
+ let data = try readRegularFile(path, maximumBytes: 64 * 1024, requireOwnerOnly: true)
224
+ let state = try JSONDecoder().decode(BootstrapReleaseState.self, from: data)
225
+ let payloadDigest = sha256(try payload.canonicalData())
226
+ guard state.schemaVersion == RecordingsUpdateConstants.protocolVersion,
227
+ state.purpose == "bootstrap",
228
+ (state.phase == "seen" || state.phase == "committed"),
229
+ (!requireCommitted || state.phase == "committed"),
230
+ state.keyEpoch == payload.keyEpoch,
231
+ state.releaseSequence == payload.releaseSequence,
232
+ state.releaseID == payload.releaseID,
233
+ state.version == payload.version,
234
+ state.build == payload.build,
235
+ state.cohortPackageSHA256 == payload.packageSHA256,
236
+ state.envelopePayloadSHA256 == payloadDigest
237
+ else {
238
+ throw PreflightError.releaseStateMismatch
239
+ }
240
+ }
241
+
242
+ private static func validateStaticCode(
243
+ path: String,
244
+ requirementText: String,
245
+ expectedTeam: String,
246
+ checkNestedCode: Bool = false
247
+ ) throws {
248
+ var staticCode: SecStaticCode?
249
+ let createStatus = SecStaticCodeCreateWithPath(URL(fileURLWithPath: path) as CFURL, [], &staticCode)
250
+ guard createStatus == errSecSuccess, let staticCode else {
251
+ throw PreflightError.codeIdentityRejected
252
+ }
253
+ var requirement: SecRequirement?
254
+ let requirementStatus = SecRequirementCreateWithString(requirementText as CFString, [], &requirement)
255
+ guard requirementStatus == errSecSuccess, let requirement else {
256
+ throw PreflightError.codeIdentityRejected
257
+ }
258
+ var rawFlags = kSecCSStrictValidate | kSecCSCheckAllArchitectures
259
+ if checkNestedCode { rawFlags |= kSecCSCheckNestedCode }
260
+ let validityStatus = SecStaticCodeCheckValidity(
261
+ staticCode,
262
+ SecCSFlags(rawValue: rawFlags),
263
+ requirement
264
+ )
265
+ guard validityStatus == errSecSuccess else { throw PreflightError.codeIdentityRejected }
266
+ var information: CFDictionary?
267
+ let informationStatus = SecCodeCopySigningInformation(
268
+ staticCode,
269
+ SecCSFlags(rawValue: kSecCSSigningInformation),
270
+ &information
271
+ )
272
+ guard informationStatus == errSecSuccess,
273
+ let values = information as? [String: Any],
274
+ values[kSecCodeInfoTeamIdentifier as String] as? String == expectedTeam
275
+ else {
276
+ throw PreflightError.codeIdentityRejected
277
+ }
278
+ }
279
+
280
+ private static func treeDigest(using verifier: String, app: String) throws -> String {
281
+ let result: BoundedProcessResult
282
+ do {
283
+ result = try BoundedProcessRunner.run(
284
+ executablePath: verifier,
285
+ arguments: ["tree-digest", "--path", app],
286
+ environment: [
287
+ "HOME": "/var/empty",
288
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
289
+ "TMPDIR": "/private/var/empty",
290
+ "LC_ALL": "C",
291
+ "LANG": "C",
292
+ "TZ": "UTC0",
293
+ ],
294
+ maximumOutputBytes: 4_096,
295
+ timeout: 10
296
+ )
297
+ } catch {
298
+ throw PreflightError.treeDigestFailed
299
+ }
300
+ guard result.exitedNormally,
301
+ result.terminationStatus == 0,
302
+ let value = String(data: result.standardOutput, encoding: .utf8)?
303
+ .trimmingCharacters(in: .whitespacesAndNewlines),
304
+ isLowerSHA256(value)
305
+ else {
306
+ throw PreflightError.treeDigestFailed
307
+ }
308
+ return value
309
+ }
310
+
311
+ private static func requireArchitectures(_ path: String, expected: [String]) throws {
312
+ let result: BoundedProcessResult
313
+ do {
314
+ result = try BoundedProcessRunner.run(
315
+ executablePath: "/usr/bin/lipo",
316
+ arguments: ["-archs", path],
317
+ environment: [
318
+ "HOME": "/var/empty",
319
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
320
+ "TMPDIR": "/private/var/empty",
321
+ ],
322
+ maximumOutputBytes: 4_096,
323
+ timeout: 5
324
+ )
325
+ } catch {
326
+ throw PreflightError.architectureMismatch
327
+ }
328
+ let actual = String(data: result.standardOutput, encoding: .utf8)?
329
+ .split(whereSeparator: \.isWhitespace)
330
+ .map(String.init) ?? []
331
+ guard result.exitedNormally,
332
+ result.terminationStatus == 0,
333
+ !actual.isEmpty,
334
+ Set(actual).count == actual.count,
335
+ actual.sorted(by: { $0.utf8.lexicographicallyPrecedes($1.utf8) }) == expected
336
+ else {
337
+ throw PreflightError.architectureMismatch
338
+ }
339
+ }
340
+
341
+ private static func hostProductVersion() throws -> String {
342
+ let result = try BoundedProcessRunner.run(
343
+ executablePath: "/usr/bin/sw_vers",
344
+ arguments: ["-productVersion"],
345
+ environment: [
346
+ "HOME": "/var/empty",
347
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
348
+ "TMPDIR": "/private/var/empty",
349
+ ],
350
+ maximumOutputBytes: 65,
351
+ timeout: 2
352
+ )
353
+ guard result.exitedNormally,
354
+ result.terminationStatus == 0,
355
+ !result.standardOutput.isEmpty,
356
+ result.standardOutput.count <= 65,
357
+ let text = String(data: result.standardOutput, encoding: .utf8)
358
+ else {
359
+ throw PreflightError.hostCompatibilityMismatch
360
+ }
361
+ let value = text.hasSuffix("\n") ? String(text.dropLast()) : text
362
+ guard !value.isEmpty,
363
+ value.utf8.count <= 64,
364
+ !value.contains(where: { $0.isWhitespace || $0.isNewline })
365
+ else {
366
+ throw PreflightError.hostCompatibilityMismatch
367
+ }
368
+ return value
369
+ }
370
+
371
+ private static func requireDigest(_ path: String, expected: String) throws {
372
+ guard try sha256RegularFile(path, maximumBytes: 2 * 1024 * 1024 * 1024) == expected else {
373
+ throw PreflightError.fileDigestMismatch
374
+ }
375
+ }
376
+
377
+ private static func sha256RegularFile(_ path: String, maximumBytes: Int64) throws -> String {
378
+ let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
379
+ guard descriptor >= 0 else { throw PreflightError.unsafeInput }
380
+ defer { Darwin.close(descriptor) }
381
+ var before = stat()
382
+ guard fstat(descriptor, &before) == 0,
383
+ (before.st_mode & S_IFMT) == S_IFREG,
384
+ before.st_uid == 0,
385
+ (before.st_mode & 0o022) == 0,
386
+ before.st_nlink == 1,
387
+ before.st_size > 0,
388
+ before.st_size <= maximumBytes
389
+ else {
390
+ throw PreflightError.unsafeInput
391
+ }
392
+ var hasher = SHA256()
393
+ var offset: off_t = 0
394
+ var buffer = [UInt8](repeating: 0, count: 1024 * 1024)
395
+ while offset < before.st_size {
396
+ let requested = min(buffer.count, Int(before.st_size - offset))
397
+ let count = buffer.withUnsafeMutableBytes {
398
+ pread(descriptor, $0.baseAddress, requested, offset)
399
+ }
400
+ if count < 0, errno == EINTR { continue }
401
+ guard count > 0 else { throw PreflightError.inputChanged }
402
+ hasher.update(data: Data(buffer.prefix(count)))
403
+ offset += off_t(count)
404
+ }
405
+ var extra: UInt8 = 0
406
+ guard pread(descriptor, &extra, 1, before.st_size) == 0 else {
407
+ throw PreflightError.inputChanged
408
+ }
409
+ var after = stat()
410
+ guard fstat(descriptor, &after) == 0,
411
+ before.st_dev == after.st_dev,
412
+ before.st_ino == after.st_ino,
413
+ before.st_size == after.st_size,
414
+ before.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
415
+ before.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
416
+ before.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
417
+ before.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec
418
+ else {
419
+ throw PreflightError.inputChanged
420
+ }
421
+ return hasher.finalize().map { String(format: "%02x", $0) }.joined()
422
+ }
423
+
424
+ private static func regularFileSize(_ path: String, maximumBytes: Int64) throws -> Int64 {
425
+ let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
426
+ guard descriptor >= 0 else { throw PreflightError.unsafeInput }
427
+ defer { Darwin.close(descriptor) }
428
+ var metadata = stat()
429
+ guard fstat(descriptor, &metadata) == 0,
430
+ (metadata.st_mode & S_IFMT) == S_IFREG,
431
+ metadata.st_uid == 0,
432
+ (metadata.st_mode & 0o022) == 0,
433
+ metadata.st_nlink == 1,
434
+ metadata.st_size > 0,
435
+ metadata.st_size <= maximumBytes
436
+ else {
437
+ throw PreflightError.unsafeInput
438
+ }
439
+ return metadata.st_size
440
+ }
441
+
442
+ private static func readRegularFile(
443
+ _ path: String,
444
+ maximumBytes: Int64,
445
+ requireOwnerOnly: Bool = false
446
+ ) throws -> Data {
447
+ let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
448
+ guard descriptor >= 0 else { throw PreflightError.unsafeInput }
449
+ defer { Darwin.close(descriptor) }
450
+ var before = stat()
451
+ guard fstat(descriptor, &before) == 0,
452
+ (before.st_mode & S_IFMT) == S_IFREG,
453
+ before.st_uid == 0,
454
+ (before.st_mode & (requireOwnerOnly ? 0o077 : 0o022)) == 0,
455
+ before.st_nlink == 1,
456
+ before.st_size > 0,
457
+ before.st_size <= maximumBytes
458
+ else {
459
+ throw PreflightError.unsafeInput
460
+ }
461
+ var data = Data(count: Int(before.st_size))
462
+ var offset: off_t = 0
463
+ try data.withUnsafeMutableBytes { bytes in
464
+ guard let base = bytes.baseAddress else { return }
465
+ while offset < before.st_size {
466
+ let count = pread(
467
+ descriptor,
468
+ base.advanced(by: Int(offset)),
469
+ Int(before.st_size - offset),
470
+ offset
471
+ )
472
+ if count < 0, errno == EINTR { continue }
473
+ guard count > 0 else { throw PreflightError.inputChanged }
474
+ offset += off_t(count)
475
+ }
476
+ }
477
+ var extra: UInt8 = 0
478
+ guard pread(descriptor, &extra, 1, before.st_size) == 0 else {
479
+ throw PreflightError.inputChanged
480
+ }
481
+ var after = stat()
482
+ guard fstat(descriptor, &after) == 0,
483
+ before.st_dev == after.st_dev,
484
+ before.st_ino == after.st_ino,
485
+ before.st_size == after.st_size,
486
+ before.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
487
+ before.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
488
+ before.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
489
+ before.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec
490
+ else {
491
+ throw PreflightError.inputChanged
492
+ }
493
+ return data
494
+ }
495
+
496
+ private static func sha256(_ data: Data) -> String {
497
+ SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
498
+ }
499
+
500
+ private static func isLowerSHA256(_ value: String) -> Bool {
501
+ value.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil
502
+ }
503
+
504
+ private static func sanitized(_ error: Error) -> String {
505
+ if let error = error as? PreflightError { return error.description }
506
+ if let error = error as? ReleaseEnvelopeValidationError { return error.description }
507
+ return "verification failed"
508
+ }
509
+ }
510
+
511
+ private struct Arguments {
512
+ let packagePath: String
513
+ let manifestPath: String
514
+ let envelopePath: String
515
+ let payloadRoot: String
516
+ let expectedTeamIdentifier: String
517
+ let installerCertificateSHA256: String
518
+ let releaseStatePath: String?
519
+ let requireCommittedState: Bool
520
+
521
+ static func parse(_ values: [String]) throws -> Arguments {
522
+ var parsed: [String: String] = [:]
523
+ var requireCommittedState = false
524
+ var index = 1
525
+ while index < values.count {
526
+ if values[index] == "--require-committed-state" {
527
+ guard !requireCommittedState else { throw PreflightError.usage }
528
+ requireCommittedState = true
529
+ index += 1
530
+ continue
531
+ }
532
+ guard index + 1 < values.count,
533
+ values[index].hasPrefix("--"),
534
+ parsed[values[index]] == nil
535
+ else {
536
+ throw PreflightError.usage
537
+ }
538
+ parsed[values[index]] = values[index + 1]
539
+ index += 2
540
+ }
541
+ let required = [
542
+ "--package",
543
+ "--manifest",
544
+ "--envelope",
545
+ "--payload-root",
546
+ "--expected-team-id",
547
+ "--installer-certificate-sha256",
548
+ ]
549
+ let requiredPaths = ["--package", "--manifest", "--envelope", "--payload-root"]
550
+ guard required.allSatisfy({ parsed[$0] != nil }),
551
+ requiredPaths.allSatisfy({ parsed[$0]?.hasPrefix("/") == true }),
552
+ let packagePath = parsed["--package"],
553
+ let manifestPath = parsed["--manifest"],
554
+ let envelopePath = parsed["--envelope"],
555
+ let payloadRoot = parsed["--payload-root"],
556
+ let expectedTeamIdentifier = parsed["--expected-team-id"],
557
+ let installerCertificateSHA256 = parsed["--installer-certificate-sha256"],
558
+ expectedTeamIdentifier.range(
559
+ of: "^[A-Z0-9]{10}$",
560
+ options: .regularExpression
561
+ ) != nil,
562
+ installerCertificateSHA256.range(
563
+ of: "^[a-f0-9]{64}$",
564
+ options: .regularExpression
565
+ ) != nil,
566
+ Set(parsed.keys).isSubset(of: Set(required + ["--release-state"])),
567
+ parsed["--release-state"] == nil || parsed["--release-state"]?.hasPrefix("/") == true
568
+ else {
569
+ throw PreflightError.usage
570
+ }
571
+ return Arguments(
572
+ packagePath: packagePath,
573
+ manifestPath: manifestPath,
574
+ envelopePath: envelopePath,
575
+ payloadRoot: payloadRoot,
576
+ expectedTeamIdentifier: expectedTeamIdentifier,
577
+ installerCertificateSHA256: installerCertificateSHA256,
578
+ releaseStatePath: parsed["--release-state"],
579
+ requireCommittedState: requireCommittedState
580
+ )
581
+ }
582
+ }
583
+
584
+ private struct ReleaseManifest: Codable {
585
+ let schemaVersion: Int
586
+ let artifactType: String
587
+ let bundleIdentifier: String
588
+ let bundleVersion: String
589
+ let bundleBuildVersion: String
590
+ let sourceCommit: String
591
+ let architectures: [String]
592
+ let teamIdentifier: String
593
+ let minimumMacOS: String
594
+ let applicationSHA256: String
595
+ let binding: Binding
596
+ let provenanceSHA256: String
597
+ let signing: Signing
598
+ let companion: Companion
599
+ let archive: Archive
600
+
601
+ enum CodingKeys: String, CodingKey {
602
+ case schemaVersion = "schema_version"
603
+ case artifactType = "artifact_type"
604
+ case bundleIdentifier = "bundle_id"
605
+ case bundleVersion = "bundle_version"
606
+ case bundleBuildVersion = "bundle_build_version"
607
+ case sourceCommit = "git_sha"
608
+ case architectures
609
+ case teamIdentifier = "team_id"
610
+ case minimumMacOS = "minimum_macos"
611
+ case applicationSHA256 = "app_sha256"
612
+ case binding
613
+ case provenanceSHA256 = "provenance_sha256"
614
+ case signing
615
+ case companion
616
+ case archive
617
+ }
618
+
619
+ struct Binding: Codable {
620
+ let bundleTreeSHA256: String
621
+ enum CodingKeys: String, CodingKey { case bundleTreeSHA256 = "bundle_tree_sha256" }
622
+ }
623
+
624
+ struct Signing: Codable {
625
+ let teamIdentifier: String
626
+ let helperTeamIdentifier: String
627
+ let designatedRequirementSHA256: String
628
+ enum CodingKeys: String, CodingKey {
629
+ case teamIdentifier = "team_id"
630
+ case helperTeamIdentifier = "helper_team_id"
631
+ case designatedRequirementSHA256 = "designated_requirement_sha256"
632
+ }
633
+ }
634
+
635
+ struct Companion: Codable {
636
+ let sha256: String
637
+ let architectures: [String]
638
+ }
639
+
640
+ struct Archive: Codable { let sha256: String }
641
+ }
642
+
643
+ private struct BootstrapMarker: Codable {
644
+ let schemaVersion: Int
645
+ let keyEpoch: UInt64
646
+ let releaseSequence: UInt64
647
+ let releaseID: String
648
+ let version: String
649
+ let sourceCommit: String
650
+ let signingTeamIdentifier: String
651
+ let appTreeSHA256: String
652
+ let updateClientSHA256: String
653
+ let updateBrokerSHA256: String
654
+ let artifactVerifierSHA256: String
655
+ let lifecycle: String
656
+ let rootMaintenanceSupported: Bool
657
+ let keyRotationSupported: Bool
658
+
659
+ enum CodingKeys: String, CodingKey {
660
+ case schemaVersion = "schema_version"
661
+ case keyEpoch = "key_epoch"
662
+ case releaseSequence = "release_sequence"
663
+ case releaseID = "release_id"
664
+ case version
665
+ case sourceCommit = "source_commit"
666
+ case signingTeamIdentifier = "signing_team_identifier"
667
+ case appTreeSHA256 = "app_tree_sha256"
668
+ case updateClientSHA256 = "update_client_sha256"
669
+ case updateBrokerSHA256 = "update_broker_sha256"
670
+ case artifactVerifierSHA256 = "artifact_verifier_sha256"
671
+ case lifecycle
672
+ case rootMaintenanceSupported = "root_maintenance_supported"
673
+ case keyRotationSupported = "key_rotation_supported"
674
+ }
675
+ }
676
+
677
+ private struct BootstrapReleaseState: Codable {
678
+ let schemaVersion: Int
679
+ let purpose: String
680
+ let phase: String
681
+ let keyEpoch: UInt64
682
+ let releaseSequence: UInt64
683
+ let releaseID: String
684
+ let version: String
685
+ let build: String
686
+ let cohortPackageSHA256: String
687
+ let envelopePayloadSHA256: String
688
+
689
+ enum CodingKeys: String, CodingKey {
690
+ case schemaVersion = "schema_version"
691
+ case purpose
692
+ case phase
693
+ case keyEpoch = "key_epoch"
694
+ case releaseSequence = "release_sequence"
695
+ case releaseID = "release_id"
696
+ case version
697
+ case build
698
+ case cohortPackageSHA256 = "cohort_package_sha256"
699
+ case envelopePayloadSHA256 = "envelope_payload_sha256"
700
+ }
701
+ }
702
+
703
+ private enum PreflightError: Error, CustomStringConvertible {
704
+ case usage
705
+ case notRoot
706
+ case unsafeInput
707
+ case inputChanged
708
+ case publicKeyLayoutMismatch
709
+ case envelopeBindingMismatch
710
+ case manifestBindingMismatch
711
+ case fileDigestMismatch
712
+ case codeIdentityRejected
713
+ case architectureMismatch
714
+ case hostCompatibilityMismatch
715
+ case treeDigestFailed
716
+ case applicationBindingMismatch
717
+ case bootstrapMarkerMismatch
718
+ case releaseStateMismatch
719
+
720
+ var description: String {
721
+ switch self {
722
+ case .usage: "invalid arguments"
723
+ case .notRoot: "preflight verification must run as root"
724
+ case .unsafeInput: "an input is missing, mutable, linked, or otherwise unsafe"
725
+ case .inputChanged: "an input changed while it was verified"
726
+ case .publicKeyLayoutMismatch: "the package does not contain one exact bootstrap public key"
727
+ case .envelopeBindingMismatch: "the signed bootstrap envelope does not bind the staged inputs"
728
+ case .manifestBindingMismatch: "the signed bootstrap envelope and app manifest disagree"
729
+ case .fileDigestMismatch: "a protected package component has the wrong digest"
730
+ case .codeIdentityRejected: "a protected package component has the wrong code identity"
731
+ case .architectureMismatch: "a protected package component has the wrong architectures"
732
+ case .hostCompatibilityMismatch: "the live host does not satisfy the signed minimum macOS version"
733
+ case .treeDigestFailed: "the packaged application tree could not be authenticated"
734
+ case .applicationBindingMismatch: "the packaged application does not match the signed release"
735
+ case .bootstrapMarkerMismatch: "the package bootstrap marker does not match the signed release"
736
+ case .releaseStateMismatch: "the installed bootstrap state conflicts with the signed release"
737
+ }
738
+ }
739
+ }