@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.
- package/README.md +350 -35
- 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 +1922 -77
- 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 +1990 -29
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import Darwin
|
|
2
|
+
import Foundation
|
|
3
|
+
import RecordingsUpdateProtocol
|
|
4
|
+
|
|
5
|
+
enum InstallJournalOperation: String, Codable, Sendable {
|
|
6
|
+
case applicationActivation = "application-activation"
|
|
7
|
+
case bootstrapCommit = "bootstrap-commit"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
struct BrokerInstallJournal: Codable, Sendable {
|
|
11
|
+
let schemaVersion: Int
|
|
12
|
+
let transactionID: String
|
|
13
|
+
var phase: String
|
|
14
|
+
let releaseID: String
|
|
15
|
+
let releaseSequence: UInt64
|
|
16
|
+
let keyEpoch: UInt64
|
|
17
|
+
let envelopePayloadSHA256: String
|
|
18
|
+
let artifactSHA256: String
|
|
19
|
+
let manifestSHA256: String
|
|
20
|
+
let cohortPackageSHA256: String
|
|
21
|
+
let candidateTreeSHA256: String
|
|
22
|
+
let minimumOSVersion: String?
|
|
23
|
+
let candidateApplicationMode: UInt16
|
|
24
|
+
let candidateExecutableModes: [String: UInt16]
|
|
25
|
+
let previousTreeSHA256: String?
|
|
26
|
+
let previousApplicationMode: UInt16?
|
|
27
|
+
let previousExecutableModes: [String: UInt16]?
|
|
28
|
+
let previousApplicationPath: String?
|
|
29
|
+
let candidateApplicationPath: String
|
|
30
|
+
let applicationPath: String
|
|
31
|
+
/// Optional for schema-v1 compatibility. Every journal written before this
|
|
32
|
+
/// discriminator existed represented an ordinary application activation.
|
|
33
|
+
let operation: InstallJournalOperation?
|
|
34
|
+
/// Bootstrap journals bind the exact protected transaction path rather than
|
|
35
|
+
/// relying only on a UUID-derived reconstruction during recovery.
|
|
36
|
+
let transactionDirectory: String?
|
|
37
|
+
/// A bootstrap advance is legal only from an absent monotonic state. Recording
|
|
38
|
+
/// that fact before `seen` makes the one-time transition explicit and recoverable.
|
|
39
|
+
let bootstrapPriorMonotonicState: String?
|
|
40
|
+
|
|
41
|
+
var resolvedOperation: InstallJournalOperation {
|
|
42
|
+
journalOperation
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var expectedPurpose: String {
|
|
46
|
+
journalOperation == .bootstrapCommit ? "bootstrap" : "update"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private var journalOperation: InstallJournalOperation {
|
|
50
|
+
operation ?? .applicationActivation
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
init(
|
|
54
|
+
schemaVersion: Int,
|
|
55
|
+
transactionID: String,
|
|
56
|
+
phase: String,
|
|
57
|
+
releaseID: String,
|
|
58
|
+
releaseSequence: UInt64,
|
|
59
|
+
keyEpoch: UInt64,
|
|
60
|
+
envelopePayloadSHA256: String,
|
|
61
|
+
artifactSHA256: String,
|
|
62
|
+
manifestSHA256: String,
|
|
63
|
+
cohortPackageSHA256: String,
|
|
64
|
+
candidateTreeSHA256: String,
|
|
65
|
+
minimumOSVersion: String? = nil,
|
|
66
|
+
candidateApplicationMode: UInt16,
|
|
67
|
+
candidateExecutableModes: [String: UInt16],
|
|
68
|
+
previousTreeSHA256: String?,
|
|
69
|
+
previousApplicationMode: UInt16?,
|
|
70
|
+
previousExecutableModes: [String: UInt16]?,
|
|
71
|
+
previousApplicationPath: String?,
|
|
72
|
+
candidateApplicationPath: String,
|
|
73
|
+
applicationPath: String,
|
|
74
|
+
operation: InstallJournalOperation? = nil,
|
|
75
|
+
transactionDirectory: String? = nil,
|
|
76
|
+
bootstrapPriorMonotonicState: String? = nil
|
|
77
|
+
) {
|
|
78
|
+
self.schemaVersion = schemaVersion
|
|
79
|
+
self.transactionID = transactionID
|
|
80
|
+
self.phase = phase
|
|
81
|
+
self.releaseID = releaseID
|
|
82
|
+
self.releaseSequence = releaseSequence
|
|
83
|
+
self.keyEpoch = keyEpoch
|
|
84
|
+
self.envelopePayloadSHA256 = envelopePayloadSHA256
|
|
85
|
+
self.artifactSHA256 = artifactSHA256
|
|
86
|
+
self.manifestSHA256 = manifestSHA256
|
|
87
|
+
self.cohortPackageSHA256 = cohortPackageSHA256
|
|
88
|
+
self.candidateTreeSHA256 = candidateTreeSHA256
|
|
89
|
+
self.minimumOSVersion = minimumOSVersion
|
|
90
|
+
self.candidateApplicationMode = candidateApplicationMode
|
|
91
|
+
self.candidateExecutableModes = candidateExecutableModes
|
|
92
|
+
self.previousTreeSHA256 = previousTreeSHA256
|
|
93
|
+
self.previousApplicationMode = previousApplicationMode
|
|
94
|
+
self.previousExecutableModes = previousExecutableModes
|
|
95
|
+
self.previousApplicationPath = previousApplicationPath
|
|
96
|
+
self.candidateApplicationPath = candidateApplicationPath
|
|
97
|
+
self.applicationPath = applicationPath
|
|
98
|
+
self.operation = operation
|
|
99
|
+
self.transactionDirectory = transactionDirectory
|
|
100
|
+
self.bootstrapPriorMonotonicState = bootstrapPriorMonotonicState
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
enum CodingKeys: String, CodingKey {
|
|
104
|
+
case schemaVersion = "schema_version"
|
|
105
|
+
case transactionID = "transaction_id"
|
|
106
|
+
case phase
|
|
107
|
+
case releaseID = "release_id"
|
|
108
|
+
case releaseSequence = "release_sequence"
|
|
109
|
+
case keyEpoch = "key_epoch"
|
|
110
|
+
case envelopePayloadSHA256 = "envelope_payload_sha256"
|
|
111
|
+
case artifactSHA256 = "artifact_sha256"
|
|
112
|
+
case manifestSHA256 = "manifest_sha256"
|
|
113
|
+
case cohortPackageSHA256 = "cohort_package_sha256"
|
|
114
|
+
case candidateTreeSHA256 = "candidate_tree_sha256"
|
|
115
|
+
case minimumOSVersion = "minimum_os_version"
|
|
116
|
+
case candidateApplicationMode = "candidate_application_mode"
|
|
117
|
+
case candidateExecutableModes = "candidate_executable_modes"
|
|
118
|
+
case previousTreeSHA256 = "previous_tree_sha256"
|
|
119
|
+
case previousApplicationMode = "previous_application_mode"
|
|
120
|
+
case previousExecutableModes = "previous_executable_modes"
|
|
121
|
+
case previousApplicationPath = "previous_application_path"
|
|
122
|
+
case candidateApplicationPath = "candidate_application_path"
|
|
123
|
+
case applicationPath = "application_path"
|
|
124
|
+
case operation
|
|
125
|
+
case transactionDirectory = "transaction_directory"
|
|
126
|
+
case bootstrapPriorMonotonicState = "bootstrap_prior_monotonic_state"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
struct DurableInstallJournal {
|
|
131
|
+
let transactionDirectory: String
|
|
132
|
+
|
|
133
|
+
func read() throws -> BrokerInstallJournal? {
|
|
134
|
+
let directory = try openValidatedDirectory()
|
|
135
|
+
defer { Darwin.close(directory) }
|
|
136
|
+
let input = openat(directory, "install-journal.json", O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
|
|
137
|
+
if input < 0, errno == ENOENT { return nil }
|
|
138
|
+
guard input >= 0 else { throw InstallJournalError.invalidJournal }
|
|
139
|
+
defer { Darwin.close(input) }
|
|
140
|
+
var metadata = stat()
|
|
141
|
+
guard fstat(input, &metadata) == 0,
|
|
142
|
+
(metadata.st_mode & S_IFMT) == S_IFREG,
|
|
143
|
+
metadata.st_uid == 0,
|
|
144
|
+
(metadata.st_mode & 0o077) == 0,
|
|
145
|
+
metadata.st_nlink == 1,
|
|
146
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(input),
|
|
147
|
+
metadata.st_size > 0,
|
|
148
|
+
metadata.st_size <= 64 * 1024
|
|
149
|
+
else {
|
|
150
|
+
throw InstallJournalError.invalidJournal
|
|
151
|
+
}
|
|
152
|
+
var data = Data(count: Int(metadata.st_size))
|
|
153
|
+
var offset: off_t = 0
|
|
154
|
+
try data.withUnsafeMutableBytes { bytes in
|
|
155
|
+
guard let base = bytes.baseAddress else { return }
|
|
156
|
+
while offset < metadata.st_size {
|
|
157
|
+
let count = pread(
|
|
158
|
+
input,
|
|
159
|
+
base.advanced(by: Int(offset)),
|
|
160
|
+
Int(metadata.st_size - offset),
|
|
161
|
+
offset
|
|
162
|
+
)
|
|
163
|
+
if count < 0, errno == EINTR { continue }
|
|
164
|
+
guard count > 0 else { throw InstallJournalError.invalidJournal }
|
|
165
|
+
offset += off_t(count)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
var after = stat()
|
|
169
|
+
guard fstat(input, &after) == 0,
|
|
170
|
+
metadata.st_dev == after.st_dev,
|
|
171
|
+
metadata.st_ino == after.st_ino,
|
|
172
|
+
metadata.st_size == after.st_size,
|
|
173
|
+
metadata.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
|
|
174
|
+
metadata.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
|
|
175
|
+
metadata.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
|
|
176
|
+
metadata.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec,
|
|
177
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(input),
|
|
178
|
+
let journal = try? JSONDecoder().decode(BrokerInstallJournal.self, from: data)
|
|
179
|
+
else {
|
|
180
|
+
throw InstallJournalError.invalidJournal
|
|
181
|
+
}
|
|
182
|
+
try validate(journal)
|
|
183
|
+
return journal
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
func write(_ journal: BrokerInstallJournal) throws {
|
|
187
|
+
try validate(journal)
|
|
188
|
+
let directory = try openValidatedDirectory()
|
|
189
|
+
defer { Darwin.close(directory) }
|
|
190
|
+
let encoder = JSONEncoder()
|
|
191
|
+
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
|
|
192
|
+
let data = try encoder.encode(journal) + Data([0x0a])
|
|
193
|
+
let temporary = ".install-journal.\(UUID().uuidString.lowercased()).tmp"
|
|
194
|
+
let output = openat(
|
|
195
|
+
directory,
|
|
196
|
+
temporary,
|
|
197
|
+
O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
|
|
198
|
+
0o600
|
|
199
|
+
)
|
|
200
|
+
guard output >= 0 else { throw InstallJournalError.couldNotPersist }
|
|
201
|
+
var removeTemporary = true
|
|
202
|
+
defer {
|
|
203
|
+
Darwin.close(output)
|
|
204
|
+
if removeTemporary { unlinkat(directory, temporary, 0) }
|
|
205
|
+
}
|
|
206
|
+
guard DarwinACLValidator.descriptorHasNoExtendedACL(output) else {
|
|
207
|
+
throw InstallJournalError.couldNotPersist
|
|
208
|
+
}
|
|
209
|
+
try data.withUnsafeBytes { bytes in
|
|
210
|
+
guard let base = bytes.baseAddress else { return }
|
|
211
|
+
var offset = 0
|
|
212
|
+
while offset < bytes.count {
|
|
213
|
+
let count = Darwin.write(output, base.advanced(by: offset), bytes.count - offset)
|
|
214
|
+
if count < 0, errno == EINTR { continue }
|
|
215
|
+
guard count > 0 else { throw InstallJournalError.couldNotPersist }
|
|
216
|
+
offset += count
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
guard fsync(output) == 0,
|
|
220
|
+
renameat(directory, temporary, directory, "install-journal.json") == 0,
|
|
221
|
+
fsync(directory) == 0
|
|
222
|
+
else {
|
|
223
|
+
throw InstallJournalError.couldNotPersist
|
|
224
|
+
}
|
|
225
|
+
removeTemporary = false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private func openValidatedDirectory() throws -> Int32 {
|
|
229
|
+
guard DarwinACLValidator.rootOwnedDirectoryAncestryHasNoExtendedACL(
|
|
230
|
+
to: transactionDirectory
|
|
231
|
+
) else {
|
|
232
|
+
throw InstallJournalError.unsafeDirectory
|
|
233
|
+
}
|
|
234
|
+
let directory = Darwin.open(
|
|
235
|
+
transactionDirectory,
|
|
236
|
+
O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW
|
|
237
|
+
)
|
|
238
|
+
guard directory >= 0 else { throw InstallJournalError.unsafeDirectory }
|
|
239
|
+
var metadata = stat()
|
|
240
|
+
guard fstat(directory, &metadata) == 0,
|
|
241
|
+
(metadata.st_mode & S_IFMT) == S_IFDIR,
|
|
242
|
+
metadata.st_uid == 0,
|
|
243
|
+
(metadata.st_mode & 0o077) == 0,
|
|
244
|
+
DarwinACLValidator.descriptorHasNoExtendedACL(directory)
|
|
245
|
+
else {
|
|
246
|
+
Darwin.close(directory)
|
|
247
|
+
throw InstallJournalError.unsafeDirectory
|
|
248
|
+
}
|
|
249
|
+
return directory
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private func validate(_ journal: BrokerInstallJournal) throws {
|
|
253
|
+
let leaf = URL(fileURLWithPath: transactionDirectory).lastPathComponent
|
|
254
|
+
guard leaf.hasPrefix("transaction-") else { throw InstallJournalError.invalidJournal }
|
|
255
|
+
let transactionID = String(leaf.dropFirst("transaction-".count))
|
|
256
|
+
let expectedCandidate = transactionDirectory + "/candidate/Recordings.app"
|
|
257
|
+
let activationPhases: Set<String> = [
|
|
258
|
+
"prepared", "launch-barrier-pending", "launch-barrier-held",
|
|
259
|
+
"swap-pending", "swapped", "previous-retaining",
|
|
260
|
+
"previous-retained", "first-install-pending", "first-installed",
|
|
261
|
+
"launch-barrier-releasing", "launch-barrier-released",
|
|
262
|
+
"activated", "rollback-started", "rolled-back", "committed",
|
|
263
|
+
]
|
|
264
|
+
let bootstrapPhases: Set<String> = [
|
|
265
|
+
"bootstrap-prepared", "bootstrap-commit-pending",
|
|
266
|
+
"bootstrap-aborted", "bootstrap-committed",
|
|
267
|
+
]
|
|
268
|
+
guard journal.schemaVersion == RecordingsUpdateConstants.protocolVersion,
|
|
269
|
+
journal.transactionID == transactionID,
|
|
270
|
+
UUID(uuidString: journal.transactionID)?.uuidString.lowercased() == journal.transactionID,
|
|
271
|
+
UUID(uuidString: journal.releaseID) != nil,
|
|
272
|
+
journal.releaseSequence > 0,
|
|
273
|
+
journal.keyEpoch > 0,
|
|
274
|
+
Self.isSHA256(journal.envelopePayloadSHA256),
|
|
275
|
+
Self.isSHA256(journal.artifactSHA256),
|
|
276
|
+
Self.isSHA256(journal.manifestSHA256),
|
|
277
|
+
Self.isSHA256(journal.cohortPackageSHA256),
|
|
278
|
+
Self.isSHA256(journal.candidateTreeSHA256),
|
|
279
|
+
Self.isValidMinimumOSVersion(journal.minimumOSVersion),
|
|
280
|
+
Self.isSafeApplicationMode(journal.candidateApplicationMode),
|
|
281
|
+
Self.areSafeExecutableModes(journal.candidateExecutableModes),
|
|
282
|
+
journal.applicationPath == RecordingsUpdateConstants.applicationPath
|
|
283
|
+
else {
|
|
284
|
+
throw InstallJournalError.invalidJournal
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
switch journal.operation ?? .applicationActivation {
|
|
288
|
+
case .applicationActivation:
|
|
289
|
+
// `operation == nil` is the only accepted legacy migration: schema-v1
|
|
290
|
+
// journals predating the discriminator were all application activations.
|
|
291
|
+
guard activationPhases.contains(journal.phase),
|
|
292
|
+
journal.transactionDirectory == nil ||
|
|
293
|
+
journal.transactionDirectory == transactionDirectory,
|
|
294
|
+
journal.bootstrapPriorMonotonicState == nil,
|
|
295
|
+
journal.candidateApplicationPath == expectedCandidate,
|
|
296
|
+
(journal.previousTreeSHA256 == nil) == (journal.previousApplicationPath == nil),
|
|
297
|
+
(journal.previousTreeSHA256 == nil) == (journal.previousApplicationMode == nil),
|
|
298
|
+
(journal.previousTreeSHA256 == nil) == (journal.previousExecutableModes == nil)
|
|
299
|
+
else {
|
|
300
|
+
throw InstallJournalError.invalidJournal
|
|
301
|
+
}
|
|
302
|
+
if let previousDigest = journal.previousTreeSHA256,
|
|
303
|
+
(!Self.isSHA256(previousDigest) ||
|
|
304
|
+
!Self.isSafeApplicationMode(journal.previousApplicationMode ?? 0) ||
|
|
305
|
+
!Self.areSafeExecutableModes(journal.previousExecutableModes ?? [:]) ||
|
|
306
|
+
journal.previousApplicationPath != transactionDirectory + "/previous.app") {
|
|
307
|
+
throw InstallJournalError.invalidJournal
|
|
308
|
+
}
|
|
309
|
+
case .bootstrapCommit:
|
|
310
|
+
guard bootstrapPhases.contains(journal.phase),
|
|
311
|
+
journal.transactionDirectory == transactionDirectory,
|
|
312
|
+
journal.bootstrapPriorMonotonicState == "absent",
|
|
313
|
+
journal.candidateApplicationPath == RecordingsUpdateConstants.applicationPath,
|
|
314
|
+
journal.previousTreeSHA256 == nil,
|
|
315
|
+
journal.previousApplicationMode == nil,
|
|
316
|
+
journal.previousExecutableModes == nil,
|
|
317
|
+
journal.previousApplicationPath == nil
|
|
318
|
+
else {
|
|
319
|
+
throw InstallJournalError.invalidJournal
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private static func isSHA256(_ value: String) -> Bool {
|
|
325
|
+
value.range(of: "^[a-f0-9]{64}$", options: .regularExpression) != nil
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private static func isValidMinimumOSVersion(_ value: String?) -> Bool {
|
|
329
|
+
guard let value else { return true }
|
|
330
|
+
return HostOSVersionPolicy.isValidNumericVersion(value)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private static func isSafeApplicationMode(_ value: UInt16) -> Bool {
|
|
334
|
+
value <= 0o777 && (value & 0o500) == 0o500 && (value & 0o022) == 0
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private static func isSafeExecutableMode(_ value: UInt16) -> Bool {
|
|
338
|
+
value <= 0o777 && (value & 0o500) == 0o500 && (value & 0o022) == 0
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private static func areSafeExecutableModes(_ values: [String: UInt16]) -> Bool {
|
|
342
|
+
Set(values.keys) == Set([
|
|
343
|
+
"Contents/MacOS/Recordings",
|
|
344
|
+
"Contents/Helpers/recordings",
|
|
345
|
+
"Contents/Helpers/recordings-update-client",
|
|
346
|
+
]) && values.values.allSatisfy { Self.isSafeExecutableMode($0) }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
enum InstallJournalError: Error, CustomStringConvertible {
|
|
351
|
+
case unsafeDirectory
|
|
352
|
+
case invalidJournal
|
|
353
|
+
case couldNotPersist
|
|
354
|
+
|
|
355
|
+
var description: String {
|
|
356
|
+
switch self {
|
|
357
|
+
case .unsafeDirectory: "The install-journal directory is unsafe"
|
|
358
|
+
case .invalidJournal: "The install journal is malformed, unsafe, or ambiguously bound"
|
|
359
|
+
case .couldNotPersist: "Could not durably persist the install journal"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|