@hasna/recordings 0.2.11 → 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 +349 -69
  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 +1919 -98
  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 +1973 -236
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -0,0 +1,229 @@
1
+ import CryptoKit
2
+ import Foundation
3
+
4
+ public struct ReleaseEnvelopePayload: Codable, Equatable, Sendable {
5
+ public let schemaVersion: Int
6
+ public let purpose: String
7
+ public let keyEpoch: UInt64
8
+ public let releaseSequence: UInt64
9
+ public let releaseID: String
10
+ public let version: String
11
+ public let build: String
12
+ public let sourceCommit: String
13
+ public let artifactSHA256: String
14
+ public let artifactByteCount: UInt64
15
+ public let manifestSHA256: String
16
+ public let manifestByteCount: UInt64
17
+ public let candidateTreeSHA256: String
18
+ public let packageSHA256: String
19
+ public let updateClientSHA256: String
20
+ public let updateBrokerSHA256: String
21
+ public let artifactVerifierSHA256: String
22
+ public let bootstrapMarkerSHA256: String
23
+ public let architectures: [String]
24
+ public let minimumOSVersion: String
25
+ public let minimumBrokerVersion: String
26
+ public let signingTeamIdentifier: String
27
+ public let applicationDesignatedRequirement: String
28
+ public let updateClientDesignatedRequirement: String
29
+ public let updateBrokerDesignatedRequirement: String
30
+ public let artifactVerifierDesignatedRequirement: String
31
+ public let installerCertificateSHA256: String
32
+ public let issuedAtUTC: String
33
+ public let expiresAtUTC: String
34
+
35
+ enum CodingKeys: String, CodingKey {
36
+ case schemaVersion = "schema_version"
37
+ case purpose
38
+ case keyEpoch = "key_epoch"
39
+ case releaseSequence = "release_sequence"
40
+ case releaseID = "release_id"
41
+ case version
42
+ case build
43
+ case sourceCommit = "source_commit"
44
+ case artifactSHA256 = "artifact_sha256"
45
+ case artifactByteCount = "artifact_byte_count"
46
+ case manifestSHA256 = "manifest_sha256"
47
+ case manifestByteCount = "manifest_byte_count"
48
+ case candidateTreeSHA256 = "candidate_tree_sha256"
49
+ case packageSHA256 = "package_sha256"
50
+ case updateClientSHA256 = "update_client_sha256"
51
+ case updateBrokerSHA256 = "update_broker_sha256"
52
+ case artifactVerifierSHA256 = "artifact_verifier_sha256"
53
+ case bootstrapMarkerSHA256 = "bootstrap_marker_sha256"
54
+ case architectures
55
+ case minimumOSVersion = "minimum_os_version"
56
+ case minimumBrokerVersion = "minimum_broker_version"
57
+ case signingTeamIdentifier = "signing_team_identifier"
58
+ case applicationDesignatedRequirement = "application_designated_requirement"
59
+ case updateClientDesignatedRequirement = "update_client_designated_requirement"
60
+ case updateBrokerDesignatedRequirement = "update_broker_designated_requirement"
61
+ case artifactVerifierDesignatedRequirement = "artifact_verifier_designated_requirement"
62
+ case installerCertificateSHA256 = "installer_certificate_sha256"
63
+ case issuedAtUTC = "issued_at_utc"
64
+ case expiresAtUTC = "expires_at_utc"
65
+ }
66
+
67
+ public func canonicalData() throws -> Data {
68
+ let encoder = JSONEncoder()
69
+ encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
70
+ return try encoder.encode(self)
71
+ }
72
+
73
+ public func validate(now: Date = Date(), brokerVersion: String = RecordingsUpdateConstants.brokerVersion) throws {
74
+ guard schemaVersion == RecordingsUpdateConstants.protocolVersion else {
75
+ throw ReleaseEnvelopeValidationError.unsupportedSchema
76
+ }
77
+ guard purpose == "bootstrap" || purpose == "update" else {
78
+ throw ReleaseEnvelopeValidationError.invalidField("purpose")
79
+ }
80
+ guard keyEpoch > 0, releaseSequence > 0 else {
81
+ throw ReleaseEnvelopeValidationError.invalidField("release_epoch_or_sequence")
82
+ }
83
+ guard UUID(uuidString: releaseID) != nil else {
84
+ throw ReleaseEnvelopeValidationError.invalidField("release_id")
85
+ }
86
+ guard Self.isVersion(version), Self.isBuild(build), Self.isVersion(minimumBrokerVersion) else {
87
+ throw ReleaseEnvelopeValidationError.invalidField("version")
88
+ }
89
+ guard Self.isLowerHex(sourceCommit, count: 40) else {
90
+ throw ReleaseEnvelopeValidationError.invalidField("source_commit")
91
+ }
92
+ for (name, value) in [
93
+ ("artifact_sha256", artifactSHA256),
94
+ ("manifest_sha256", manifestSHA256),
95
+ ("candidate_tree_sha256", candidateTreeSHA256),
96
+ ("package_sha256", packageSHA256),
97
+ ("update_client_sha256", updateClientSHA256),
98
+ ("update_broker_sha256", updateBrokerSHA256),
99
+ ("artifact_verifier_sha256", artifactVerifierSHA256),
100
+ ("bootstrap_marker_sha256", bootstrapMarkerSHA256),
101
+ ("installer_certificate_sha256", installerCertificateSHA256),
102
+ ] where !Self.isLowerHex(value, count: 64) {
103
+ throw ReleaseEnvelopeValidationError.invalidField(name)
104
+ }
105
+ guard purpose != "bootstrap" || artifactSHA256 == packageSHA256 else {
106
+ throw ReleaseEnvelopeValidationError.invalidField("package_sha256")
107
+ }
108
+ guard artifactByteCount > 0,
109
+ artifactByteCount <= 2 * 1024 * 1024 * 1024,
110
+ manifestByteCount > 0,
111
+ manifestByteCount <= 16 * 1024 * 1024
112
+ else {
113
+ throw ReleaseEnvelopeValidationError.invalidField("release_input_size")
114
+ }
115
+ guard architectures == ["arm64", "x86_64"] else {
116
+ throw ReleaseEnvelopeValidationError.invalidField("architectures")
117
+ }
118
+ guard HostOSVersionPolicy.isValidNumericVersion(minimumOSVersion) else {
119
+ throw ReleaseEnvelopeValidationError.invalidField("minimum_os_version")
120
+ }
121
+ guard signingTeamIdentifier.range(of: #"^[A-Z0-9]{10}$"#, options: .regularExpression) != nil else {
122
+ throw ReleaseEnvelopeValidationError.invalidField("signing_team_identifier")
123
+ }
124
+ for (name, value) in [
125
+ ("application_designated_requirement", applicationDesignatedRequirement),
126
+ ("update_client_designated_requirement", updateClientDesignatedRequirement),
127
+ ("update_broker_designated_requirement", updateBrokerDesignatedRequirement),
128
+ ("artifact_verifier_designated_requirement", artifactVerifierDesignatedRequirement),
129
+ ] where value.isEmpty || value.utf8.count > 8_192 {
130
+ throw ReleaseEnvelopeValidationError.invalidField(name)
131
+ }
132
+ guard Self.compareVersions(brokerVersion, minimumBrokerVersion) >= 0 else {
133
+ throw ReleaseEnvelopeValidationError.brokerTooOld
134
+ }
135
+ guard let issued = Self.parseTimestamp(issuedAtUTC), let expires = Self.parseTimestamp(expiresAtUTC) else {
136
+ throw ReleaseEnvelopeValidationError.invalidField("release_timestamp")
137
+ }
138
+ guard issued <= expires, now >= issued.addingTimeInterval(-300), now <= expires else {
139
+ throw ReleaseEnvelopeValidationError.expiredOrNotYetValid
140
+ }
141
+ guard expires.timeIntervalSince(issued) <= 31 * 24 * 60 * 60 else {
142
+ throw ReleaseEnvelopeValidationError.invalidField("release_validity_window")
143
+ }
144
+ }
145
+
146
+ private static func isLowerHex(_ value: String, count: Int) -> Bool {
147
+ value.utf8.count == count && value.range(of: "^[a-f0-9]{\(count)}$", options: .regularExpression) != nil
148
+ }
149
+
150
+ private static func isVersion(_ value: String) -> Bool {
151
+ value.range(of: #"^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$"#, options: .regularExpression) != nil
152
+ }
153
+
154
+ private static func isBuild(_ value: String) -> Bool {
155
+ guard !value.isEmpty, value.utf8.count <= 32 else { return false }
156
+ return value.range(
157
+ of: #"^[0-9]+(?:\.[0-9]+){0,2}$"#,
158
+ options: .regularExpression
159
+ ) != nil
160
+ }
161
+
162
+ private static func compareVersions(_ lhs: String, _ rhs: String) -> Int {
163
+ let left = lhs.split(separator: "-", maxSplits: 1)[0].split(separator: ".").compactMap { Int($0) }
164
+ let right = rhs.split(separator: "-", maxSplits: 1)[0].split(separator: ".").compactMap { Int($0) }
165
+ guard left.count == 3, right.count == 3 else { return -1 }
166
+ for index in 0..<3 where left[index] != right[index] {
167
+ return left[index] < right[index] ? -1 : 1
168
+ }
169
+ return 0
170
+ }
171
+
172
+ private static func parseTimestamp(_ value: String) -> Date? {
173
+ let formatter = ISO8601DateFormatter()
174
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
175
+ return formatter.date(from: value)
176
+ }
177
+ }
178
+
179
+ public struct SignedReleaseEnvelope: Codable, Sendable {
180
+ public let payload: ReleaseEnvelopePayload
181
+ public let signature: String
182
+
183
+ public init(payload: ReleaseEnvelopePayload, signature: String) {
184
+ self.payload = payload
185
+ self.signature = signature
186
+ }
187
+
188
+ public func verify(publicKeyData: Data, now: Date = Date()) throws -> ReleaseEnvelopePayload {
189
+ try payload.validate(now: now)
190
+ guard publicKeyData.count == 32 else {
191
+ throw ReleaseEnvelopeValidationError.invalidPublicKey
192
+ }
193
+ guard let signatureData = Data(base64Encoded: signature), signatureData.count == 64 else {
194
+ throw ReleaseEnvelopeValidationError.invalidSignatureEncoding
195
+ }
196
+ let publicKey: Curve25519.Signing.PublicKey
197
+ do {
198
+ publicKey = try Curve25519.Signing.PublicKey(rawRepresentation: publicKeyData)
199
+ } catch {
200
+ throw ReleaseEnvelopeValidationError.invalidPublicKey
201
+ }
202
+ guard publicKey.isValidSignature(signatureData, for: try payload.canonicalData()) else {
203
+ throw ReleaseEnvelopeValidationError.invalidSignature
204
+ }
205
+ return payload
206
+ }
207
+ }
208
+
209
+ public enum ReleaseEnvelopeValidationError: Error, CustomStringConvertible {
210
+ case unsupportedSchema
211
+ case invalidField(String)
212
+ case brokerTooOld
213
+ case expiredOrNotYetValid
214
+ case invalidPublicKey
215
+ case invalidSignatureEncoding
216
+ case invalidSignature
217
+
218
+ public var description: String {
219
+ switch self {
220
+ case .unsupportedSchema: "Unsupported release-envelope schema"
221
+ case let .invalidField(name): "Invalid release-envelope field: \(name)"
222
+ case .brokerTooOld: "The installed update broker is too old for this release"
223
+ case .expiredOrNotYetValid: "The release envelope is expired or not yet valid"
224
+ case .invalidPublicKey: "The pinned release-envelope public key is invalid"
225
+ case .invalidSignatureEncoding: "The release-envelope signature encoding is invalid"
226
+ case .invalidSignature: "The release-envelope signature is invalid"
227
+ }
228
+ }
229
+ }
@@ -0,0 +1,119 @@
1
+ import Foundation
2
+
3
+ public enum RecordingsUpdateConstants {
4
+ public static let protocolVersion = 1
5
+ public static let brokerVersion = "1.0.0"
6
+ public static let lifecycle = "bootstrap-v1-app-updates-only"
7
+ public static let rootMaintenanceSupported = false
8
+ public static let keyRotationSupported = false
9
+ public static let machServiceName = "com.hasna.recordings.updater"
10
+ public static let applicationPath = "/Applications/Recordings.app"
11
+ public static let brokerExecutablePath = "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
12
+ public static let stateRoot = "/Library/Application Support/Hasna/Recordings/Updates"
13
+ public static let trustRoot = "/Library/Application Support/Hasna/Recordings/Trust"
14
+ public static let policyPath = trustRoot + "/broker-policy.json"
15
+ public static let envelopePublicKeyDirectory = trustRoot + "/envelope-keys"
16
+ public static let monotonicStateDirectory = "/private/var/db/com.hasna.recordings.updater"
17
+ public static let monotonicStatePath = monotonicStateDirectory + "/release-state.json"
18
+ public static let artifactVerifierPath = "/Library/PrivilegedHelperTools/com.hasna.recordings.artifact-verifier"
19
+ public static let artifactVerifierSandboxProfilePath = trustRoot + "/artifact-verifier.sb"
20
+ public static let bootstrapMarkerPath = trustRoot + "/bootstrap-marker.json"
21
+ public static let artifactVerifierAccount = "_recordingsverify"
22
+ public static let updateClientRelativePath = "Contents/Helpers/recordings-update-client"
23
+ }
24
+
25
+ public enum RecordingsUpdateErrorCode: String, Codable, Sendable {
26
+ case invalidRequest = "invalid_request"
27
+ case unauthorizedPeer = "unauthorized_peer"
28
+ case brokerUnavailable = "broker_unavailable"
29
+ case brokerNotPrivileged = "broker_not_privileged"
30
+ case invalidDescriptor = "invalid_descriptor"
31
+ case artifactTooLarge = "artifact_too_large"
32
+ case invalidEnvelope = "invalid_envelope"
33
+ case signatureRejected = "signature_rejected"
34
+ case artifactMismatch = "artifact_mismatch"
35
+ case rollbackRejected = "rollback_rejected"
36
+ case unsupportedLifecycle = "unsupported_lifecycle"
37
+ case codeIdentityRejected = "code_identity_rejected"
38
+ case candidateRejected = "candidate_rejected"
39
+ case activationFailed = "activation_failed"
40
+ case internalFailure = "internal_failure"
41
+ }
42
+
43
+ public enum RecordingsUpdateReplyKey {
44
+ public static let success = "success"
45
+ public static let code = "code"
46
+ public static let message = "message"
47
+ public static let transactionID = "transaction_id"
48
+ public static let releaseID = "release_id"
49
+ public static let installedVersion = "installed_version"
50
+ public static let lifecycle = "lifecycle"
51
+ public static let rootMaintenanceSupported = "root_maintenance_supported"
52
+ public static let keyRotationSupported = "key_rotation_supported"
53
+ public static let keyEpoch = "key_epoch"
54
+ }
55
+
56
+ public func updateSuccessReply(
57
+ transactionID: String? = nil,
58
+ releaseID: String? = nil,
59
+ installedVersion: String? = nil
60
+ ) -> NSDictionary {
61
+ let reply = NSMutableDictionary()
62
+ reply[RecordingsUpdateReplyKey.success] = true
63
+ reply[RecordingsUpdateReplyKey.code] = "ok"
64
+ reply[RecordingsUpdateReplyKey.message] = "Update completed"
65
+ reply[RecordingsUpdateReplyKey.lifecycle] = RecordingsUpdateConstants.lifecycle
66
+ reply[RecordingsUpdateReplyKey.rootMaintenanceSupported] =
67
+ RecordingsUpdateConstants.rootMaintenanceSupported
68
+ reply[RecordingsUpdateReplyKey.keyRotationSupported] =
69
+ RecordingsUpdateConstants.keyRotationSupported
70
+ if let transactionID { reply[RecordingsUpdateReplyKey.transactionID] = transactionID }
71
+ if let releaseID { reply[RecordingsUpdateReplyKey.releaseID] = releaseID }
72
+ if let installedVersion { reply[RecordingsUpdateReplyKey.installedVersion] = installedVersion }
73
+ return reply
74
+ }
75
+
76
+ public func updateFailureReply(_ code: RecordingsUpdateErrorCode, message: String) -> NSDictionary {
77
+ let reply = NSMutableDictionary()
78
+ reply[RecordingsUpdateReplyKey.success] = false
79
+ reply[RecordingsUpdateReplyKey.code] = code.rawValue
80
+ reply[RecordingsUpdateReplyKey.message] = message
81
+ reply[RecordingsUpdateReplyKey.lifecycle] = RecordingsUpdateConstants.lifecycle
82
+ reply[RecordingsUpdateReplyKey.rootMaintenanceSupported] =
83
+ RecordingsUpdateConstants.rootMaintenanceSupported
84
+ reply[RecordingsUpdateReplyKey.keyRotationSupported] =
85
+ RecordingsUpdateConstants.keyRotationSupported
86
+ return reply
87
+ }
88
+
89
+ @objc(RecordingsUpdateXPCProtocol)
90
+ public protocol RecordingsUpdateXPCProtocol {
91
+ /// File descriptors, not paths, cross the trust boundary. The broker copies every
92
+ /// descriptor into root-owned storage before parsing or executing any content.
93
+ @objc(installWithArchive:manifest:envelope:reply:)
94
+ func install(
95
+ archive: FileHandle,
96
+ manifest: FileHandle,
97
+ envelope: FileHandle,
98
+ withReply reply: @escaping (NSDictionary) -> Void
99
+ )
100
+
101
+ @objc(queryStatusWithReply:)
102
+ func queryStatus(withReply reply: @escaping (NSDictionary) -> Void)
103
+ }
104
+
105
+ /// XPC decoding is intentionally limited to retained file handles on request and
106
+ /// scalar dictionaries on reply. No path-bearing or arbitrary option object is accepted.
107
+ public func makeRecordingsUpdateXPCInterface() -> NSXPCInterface {
108
+ let interface = NSXPCInterface(with: RecordingsUpdateXPCProtocol.self)
109
+ let installSelector = NSSelectorFromString("installWithArchive:manifest:envelope:reply:")
110
+ let statusSelector = NSSelectorFromString("queryStatusWithReply:")
111
+ let fileHandleClasses: Set<AnyHashable> = [FileHandle.self]
112
+ for index in 0..<3 {
113
+ interface.setClasses(fileHandleClasses, for: installSelector, argumentIndex: index, ofReply: false)
114
+ }
115
+ let replyClasses: Set<AnyHashable> = [NSDictionary.self, NSString.self, NSNumber.self]
116
+ interface.setClasses(replyClasses, for: installSelector, argumentIndex: 0, ofReply: true)
117
+ interface.setClasses(replyClasses, for: statusSelector, argumentIndex: 0, ofReply: true)
118
+ return interface
119
+ }
@@ -0,0 +1,60 @@
1
+ import Foundation
2
+ import Testing
3
+ @testable import RecordingsUpdateProtocol
4
+
5
+ struct BoundedProcessRunnerTests {
6
+ @Test("captures small successful output exactly")
7
+ func capturesSmallOutput() throws {
8
+ let result = try BoundedProcessRunner.run(
9
+ executablePath: "/bin/sh",
10
+ arguments: ["-c", "printf '26.0.1\\n'"],
11
+ environment: [:],
12
+ maximumOutputBytes: 64,
13
+ timeout: 1
14
+ )
15
+ #expect(result.exitedNormally)
16
+ #expect(result.terminationStatus == 0)
17
+ #expect(result.standardOutput == Data("26.0.1\n".utf8))
18
+ }
19
+
20
+ @Test("rejects output at the first byte beyond the configured limit")
21
+ func rejectsOversizedOutput() {
22
+ #expect(throws: BoundedProcessError.outputTooLarge) {
23
+ _ = try BoundedProcessRunner.run(
24
+ executablePath: "/bin/sh",
25
+ arguments: ["-c", "printf '12345'"],
26
+ environment: [:],
27
+ maximumOutputBytes: 4,
28
+ timeout: 1
29
+ )
30
+ }
31
+ }
32
+
33
+ @Test("kills a subprocess that exceeds its deadline")
34
+ func rejectsTimedOutProcess() {
35
+ #expect(throws: BoundedProcessError.timedOut) {
36
+ _ = try BoundedProcessRunner.run(
37
+ executablePath: "/bin/sh",
38
+ arguments: ["-c", "exec /bin/sleep 5"],
39
+ environment: [:],
40
+ maximumOutputBytes: 64,
41
+ timeout: 0.05
42
+ )
43
+ }
44
+ }
45
+
46
+ @Test("deadline is not extended by a descendant retaining stdout")
47
+ func rejectsInheritedPipeDescendant() {
48
+ let startedAt = Date()
49
+ #expect(throws: BoundedProcessError.timedOut) {
50
+ _ = try BoundedProcessRunner.run(
51
+ executablePath: "/bin/sh",
52
+ arguments: ["-c", "(/bin/sleep 2; printf inherited) & exit 0"],
53
+ environment: [:],
54
+ maximumOutputBytes: 64,
55
+ timeout: 0.05
56
+ )
57
+ }
58
+ #expect(Date().timeIntervalSince(startedAt) < 1)
59
+ }
60
+ }
@@ -0,0 +1,168 @@
1
+ import Testing
2
+ @testable import RecordingsUpdateProtocol
3
+
4
+ struct CandidateMetadataPolicyTests {
5
+ @Test("exact release application and provenance metadata are admitted")
6
+ func exactMetadataIsAdmitted() throws {
7
+ try validate()
8
+ }
9
+
10
+ @Test("bundle identifier and executable mismatches are rejected")
11
+ func applicationIdentityMismatchIsRejected() {
12
+ #expect(throws: CandidateMetadataPolicyError.applicationIdentifierMismatch) {
13
+ try validate(applicationIdentifier: "com.example.other")
14
+ }
15
+ #expect(throws: CandidateMetadataPolicyError.applicationExecutableMismatch) {
16
+ try validate(executable: "Other")
17
+ }
18
+ }
19
+
20
+ @Test("application and provenance version mismatches are rejected")
21
+ func versionMismatchIsRejected() {
22
+ #expect(throws: CandidateMetadataPolicyError.applicationVersionMismatch) {
23
+ try validate(applicationVersion: "0.2.12")
24
+ }
25
+ #expect(throws: CandidateMetadataPolicyError.provenanceVersionMismatch) {
26
+ try validate(provenanceVersion: "0.2.12")
27
+ }
28
+ }
29
+
30
+ @Test("application and provenance build mismatches are rejected")
31
+ func buildMismatchIsRejected() {
32
+ #expect(throws: CandidateMetadataPolicyError.applicationBuildMismatch) {
33
+ try validate(applicationBuild: "0.2.12")
34
+ }
35
+ #expect(throws: CandidateMetadataPolicyError.provenanceBuildMismatch) {
36
+ try validate(provenanceBuild: "0.2.12")
37
+ }
38
+ }
39
+
40
+ @Test("provenance source and Team ID mismatches are rejected")
41
+ func provenanceIdentityMismatchIsRejected() {
42
+ #expect(throws: CandidateMetadataPolicyError.provenanceSourceMismatch) {
43
+ try validate(sourceCommit: String(repeating: "b", count: 40))
44
+ }
45
+ #expect(throws: CandidateMetadataPolicyError.provenanceTeamMismatch) {
46
+ try validate(teamIdentifier: "OTHERTE123")
47
+ }
48
+ }
49
+
50
+ @Test("application and provenance minimum OS mismatches are rejected")
51
+ func minimumOSMismatchIsRejected() {
52
+ #expect(throws: CandidateMetadataPolicyError.applicationMinimumOSMismatch) {
53
+ try validate(applicationMinimumOS: "25.0")
54
+ }
55
+ #expect(throws: CandidateMetadataPolicyError.provenanceMinimumOSMismatch) {
56
+ try validate(provenanceMinimumOS: "25.0")
57
+ }
58
+ }
59
+
60
+ @Test("actual, provenance, and companion architecture mismatches are rejected")
61
+ func architectureMismatchIsRejected() {
62
+ #expect(throws: CandidateMetadataPolicyError.applicationArchitectureMismatch) {
63
+ try validate(applicationArchitectures: ["arm64"])
64
+ }
65
+ #expect(throws: CandidateMetadataPolicyError.provenanceArchitectureMismatch) {
66
+ try validate(provenanceArchitectures: ["arm64"])
67
+ }
68
+ #expect(throws: CandidateMetadataPolicyError.provenanceArchitectureMismatch) {
69
+ try validate(companionArchitectures: ["arm64"])
70
+ }
71
+ #expect(throws: CandidateMetadataPolicyError.companionArchitectureMismatch) {
72
+ try validate(actualCompanionArchitectures: ["arm64"])
73
+ }
74
+ }
75
+
76
+ @Test("update client must contain exactly the signed universal architectures")
77
+ func updateClientArchitectureMismatchIsRejected() {
78
+ for architectures in [
79
+ ["arm64"],
80
+ ["x86_64"],
81
+ [],
82
+ ["arm64", "x86_64", "i386"],
83
+ ] {
84
+ #expect(throws: CandidateMetadataPolicyError.updateClientArchitectureMismatch) {
85
+ try validate(actualUpdateClientArchitectures: architectures)
86
+ }
87
+ }
88
+ }
89
+
90
+ @Test("measured companion digest and signed companion release version are bound")
91
+ func companionIdentityMismatchIsRejected() {
92
+ #expect(throws: CandidateMetadataPolicyError.companionDigestMismatch) {
93
+ try validate(actualCompanionSHA256: String(repeating: "c", count: 64))
94
+ }
95
+ #expect(throws: CandidateMetadataPolicyError.provenanceCompanionVersionMismatch) {
96
+ try validate(companionVersion: "0.2.12")
97
+ }
98
+ }
99
+
100
+ @Test("local-only provenance fields are rejected from release candidates")
101
+ func localOnlyFieldsAreRejected() {
102
+ #expect(throws: CandidateMetadataPolicyError.localOnlyProvenanceRejected) {
103
+ try validate(containsLocalOnlyFields: true)
104
+ }
105
+ }
106
+
107
+ private func validate(
108
+ applicationIdentifier: String = "com.hasna.recordings",
109
+ applicationVersion: String = "0.2.13",
110
+ applicationBuild: String = "0.2.13",
111
+ executable: String = "Recordings",
112
+ applicationMinimumOS: String = "26.0",
113
+ applicationArchitectures: [String] = ["arm64", "x86_64"],
114
+ actualUpdateClientArchitectures: [String] = ["arm64", "x86_64"],
115
+ provenanceVersion: String = "0.2.13",
116
+ provenanceBuild: String = "0.2.13",
117
+ sourceCommit: String = String(repeating: "a", count: 40),
118
+ teamIdentifier: String = "EXAMPLE123",
119
+ provenanceMinimumOS: String = "26.0",
120
+ provenanceArchitectures: [String] = ["arm64", "x86_64"],
121
+ actualCompanionSHA256: String = String(repeating: "d", count: 64),
122
+ actualCompanionArchitectures: [String] = ["arm64", "x86_64"],
123
+ companionVersion: String = "0.2.13",
124
+ companionSHA256: String = String(repeating: "d", count: 64),
125
+ companionArchitectures: [String] = ["arm64", "x86_64"],
126
+ containsLocalOnlyFields: Bool = false
127
+ ) throws {
128
+ try CandidateMetadataPolicy.validate(
129
+ application: CandidateApplicationMetadata(
130
+ bundleIdentifier: applicationIdentifier,
131
+ shortVersion: applicationVersion,
132
+ buildVersion: applicationBuild,
133
+ executable: executable,
134
+ minimumOSVersion: applicationMinimumOS,
135
+ architectures: applicationArchitectures
136
+ ),
137
+ updateClientArchitectures: actualUpdateClientArchitectures,
138
+ companion: CandidateCompanionMetadata(
139
+ sha256: actualCompanionSHA256,
140
+ architectures: actualCompanionArchitectures
141
+ ),
142
+ provenance: CandidateBuildProvenanceMetadata(
143
+ schemaVersion: 4,
144
+ containsLocalOnlyFields: containsLocalOnlyFields,
145
+ bundleIdentifier: "com.hasna.recordings",
146
+ bundleVersion: provenanceVersion,
147
+ bundleBuildVersion: provenanceBuild,
148
+ sourceCommit: sourceCommit,
149
+ teamIdentifier: teamIdentifier,
150
+ minimumMacOS: provenanceMinimumOS,
151
+ architectures: provenanceArchitectures,
152
+ companionVersion: companionVersion,
153
+ companionSHA256: companionSHA256,
154
+ companionArchitectures: companionArchitectures
155
+ ),
156
+ expected: CandidateReleaseMetadataExpectation(
157
+ applicationIdentifier: "com.hasna.recordings",
158
+ applicationExecutable: "Recordings",
159
+ version: "0.2.13",
160
+ build: "0.2.13",
161
+ sourceCommit: String(repeating: "a", count: 40),
162
+ signingTeamIdentifier: "EXAMPLE123",
163
+ minimumOSVersion: "26.0",
164
+ architectures: ["arm64", "x86_64"]
165
+ )
166
+ )
167
+ }
168
+ }
@@ -0,0 +1,62 @@
1
+ import Testing
2
+ @testable import RecordingsUpdateProtocol
3
+
4
+ struct HostOSVersionPolicyTests {
5
+ @Test("equal minimum and host versions are admitted")
6
+ func equalVersionIsAdmitted() throws {
7
+ try HostOSVersionPolicy.validate(
8
+ candidateMinimumOSVersion: "26.0",
9
+ hostProductVersion: "26.0.0"
10
+ )
11
+ }
12
+
13
+ @Test("an older minimum OS is admitted on a newer host using numeric semantics")
14
+ func olderMinimumIsAdmitted() throws {
15
+ try HostOSVersionPolicy.validate(
16
+ candidateMinimumOSVersion: "15.9.12",
17
+ hostProductVersion: "15.10"
18
+ )
19
+ }
20
+
21
+ @Test("a newer minimum OS is rejected")
22
+ func newerMinimumIsRejected() {
23
+ #expect(throws: HostOSVersionPolicyError.candidateRequiresNewerOS) {
24
+ try HostOSVersionPolicy.validate(
25
+ candidateMinimumOSVersion: "26.1",
26
+ hostProductVersion: "26.0.9"
27
+ )
28
+ }
29
+ }
30
+
31
+ @Test("malformed host ProductVersion evidence is rejected")
32
+ func malformedHostEvidenceIsRejected() {
33
+ for value in ["", "26", "26.beta", "26.0.0.1", "026.0", "26..1"] {
34
+ #expect(throws: HostOSVersionPolicyError.malformedHostProductVersion) {
35
+ try HostOSVersionPolicy.validate(
36
+ candidateMinimumOSVersion: "26.0",
37
+ hostProductVersion: value
38
+ )
39
+ }
40
+ }
41
+ }
42
+
43
+ @Test("malformed signed minimum OS evidence is rejected")
44
+ func malformedCandidateEvidenceIsRejected() {
45
+ #expect(throws: HostOSVersionPolicyError.malformedCandidateMinimumOSVersion) {
46
+ try HostOSVersionPolicy.validate(
47
+ candidateMinimumOSVersion: "26.beta",
48
+ hostProductVersion: "26.0"
49
+ )
50
+ }
51
+ }
52
+
53
+ @Test("unavailable host ProductVersion evidence is rejected")
54
+ func unavailableHostEvidenceIsRejected() {
55
+ #expect(throws: HostOSVersionPolicyError.hostProductVersionUnavailable) {
56
+ try HostOSVersionPolicy.validate(
57
+ candidateMinimumOSVersion: "26.0",
58
+ hostProductVersion: nil
59
+ )
60
+ }
61
+ }
62
+ }