@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.
- package/README.md +349 -69
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1919 -98
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1973 -236
- 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
|
+
}
|