@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,172 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
@testable import RecordingsUpdateProtocol
|
|
4
|
+
|
|
5
|
+
struct MonotonicReleasePolicyTests {
|
|
6
|
+
private let epoch: UInt64 = 7
|
|
7
|
+
private let cohort = String(repeating: "a", count: 64)
|
|
8
|
+
private let digest = String(repeating: "b", count: 64)
|
|
9
|
+
private let releaseID = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"
|
|
10
|
+
|
|
11
|
+
@Test("an absent state admits only the bootstrap release")
|
|
12
|
+
func absentStateRequiresBootstrap() throws {
|
|
13
|
+
#expect(try assess(candidate()) == .advance)
|
|
14
|
+
#expect(throws: MonotonicReleasePolicyError.bootstrapRequired) {
|
|
15
|
+
try assess(candidate(purpose: "update"))
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@Test("an exact seen bootstrap resumes without advancing")
|
|
20
|
+
func exactSeenBootstrapResumes() throws {
|
|
21
|
+
let bootstrap = candidate()
|
|
22
|
+
#expect(try assess(bootstrap, current: state(from: bootstrap, phase: .seen)) == .resumeSeen)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Test("an exact committed bootstrap is idempotent")
|
|
26
|
+
func exactCommittedBootstrapIsIdempotent() throws {
|
|
27
|
+
let bootstrap = candidate()
|
|
28
|
+
#expect(
|
|
29
|
+
try assess(bootstrap, current: state(from: bootstrap, phase: .committed))
|
|
30
|
+
== .alreadyCommitted
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Test("a higher update cannot supersede a seen release")
|
|
35
|
+
func higherUpdateIsBlockedWhileSeen() {
|
|
36
|
+
let pending = candidate(purpose: "update", sequence: 2)
|
|
37
|
+
let next = candidate(purpose: "update", sequence: 3, releaseID: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff", digest: String(repeating: "c", count: 64))
|
|
38
|
+
#expect(throws: MonotonicReleasePolicyError.pendingSeenRecoveryRequired) {
|
|
39
|
+
try assess(next, current: state(from: pending, phase: .seen))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Test("a higher update advances after the current release commits")
|
|
44
|
+
func higherUpdateAdvancesAfterCommit() throws {
|
|
45
|
+
let committed = candidate(purpose: "update", sequence: 2)
|
|
46
|
+
let next = candidate(purpose: "update", sequence: 3, releaseID: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff", digest: String(repeating: "c", count: 64))
|
|
47
|
+
#expect(try assess(next, current: state(from: committed, phase: .committed)) == .advance)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test("a rolled-back seen release admits only a strictly newer cohort-compatible update")
|
|
51
|
+
func abortedReleasePreservesMonotonicBarrier() throws {
|
|
52
|
+
let abortedCandidate = candidate(purpose: "update", sequence: 2)
|
|
53
|
+
let aborted = state(from: abortedCandidate, phase: .aborted)
|
|
54
|
+
let next = candidate(
|
|
55
|
+
purpose: "update",
|
|
56
|
+
sequence: 3,
|
|
57
|
+
releaseID: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff",
|
|
58
|
+
digest: String(repeating: "c", count: 64)
|
|
59
|
+
)
|
|
60
|
+
#expect(try assess(next, current: aborted) == .advance)
|
|
61
|
+
#expect(throws: MonotonicReleasePolicyError.sequenceRollbackOrConflict) {
|
|
62
|
+
try assess(abortedCandidate, current: aborted)
|
|
63
|
+
}
|
|
64
|
+
#expect(throws: MonotonicReleasePolicyError.immutableCohortChange) {
|
|
65
|
+
try assess(
|
|
66
|
+
candidate(
|
|
67
|
+
purpose: "update",
|
|
68
|
+
sequence: 3,
|
|
69
|
+
cohort: String(repeating: "c", count: 64)
|
|
70
|
+
),
|
|
71
|
+
current: aborted
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@Test("an exact committed update is idempotent")
|
|
77
|
+
func exactCommittedUpdateIsIdempotent() throws {
|
|
78
|
+
let update = candidate(purpose: "update", sequence: 2)
|
|
79
|
+
#expect(
|
|
80
|
+
try assess(update, current: state(from: update, phase: .committed))
|
|
81
|
+
== .alreadyCommitted
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Test("seen state rejects wrong cohort, key, identity, and digest")
|
|
86
|
+
func seenStateRejectsEveryNonExactReplay() {
|
|
87
|
+
let pending = candidate(purpose: "update", sequence: 2)
|
|
88
|
+
let seen = state(from: pending, phase: .seen)
|
|
89
|
+
let conflicts = [
|
|
90
|
+
candidate(purpose: "update", sequence: 2, cohort: String(repeating: "c", count: 64)),
|
|
91
|
+
candidate(purpose: "update", keyEpoch: epoch + 1, sequence: 2),
|
|
92
|
+
candidate(purpose: "update", sequence: 2, releaseID: "bbbbbbbb-cccc-4ddd-8eee-ffffffffffff"),
|
|
93
|
+
candidate(purpose: "update", sequence: 2, digest: String(repeating: "c", count: 64)),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
for conflict in conflicts {
|
|
97
|
+
let expected: MonotonicReleasePolicyError = conflict.keyEpoch == epoch
|
|
98
|
+
? .pendingSeenRecoveryRequired
|
|
99
|
+
: .immutableCohortChange
|
|
100
|
+
#expect(throws: expected) {
|
|
101
|
+
try assess(conflict, current: seen)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Test("committed state rejects cohort, key, and same-sequence digest conflicts")
|
|
107
|
+
func committedStateRejectsBindingConflicts() {
|
|
108
|
+
let committedCandidate = candidate(purpose: "update", sequence: 2)
|
|
109
|
+
let committed = state(from: committedCandidate, phase: .committed)
|
|
110
|
+
|
|
111
|
+
#expect(throws: MonotonicReleasePolicyError.immutableCohortChange) {
|
|
112
|
+
try assess(
|
|
113
|
+
candidate(purpose: "update", sequence: 3, cohort: String(repeating: "c", count: 64)),
|
|
114
|
+
current: committed
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
#expect(throws: MonotonicReleasePolicyError.immutableCohortChange) {
|
|
118
|
+
try assess(candidate(purpose: "update", keyEpoch: epoch + 1, sequence: 3), current: committed)
|
|
119
|
+
}
|
|
120
|
+
#expect(throws: MonotonicReleasePolicyError.sequenceRollbackOrConflict) {
|
|
121
|
+
try assess(
|
|
122
|
+
candidate(purpose: "update", sequence: 2, digest: String(repeating: "c", count: 64)),
|
|
123
|
+
current: committed
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private func assess(
|
|
129
|
+
_ value: MonotonicReleaseCandidate,
|
|
130
|
+
current: MonotonicReleaseStateSnapshot? = nil
|
|
131
|
+
) throws -> MonotonicReleasePolicyDecision {
|
|
132
|
+
try MonotonicReleasePolicy.assess(
|
|
133
|
+
candidate: value,
|
|
134
|
+
current: current,
|
|
135
|
+
initialKeyEpoch: epoch,
|
|
136
|
+
allowedKeyEpochs: [epoch]
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private func candidate(
|
|
141
|
+
purpose: String = "bootstrap",
|
|
142
|
+
keyEpoch: UInt64? = nil,
|
|
143
|
+
sequence: UInt64 = 1,
|
|
144
|
+
releaseID: String? = nil,
|
|
145
|
+
cohort: String? = nil,
|
|
146
|
+
digest: String? = nil
|
|
147
|
+
) -> MonotonicReleaseCandidate {
|
|
148
|
+
MonotonicReleaseCandidate(
|
|
149
|
+
purpose: purpose,
|
|
150
|
+
keyEpoch: keyEpoch ?? epoch,
|
|
151
|
+
releaseSequence: sequence,
|
|
152
|
+
releaseID: releaseID ?? self.releaseID,
|
|
153
|
+
cohortPackageSHA256: cohort ?? self.cohort,
|
|
154
|
+
envelopePayloadSHA256: digest ?? self.digest
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private func state(
|
|
159
|
+
from candidate: MonotonicReleaseCandidate,
|
|
160
|
+
phase: MonotonicReleasePhase
|
|
161
|
+
) -> MonotonicReleaseStateSnapshot {
|
|
162
|
+
MonotonicReleaseStateSnapshot(
|
|
163
|
+
purpose: candidate.purpose,
|
|
164
|
+
phase: phase,
|
|
165
|
+
keyEpoch: candidate.keyEpoch,
|
|
166
|
+
releaseSequence: candidate.releaseSequence,
|
|
167
|
+
releaseID: candidate.releaseID,
|
|
168
|
+
cohortPackageSHA256: candidate.cohortPackageSHA256,
|
|
169
|
+
envelopePayloadSHA256: candidate.envelopePayloadSHA256
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Testing
|
|
3
|
+
@testable import RecordingsUpdateProtocol
|
|
4
|
+
|
|
5
|
+
struct ReleaseEnvelopeValidationTests {
|
|
6
|
+
@Test("macOS numeric dotted bundle builds are accepted")
|
|
7
|
+
func numericDottedBundleBuildIsAccepted() throws {
|
|
8
|
+
try payload(build: "0.2.13").validate(
|
|
9
|
+
now: fixedDate("2026-07-19T12:00:00.000Z"),
|
|
10
|
+
brokerVersion: "1.0.0"
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@Test("nonnumeric bundle build components are rejected")
|
|
15
|
+
func nonnumericBundleBuildIsRejected() {
|
|
16
|
+
#expect(throws: ReleaseEnvelopeValidationError.self) {
|
|
17
|
+
try payload(build: "0.2.beta").validate(
|
|
18
|
+
now: fixedDate("2026-07-19T12:00:00.000Z"),
|
|
19
|
+
brokerVersion: "1.0.0"
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private func payload(build: String) -> ReleaseEnvelopePayload {
|
|
25
|
+
ReleaseEnvelopePayload(
|
|
26
|
+
schemaVersion: RecordingsUpdateConstants.protocolVersion,
|
|
27
|
+
purpose: "update",
|
|
28
|
+
keyEpoch: 1,
|
|
29
|
+
releaseSequence: 2,
|
|
30
|
+
releaseID: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
|
|
31
|
+
version: "0.2.13",
|
|
32
|
+
build: build,
|
|
33
|
+
sourceCommit: String(repeating: "a", count: 40),
|
|
34
|
+
artifactSHA256: String(repeating: "b", count: 64),
|
|
35
|
+
artifactByteCount: 1024,
|
|
36
|
+
manifestSHA256: String(repeating: "c", count: 64),
|
|
37
|
+
manifestByteCount: 512,
|
|
38
|
+
candidateTreeSHA256: String(repeating: "d", count: 64),
|
|
39
|
+
packageSHA256: String(repeating: "e", count: 64),
|
|
40
|
+
updateClientSHA256: String(repeating: "f", count: 64),
|
|
41
|
+
updateBrokerSHA256: String(repeating: "1", count: 64),
|
|
42
|
+
artifactVerifierSHA256: String(repeating: "2", count: 64),
|
|
43
|
+
bootstrapMarkerSHA256: String(repeating: "3", count: 64),
|
|
44
|
+
architectures: ["arm64", "x86_64"],
|
|
45
|
+
minimumOSVersion: "26.0",
|
|
46
|
+
minimumBrokerVersion: "1.0.0",
|
|
47
|
+
signingTeamIdentifier: "EXAMPLE123",
|
|
48
|
+
applicationDesignatedRequirement: "identifier \"com.hasna.recordings\"",
|
|
49
|
+
updateClientDesignatedRequirement:
|
|
50
|
+
"identifier \"com.hasna.recordings.update-client\"",
|
|
51
|
+
updateBrokerDesignatedRequirement: "identifier \"com.hasna.recordings.updater\"",
|
|
52
|
+
artifactVerifierDesignatedRequirement:
|
|
53
|
+
"identifier \"com.hasna.recordings.artifact-verifier\"",
|
|
54
|
+
installerCertificateSHA256: String(repeating: "4", count: 64),
|
|
55
|
+
issuedAtUTC: "2026-07-19T00:00:00.000Z",
|
|
56
|
+
expiresAtUTC: "2026-07-20T00:00:00.000Z"
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private func fixedDate(_ value: String) -> Date {
|
|
61
|
+
let formatter = ISO8601DateFormatter()
|
|
62
|
+
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
63
|
+
return formatter.date(from: value)!
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import CryptoKit
|
|
2
|
+
import Darwin
|
|
3
|
+
import Foundation
|
|
4
|
+
import RecordingsUpdateProtocol
|
|
5
|
+
|
|
6
|
+
@main
|
|
7
|
+
enum RecordingsEnvelopeSigner {
|
|
8
|
+
static func main() {
|
|
9
|
+
do {
|
|
10
|
+
let arguments = try Arguments.parse(CommandLine.arguments)
|
|
11
|
+
let payloadData = try readRegularFile(
|
|
12
|
+
path: arguments.payloadPath,
|
|
13
|
+
maximumBytes: 1024 * 1024,
|
|
14
|
+
requireOwnerOnly: false
|
|
15
|
+
)
|
|
16
|
+
let payload = try JSONDecoder().decode(ReleaseEnvelopePayload.self, from: payloadData)
|
|
17
|
+
try payload.validate()
|
|
18
|
+
let privateKeyData = try readRegularFile(
|
|
19
|
+
path: arguments.privateKeyPath,
|
|
20
|
+
maximumBytes: 32,
|
|
21
|
+
requireOwnerOnly: true
|
|
22
|
+
)
|
|
23
|
+
guard privateKeyData.count == 32 else { throw SignerError.invalidPrivateKey }
|
|
24
|
+
let privateKey: Curve25519.Signing.PrivateKey
|
|
25
|
+
do {
|
|
26
|
+
privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: privateKeyData)
|
|
27
|
+
} catch {
|
|
28
|
+
throw SignerError.invalidPrivateKey
|
|
29
|
+
}
|
|
30
|
+
let publicKeyData = try readRegularFile(
|
|
31
|
+
path: arguments.publicKeyPath,
|
|
32
|
+
maximumBytes: 32,
|
|
33
|
+
requireOwnerOnly: false
|
|
34
|
+
)
|
|
35
|
+
guard publicKeyData.count == 32,
|
|
36
|
+
privateKey.publicKey.rawRepresentation == publicKeyData
|
|
37
|
+
else {
|
|
38
|
+
throw SignerError.publicKeyMismatch
|
|
39
|
+
}
|
|
40
|
+
let signature = try privateKey.signature(for: payload.canonicalData())
|
|
41
|
+
let envelope = SignedReleaseEnvelope(
|
|
42
|
+
payload: payload,
|
|
43
|
+
signature: signature.base64EncodedString()
|
|
44
|
+
)
|
|
45
|
+
let encoder = JSONEncoder()
|
|
46
|
+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
|
|
47
|
+
let output = try encoder.encode(envelope) + Data([0x0a])
|
|
48
|
+
try writeExclusive(path: arguments.outputPath, data: output)
|
|
49
|
+
} catch {
|
|
50
|
+
let message = "recordings-envelope-signer: \(sanitized(error))\n"
|
|
51
|
+
FileHandle.standardError.write(Data(message.utf8))
|
|
52
|
+
Darwin.exit(1)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private static func readRegularFile(
|
|
57
|
+
path: String,
|
|
58
|
+
maximumBytes: Int64,
|
|
59
|
+
requireOwnerOnly: Bool
|
|
60
|
+
) throws -> Data {
|
|
61
|
+
let descriptor = Darwin.open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW)
|
|
62
|
+
guard descriptor >= 0 else { throw SignerError.couldNotOpenInput }
|
|
63
|
+
defer { Darwin.close(descriptor) }
|
|
64
|
+
var metadata = stat()
|
|
65
|
+
guard fstat(descriptor, &metadata) == 0,
|
|
66
|
+
(metadata.st_mode & S_IFMT) == S_IFREG,
|
|
67
|
+
metadata.st_size > 0,
|
|
68
|
+
metadata.st_size <= maximumBytes,
|
|
69
|
+
(!requireOwnerOnly || (metadata.st_uid == geteuid() && (metadata.st_mode & 0o077) == 0))
|
|
70
|
+
else {
|
|
71
|
+
throw SignerError.unsafeInput
|
|
72
|
+
}
|
|
73
|
+
let before = metadata
|
|
74
|
+
var data = Data(count: Int(metadata.st_size))
|
|
75
|
+
var offset: off_t = 0
|
|
76
|
+
try data.withUnsafeMutableBytes { bytes in
|
|
77
|
+
guard let base = bytes.baseAddress else { return }
|
|
78
|
+
while offset < before.st_size {
|
|
79
|
+
let count = pread(
|
|
80
|
+
descriptor,
|
|
81
|
+
base.advanced(by: Int(offset)),
|
|
82
|
+
Int(before.st_size - offset),
|
|
83
|
+
offset
|
|
84
|
+
)
|
|
85
|
+
if count < 0, errno == EINTR { continue }
|
|
86
|
+
guard count > 0 else { throw SignerError.couldNotReadInput }
|
|
87
|
+
offset += off_t(count)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var extra: UInt8 = 0
|
|
91
|
+
guard pread(descriptor, &extra, 1, metadata.st_size) == 0 else {
|
|
92
|
+
throw SignerError.inputChanged
|
|
93
|
+
}
|
|
94
|
+
var after = stat()
|
|
95
|
+
guard fstat(descriptor, &after) == 0,
|
|
96
|
+
before.st_dev == after.st_dev,
|
|
97
|
+
before.st_ino == after.st_ino,
|
|
98
|
+
before.st_size == after.st_size,
|
|
99
|
+
before.st_mtimespec.tv_sec == after.st_mtimespec.tv_sec,
|
|
100
|
+
before.st_mtimespec.tv_nsec == after.st_mtimespec.tv_nsec,
|
|
101
|
+
before.st_ctimespec.tv_sec == after.st_ctimespec.tv_sec,
|
|
102
|
+
before.st_ctimespec.tv_nsec == after.st_ctimespec.tv_nsec
|
|
103
|
+
else {
|
|
104
|
+
throw SignerError.inputChanged
|
|
105
|
+
}
|
|
106
|
+
return data
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static func writeExclusive(path: String, data: Data) throws {
|
|
110
|
+
let outputURL = URL(fileURLWithPath: path)
|
|
111
|
+
let parentPath = outputURL.deletingLastPathComponent().path
|
|
112
|
+
let leaf = outputURL.lastPathComponent
|
|
113
|
+
guard !leaf.isEmpty, leaf != ".", leaf != "..", !leaf.contains("/") else {
|
|
114
|
+
throw SignerError.outputAlreadyExistsOrUnsafe
|
|
115
|
+
}
|
|
116
|
+
let parent = Darwin.open(parentPath, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)
|
|
117
|
+
guard parent >= 0 else { throw SignerError.outputAlreadyExistsOrUnsafe }
|
|
118
|
+
defer { Darwin.close(parent) }
|
|
119
|
+
var parentMetadata = stat()
|
|
120
|
+
guard fstat(parent, &parentMetadata) == 0,
|
|
121
|
+
(parentMetadata.st_mode & S_IFMT) == S_IFDIR,
|
|
122
|
+
parentMetadata.st_uid == geteuid(),
|
|
123
|
+
(parentMetadata.st_mode & 0o022) == 0
|
|
124
|
+
else {
|
|
125
|
+
throw SignerError.outputAlreadyExistsOrUnsafe
|
|
126
|
+
}
|
|
127
|
+
let descriptor = openat(parent, leaf, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0o600)
|
|
128
|
+
guard descriptor >= 0 else { throw SignerError.outputAlreadyExistsOrUnsafe }
|
|
129
|
+
var keep = false
|
|
130
|
+
defer {
|
|
131
|
+
Darwin.close(descriptor)
|
|
132
|
+
if !keep { unlinkat(parent, leaf, 0) }
|
|
133
|
+
}
|
|
134
|
+
try data.withUnsafeBytes { bytes in
|
|
135
|
+
guard let base = bytes.baseAddress else { return }
|
|
136
|
+
var offset = 0
|
|
137
|
+
while offset < bytes.count {
|
|
138
|
+
let count = Darwin.write(descriptor, base.advanced(by: offset), bytes.count - offset)
|
|
139
|
+
if count < 0, errno == EINTR { continue }
|
|
140
|
+
guard count > 0 else { throw SignerError.couldNotWriteOutput }
|
|
141
|
+
offset += count
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
guard fsync(descriptor) == 0, fsync(parent) == 0 else {
|
|
145
|
+
throw SignerError.couldNotWriteOutput
|
|
146
|
+
}
|
|
147
|
+
keep = true
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private static func sanitized(_ error: Error) -> String {
|
|
151
|
+
if let error = error as? SignerError { return error.description }
|
|
152
|
+
if let error = error as? ReleaseEnvelopeValidationError { return error.description }
|
|
153
|
+
return "operation failed"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private struct Arguments {
|
|
158
|
+
let payloadPath: String
|
|
159
|
+
let privateKeyPath: String
|
|
160
|
+
let publicKeyPath: String
|
|
161
|
+
let outputPath: String
|
|
162
|
+
|
|
163
|
+
static func parse(_ values: [String]) throws -> Arguments {
|
|
164
|
+
guard values.count == 9,
|
|
165
|
+
values[1] == "--payload",
|
|
166
|
+
values[3] == "--private-key",
|
|
167
|
+
values[5] == "--public-key",
|
|
168
|
+
values[7] == "--output",
|
|
169
|
+
values[2].hasPrefix("/"),
|
|
170
|
+
values[4].hasPrefix("/"),
|
|
171
|
+
values[6].hasPrefix("/"),
|
|
172
|
+
values[8].hasPrefix("/")
|
|
173
|
+
else {
|
|
174
|
+
throw SignerError.usage
|
|
175
|
+
}
|
|
176
|
+
return Arguments(
|
|
177
|
+
payloadPath: values[2],
|
|
178
|
+
privateKeyPath: values[4],
|
|
179
|
+
publicKeyPath: values[6],
|
|
180
|
+
outputPath: values[8]
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private enum SignerError: Error, CustomStringConvertible {
|
|
186
|
+
case usage
|
|
187
|
+
case couldNotOpenInput
|
|
188
|
+
case unsafeInput
|
|
189
|
+
case couldNotReadInput
|
|
190
|
+
case inputChanged
|
|
191
|
+
case invalidPrivateKey
|
|
192
|
+
case publicKeyMismatch
|
|
193
|
+
case outputAlreadyExistsOrUnsafe
|
|
194
|
+
case couldNotWriteOutput
|
|
195
|
+
|
|
196
|
+
var description: String {
|
|
197
|
+
switch self {
|
|
198
|
+
case .usage:
|
|
199
|
+
"usage: recordings-envelope-signer --payload /absolute/payload.json --private-key /absolute/private-key.raw --public-key /absolute/public-key.raw --output /absolute/envelope.json"
|
|
200
|
+
case .couldNotOpenInput: "could not open an input file"
|
|
201
|
+
case .unsafeInput: "an input file has an unsafe type, size, owner, or mode"
|
|
202
|
+
case .couldNotReadInput: "could not read an input file"
|
|
203
|
+
case .inputChanged: "an input file changed while it was read"
|
|
204
|
+
case .invalidPrivateKey: "the private signing key is invalid"
|
|
205
|
+
case .publicKeyMismatch: "the private signing key does not match the selected epoch public key"
|
|
206
|
+
case .outputAlreadyExistsOrUnsafe: "the output path already exists or is unsafe"
|
|
207
|
+
case .couldNotWriteOutput: "could not durably write the output file"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|