@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,561 @@
1
+ import CryptoKit
2
+ import Darwin
3
+ import Foundation
4
+ import RecordingsUpdateProtocol
5
+ import Security
6
+
7
+ struct CandidateValidationResult: Sendable {
8
+ let treeSHA256: String
9
+ let applicationIdentifier: String
10
+ }
11
+
12
+ /// Cross-runtime release material is ordered by its unsigned UTF-8 bytes.
13
+ ///
14
+ /// Swift's native `String` comparison is normalization-aware, while the
15
+ /// artifact tooling deliberately compares the encoded bytes. Keep this in
16
+ /// lockstep with `compareUnsignedUtf8` in `scripts/macos_artifact.ts`.
17
+ enum CanonicalReleaseOrder {
18
+ static func unsignedUTF8Precedes(_ left: String, _ right: String) -> Bool {
19
+ left.utf8.lexicographicallyPrecedes(right.utf8)
20
+ }
21
+
22
+ static func sorted(_ values: [String]) -> [String] {
23
+ values.sorted(by: unsignedUTF8Precedes)
24
+ }
25
+ }
26
+
27
+ enum ReleaseCodeValidator {
28
+ static func validateProtectedComponents(payload: ReleaseEnvelopePayload) throws {
29
+ try validateRegularDigest(
30
+ path: RecordingsUpdateConstants.brokerExecutablePath,
31
+ expected: payload.updateBrokerSHA256
32
+ )
33
+ try validateStaticCode(
34
+ path: RecordingsUpdateConstants.brokerExecutablePath,
35
+ requirementText: payload.updateBrokerDesignatedRequirement,
36
+ expectedTeam: payload.signingTeamIdentifier
37
+ )
38
+ try validateRegularDigest(
39
+ path: RecordingsUpdateConstants.artifactVerifierPath,
40
+ expected: payload.artifactVerifierSHA256
41
+ )
42
+ try validateStaticCode(
43
+ path: RecordingsUpdateConstants.artifactVerifierPath,
44
+ requirementText: payload.artifactVerifierDesignatedRequirement,
45
+ expectedTeam: payload.signingTeamIdentifier
46
+ )
47
+ }
48
+
49
+ static func validateCandidate(
50
+ at applicationPath: String,
51
+ payload: ReleaseEnvelopePayload,
52
+ policy: BrokerPolicy
53
+ ) throws -> CandidateValidationResult {
54
+ let treeDigest = try CanonicalTree.digest(at: applicationPath)
55
+ guard treeDigest == payload.candidateTreeSHA256 else {
56
+ throw CodeValidationError.treeDigestMismatch
57
+ }
58
+ try validateStaticCode(
59
+ path: applicationPath,
60
+ requirementText: payload.applicationDesignatedRequirement,
61
+ expectedTeam: payload.signingTeamIdentifier
62
+ )
63
+ let clientPath = applicationPath + "/" + RecordingsUpdateConstants.updateClientRelativePath
64
+ try validateRegularDigest(path: clientPath, expected: payload.updateClientSHA256)
65
+ try validateStaticCode(
66
+ path: clientPath,
67
+ requirementText: payload.updateClientDesignatedRequirement,
68
+ expectedTeam: payload.signingTeamIdentifier
69
+ )
70
+ let actualUpdateClientArchitectures = try readArchitectures(clientPath)
71
+
72
+ let informationPath = applicationPath + "/Contents/Info.plist"
73
+ let informationData = try readBoundedRegularFile(path: informationPath, maximumBytes: 1024 * 1024)
74
+ guard let propertyList = try PropertyListSerialization.propertyList(
75
+ from: informationData,
76
+ options: [],
77
+ format: nil
78
+ ) as? [String: Any],
79
+ let identifier = propertyList["CFBundleIdentifier"] as? String,
80
+ let shortVersion = propertyList["CFBundleShortVersionString"] as? String,
81
+ let buildVersion = propertyList["CFBundleVersion"] as? String,
82
+ let executable = propertyList["CFBundleExecutable"] as? String,
83
+ let minimumOS = propertyList["LSMinimumSystemVersion"] as? String
84
+ else {
85
+ throw CodeValidationError.invalidApplicationMetadata
86
+ }
87
+
88
+ let provenancePath =
89
+ applicationPath + "/Contents/Resources/recordings-build-provenance.json"
90
+ let provenanceData = try readBoundedRegularFile(
91
+ path: provenancePath,
92
+ maximumBytes: 64 * 1024
93
+ )
94
+ let provenance: ReleaseBuildProvenance
95
+ let provenanceObject: [String: Any]
96
+ do {
97
+ provenance = try JSONDecoder().decode(ReleaseBuildProvenance.self, from: provenanceData)
98
+ guard let object = try JSONSerialization.jsonObject(with: provenanceData) as? [String: Any]
99
+ else {
100
+ throw CodeValidationError.invalidBuildProvenance
101
+ }
102
+ provenanceObject = object
103
+ } catch {
104
+ throw CodeValidationError.invalidBuildProvenance
105
+ }
106
+ let expectedProvenanceFields: Set<String> = [
107
+ "schema_version",
108
+ "bundle_id",
109
+ "bundle_version",
110
+ "bundle_build_version",
111
+ "git_sha",
112
+ "architectures",
113
+ "team_id",
114
+ "minimum_macos",
115
+ "companion",
116
+ ]
117
+ let expectedCompanionFields: Set<String> = ["version", "sha256", "architectures"]
118
+ guard Set(provenanceObject.keys) == expectedProvenanceFields,
119
+ let companionObject = provenanceObject["companion"] as? [String: Any],
120
+ Set(companionObject.keys) == expectedCompanionFields
121
+ else {
122
+ throw CodeValidationError.invalidBuildProvenance
123
+ }
124
+ let executablePath = applicationPath + "/Contents/MacOS/Recordings"
125
+ let actualArchitectures = try readArchitectures(executablePath)
126
+ let companionPath = applicationPath + "/Contents/Helpers/recordings"
127
+ let actualCompanionSHA256 = try sha256RegularFile(path: companionPath)
128
+ let actualCompanionArchitectures = try readArchitectures(companionPath)
129
+ let localOnlyFieldNames: Set<String> = [
130
+ "artifact_policy",
131
+ "approved_target",
132
+ "approved_target_identity_kind",
133
+ "approved_target_identity_sha256",
134
+ "builder_identity_kind",
135
+ "builder_identity_sha256",
136
+ "non_notarized",
137
+ ]
138
+ do {
139
+ try CandidateMetadataPolicy.validate(
140
+ application: CandidateApplicationMetadata(
141
+ bundleIdentifier: identifier,
142
+ shortVersion: shortVersion,
143
+ buildVersion: buildVersion,
144
+ executable: executable,
145
+ minimumOSVersion: minimumOS,
146
+ architectures: actualArchitectures
147
+ ),
148
+ updateClientArchitectures: actualUpdateClientArchitectures,
149
+ companion: CandidateCompanionMetadata(
150
+ sha256: actualCompanionSHA256,
151
+ architectures: actualCompanionArchitectures
152
+ ),
153
+ provenance: CandidateBuildProvenanceMetadata(
154
+ schemaVersion: provenance.schemaVersion,
155
+ containsLocalOnlyFields:
156
+ !localOnlyFieldNames.isDisjoint(with: Set(provenanceObject.keys)),
157
+ bundleIdentifier: provenance.bundleIdentifier,
158
+ bundleVersion: provenance.bundleVersion,
159
+ bundleBuildVersion: provenance.bundleBuildVersion,
160
+ sourceCommit: provenance.sourceCommit,
161
+ teamIdentifier: provenance.teamIdentifier,
162
+ minimumMacOS: provenance.minimumMacOS,
163
+ architectures: provenance.architectures,
164
+ companionVersion: provenance.companion.version,
165
+ companionSHA256: provenance.companion.sha256,
166
+ companionArchitectures: provenance.companion.architectures
167
+ ),
168
+ expected: CandidateReleaseMetadataExpectation(
169
+ applicationIdentifier: policy.applicationIdentifier,
170
+ applicationExecutable: "Recordings",
171
+ version: payload.version,
172
+ build: payload.build,
173
+ sourceCommit: payload.sourceCommit,
174
+ signingTeamIdentifier: payload.signingTeamIdentifier,
175
+ minimumOSVersion: payload.minimumOSVersion,
176
+ architectures: payload.architectures
177
+ )
178
+ )
179
+ } catch let error as CandidateMetadataPolicyError {
180
+ throw mapMetadataPolicyError(error)
181
+ }
182
+ return CandidateValidationResult(treeSHA256: treeDigest, applicationIdentifier: identifier)
183
+ }
184
+
185
+ static func validateRegularDigest(path: String, expected: String) throws {
186
+ guard try sha256RegularFile(path: path) == expected else {
187
+ throw CodeValidationError.fileDigestMismatch
188
+ }
189
+ }
190
+
191
+ static func sha256RegularFile(
192
+ path: String,
193
+ requireRootOwnedAncestry: Bool = true
194
+ ) throws -> String {
195
+ if requireRootOwnedAncestry {
196
+ let parentDirectory = URL(fileURLWithPath: path).deletingLastPathComponent().path
197
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
198
+ to: parentDirectory
199
+ ) else {
200
+ throw CodeValidationError.unsafeRegularFile
201
+ }
202
+ }
203
+ let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
204
+ guard descriptor >= 0 else { throw CodeValidationError.unsafeRegularFile }
205
+ defer { Darwin.close(descriptor) }
206
+ var before = stat()
207
+ guard fstat(descriptor, &before) == 0,
208
+ (before.st_mode & S_IFMT) == S_IFREG,
209
+ before.st_uid == 0,
210
+ (before.st_mode & 0o022) == 0,
211
+ before.st_nlink == 1,
212
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor),
213
+ before.st_size >= 0
214
+ else {
215
+ throw CodeValidationError.unsafeRegularFile
216
+ }
217
+ var hasher = SHA256()
218
+ var offset: off_t = 0
219
+ var buffer = [UInt8](repeating: 0, count: 1024 * 1024)
220
+ while offset < before.st_size {
221
+ let requested = min(buffer.count, Int(before.st_size - offset))
222
+ let count = buffer.withUnsafeMutableBytes {
223
+ pread(descriptor, $0.baseAddress, requested, offset)
224
+ }
225
+ if count < 0, errno == EINTR { continue }
226
+ guard count > 0 else { throw CodeValidationError.fileChanged }
227
+ hasher.update(data: Data(buffer.prefix(count)))
228
+ offset += off_t(count)
229
+ }
230
+ var after = stat()
231
+ guard fstat(descriptor, &after) == 0,
232
+ before.st_dev == after.st_dev,
233
+ before.st_ino == after.st_ino,
234
+ before.st_size == after.st_size,
235
+ before.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
236
+ before.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
237
+ before.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
238
+ before.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec,
239
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
240
+ else {
241
+ throw CodeValidationError.fileChanged
242
+ }
243
+ return hasher.finalize().map { String(format: "%02x", $0) }.joined()
244
+ }
245
+
246
+ private static func validateStaticCode(
247
+ path: String,
248
+ requirementText: String,
249
+ expectedTeam: String
250
+ ) throws {
251
+ let parentDirectory = URL(fileURLWithPath: path).deletingLastPathComponent().path
252
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
253
+ to: parentDirectory
254
+ ) else {
255
+ throw CodeValidationError.unsafeTreeEntry
256
+ }
257
+ var pathMetadata = stat()
258
+ guard lstat(path, &pathMetadata) == 0,
259
+ DarwinACLValidator.pathHasNoExtendedACL(
260
+ path,
261
+ directory: (pathMetadata.st_mode & S_IFMT) == S_IFDIR
262
+ )
263
+ else {
264
+ throw CodeValidationError.unsafeTreeEntry
265
+ }
266
+ var staticCode: SecStaticCode?
267
+ let createStatus = SecStaticCodeCreateWithPath(
268
+ URL(fileURLWithPath: path) as CFURL,
269
+ [],
270
+ &staticCode
271
+ )
272
+ guard createStatus == errSecSuccess, let staticCode else {
273
+ throw CodeValidationError.codeIdentityUnavailable(createStatus)
274
+ }
275
+ var requirement: SecRequirement?
276
+ let requirementStatus = SecRequirementCreateWithString(
277
+ requirementText as CFString,
278
+ [],
279
+ &requirement
280
+ )
281
+ guard requirementStatus == errSecSuccess, let requirement else {
282
+ throw CodeValidationError.invalidDesignatedRequirement
283
+ }
284
+ let validityStatus = SecStaticCodeCheckValidity(
285
+ staticCode,
286
+ SecCSFlags(rawValue: kSecCSStrictValidate | kSecCSCheckAllArchitectures | kSecCSCheckNestedCode),
287
+ requirement
288
+ )
289
+ guard validityStatus == errSecSuccess else {
290
+ throw CodeValidationError.codeIdentityRejected(validityStatus)
291
+ }
292
+ var information: CFDictionary?
293
+ let informationStatus = SecCodeCopySigningInformation(
294
+ staticCode,
295
+ SecCSFlags(rawValue: kSecCSSigningInformation),
296
+ &information
297
+ )
298
+ guard informationStatus == errSecSuccess,
299
+ let values = information as? [String: Any],
300
+ values[kSecCodeInfoTeamIdentifier as String] as? String == expectedTeam
301
+ else {
302
+ throw CodeValidationError.codeIdentityRejected(informationStatus)
303
+ }
304
+ }
305
+
306
+ private static func readArchitectures(_ path: String) throws -> [String] {
307
+ guard DarwinACLValidator.pathHasNoExtendedACL(path, directory: false) else {
308
+ throw CodeValidationError.architectureCheckFailed
309
+ }
310
+ let result: BoundedProcessResult
311
+ do {
312
+ result = try BoundedProcessRunner.run(
313
+ executablePath: "/usr/bin/lipo",
314
+ arguments: ["-archs", path],
315
+ environment: [
316
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
317
+ "HOME": "/var/empty",
318
+ "TMPDIR": "/tmp",
319
+ ],
320
+ maximumOutputBytes: 4_096,
321
+ timeout: 5
322
+ )
323
+ } catch {
324
+ throw CodeValidationError.architectureCheckFailed
325
+ }
326
+ guard result.exitedNormally,
327
+ result.terminationStatus == 0,
328
+ result.standardOutput.count <= 4_096,
329
+ let text = String(data: result.standardOutput, encoding: .utf8)
330
+ else {
331
+ throw CodeValidationError.architectureCheckFailed
332
+ }
333
+ let actual = text.split(whereSeparator: \.isWhitespace).map(String.init)
334
+ guard !actual.isEmpty, Set(actual).count == actual.count else {
335
+ throw CodeValidationError.architectureCheckFailed
336
+ }
337
+ return CanonicalReleaseOrder.sorted(actual)
338
+ }
339
+
340
+ private static func mapMetadataPolicyError(
341
+ _ error: CandidateMetadataPolicyError
342
+ ) -> CodeValidationError {
343
+ switch error {
344
+ case .applicationIdentifierMismatch,
345
+ .applicationVersionMismatch,
346
+ .applicationBuildMismatch,
347
+ .applicationExecutableMismatch,
348
+ .applicationMinimumOSMismatch,
349
+ .applicationArchitectureMismatch,
350
+ .updateClientArchitectureMismatch,
351
+ .companionArchitectureMismatch:
352
+ return .invalidApplicationMetadata
353
+ case .provenanceSchemaMismatch,
354
+ .localOnlyProvenanceRejected,
355
+ .provenanceIdentifierMismatch,
356
+ .provenanceVersionMismatch,
357
+ .provenanceBuildMismatch,
358
+ .provenanceSourceMismatch,
359
+ .provenanceTeamMismatch,
360
+ .provenanceMinimumOSMismatch,
361
+ .provenanceArchitectureMismatch,
362
+ .companionDigestMismatch,
363
+ .provenanceCompanionVersionMismatch:
364
+ return .invalidBuildProvenance
365
+ }
366
+ }
367
+
368
+ private static func readBoundedRegularFile(path: String, maximumBytes: Int64) throws -> Data {
369
+ let parentDirectory = URL(fileURLWithPath: path).deletingLastPathComponent().path
370
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
371
+ to: parentDirectory
372
+ ) else {
373
+ throw CodeValidationError.unsafeRegularFile
374
+ }
375
+ let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
376
+ guard descriptor >= 0 else { throw CodeValidationError.unsafeRegularFile }
377
+ defer { Darwin.close(descriptor) }
378
+ var metadata = stat()
379
+ guard fstat(descriptor, &metadata) == 0,
380
+ (metadata.st_mode & S_IFMT) == S_IFREG,
381
+ metadata.st_uid == 0,
382
+ (metadata.st_mode & 0o022) == 0,
383
+ metadata.st_nlink == 1,
384
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor),
385
+ metadata.st_size > 0,
386
+ metadata.st_size <= maximumBytes
387
+ else {
388
+ throw CodeValidationError.unsafeRegularFile
389
+ }
390
+ var data = Data(count: Int(metadata.st_size))
391
+ var offset: off_t = 0
392
+ try data.withUnsafeMutableBytes { bytes in
393
+ guard let base = bytes.baseAddress else { return }
394
+ while offset < metadata.st_size {
395
+ let count = pread(descriptor, base.advanced(by: Int(offset)), Int(metadata.st_size - offset), offset)
396
+ if count < 0, errno == EINTR { continue }
397
+ guard count > 0 else { throw CodeValidationError.fileChanged }
398
+ offset += off_t(count)
399
+ }
400
+ }
401
+ var trailingByte: UInt8 = 0
402
+ guard pread(descriptor, &trailingByte, 1, offset) == 0 else {
403
+ throw CodeValidationError.fileChanged
404
+ }
405
+ var after = stat()
406
+ guard fstat(descriptor, &after) == 0,
407
+ metadata.st_dev == after.st_dev,
408
+ metadata.st_ino == after.st_ino,
409
+ metadata.st_mode == after.st_mode,
410
+ metadata.st_uid == after.st_uid,
411
+ metadata.st_gid == after.st_gid,
412
+ metadata.st_nlink == after.st_nlink,
413
+ metadata.st_size == after.st_size,
414
+ metadata.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
415
+ metadata.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
416
+ metadata.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
417
+ metadata.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec,
418
+ DarwinACLValidator.descriptorHasNoExtendedACL(descriptor)
419
+ else {
420
+ throw CodeValidationError.fileChanged
421
+ }
422
+ return data
423
+ }
424
+ }
425
+
426
+ private struct ReleaseBuildProvenance: Codable {
427
+ let schemaVersion: Int
428
+ let bundleIdentifier: String
429
+ let bundleVersion: String
430
+ let bundleBuildVersion: String
431
+ let sourceCommit: String
432
+ let architectures: [String]
433
+ let teamIdentifier: String
434
+ let minimumMacOS: String
435
+ let companion: Companion
436
+
437
+ struct Companion: Codable {
438
+ let version: String
439
+ let sha256: String
440
+ let architectures: [String]
441
+ }
442
+
443
+ enum CodingKeys: String, CodingKey {
444
+ case schemaVersion = "schema_version"
445
+ case bundleIdentifier = "bundle_id"
446
+ case bundleVersion = "bundle_version"
447
+ case bundleBuildVersion = "bundle_build_version"
448
+ case sourceCommit = "git_sha"
449
+ case architectures
450
+ case teamIdentifier = "team_id"
451
+ case minimumMacOS = "minimum_macos"
452
+ case companion
453
+ }
454
+ }
455
+
456
+ enum CanonicalTree {
457
+ static func digest(
458
+ at root: String,
459
+ modeOverrides: [String: UInt16] = [:]
460
+ ) throws -> String {
461
+ let parent = URL(fileURLWithPath: root).deletingLastPathComponent().path
462
+ guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(to: parent) else {
463
+ throw CodeValidationError.unsafeTreeEntry
464
+ }
465
+ var records: [String] = []
466
+ try collectRecords(
467
+ path: root,
468
+ root: root,
469
+ modeOverrides: modeOverrides,
470
+ records: &records
471
+ )
472
+ return SHA256.hash(data: Data(records.joined(separator: "\n").utf8))
473
+ .map { String(format: "%02x", $0) }
474
+ .joined()
475
+ }
476
+
477
+ private static func collectRecords(
478
+ path: String,
479
+ root: String,
480
+ modeOverrides: [String: UInt16],
481
+ records: inout [String]
482
+ ) throws {
483
+ var metadata = stat()
484
+ guard lstat(path, &metadata) == 0 else { throw CodeValidationError.unsafeTreeEntry }
485
+ let relative = path == root ? "." : String(path.dropFirst(root.count + 1))
486
+ let recordedMode: mode_t
487
+ if let modeOverride = modeOverrides[relative] {
488
+ recordedMode = mode_t(modeOverride)
489
+ } else {
490
+ recordedMode = metadata.st_mode & 0o777
491
+ }
492
+ let mode = String(recordedMode, radix: 8)
493
+ switch metadata.st_mode & S_IFMT {
494
+ case S_IFDIR:
495
+ guard metadata.st_uid == 0,
496
+ (metadata.st_mode & 0o022) == 0,
497
+ DarwinACLValidator.pathHasNoExtendedACL(path, directory: true)
498
+ else {
499
+ throw CodeValidationError.unsafeTreeEntry
500
+ }
501
+ records.append("d\0\(relative)\0\(mode)")
502
+ let children = try FileManager.default.contentsOfDirectory(atPath: path)
503
+ for child in CanonicalReleaseOrder.sorted(children) {
504
+ guard child != ".", child != "..", !child.contains("/") else {
505
+ throw CodeValidationError.unsafeTreeEntry
506
+ }
507
+ try collectRecords(
508
+ path: path + "/" + child,
509
+ root: root,
510
+ modeOverrides: modeOverrides,
511
+ records: &records
512
+ )
513
+ }
514
+ case S_IFREG:
515
+ guard metadata.st_uid == 0,
516
+ (metadata.st_mode & 0o022) == 0,
517
+ metadata.st_nlink == 1,
518
+ DarwinACLValidator.pathHasNoExtendedACL(path, directory: false)
519
+ else {
520
+ throw CodeValidationError.unsafeTreeEntry
521
+ }
522
+ let digest = try ReleaseCodeValidator.sha256RegularFile(
523
+ path: path,
524
+ requireRootOwnedAncestry: false
525
+ )
526
+ records.append("f\0\(relative)\0\(mode)\0\(metadata.st_size)\0\(digest)")
527
+ default:
528
+ throw CodeValidationError.unsafeTreeEntry
529
+ }
530
+ }
531
+ }
532
+
533
+ enum CodeValidationError: Error, CustomStringConvertible {
534
+ case unsafeRegularFile
535
+ case fileChanged
536
+ case fileDigestMismatch
537
+ case unsafeTreeEntry
538
+ case treeDigestMismatch
539
+ case invalidApplicationMetadata
540
+ case invalidBuildProvenance
541
+ case architectureCheckFailed
542
+ case invalidDesignatedRequirement
543
+ case codeIdentityUnavailable(OSStatus)
544
+ case codeIdentityRejected(OSStatus)
545
+
546
+ var description: String {
547
+ switch self {
548
+ case .unsafeRegularFile: "A release component is not a safe root-owned regular file"
549
+ case .fileChanged: "A release component changed during validation"
550
+ case .fileDigestMismatch: "A release component digest does not match the signed envelope"
551
+ case .unsafeTreeEntry: "The candidate contains a link, special file, unsafe owner, mode, or hard link"
552
+ case .treeDigestMismatch: "The candidate tree digest does not match the signed envelope"
553
+ case .invalidApplicationMetadata: "The candidate application metadata is invalid"
554
+ case .invalidBuildProvenance: "The candidate build provenance does not match the signed envelope"
555
+ case .architectureCheckFailed: "The candidate application architectures do not match the signed envelope"
556
+ case .invalidDesignatedRequirement: "A signed designated requirement could not be compiled"
557
+ case let .codeIdentityUnavailable(status): "A release code identity is unavailable (\(status))"
558
+ case let .codeIdentityRejected(status): "A release code identity was rejected (\(status))"
559
+ }
560
+ }
561
+ }
@@ -0,0 +1,66 @@
1
+ import Darwin
2
+ import Foundation
3
+ import RecordingsVerifierLauncher
4
+
5
+ enum DarwinACLValidator {
6
+ static func descriptorHasNoExtendedACL(_ descriptor: Int32) -> Bool {
7
+ recordings_descriptor_has_no_extended_acl(descriptor) == 1
8
+ }
9
+
10
+ static func pathHasNoExtendedACL(_ path: String, directory: Bool) -> Bool {
11
+ let flags = O_RDONLY | O_CLOEXEC | O_NOFOLLOW | (directory ? O_DIRECTORY : 0)
12
+ let descriptor = Darwin.open(path, flags)
13
+ guard descriptor >= 0 else { return false }
14
+ defer { Darwin.close(descriptor) }
15
+ return descriptorHasNoExtendedACL(descriptor)
16
+ }
17
+
18
+ static func descriptorIsSafeRootOwnedDirectory(
19
+ _ descriptor: Int32,
20
+ exactPath: String
21
+ ) -> Bool {
22
+ var metadata = stat()
23
+ guard fstat(descriptor, &metadata) == 0,
24
+ (metadata.st_mode & S_IFMT) == S_IFDIR,
25
+ metadata.st_uid == 0,
26
+ descriptorHasNoExtendedACL(descriptor)
27
+ else {
28
+ return false
29
+ }
30
+ let permissions = metadata.st_mode & 0o777
31
+ if (permissions & 0o022) == 0 { return true }
32
+ return exactPath == "/Applications" &&
33
+ metadata.st_gid == 80 &&
34
+ permissions == 0o775
35
+ }
36
+
37
+ static func rootOwnedDirectoryAncestryHasNoExtendedACL(to directoryPath: String) -> Bool {
38
+ let standardized = URL(fileURLWithPath: directoryPath).standardized.path
39
+ guard standardized.hasPrefix("/") else { return false }
40
+ let flags = O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW
41
+ var descriptor = Darwin.open("/", flags)
42
+ guard descriptor >= 0 else { return false }
43
+ defer { Darwin.close(descriptor) }
44
+
45
+ var currentPath = "/"
46
+ guard descriptorIsSafeRootOwnedDirectory(descriptor, exactPath: currentPath) else {
47
+ return false
48
+ }
49
+ let components = URL(fileURLWithPath: standardized).pathComponents.dropFirst()
50
+ for component in components {
51
+ let child = component.withCString { openat(descriptor, $0, flags) }
52
+ currentPath = currentPath == "/"
53
+ ? "/" + component
54
+ : currentPath + "/" + component
55
+ guard child >= 0,
56
+ descriptorIsSafeRootOwnedDirectory(child, exactPath: currentPath)
57
+ else {
58
+ if child >= 0 { Darwin.close(child) }
59
+ return false
60
+ }
61
+ Darwin.close(descriptor)
62
+ descriptor = child
63
+ }
64
+ return true
65
+ }
66
+ }
@@ -0,0 +1,50 @@
1
+ import Foundation
2
+
3
+ enum HostOSProductVersionQueryError: Error {
4
+ case unavailable
5
+ }
6
+
7
+ /// Reads the live macOS ProductVersion without a shell, search path, or inherited
8
+ /// environment. Any launch, exit-status, encoding, size, or shape anomaly fails closed.
9
+ enum HostOSProductVersionReader {
10
+ static func read() throws -> String {
11
+ let result: BoundedProcessResult
12
+ do {
13
+ result = try BoundedProcessRunner.run(
14
+ executablePath: "/usr/bin/sw_vers",
15
+ arguments: ["-productVersion"],
16
+ environment: [
17
+ "PATH": "/usr/bin:/bin:/usr/sbin:/sbin",
18
+ "HOME": "/var/empty",
19
+ "TMPDIR": "/tmp",
20
+ ],
21
+ maximumOutputBytes: 65,
22
+ timeout: 2
23
+ )
24
+ } catch {
25
+ throw HostOSProductVersionQueryError.unavailable
26
+ }
27
+ let data = result.standardOutput
28
+ guard result.exitedNormally,
29
+ result.terminationStatus == 0,
30
+ !data.isEmpty,
31
+ data.count <= 65,
32
+ let text = String(data: data, encoding: .utf8)
33
+ else {
34
+ throw HostOSProductVersionQueryError.unavailable
35
+ }
36
+ let value: String
37
+ if text.hasSuffix("\n") {
38
+ value = String(text.dropLast())
39
+ } else {
40
+ value = text
41
+ }
42
+ guard !value.isEmpty,
43
+ value.utf8.count <= 64,
44
+ !value.contains(where: { $0.isWhitespace || $0.isNewline })
45
+ else {
46
+ throw HostOSProductVersionQueryError.unavailable
47
+ }
48
+ return value
49
+ }
50
+ }