@hasna/recordings 0.2.10 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +350 -35
  2. package/bun.lock +3 -0
  3. package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
  4. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
  5. package/dist/cli/index.js +1728 -320
  6. package/dist/cli/macos-permissions.d.ts +13 -0
  7. package/dist/cli/macos-permissions.d.ts.map +1 -0
  8. package/dist/cli/options.d.ts +12 -0
  9. package/dist/cli/options.d.ts.map +1 -1
  10. package/dist/db/pg-migrations.d.ts.map +1 -1
  11. package/dist/db/recordings.d.ts +2 -1
  12. package/dist/db/recordings.d.ts.map +1 -1
  13. package/dist/db/remote-storage.d.ts +5 -1
  14. package/dist/db/remote-storage.d.ts.map +1 -1
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +628 -147
  18. package/dist/lib/bun-runtime.d.ts +11 -0
  19. package/dist/lib/bun-runtime.d.ts.map +1 -0
  20. package/dist/lib/config.d.ts +8 -1
  21. package/dist/lib/config.d.ts.map +1 -1
  22. package/dist/lib/enhancer.d.ts +10 -2
  23. package/dist/lib/enhancer.d.ts.map +1 -1
  24. package/dist/lib/install-maintenance.d.ts +24 -0
  25. package/dist/lib/install-maintenance.d.ts.map +1 -0
  26. package/dist/lib/machine.d.ts +2 -0
  27. package/dist/lib/machine.d.ts.map +1 -0
  28. package/dist/lib/recorder.d.ts +2 -1
  29. package/dist/lib/recorder.d.ts.map +1 -1
  30. package/dist/lib/recording-create-identity.d.ts +13 -0
  31. package/dist/lib/recording-create-identity.d.ts.map +1 -0
  32. package/dist/lib/release-install-policy.d.ts +31 -0
  33. package/dist/lib/release-install-policy.d.ts.map +1 -0
  34. package/dist/lib/transcriber.d.ts.map +1 -1
  35. package/dist/mcp/index.d.ts.map +1 -1
  36. package/dist/mcp/index.js +977 -194
  37. package/dist/sdk/index.js +5 -1
  38. package/dist/sdk/v1.generated.d.ts +4 -0
  39. package/dist/sdk/v1.generated.d.ts.map +1 -1
  40. package/dist/server/cloud-config.d.ts +9 -0
  41. package/dist/server/cloud-config.d.ts.map +1 -0
  42. package/dist/server/cloud-readiness.d.ts +10 -0
  43. package/dist/server/cloud-readiness.d.ts.map +1 -0
  44. package/dist/server/cloud.d.ts +9 -18
  45. package/dist/server/cloud.d.ts.map +1 -1
  46. package/dist/server/index.js +1654 -304
  47. package/dist/server/migrate-command.d.ts +11 -0
  48. package/dist/server/migrate-command.d.ts.map +1 -0
  49. package/dist/server/openapi.d.ts +22 -0
  50. package/dist/server/openapi.d.ts.map +1 -1
  51. package/dist/server/repo.d.ts +8 -1
  52. package/dist/server/repo.d.ts.map +1 -1
  53. package/dist/server/serve.d.ts +6 -1
  54. package/dist/server/serve.d.ts.map +1 -1
  55. package/dist/server/v1.d.ts.map +1 -1
  56. package/dist/storage.js +515 -80
  57. package/dist/store.d.ts +3 -1
  58. package/dist/store.d.ts.map +1 -1
  59. package/dist/types/index.d.ts +10 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/dist/version.d.ts +1 -1
  62. package/package.json +7 -4
  63. package/packaging/macos/Empty.entitlements +5 -0
  64. package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
  65. package/packaging/macos/Verifier.entitlements +10 -0
  66. package/packaging/macos/artifact-verifier.sb +33 -0
  67. package/packaging/macos/build_release_pkg.sh +872 -0
  68. package/packaging/macos/managed_bootstrap.sh +623 -0
  69. package/packaging/macos/pkgutil_fingerprint.awk +37 -0
  70. package/packaging/macos/release_lifecycle.ts +463 -0
  71. package/packaging/macos/scripts/postinstall +565 -0
  72. package/packaging/macos/scripts/preinstall +297 -0
  73. package/scripts/build_companion_cli.sh +337 -0
  74. package/scripts/build_native_fs_guard.sh +59 -0
  75. package/scripts/generate-sdk.ts +19 -1
  76. package/scripts/install_macos_app.sh +1922 -77
  77. package/scripts/macos_artifact.ts +5316 -0
  78. package/scripts/migrate.ts +38 -10
  79. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  80. package/scripts/native/recordings_fs_guard.c +1167 -0
  81. package/scripts/native_fs_guard.ts +158 -0
  82. package/scripts/release-guard.ts +20 -1
  83. package/scripts/resolve_tailscale_cli.sh +389 -0
  84. package/scripts/smoke_macos_app.sh +573 -0
  85. package/src/native/Recordings/App/ContentView.swift +83 -0
  86. package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
  87. package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
  88. package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
  89. package/src/native/Recordings/App/RecordingsApp.swift +367 -44
  90. package/src/native/Recordings/App/RecordingsListView.swift +130 -0
  91. package/src/native/Recordings/App/RecordingsStore.swift +206 -0
  92. package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
  93. package/src/native/Recordings/App/SidebarView.swift +212 -0
  94. package/src/native/Recordings/App/Theme.swift +87 -0
  95. package/src/native/Recordings/Package.resolved +5 -5
  96. package/src/native/Recordings/Package.swift +62 -2
  97. package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
  98. package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
  99. package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
  101. package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
  102. package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
  103. package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
  104. package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
  105. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
  106. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
  107. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
  108. package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
  109. package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
  110. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
  111. package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
  112. package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
  113. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
  114. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
  115. package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
  116. package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
  117. package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
  118. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
  119. package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
  120. package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
  121. package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
  122. package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
  123. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
  124. package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
  125. package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
  126. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
  127. package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
  128. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
  129. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
  130. package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
  131. package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
  132. package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
  133. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
  134. package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
  135. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
  136. package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
  137. package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
  138. package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
  139. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
  140. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
  141. package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
  142. package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
  143. package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
  144. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
  145. package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
  146. package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
  147. package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
  148. package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
  149. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
  150. package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
  151. package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
  152. package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
  153. package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
  154. package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
  155. package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
  156. package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
  157. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
  158. package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
  159. package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
  160. package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
  161. package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
  162. package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
  163. package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
  164. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
  165. package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
  166. package/src/native/Recordings/build.sh +1990 -29
  167. package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
@@ -1,19 +1,245 @@
1
- #!/usr/bin/env bash
2
- set -u
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ umask 077
4
+ export LC_ALL=C
5
+ export LANG=C
6
+ export TZ=UTC0
7
+ export PATH=/usr/bin:/bin:/usr/sbin:/sbin
3
8
 
4
- MODE="release"
5
- POSTINSTALL=0
9
+ # Release installs never resolve integrity-sensitive tools through PATH. The
10
+ # test overrides are accepted only when the real host kernel is not Darwin, so
11
+ # an inherited environment cannot replace pinned macOS tools in production.
12
+ if ! REAL_HOST_KERNEL="$(/usr/bin/uname -s)"; then
13
+ echo "Could not determine the host platform with pinned /usr/bin/uname." >&2
14
+ exit 1
15
+ fi
16
+ case "$REAL_HOST_KERNEL" in
17
+ Darwin|Linux) ;;
18
+ *)
19
+ echo "Unsupported host platform from pinned /usr/bin/uname: ${REAL_HOST_KERNEL:-<empty>}." >&2
20
+ exit 1
21
+ ;;
22
+ esac
23
+ if [ "$REAL_HOST_KERNEL" = "Darwin" ]; then
24
+ AWK_EXECUTABLE="/usr/bin/awk"
25
+ BASENAME_EXECUTABLE="/usr/bin/basename"
26
+ CHMOD_EXECUTABLE="/bin/chmod"
27
+ CODESIGN_EXECUTABLE="/usr/bin/codesign"
28
+ CP_EXECUTABLE="/bin/cp"
29
+ DATE_EXECUTABLE="/bin/date"
30
+ DD_EXECUTABLE="/bin/dd"
31
+ DF_EXECUTABLE="/bin/df"
32
+ DIFF_EXECUTABLE="/usr/bin/diff"
33
+ DIRNAME_EXECUTABLE="/usr/bin/dirname"
34
+ DITTO_EXECUTABLE="/usr/bin/ditto"
35
+ DU_EXECUTABLE="/usr/bin/du"
36
+ GREP_EXECUTABLE="/usr/bin/grep"
37
+ HEAD_EXECUTABLE="/usr/bin/head"
38
+ HOSTNAME_EXECUTABLE="/bin/hostname"
39
+ ID_EXECUTABLE="/usr/bin/id"
40
+ IOREG_EXECUTABLE="/usr/sbin/ioreg"
41
+ LS_EXECUTABLE="/bin/ls"
42
+ LSOF_EXECUTABLE="/usr/sbin/lsof"
43
+ MDFIND_EXECUTABLE="/usr/bin/mdfind"
44
+ MKDIR_EXECUTABLE="/bin/mkdir"
45
+ MKTEMP_EXECUTABLE="/usr/bin/mktemp"
46
+ MV_EXECUTABLE="/bin/mv"
47
+ OPEN_EXECUTABLE="/usr/bin/open"
48
+ PS_EXECUTABLE="/bin/ps"
49
+ RM_EXECUTABLE="/bin/rm"
50
+ RMDIR_EXECUTABLE="/bin/rmdir"
51
+ SED_EXECUTABLE="/usr/bin/sed"
52
+ SHASUM_EXECUTABLE="/usr/bin/shasum"
53
+ SLEEP_EXECUTABLE="/bin/sleep"
54
+ SPCTL_EXECUTABLE="/usr/sbin/spctl"
55
+ SQLITE3_EXECUTABLE="/usr/bin/sqlite3"
56
+ STAT_EXECUTABLE="/usr/bin/stat"
57
+ SW_VERS_EXECUTABLE="/usr/bin/sw_vers"
58
+ SYSPOLICY_CHECK_EXECUTABLE="/usr/bin/syspolicy_check"
59
+ TAIL_EXECUTABLE="/usr/bin/tail"
60
+ TR_EXECUTABLE="/usr/bin/tr"
61
+ UNAME_EXECUTABLE="/usr/bin/uname"
62
+ XCRUN_EXECUTABLE="/usr/bin/xcrun"
63
+ else
64
+ AWK_EXECUTABLE="${RECORDINGS_TEST_INSTALL_AWK_EXECUTABLE:-/usr/bin/awk}"
65
+ BASENAME_EXECUTABLE="${RECORDINGS_TEST_INSTALL_BASENAME_EXECUTABLE:-/usr/bin/basename}"
66
+ CHMOD_EXECUTABLE="${RECORDINGS_TEST_INSTALL_CHMOD_EXECUTABLE:-/bin/chmod}"
67
+ CODESIGN_EXECUTABLE="${RECORDINGS_TEST_INSTALL_CODESIGN_EXECUTABLE:-/usr/bin/codesign}"
68
+ CP_EXECUTABLE="${RECORDINGS_TEST_INSTALL_CP_EXECUTABLE:-/bin/cp}"
69
+ DATE_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DATE_EXECUTABLE:-/bin/date}"
70
+ DD_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DD_EXECUTABLE:-/bin/dd}"
71
+ DF_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DF_EXECUTABLE:-/bin/df}"
72
+ DIFF_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DIFF_EXECUTABLE:-/usr/bin/diff}"
73
+ DIRNAME_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DIRNAME_EXECUTABLE:-/usr/bin/dirname}"
74
+ DITTO_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DITTO_EXECUTABLE:-/usr/bin/ditto}"
75
+ DU_EXECUTABLE="${RECORDINGS_TEST_INSTALL_DU_EXECUTABLE:-/usr/bin/du}"
76
+ GREP_EXECUTABLE="${RECORDINGS_TEST_INSTALL_GREP_EXECUTABLE:-/usr/bin/grep}"
77
+ HEAD_EXECUTABLE="${RECORDINGS_TEST_INSTALL_HEAD_EXECUTABLE:-/usr/bin/head}"
78
+ HOSTNAME_EXECUTABLE="${RECORDINGS_TEST_INSTALL_HOSTNAME_EXECUTABLE:-/bin/hostname}"
79
+ ID_EXECUTABLE="${RECORDINGS_TEST_INSTALL_ID_EXECUTABLE:-/usr/bin/id}"
80
+ IOREG_EXECUTABLE="${RECORDINGS_TEST_INSTALL_IOREG_EXECUTABLE:-/usr/sbin/ioreg}"
81
+ LS_EXECUTABLE="${RECORDINGS_TEST_INSTALL_LS_EXECUTABLE:-/bin/ls}"
82
+ LSOF_EXECUTABLE="${RECORDINGS_TEST_INSTALL_LSOF_EXECUTABLE:-/usr/sbin/lsof}"
83
+ MDFIND_EXECUTABLE="${RECORDINGS_TEST_INSTALL_MDFIND_EXECUTABLE:-/usr/bin/mdfind}"
84
+ MKDIR_EXECUTABLE="${RECORDINGS_TEST_INSTALL_MKDIR_EXECUTABLE:-/bin/mkdir}"
85
+ MKTEMP_EXECUTABLE="${RECORDINGS_TEST_INSTALL_MKTEMP_EXECUTABLE:-/usr/bin/mktemp}"
86
+ MV_EXECUTABLE="${RECORDINGS_TEST_INSTALL_MV_EXECUTABLE:-/bin/mv}"
87
+ OPEN_EXECUTABLE="${RECORDINGS_TEST_INSTALL_OPEN_EXECUTABLE:-/usr/bin/open}"
88
+ PS_EXECUTABLE="${RECORDINGS_TEST_INSTALL_PS_EXECUTABLE:-/bin/ps}"
89
+ RM_EXECUTABLE="${RECORDINGS_TEST_INSTALL_RM_EXECUTABLE:-/bin/rm}"
90
+ RMDIR_EXECUTABLE="${RECORDINGS_TEST_INSTALL_RMDIR_EXECUTABLE:-/bin/rmdir}"
91
+ SED_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SED_EXECUTABLE:-/usr/bin/sed}"
92
+ SHASUM_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SHASUM_EXECUTABLE:-/usr/bin/shasum}"
93
+ SLEEP_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SLEEP_EXECUTABLE:-/bin/sleep}"
94
+ SPCTL_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SPCTL_EXECUTABLE:-/usr/sbin/spctl}"
95
+ SQLITE3_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SQLITE3_EXECUTABLE:-/usr/bin/sqlite3}"
96
+ STAT_EXECUTABLE="${RECORDINGS_TEST_INSTALL_STAT_EXECUTABLE:-/usr/bin/stat}"
97
+ SW_VERS_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SW_VERS_EXECUTABLE:-/usr/bin/sw_vers}"
98
+ SYSPOLICY_CHECK_EXECUTABLE="${RECORDINGS_TEST_INSTALL_SYSPOLICY_CHECK_EXECUTABLE:-/usr/bin/syspolicy_check}"
99
+ TAIL_EXECUTABLE="${RECORDINGS_TEST_INSTALL_TAIL_EXECUTABLE:-/usr/bin/tail}"
100
+ TR_EXECUTABLE="${RECORDINGS_TEST_INSTALL_TR_EXECUTABLE:-/usr/bin/tr}"
101
+ UNAME_EXECUTABLE="${RECORDINGS_TEST_INSTALL_UNAME_EXECUTABLE:-/usr/bin/uname}"
102
+ XCRUN_EXECUTABLE="${RECORDINGS_TEST_INSTALL_XCRUN_EXECUTABLE:-/usr/bin/xcrun}"
103
+ fi
104
+ BUN_EXECUTABLE="${RECORDINGS_BUN_EXECUTABLE:-}"
105
+
106
+ test_fault_hooks_enabled() {
107
+ [ "$REAL_HOST_KERNEL" != "Darwin" ] && \
108
+ [ "${RECORDINGS_TEST_ENABLE_RECOVERY_HOOKS:-0}" = 1 ]
109
+ }
110
+
111
+ require_absolute_executable() {
112
+ local label="$1"
113
+ local executable="$2"
114
+ case "$executable" in
115
+ /*) ;;
116
+ *) echo "${label} must be an absolute executable path." >&2; exit 1 ;;
117
+ esac
118
+ [ -x "$executable" ] || {
119
+ echo "${label} is missing or is not executable: ${executable}" >&2
120
+ exit 1
121
+ }
122
+ }
123
+
124
+ require_absolute_canonical_owned_home() {
125
+ local candidate="${HOME:-}"
126
+ local canonical
127
+ case "$candidate" in
128
+ /*) ;;
129
+ *) echo "Recordings.app installation requires HOME to be an absolute canonical path." >&2; exit 1 ;;
130
+ esac
131
+ [ -d "$candidate" ] && [ ! -L "$candidate" ] || {
132
+ echo "Recordings.app installation requires HOME to be an absolute canonical path to a non-symlink directory." >&2
133
+ exit 1
134
+ }
135
+ canonical="$(cd "$candidate" 2>/dev/null && builtin pwd -P)" || {
136
+ echo "Recordings.app installation could not resolve HOME canonically." >&2
137
+ exit 1
138
+ }
139
+ [ "$candidate" = "$canonical" ] || {
140
+ echo "Recordings.app installation requires HOME to be an absolute canonical path." >&2
141
+ exit 1
142
+ }
143
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$candidate")" = "$("$ID_EXECUTABLE" -u)" ] || {
144
+ echo "Home ancestor has an unexpected owner." >&2
145
+ exit 1
146
+ }
147
+ }
148
+
149
+ ARTIFACT_PATH=""
150
+ MANIFEST_PATH=""
151
+ EXPECTED_TEAM_ID="${RECORDINGS_EXPECTED_TEAM_IDENTIFIER:-}"
152
+ EXPECTED_MANIFEST_SHA256=""
153
+ EXPECTED_SOURCE_SHA=""
154
+ EXPECTED_VERSION=""
155
+ EXPECTED_OLD_IDENTITY_SHA256=""
156
+ EXPECTED_NEW_IDENTITY_SHA256=""
157
+ EXPECTED_HOSTNAME=""
158
+ EXPECTED_HOSTNAME_SET=0
159
+ ARTIFACT_POLICY="release"
160
+ APPROVED_TARGET="fleet"
161
+ APPROVED_TARGET_IDENTITY_KIND=""
162
+ APPROVED_TARGET_IDENTITY_SHA256="none"
163
+ ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS=0
164
+ ALLOW_IDENTITY_MIGRATION=0
165
+ LAUNCH_APP=0
166
+ LAUNCH_TIMEOUT_SECONDS="${RECORDINGS_LAUNCH_TIMEOUT_SECONDS:-10}"
6
167
 
7
168
  while [ "$#" -gt 0 ]; do
8
169
  case "$1" in
9
- --mode)
10
- MODE="${2:-release}"
170
+ --artifact)
171
+ ARTIFACT_PATH="${2:-}"
172
+ shift 2
173
+ ;;
174
+ --manifest)
175
+ MANIFEST_PATH="${2:-}"
176
+ shift 2
177
+ ;;
178
+ --expected-team-id)
179
+ EXPECTED_TEAM_ID="${2:-}"
180
+ shift 2
181
+ ;;
182
+ --manifest-sha256)
183
+ EXPECTED_MANIFEST_SHA256="${2:-}"
184
+ shift 2
185
+ ;;
186
+ --expected-source-sha)
187
+ EXPECTED_SOURCE_SHA="${2:-}"
188
+ shift 2
189
+ ;;
190
+ --expected-version)
191
+ EXPECTED_VERSION="${2:-}"
192
+ shift 2
193
+ ;;
194
+ --expected-hostname)
195
+ EXPECTED_HOSTNAME="${2:-}"
196
+ EXPECTED_HOSTNAME_SET=1
197
+ shift 2
198
+ ;;
199
+ --artifact-policy)
200
+ case "${2:-}" in
201
+ release) ARTIFACT_POLICY="release" ;;
202
+ local-only|local_only) ARTIFACT_POLICY="local_only" ;;
203
+ *) echo "Artifact policy must be release or local-only." >&2; exit 2 ;;
204
+ esac
205
+ shift 2
206
+ ;;
207
+ --approved-target)
208
+ APPROVED_TARGET="${2:-}"
209
+ shift 2
210
+ ;;
211
+ --approved-target-identity-sha256)
212
+ APPROVED_TARGET_IDENTITY_SHA256="${2:-}"
213
+ shift 2
214
+ ;;
215
+ --approved-target-identity-kind)
216
+ APPROVED_TARGET_IDENTITY_KIND="${2:-}"
11
217
  shift 2
12
218
  ;;
13
- --postinstall)
14
- POSTINSTALL=1
219
+ --acknowledge-local-signing-and-permissions)
220
+ ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS=1
15
221
  shift
16
222
  ;;
223
+ --expected-old-identity-sha256)
224
+ EXPECTED_OLD_IDENTITY_SHA256="${2:-}"
225
+ shift 2
226
+ ;;
227
+ --expected-new-identity-sha256)
228
+ EXPECTED_NEW_IDENTITY_SHA256="${2:-}"
229
+ shift 2
230
+ ;;
231
+ --allow-signing-identity-migration)
232
+ ALLOW_IDENTITY_MIGRATION=1
233
+ shift
234
+ ;;
235
+ --launch)
236
+ LAUNCH_APP=1
237
+ shift
238
+ ;;
239
+ --launch-timeout)
240
+ LAUNCH_TIMEOUT_SECONDS="${2:-}"
241
+ shift 2
242
+ ;;
17
243
  *)
18
244
  echo "Unknown argument: $1" >&2
19
245
  exit 2
@@ -21,105 +247,1724 @@ while [ "$#" -gt 0 ]; do
21
247
  esac
22
248
  done
23
249
 
24
- case "$MODE" in
25
- debug|release) ;;
26
- *)
27
- echo "Mode must be debug or release" >&2
250
+ if [ "$REAL_HOST_KERNEL" = "Darwin" ] && [ "$ARTIFACT_POLICY" = "release" ]; then
251
+ echo "Release installation requires the root-owned Recordings updater broker; the user-owned shell installer is local-development only." >&2
252
+ exit 1
253
+ fi
254
+
255
+ for tool_variable in \
256
+ AWK_EXECUTABLE BASENAME_EXECUTABLE CHMOD_EXECUTABLE CODESIGN_EXECUTABLE \
257
+ CP_EXECUTABLE DATE_EXECUTABLE DD_EXECUTABLE DF_EXECUTABLE DIFF_EXECUTABLE \
258
+ DIRNAME_EXECUTABLE DITTO_EXECUTABLE DU_EXECUTABLE GREP_EXECUTABLE \
259
+ HEAD_EXECUTABLE HOSTNAME_EXECUTABLE ID_EXECUTABLE IOREG_EXECUTABLE \
260
+ LS_EXECUTABLE LSOF_EXECUTABLE MDFIND_EXECUTABLE MKDIR_EXECUTABLE MKTEMP_EXECUTABLE \
261
+ MV_EXECUTABLE OPEN_EXECUTABLE PS_EXECUTABLE RM_EXECUTABLE RMDIR_EXECUTABLE \
262
+ SED_EXECUTABLE SHASUM_EXECUTABLE SLEEP_EXECUTABLE SPCTL_EXECUTABLE \
263
+ SQLITE3_EXECUTABLE STAT_EXECUTABLE SW_VERS_EXECUTABLE \
264
+ SYSPOLICY_CHECK_EXECUTABLE TAIL_EXECUTABLE TR_EXECUTABLE UNAME_EXECUTABLE \
265
+ XCRUN_EXECUTABLE; do
266
+ require_absolute_executable "$tool_variable" "${!tool_variable}"
267
+ done
268
+ require_absolute_executable "BUN_EXECUTABLE" "$BUN_EXECUTABLE"
269
+ require_absolute_canonical_owned_home
270
+
271
+ if [ "$("$UNAME_EXECUTABLE" -s)" != "Darwin" ]; then
272
+ echo "Recordings.app installation is only supported on macOS." >&2
273
+ exit 1
274
+ fi
275
+
276
+ if [ -z "$ARTIFACT_PATH" ] || [ -z "$MANIFEST_PATH" ] || \
277
+ [ -z "$EXPECTED_MANIFEST_SHA256" ] || [ -z "$EXPECTED_SOURCE_SHA" ] || \
278
+ [ -z "$EXPECTED_VERSION" ]; then
279
+ echo "Install requires artifact, manifest, authenticated manifest SHA-256, exact source SHA, and exact version." >&2
280
+ exit 2
281
+ fi
282
+ if [ "$EXPECTED_HOSTNAME_SET" -eq 1 ] && [ -z "$EXPECTED_HOSTNAME" ]; then
283
+ echo "Expected hostname must not be empty." >&2
284
+ exit 2
285
+ fi
286
+ WORK_DIR="$("$MKTEMP_EXECUTABLE" -d /tmp/recordings-install.XXXXXX)"
287
+ cleanup_preflight_work() {
288
+ local status=$?
289
+ trap - EXIT
290
+ "$RM_EXECUTABLE" -rf "$WORK_DIR"
291
+ exit "$status"
292
+ }
293
+ trap cleanup_preflight_work EXIT
294
+ if [ -n "$EXPECTED_HOSTNAME" ]; then
295
+ ACTUAL_HOSTNAME="$("$HOSTNAME_EXECUTABLE" -s)"
296
+ if [ "$ACTUAL_HOSTNAME" != "$EXPECTED_HOSTNAME" ]; then
297
+ echo "Install target hostname ${ACTUAL_HOSTNAME} does not match the expected hostname ${EXPECTED_HOSTNAME}." >&2
298
+ exit 1
299
+ fi
300
+ fi
301
+ if [ -z "$APPROVED_TARGET_IDENTITY_KIND" ]; then
302
+ if [ "$ARTIFACT_POLICY" = "release" ]; then
303
+ APPROVED_TARGET_IDENTITY_KIND="none"
304
+ else
305
+ # Schema-v3 artifacts created before the discriminator used the platform UUID hash.
306
+ APPROVED_TARGET_IDENTITY_KIND="hardware_uuid_sha256"
307
+ fi
308
+ fi
309
+ PACKAGE_ROOT="$(cd "$("$DIRNAME_EXECUTABLE" "${BASH_SOURCE[0]}")/.." && pwd)"
310
+ ARTIFACT_TOOL="${PACKAGE_ROOT}/scripts/macos_artifact.ts"
311
+ RUNTIME_SMOKE="${PACKAGE_ROOT}/scripts/smoke_macos_app.sh"
312
+ TAILSCALE_RESOLVER="${PACKAGE_ROOT}/scripts/resolve_tailscale_cli.sh"
313
+ if [ "$ARTIFACT_POLICY" = "release" ]; then
314
+ if [ -z "$EXPECTED_TEAM_ID" ]; then
315
+ echo "Release install requires --expected-team-id or RECORDINGS_EXPECTED_TEAM_IDENTIFIER." >&2
316
+ exit 2
317
+ fi
318
+ if [ "$APPROVED_TARGET" != "fleet" ]; then
319
+ echo "Release artifacts use the fleet target policy." >&2
320
+ exit 2
321
+ fi
322
+ if [ "$APPROVED_TARGET_IDENTITY_SHA256" != "none" ]; then
323
+ echo "Release artifacts do not accept a local-only target identity." >&2
324
+ exit 2
325
+ fi
326
+ if [ "$APPROVED_TARGET_IDENTITY_KIND" != "none" ]; then
327
+ echo "Release artifacts do not accept a local-only target identity kind." >&2
328
+ exit 2
329
+ fi
330
+ if [ "$ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS" -eq 1 ]; then
331
+ echo "Local-only acknowledgment cannot be supplied for a release artifact." >&2
332
+ exit 2
333
+ fi
334
+ else
335
+ if [ -n "$EXPECTED_TEAM_ID" ]; then
336
+ echo "Local-only artifacts do not accept --expected-team-id or a release TeamIdentifier environment value." >&2
337
+ exit 2
338
+ fi
339
+ if [ "$APPROVED_TARGET" != "station06" ]; then
340
+ echo "Local-only install is currently restricted to --approved-target station06." >&2
341
+ exit 2
342
+ fi
343
+ if ! [[ "$APPROVED_TARGET_IDENTITY_SHA256" =~ ^[a-f0-9]{64}$ ]]; then
344
+ echo "Local-only install requires --approved-target-identity-sha256 from the approved machine registry." >&2
345
+ exit 2
346
+ fi
347
+ case "$APPROVED_TARGET_IDENTITY_KIND" in
348
+ hardware_uuid_sha256|tailscale_node_id_sha256) ;;
349
+ *) echo "Local-only install requires a supported --approved-target-identity-kind." >&2; exit 2 ;;
350
+ esac
351
+ if [ "$ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS" -ne 1 ]; then
352
+ echo "Local-only install requires --acknowledge-local-signing-and-permissions because code identity can change and macOS may require Microphone or Accessibility reauthorization." >&2
353
+ exit 2
354
+ fi
355
+ ACTUAL_TARGET="$("$HOSTNAME_EXECUTABLE" -s)"
356
+ if [ "$ACTUAL_TARGET" != "$APPROVED_TARGET" ]; then
357
+ echo "Local-only artifact target ${APPROVED_TARGET} does not match this Mac (${ACTUAL_TARGET})." >&2
358
+ exit 1
359
+ fi
360
+ if [ "$APPROVED_TARGET_IDENTITY_KIND" = "tailscale_node_id_sha256" ]; then
361
+ if [ ! -r "$TAILSCALE_RESOLVER" ]; then
362
+ echo "Packaged Tailscale CLI resolver is missing." >&2
363
+ exit 1
364
+ fi
365
+ # shellcheck source=/dev/null
366
+ source "$TAILSCALE_RESOLVER"
367
+ if ! TAILSCALE_CLI="$(recordings_resolve_trusted_tailscale_app_cli "$WORK_DIR")"; then
368
+ echo "Tailscale is required to verify this local-only target identity." >&2
369
+ exit 1
370
+ fi
371
+ if ! ACTUAL_TARGET_IDENTITY_SHA256="$(recordings_run_trusted_tailscale_status "$TAILSCALE_CLI" "$WORK_DIR" | "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tailscale-node-id-sha256 --expected-hostname "$APPROVED_TARGET")"; then
372
+ echo "Could not verify the live Tailscale identity for this local-only target." >&2
373
+ exit 1
374
+ fi
375
+ else
376
+ ACTUAL_PLATFORM_ID="$("$IOREG_EXECUTABLE" -rd1 -c IOPlatformExpertDevice | "$AWK_EXECUTABLE" -F'"' '/IOPlatformUUID/ {print $(NF-1); exit}' | "$TR_EXECUTABLE" '[:upper:]' '[:lower:]')"
377
+ if [ -z "$ACTUAL_PLATFORM_ID" ]; then
378
+ echo "Could not read this Mac platform identity." >&2
379
+ exit 1
380
+ fi
381
+ ACTUAL_TARGET_IDENTITY_SHA256="$(printf '%s' "$ACTUAL_PLATFORM_ID" | "$SHASUM_EXECUTABLE" -a 256 | "$AWK_EXECUTABLE" '{print $1}')"
382
+ unset ACTUAL_PLATFORM_ID
383
+ fi
384
+ if [ "$ACTUAL_TARGET_IDENTITY_SHA256" != "$APPROVED_TARGET_IDENTITY_SHA256" ]; then
385
+ echo "Local-only artifact does not match this Mac's approved machine identity." >&2
386
+ exit 1
387
+ fi
388
+ if [ "$ALLOW_IDENTITY_MIGRATION" -eq 1 ] || [ -n "$EXPECTED_OLD_IDENTITY_SHA256" ] || [ -n "$EXPECTED_NEW_IDENTITY_SHA256" ]; then
389
+ echo "Release identity-migration flags are not valid for local-only artifacts." >&2
390
+ exit 2
391
+ fi
392
+ EXPECTED_TEAM_ID="ADHOC"
393
+ echo "WARNING: installing a non-notarized local-only artifact approved only for ${APPROVED_TARGET}." >&2
394
+ echo "WARNING: code identity can change; Microphone or Accessibility may require manual reauthorization." >&2
395
+ fi
396
+ if [ "$ARTIFACT_POLICY" = "release" ] && [ "$ALLOW_IDENTITY_MIGRATION" -eq 1 ] && \
397
+ { [ -z "$EXPECTED_OLD_IDENTITY_SHA256" ] || [ -z "$EXPECTED_NEW_IDENTITY_SHA256" ]; }; then
398
+ echo "Identity migration requires exact --expected-old-identity-sha256 and --expected-new-identity-sha256 values." >&2
399
+ exit 2
400
+ fi
401
+ if [ "$ARTIFACT_POLICY" = "release" ] && [ "$ALLOW_IDENTITY_MIGRATION" -eq 0 ] && \
402
+ { [ -n "$EXPECTED_OLD_IDENTITY_SHA256" ] || [ -n "$EXPECTED_NEW_IDENTITY_SHA256" ]; }; then
403
+ echo "Identity migration digests require --allow-signing-identity-migration." >&2
404
+ exit 2
405
+ fi
406
+ case "$LAUNCH_TIMEOUT_SECONDS" in
407
+ ''|*[!0-9]*)
408
+ echo "Launch timeout must be an integer number of seconds." >&2
28
409
  exit 2
29
410
  ;;
30
411
  esac
31
-
32
- PACKAGE_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
412
+ if [ "$LAUNCH_TIMEOUT_SECONDS" -lt 1 ] || [ "$LAUNCH_TIMEOUT_SECONDS" -gt 120 ]; then
413
+ echo "Launch timeout must be between 1 and 120 seconds." >&2
414
+ exit 2
415
+ fi
416
+ if [ ! -f "$ARTIFACT_PATH" ] || [ ! -f "$MANIFEST_PATH" ]; then
417
+ echo "Artifact ZIP or manifest does not exist." >&2
418
+ exit 1
419
+ fi
420
+ [ -x "$RUNTIME_SMOKE" ] || { echo "Packaged runtime smoke verifier is missing." >&2; exit 1; }
421
+ # Fail before creating Applications, state, lock, or maintenance paths when the
422
+ # fixed production descriptor guard prebuild is absent or unsafe.
423
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" native-fs-guard-check
33
424
  DATA_DIR="${HOME}/.hasna/recordings"
34
- NATIVE_DIR="${PACKAGE_ROOT}/src/native/Recordings"
35
- APP_SOURCE="${NATIVE_DIR}/.build/${MODE}/Recordings.app"
36
- APP_DEST="${DATA_DIR}/Recordings.app"
425
+ DATA_PARENT="$("$DIRNAME_EXECUTABLE" "$DATA_DIR")"
426
+ APP_DEST="${HOME}/Applications/Recordings.app"
427
+ APP_PARENT="$("$DIRNAME_EXECUTABLE" "$APP_DEST")"
428
+ ROLLBACK_DIR="${DATA_DIR}/rollbacks"
429
+ JOURNAL_PATH="${APP_PARENT}/.Recordings-install-transaction.json"
430
+ LOCK_DIR="${APP_PARENT}/.Recordings-install-lock"
431
+ MAINTENANCE_DIR="${DATA_PARENT}/.recordings-install-maintenance"
432
+ READER_LEASES_DIR="${DATA_PARENT}/.recordings-store-readers"
433
+ LOCK_OWNED=0
434
+ MAINTENANCE_OWNED=0
435
+ MAINTENANCE_CLAIM_DIR=""
436
+ PRESERVE_MAINTENANCE_MARKER=0
437
+ RECOVERED_APP_RESTART_ON_ABORT=0
438
+ TRANSACTION_COMMITTED=0
439
+ STAGING_REMOVAL_DELEGATED=0
440
+ TRANSACTION_DIR=""
441
+ TRANSACTION_NONCE=""
442
+ STOPPED_RUNNING_APP=0
443
+ was_running=0
444
+ OLD_PIDS=""
445
+ RUNNING_EXECUTABLES=()
446
+ PRIOR_RUNNING_APP_PATHS=()
447
+ RECOVERED_RUNNING_APP_PATHS=()
448
+ SQLITE_BARRIER_ACTIVE=0
449
+ SQLITE_BARRIER_PID=""
450
+ SQLITE_BARRIER_DIR=""
451
+ MOVED_ORIGINALS=()
452
+ MOVED_PATHS=()
453
+ MOVED_DIGESTS=()
454
+ MOVED_ORIGINAL_COUNT=0
455
+ INSTALLER_OWNED_STATE_PATHS=()
456
+ INSTALLER_OWNED_STATE_DIGESTS=()
37
457
 
38
- if [ "$(uname -s)" != "Darwin" ]; then
39
- exit 0
458
+ verify_secure_parent() {
459
+ local path="$1"
460
+ local private_mode="${2:-0}"
461
+ [ -d "$path" ] && [ ! -L "$path" ] || { echo "Secure install path must be a non-symlink directory: ${path}" >&2; exit 1; }
462
+ local actual_uid
463
+ local mode
464
+ actual_uid="$("$STAT_EXECUTABLE" -f '%u' "$path")"
465
+ mode="$("$STAT_EXECUTABLE" -f '%Lp' "$path")"
466
+ [ "$actual_uid" = "$("$ID_EXECUTABLE" -u)" ] || { echo "Secure install path has an unexpected owner: ${path}" >&2; exit 1; }
467
+ if [ "$private_mode" -eq 1 ] && [ "$mode" != 700 ]; then
468
+ echo "Private Recordings state path must have mode 700: ${path}" >&2
469
+ exit 1
470
+ fi
471
+ case "$mode" in
472
+ *[2367][0-7]|*[0-7][2367])
473
+ echo "Secure install path is group/world writable: ${path}" >&2
474
+ exit 1
475
+ ;;
476
+ esac
477
+ if "$LS_EXECUTABLE" -lde "$path" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
478
+ echo "Secure install path has an unexpected ACL: ${path}" >&2
479
+ exit 1
480
+ fi
481
+ }
482
+
483
+ verify_secure_file() {
484
+ local path="$1"
485
+ local label="$2"
486
+ if [ -e "$path" ] || [ -L "$path" ]; then
487
+ [ -f "$path" ] && [ ! -L "$path" ] && \
488
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$path")" = "$("$ID_EXECUTABLE" -u)" ] && \
489
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$path")" = 600 ] || {
490
+ echo "${label} has an unsafe type, owner, or mode." >&2
491
+ exit 1
492
+ }
493
+ if "$LS_EXECUTABLE" -lde "$path" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
494
+ echo "${label} has an unexpected ACL." >&2
495
+ exit 1
496
+ fi
497
+ fi
498
+ }
499
+
500
+ verify_safe_home_ancestor() {
501
+ local path="$1"
502
+ [ -d "$path" ] && [ ! -L "$path" ] || {
503
+ echo "Home ancestor must be a non-symlink directory." >&2
504
+ exit 1
505
+ }
506
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$path")" = "$("$ID_EXECUTABLE" -u)" ] || {
507
+ echo "Home ancestor has an unexpected owner." >&2
508
+ exit 1
509
+ }
510
+ home_mode="$("$STAT_EXECUTABLE" -f '%Lp' "$path")"
511
+ case "$home_mode" in
512
+ *[2367][0-7]|*[0-7][2367])
513
+ echo "Home ancestor is group/world writable." >&2
514
+ exit 1
515
+ ;;
516
+ esac
517
+ home_acl="$("$LS_EXECUTABLE" -lde "$path" | "$TAIL_EXECUTABLE" -n +2)"
518
+ if [ -n "$home_acl" ] && printf '%s\n' "$home_acl" | "$GREP_EXECUTABLE" -v ' deny ' | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
519
+ echo "Home ancestor has an ACL that grants access." >&2
520
+ exit 1
521
+ fi
522
+ }
523
+
524
+ verify_existing_state_root() {
525
+ [ -e "$DATA_DIR" ] || [ -L "$DATA_DIR" ] || return 0
526
+ [ -d "$DATA_DIR" ] && [ ! -L "$DATA_DIR" ] || {
527
+ echo "Recordings state path must be a non-symlink directory." >&2
528
+ exit 1
529
+ }
530
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$DATA_DIR")" = "$("$ID_EXECUTABLE" -u)" ] || {
531
+ echo "Recordings state path has an unexpected owner." >&2
532
+ exit 1
533
+ }
534
+ state_mode="$("$STAT_EXECUTABLE" -f '%Lp' "$DATA_DIR")"
535
+ case "$state_mode" in
536
+ 700|755) ;;
537
+ *) echo "Recordings state path must have mode 700 or the supported legacy mode 755." >&2; exit 1 ;;
538
+ esac
539
+ if "$LS_EXECUTABLE" -lde "$DATA_DIR" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
540
+ echo "Recordings state path has an unexpected ACL." >&2
541
+ exit 1
542
+ fi
543
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$DATA_DIR" --uid "$("$ID_EXECUTABLE" -u)"
544
+ }
545
+
546
+ # The target identity above and every existing state path are validated read-only
547
+ # before creating an install parent, lock, journal, or state child.
548
+ verify_safe_home_ancestor "$HOME"
549
+ if [ -e "$DATA_PARENT" ] || [ -L "$DATA_PARENT" ]; then
550
+ verify_secure_parent "$DATA_PARENT"
40
551
  fi
552
+ if [ -e "$APP_PARENT" ] || [ -L "$APP_PARENT" ]; then
553
+ verify_secure_parent "$APP_PARENT"
554
+ fi
555
+ verify_existing_state_root
556
+ [ ! -L "$ROLLBACK_DIR" ] || { echo "Recordings rollback parent must not be a symlink." >&2; exit 1; }
557
+
558
+ if [ ! -e "$APP_PARENT" ]; then
559
+ "$MKDIR_EXECUTABLE" -m 700 "$APP_PARENT"
560
+ fi
561
+ verify_secure_parent "$APP_PARENT"
562
+ verify_secure_file "$JOURNAL_PATH" "Install transaction journal"
563
+
564
+ release_install_lock() {
565
+ if [ "$LOCK_OWNED" -eq 1 ] && [ -d "$LOCK_DIR" ] && [ ! -L "$LOCK_DIR" ]; then
566
+ owner_pid="$("$SED_EXECUTABLE" -n '1p' "$LOCK_DIR/owner" 2>/dev/null || true)"
567
+ [ "$owner_pid" != "$$" ] || "$RM_EXECUTABLE" -rf "$LOCK_DIR"
568
+ fi
569
+ LOCK_OWNED=0
570
+ }
571
+
572
+ release_maintenance_marker() {
573
+ local marker_removed=0
574
+ if [ "$MAINTENANCE_OWNED" -eq 1 ] && [ -d "$MAINTENANCE_DIR" ] && [ ! -L "$MAINTENANCE_DIR" ]; then
575
+ owner_pid="$("$SED_EXECUTABLE" -n '1p' "$MAINTENANCE_DIR/owner" 2>/dev/null || true)"
576
+ if [ "$owner_pid" = "$$" ]; then
577
+ "$RM_EXECUTABLE" -rf "$MAINTENANCE_DIR"
578
+ marker_removed=1
579
+ fi
580
+ fi
581
+ if [ -n "$MAINTENANCE_CLAIM_DIR" ] && [ -d "$MAINTENANCE_CLAIM_DIR" ] && \
582
+ [ ! -L "$MAINTENANCE_CLAIM_DIR" ]; then
583
+ "$RM_EXECUTABLE" -rf "$MAINTENANCE_CLAIM_DIR"
584
+ fi
585
+ MAINTENANCE_CLAIM_DIR=""
586
+ MAINTENANCE_OWNED=0
587
+ if [ "$marker_removed" -eq 1 ]; then
588
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$DATA_PARENT"
589
+ fi
590
+ }
591
+
592
+ restart_recorded_app_paths() {
593
+ local app_path
594
+ local executable
595
+ for app_path in ${PRIOR_RUNNING_APP_PATHS[@]+"${PRIOR_RUNNING_APP_PATHS[@]}"}; do
596
+ [ -d "$app_path" ] && [ ! -L "$app_path" ] || continue
597
+ executable="$app_path/Contents/MacOS/Recordings"
598
+ if [ -z "$(pids_for_exact_executable "$executable")" ]; then
599
+ "$OPEN_EXECUTABLE" -n "$app_path" >/dev/null 2>&1 || true
600
+ fi
601
+ done
602
+ }
603
+
604
+ release_install_coordination() {
605
+ local status=0
606
+ release_sqlite_barrier || status=1
607
+ if [ "$PRESERVE_MAINTENANCE_MARKER" -eq 0 ] && ! release_maintenance_marker; then
608
+ RECOVERED_APP_RESTART_ON_ABORT=0
609
+ status=1
610
+ fi
611
+ if [ "$PRESERVE_MAINTENANCE_MARKER" -eq 0 ] && \
612
+ [ "$RECOVERED_APP_RESTART_ON_ABORT" -eq 1 ]; then
613
+ restart_recorded_app_paths
614
+ fi
615
+ release_install_lock
616
+ return "$status"
617
+ }
618
+
619
+ acquire_install_lock() {
620
+ local stale_claim
621
+ local owner_pid
622
+ local owner_start
623
+ local actual_start
624
+ local lock_age
625
+ local now
626
+ local stale_seconds="${RECORDINGS_LOCK_STALE_SECONDS:-30}"
627
+ case "$stale_seconds" in
628
+ ''|*[!0-9]*) echo "Install lock stale threshold must be an integer." >&2; exit 2 ;;
629
+ esac
630
+ if [ "$stale_seconds" -gt 3600 ]; then
631
+ echo "Install lock stale threshold must not exceed 3600 seconds." >&2
632
+ exit 2
633
+ fi
634
+ if [ "$stale_seconds" -lt 5 ]; then
635
+ echo "Install lock stale threshold must be at least 5 seconds." >&2
636
+ exit 2
637
+ fi
638
+ if ! "$MKDIR_EXECUTABLE" -m 700 "$LOCK_DIR" 2>/dev/null; then
639
+ [ -d "$LOCK_DIR" ] && [ ! -L "$LOCK_DIR" ] || {
640
+ echo "Install lock path is not a secure directory." >&2
641
+ exit 1
642
+ }
643
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$LOCK_DIR")" = "$("$ID_EXECUTABLE" -u)" ] && \
644
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$LOCK_DIR")" = 700 ] || {
645
+ echo "Install lock has unsafe ownership or mode." >&2
646
+ exit 1
647
+ }
648
+ if "$LS_EXECUTABLE" -lde "$LOCK_DIR" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
649
+ echo "Install lock has an unexpected ACL." >&2
650
+ exit 1
651
+ fi
652
+ if [ -e "$LOCK_DIR/owner" ] || [ -L "$LOCK_DIR/owner" ]; then
653
+ [ -f "$LOCK_DIR/owner" ] && [ ! -L "$LOCK_DIR/owner" ] && \
654
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$LOCK_DIR/owner")" = "$("$ID_EXECUTABLE" -u)" ] && \
655
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$LOCK_DIR/owner")" = 600 ] || {
656
+ echo "Install lock owner metadata has an unsafe type, owner, or mode." >&2
657
+ exit 1
658
+ }
659
+ if "$LS_EXECUTABLE" -lde "$LOCK_DIR/owner" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
660
+ echo "Install lock owner metadata has an unexpected ACL." >&2
661
+ exit 1
662
+ fi
663
+ fi
664
+ owner_pid="$("$SED_EXECUTABLE" -n '1p' "$LOCK_DIR/owner" 2>/dev/null || true)"
665
+ owner_start="$("$SED_EXECUTABLE" -n '2p' "$LOCK_DIR/owner" 2>/dev/null || true)"
666
+ actual_start=""
667
+ case "$owner_pid" in
668
+ ''|*[!0-9]*) ;;
669
+ *)
670
+ if kill -0 "$owner_pid" 2>/dev/null; then
671
+ actual_start="$("$PS_EXECUTABLE" -o lstart= -p "$owner_pid" 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
672
+ if [ -z "$owner_start" ] || [ -z "$actual_start" ] || [ "$owner_start" = "$actual_start" ]; then
673
+ echo "Another Recordings.app installer owns the active install lock." >&2
674
+ exit 1
675
+ fi
676
+ fi
677
+ ;;
678
+ esac
679
+ owner_valid=1
680
+ case "$owner_pid" in ''|*[!0-9]*) owner_valid=0 ;; esac
681
+ [ -n "$owner_start" ] || owner_valid=0
682
+ if [ "$owner_valid" -eq 0 ] || [ ! -f "$LOCK_DIR/owner" ] || [ -L "$LOCK_DIR/owner" ]; then
683
+ now="$("$DATE_EXECUTABLE" +%s)"
684
+ lock_age=$((now - $("$STAT_EXECUTABLE" -f '%m' "$LOCK_DIR")))
685
+ if [ "$lock_age" -lt "$stale_seconds" ]; then
686
+ echo "Recordings.app install lock is incomplete and too recent to reclaim safely." >&2
687
+ exit 1
688
+ fi
689
+ fi
690
+ stale_claim="${LOCK_DIR}.stale.$$"
691
+ if ! "$MV_EXECUTABLE" "$LOCK_DIR" "$stale_claim" 2>/dev/null; then
692
+ echo "Recordings.app install lock changed during stale-owner recovery." >&2
693
+ exit 1
694
+ fi
695
+ "$RM_EXECUTABLE" -rf "$stale_claim"
696
+ "$MKDIR_EXECUTABLE" -m 700 "$LOCK_DIR" || { echo "Could not acquire Recordings.app install lock." >&2; exit 1; }
697
+ fi
698
+ LOCK_OWNED=1
699
+ local start
700
+ start="$("$PS_EXECUTABLE" -o lstart= -p $$ 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
701
+ printf '%s\n%s\n' "$$" "$start" >"$LOCK_DIR/owner.tmp.$$"
702
+ "$CHMOD_EXECUTABLE" 600 "$LOCK_DIR/owner.tmp.$$"
703
+ "$MV_EXECUTABLE" "$LOCK_DIR/owner.tmp.$$" "$LOCK_DIR/owner"
704
+ }
705
+
706
+ acquire_maintenance_marker() {
707
+ local stale_claim
708
+ local owner_pid
709
+ local owner_start
710
+ local actual_start
711
+ local marker_age
712
+ local now
713
+ local owner_valid
714
+ local start
715
+ local stale_seconds="${RECORDINGS_MAINTENANCE_STALE_SECONDS:-30}"
716
+ case "$stale_seconds" in
717
+ ''|*[!0-9]*) echo "Install maintenance stale threshold must be an integer." >&2; exit 2 ;;
718
+ esac
719
+ if [ "$stale_seconds" -gt 3600 ] || [ "$stale_seconds" -lt 5 ]; then
720
+ echo "Install maintenance stale threshold must be between 5 and 3600 seconds." >&2
721
+ exit 2
722
+ fi
41
723
 
42
- mkdir -p "${DATA_DIR}/audio"
724
+ if [ -e "$MAINTENANCE_DIR" ] || [ -L "$MAINTENANCE_DIR" ]; then
725
+ [ -d "$MAINTENANCE_DIR" ] && [ ! -L "$MAINTENANCE_DIR" ] || {
726
+ echo "Install maintenance marker is not a secure directory." >&2
727
+ exit 1
728
+ }
729
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$MAINTENANCE_DIR")" = "$("$ID_EXECUTABLE" -u)" ] && \
730
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$MAINTENANCE_DIR")" = 700 ] || {
731
+ echo "Install maintenance marker has unsafe ownership or mode." >&2
732
+ exit 1
733
+ }
734
+ if "$LS_EXECUTABLE" -lde "$MAINTENANCE_DIR" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
735
+ echo "Install maintenance marker has an unexpected ACL." >&2
736
+ exit 1
737
+ fi
738
+ if [ -e "$MAINTENANCE_DIR/owner" ] || [ -L "$MAINTENANCE_DIR/owner" ]; then
739
+ [ -f "$MAINTENANCE_DIR/owner" ] && [ ! -L "$MAINTENANCE_DIR/owner" ] && \
740
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$MAINTENANCE_DIR/owner")" = "$("$ID_EXECUTABLE" -u)" ] && \
741
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$MAINTENANCE_DIR/owner")" = 600 ] || {
742
+ echo "Install maintenance owner metadata has an unsafe type, owner, or mode." >&2
743
+ exit 1
744
+ }
745
+ if "$LS_EXECUTABLE" -lde "$MAINTENANCE_DIR/owner" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
746
+ echo "Install maintenance owner metadata has an unexpected ACL." >&2
747
+ exit 1
748
+ fi
749
+ fi
750
+ owner_pid="$("$SED_EXECUTABLE" -n '1p' "$MAINTENANCE_DIR/owner" 2>/dev/null || true)"
751
+ owner_start="$("$SED_EXECUTABLE" -n '2p' "$MAINTENANCE_DIR/owner" 2>/dev/null || true)"
752
+ actual_start=""
753
+ case "$owner_pid" in
754
+ ''|*[!0-9]*) ;;
755
+ *)
756
+ if kill -0 "$owner_pid" 2>/dev/null; then
757
+ actual_start="$("$PS_EXECUTABLE" -o lstart= -p "$owner_pid" 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
758
+ if [ -z "$owner_start" ] || [ -z "$actual_start" ] || [ "$owner_start" = "$actual_start" ]; then
759
+ echo "Recordings storage is already in active installation maintenance." >&2
760
+ exit 1
761
+ fi
762
+ fi
763
+ ;;
764
+ esac
765
+ owner_valid=1
766
+ case "$owner_pid" in ''|*[!0-9]*) owner_valid=0 ;; esac
767
+ [ -n "$owner_start" ] || owner_valid=0
768
+ if [ "$owner_valid" -eq 0 ] || [ ! -f "$MAINTENANCE_DIR/owner" ] || [ -L "$MAINTENANCE_DIR/owner" ]; then
769
+ echo "Install maintenance marker has incomplete ownership evidence; refusing automatic recovery." >&2
770
+ exit 1
771
+ fi
772
+ stale_claim="${MAINTENANCE_DIR}.stale.$$"
773
+ if ! "$MV_EXECUTABLE" "$MAINTENANCE_DIR" "$stale_claim" 2>/dev/null; then
774
+ echo "Install maintenance marker changed during stale-owner recovery." >&2
775
+ exit 1
776
+ fi
777
+ "$RM_EXECUTABLE" -rf "$stale_claim"
778
+ fi
43
779
 
44
- warn_or_fail() {
45
- local message="$1"
46
- if [ "$POSTINSTALL" -eq 1 ]; then
47
- echo "Recordings.app was not installed: ${message}" >&2
48
- echo "Run 'recordings app install' after fixing this machine." >&2
49
- exit 0
780
+ # Publish only a complete marker. Store clients can now treat the canonical
781
+ # path as an atomic maintenance claim: owner metadata is private, durable,
782
+ # and validated before the directory becomes visible at MAINTENANCE_DIR.
783
+ start="$("$PS_EXECUTABLE" -o lstart= -p $$ 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
784
+ [ -n "$start" ] || {
785
+ echo "Could not establish maintenance marker process identity." >&2
786
+ exit 1
787
+ }
788
+ MAINTENANCE_CLAIM_DIR="$("$MKTEMP_EXECUTABLE" -d "${MAINTENANCE_DIR}.claim.XXXXXX")"
789
+ "$CHMOD_EXECUTABLE" 700 "$MAINTENANCE_CLAIM_DIR"
790
+ printf '%s\n%s\n' "$$" "$start" >"$MAINTENANCE_CLAIM_DIR/owner"
791
+ "$CHMOD_EXECUTABLE" 600 "$MAINTENANCE_CLAIM_DIR/owner"
792
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$MAINTENANCE_CLAIM_DIR"
793
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$DATA_PARENT"
794
+ if test_fault_hooks_enabled && \
795
+ [ "${RECORDINGS_TEST_CRASH_DURING_MAINTENANCE_CLAIM:-}" = "before-rename" ]; then
796
+ kill -KILL "$$"
797
+ fi
798
+ [ ! -e "$MAINTENANCE_DIR" ] && [ ! -L "$MAINTENANCE_DIR" ] || {
799
+ echo "Recordings storage maintenance marker appeared during atomic claim." >&2
800
+ exit 1
801
+ }
802
+ "$MV_EXECUTABLE" "$MAINTENANCE_CLAIM_DIR" "$MAINTENANCE_DIR"
803
+ MAINTENANCE_CLAIM_DIR=""
804
+ MAINTENANCE_OWNED=1
805
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$DATA_PARENT"
806
+ if test_fault_hooks_enabled && \
807
+ [ "${RECORDINGS_TEST_CRASH_DURING_MAINTENANCE_CLAIM:-}" = "after-rename" ]; then
808
+ kill -KILL "$$"
50
809
  fi
51
- echo "${message}" >&2
810
+ }
811
+
812
+ drain_store_reader_leases() {
813
+ local timeout_ms="${RECORDINGS_READER_DRAIN_TIMEOUT_MS:-30000}"
814
+ local attempts
815
+ local attempt=0
816
+ local active_count
817
+ local lease
818
+ local owner_pid
819
+ local owner_start
820
+ local actual_start
821
+ local stale_claim
822
+ case "$timeout_ms" in
823
+ ''|*[!0-9]*) echo "Store reader drain timeout must be an integer." >&2; exit 2 ;;
824
+ esac
825
+ if [ "$timeout_ms" -lt 1000 ] || [ "$timeout_ms" -gt 120000 ]; then
826
+ echo "Store reader drain timeout must be between 1000 and 120000 milliseconds." >&2
827
+ exit 2
828
+ fi
829
+ [ -e "$READER_LEASES_DIR" ] || [ -L "$READER_LEASES_DIR" ] || return 0
830
+ [ -d "$READER_LEASES_DIR" ] && [ ! -L "$READER_LEASES_DIR" ] && \
831
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$READER_LEASES_DIR")" = "$("$ID_EXECUTABLE" -u)" ] && \
832
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$READER_LEASES_DIR")" = 700 ] || {
833
+ echo "Store reader lease root has an unsafe type, owner, or mode." >&2
834
+ exit 1
835
+ }
836
+ if "$LS_EXECUTABLE" -lde "$READER_LEASES_DIR" | "$TAIL_EXECUTABLE" -n +2 | "$GREP_EXECUTABLE" -q '[^[:space:]]'; then
837
+ echo "Store reader lease root has an unexpected ACL." >&2
838
+ exit 1
839
+ fi
840
+
841
+ attempts=$((timeout_ms / 50))
842
+ while [ "$attempt" -lt "$attempts" ]; do
843
+ active_count=0
844
+ for lease in "$READER_LEASES_DIR"/lease-*; do
845
+ [ -e "$lease" ] || [ -L "$lease" ] || continue
846
+ [ -d "$lease" ] && [ ! -L "$lease" ] && \
847
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$lease" 2>/dev/null || true)" = "$("$ID_EXECUTABLE" -u)" ] && \
848
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$lease" 2>/dev/null || true)" = 700 ] || {
849
+ [ ! -e "$lease" ] && continue
850
+ echo "Store reader lease has an unsafe type, owner, or mode." >&2
851
+ exit 1
852
+ }
853
+ if [ ! -e "$lease/owner" ] && [ ! -L "$lease/owner" ]; then
854
+ # A compliant reader may have completed and removed its lease between
855
+ # the directory scan and this check. A still-present incomplete lease
856
+ # is never reclaimed because its ownership cannot be proven.
857
+ [ ! -e "$lease" ] && continue
858
+ echo "Store reader lease is missing owner metadata; refusing maintenance." >&2
859
+ exit 1
860
+ fi
861
+ [ -f "$lease/owner" ] && [ ! -L "$lease/owner" ] && \
862
+ [ "$("$STAT_EXECUTABLE" -f '%u' "$lease/owner" 2>/dev/null || true)" = "$("$ID_EXECUTABLE" -u)" ] && \
863
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$lease/owner" 2>/dev/null || true)" = 600 ] || {
864
+ [ ! -e "$lease" ] && continue
865
+ echo "Store reader lease owner metadata is unsafe." >&2
866
+ exit 1
867
+ }
868
+ owner_pid="$("$SED_EXECUTABLE" -n '1p' "$lease/owner" 2>/dev/null || true)"
869
+ owner_start="$("$SED_EXECUTABLE" -n '2p' "$lease/owner" 2>/dev/null || true)"
870
+ [ -e "$lease" ] || continue
871
+ case "$owner_pid" in
872
+ ''|*[!0-9]*) echo "Store reader lease has an invalid owner PID." >&2; exit 1 ;;
873
+ esac
874
+ [ -n "$owner_start" ] || {
875
+ echo "Store reader lease has an invalid process identity." >&2
876
+ exit 1
877
+ }
878
+ actual_start="$("$PS_EXECUTABLE" -o lstart= -p "$owner_pid" 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
879
+ if [ "$actual_start" = "$owner_start" ]; then
880
+ active_count=$((active_count + 1))
881
+ continue
882
+ fi
883
+ if [ -z "$actual_start" ] && kill -0 "$owner_pid" 2>/dev/null; then
884
+ echo "Store reader process exists but its start identity cannot be verified." >&2
885
+ exit 1
886
+ fi
887
+ # Empty process identity means the recorded PID no longer exists; a
888
+ # different identity means the PID has been reused. In both cases the
889
+ # exact reader that created this lease is provably dead.
890
+ stale_claim="${lease}.stale.$$"
891
+ if ! "$MV_EXECUTABLE" "$lease" "$stale_claim" 2>/dev/null; then
892
+ [ ! -e "$lease" ] && continue
893
+ echo "Store reader lease changed during dead-owner recovery." >&2
894
+ exit 1
895
+ fi
896
+ "$RM_EXECUTABLE" -rf "$stale_claim"
897
+ done
898
+ [ "$active_count" -eq 0 ] && return 0
899
+ attempt=$((attempt + 1))
900
+ "$SLEEP_EXECUTABLE" 0.05
901
+ done
902
+ echo "Timed out waiting for active local Store operations to finish." >&2
52
903
  exit 1
53
904
  }
54
905
 
55
- if ! command -v swift >/dev/null 2>&1; then
56
- warn_or_fail "Swift toolchain not found"
906
+ validate_sqlite_busy_timeout() {
907
+ local busy_timeout_ms="${RECORDINGS_SQLITE_BUSY_TIMEOUT_MS:-30000}"
908
+ case "$busy_timeout_ms" in
909
+ ''|*[!0-9]*) echo "SQLite busy timeout must be an integer." >&2; return 2 ;;
910
+ esac
911
+ if [ "$busy_timeout_ms" -lt 1000 ] || [ "$busy_timeout_ms" -gt 120000 ]; then
912
+ echo "SQLite busy timeout must be between 1000 and 120000 milliseconds." >&2
913
+ return 2
914
+ fi
915
+ printf '%s\n' "$busy_timeout_ms"
916
+ }
917
+
918
+ canonical_database_is_sqlite() {
919
+ local database_path="${DATA_DIR}/recordings.db"
920
+ local database_header
921
+ [ -e "$database_path" ] || [ -L "$database_path" ] || return 1
922
+ [ -f "$database_path" ] && [ ! -L "$database_path" ] || {
923
+ echo "Canonical Recordings database must be a non-symlink regular file." >&2
924
+ return 2
925
+ }
926
+ # Legacy installs and lifecycle fixtures may preserve an opaque file at this
927
+ # path. Such a file cannot have SQLite readers/writers to drain; preserve it
928
+ # transactionally without asking sqlite3 to parse or mutate it.
929
+ database_header="$("$DD_EXECUTABLE" if="$database_path" bs=15 count=1 2>/dev/null || true)"
930
+ [ "$database_header" = "SQLite format 3" ]
931
+ }
932
+
933
+ acquire_sqlite_barrier() {
934
+ local database_path="${DATA_DIR}/recordings.db"
935
+ local checkpoint_output
936
+ local checkpoint_busy
937
+ local busy_timeout_ms
938
+ local parent_start
939
+ local attempts
940
+ local attempt=0
941
+ [ "$SQLITE_BARRIER_ACTIVE" -eq 0 ] || {
942
+ echo "Canonical Recordings database barrier is already active." >&2
943
+ return 1
944
+ }
945
+ if canonical_database_is_sqlite; then
946
+ :
947
+ else
948
+ local database_status=$?
949
+ [ "$database_status" -eq 1 ] && return 0
950
+ return "$database_status"
951
+ fi
952
+ busy_timeout_ms="$(validate_sqlite_busy_timeout)" || return $?
953
+ if ! checkpoint_output="$("$SQLITE3_EXECUTABLE" -batch "$database_path" 2>/dev/null <<SQL
954
+ .timeout ${busy_timeout_ms}
955
+ PRAGMA wal_checkpoint(TRUNCATE);
956
+ SQL
957
+ )"; then
958
+ echo "Could not checkpoint the canonical Recordings database; refusing maintenance." >&2
959
+ return 1
960
+ fi
961
+ checkpoint_busy="${checkpoint_output%%|*}"
962
+ if [ "$checkpoint_busy" != 0 ]; then
963
+ echo "Canonical Recordings database checkpoint remained busy; refusing maintenance." >&2
964
+ return 1
965
+ fi
966
+
967
+ SQLITE_BARRIER_DIR="$("$MKTEMP_EXECUTABLE" -d "${DATA_PARENT}/.recordings-sqlite-barrier.XXXXXX")"
968
+ "$CHMOD_EXECUTABLE" 700 "$SQLITE_BARRIER_DIR"
969
+ parent_start="$("$PS_EXECUTABLE" -o lstart= -p $$ 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
970
+ [ -n "$parent_start" ] || {
971
+ echo "Could not establish SQLite barrier owner identity." >&2
972
+ "$RM_EXECUTABLE" -rf "$SQLITE_BARRIER_DIR"
973
+ SQLITE_BARRIER_DIR=""
974
+ return 1
975
+ }
976
+ (
977
+ {
978
+ printf '%s\n' '.bail on' ".timeout ${busy_timeout_ms}" 'BEGIN EXCLUSIVE;' \
979
+ "SELECT 'RECORDINGS_SQLITE_BARRIER_READY';"
980
+ while [ ! -e "$SQLITE_BARRIER_DIR/release" ]; do
981
+ actual_parent_start="$("$PS_EXECUTABLE" -o lstart= -p $$ 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
982
+ [ "$actual_parent_start" = "$parent_start" ] || break
983
+ "$SLEEP_EXECUTABLE" 0.05
984
+ done
985
+ printf '%s\n' 'COMMIT;'
986
+ } | "$SQLITE3_EXECUTABLE" -batch "$database_path" \
987
+ >"$SQLITE_BARRIER_DIR/ready" 2>"$SQLITE_BARRIER_DIR/error"
988
+ ) &
989
+ SQLITE_BARRIER_PID=$!
990
+ attempts=$((busy_timeout_ms / 50 + 20))
991
+ while [ "$attempt" -lt "$attempts" ]; do
992
+ if "$GREP_EXECUTABLE" -q '^RECORDINGS_SQLITE_BARRIER_READY$' \
993
+ "$SQLITE_BARRIER_DIR/ready" 2>/dev/null; then
994
+ SQLITE_BARRIER_ACTIVE=1
995
+ return 0
996
+ fi
997
+ if ! kill -0 "$SQLITE_BARRIER_PID" 2>/dev/null; then
998
+ wait "$SQLITE_BARRIER_PID" 2>/dev/null || true
999
+ echo "Could not acquire an exclusive canonical Recordings database barrier." >&2
1000
+ "$RM_EXECUTABLE" -rf "$SQLITE_BARRIER_DIR"
1001
+ SQLITE_BARRIER_PID=""
1002
+ SQLITE_BARRIER_DIR=""
1003
+ return 1
1004
+ fi
1005
+ attempt=$((attempt + 1))
1006
+ "$SLEEP_EXECUTABLE" 0.05
1007
+ done
1008
+ : >"$SQLITE_BARRIER_DIR/release"
1009
+ wait "$SQLITE_BARRIER_PID" 2>/dev/null || true
1010
+ echo "Timed out acquiring an exclusive canonical Recordings database barrier." >&2
1011
+ "$RM_EXECUTABLE" -rf "$SQLITE_BARRIER_DIR"
1012
+ SQLITE_BARRIER_PID=""
1013
+ SQLITE_BARRIER_DIR=""
1014
+ return 1
1015
+ }
1016
+
1017
+ release_sqlite_barrier() {
1018
+ local status=0
1019
+ if [ "$SQLITE_BARRIER_ACTIVE" -eq 1 ] && [ -n "$SQLITE_BARRIER_DIR" ]; then
1020
+ : >"$SQLITE_BARRIER_DIR/release"
1021
+ if ! wait "$SQLITE_BARRIER_PID"; then
1022
+ echo "Canonical Recordings database barrier did not close cleanly." >&2
1023
+ status=1
1024
+ fi
1025
+ fi
1026
+ SQLITE_BARRIER_ACTIVE=0
1027
+ SQLITE_BARRIER_PID=""
1028
+ if [ -n "$SQLITE_BARRIER_DIR" ]; then
1029
+ "$RM_EXECUTABLE" -rf "$SQLITE_BARRIER_DIR"
1030
+ fi
1031
+ SQLITE_BARRIER_DIR=""
1032
+ return "$status"
1033
+ }
1034
+
1035
+ quiesce_canonical_database() {
1036
+ acquire_sqlite_barrier
1037
+ release_sqlite_barrier
1038
+ }
1039
+
1040
+ cleanup_install_coordination_and_work() {
1041
+ local status=$?
1042
+ trap - EXIT
1043
+ release_install_coordination || status=1
1044
+ "$RM_EXECUTABLE" -rf "$WORK_DIR"
1045
+ exit "$status"
1046
+ }
1047
+ trap cleanup_install_coordination_and_work EXIT
1048
+ acquire_install_lock
1049
+ if [ ! -e "$DATA_PARENT" ]; then
1050
+ "$MKDIR_EXECUTABLE" -m 700 "$DATA_PARENT"
1051
+ fi
1052
+ verify_secure_parent "$DATA_PARENT"
1053
+ acquire_maintenance_marker
1054
+ drain_store_reader_leases
1055
+ acquire_sqlite_barrier
1056
+ if test_fault_hooks_enabled && [ -n "${RECORDINGS_TEST_HOLD_AFTER_LOCK_SECONDS:-}" ]; then
1057
+ "$SLEEP_EXECUTABLE" "$RECORDINGS_TEST_HOLD_AFTER_LOCK_SECONDS"
1058
+ fi
1059
+
1060
+ pids_for_exact_executable() {
1061
+ local expected="$1"
1062
+ local pid
1063
+ local command
1064
+ local start_identity
1065
+ local observed_executable
1066
+ while read -r pid command; do
1067
+ [ -n "${pid:-}" ] || continue
1068
+ case "$command" in
1069
+ "$expected"|"$expected "*)
1070
+ start_identity="$(process_start_identity "$pid")"
1071
+ observed_executable="$(observed_executable_for_pid "$pid" "$expected")"
1072
+ if [ -n "$start_identity" ] && [ "$observed_executable" = "$expected" ]; then
1073
+ printf '%s\t%s\t%s\n' "$pid" "$start_identity" "$observed_executable"
1074
+ fi
1075
+ ;;
1076
+ esac
1077
+ done < <("$PS_EXECUTABLE" -axo pid=,command= 2>/dev/null || true)
1078
+ }
1079
+
1080
+ process_start_identity() {
1081
+ local pid="$1"
1082
+ "$PS_EXECUTABLE" -o lstart= -p "$pid" 2>/dev/null | \
1083
+ "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true
1084
+ }
1085
+
1086
+ observed_executable_for_pid() {
1087
+ local pid="$1"
1088
+ local expected="$2"
1089
+ "$LSOF_EXECUTABLE" -a -p "$pid" -d txt -Fn 2>/dev/null | \
1090
+ "$SED_EXECUTABLE" -n 's/^n//p' | \
1091
+ "$AWK_EXECUTABLE" -v expected="$expected" '$0 == expected { found = 1 } END { if (found) print expected }' || true
1092
+ }
1093
+
1094
+ process_identity_is_current() {
1095
+ local pid="$1"
1096
+ local expected_start="$2"
1097
+ local expected_executable="$3"
1098
+ local actual_start
1099
+ local actual_executable
1100
+ actual_start="$(process_start_identity "$pid")"
1101
+ [ -n "$actual_start" ] && [ "$actual_start" = "$expected_start" ] || return 1
1102
+ actual_executable="$(observed_executable_for_pid "$pid" "$expected_executable")"
1103
+ [ "$actual_executable" = "$expected_executable" ]
1104
+ }
1105
+
1106
+ signal_process_if_current() {
1107
+ local signal="$1"
1108
+ local pid="$2"
1109
+ local expected_start="$3"
1110
+ local expected_executable="$4"
1111
+ process_identity_is_current "$pid" "$expected_start" "$expected_executable" || return 0
1112
+ kill "-${signal}" "$pid" 2>/dev/null || true
1113
+ }
1114
+
1115
+ stop_uncommitted_candidate() {
1116
+ local expected="$APP_DEST/Contents/MacOS/Recordings"
1117
+ local candidate_records
1118
+ local pid
1119
+ local start_identity
1120
+ local observed_executable
1121
+ local attempts
1122
+ local attempt
1123
+ local remaining
1124
+ candidate_records="$(pids_for_exact_executable "$expected")"
1125
+ [ -n "$candidate_records" ] || return 0
1126
+ while IFS=$'\t' read -r pid start_identity observed_executable; do
1127
+ [ -n "$pid" ] || continue
1128
+ signal_process_if_current TERM "$pid" "$start_identity" "$observed_executable"
1129
+ done <<< "$candidate_records"
1130
+ attempts=$((LAUNCH_TIMEOUT_SECONDS * 10))
1131
+ for ((attempt = 0; attempt < attempts; attempt++)); do
1132
+ remaining="$(pids_for_exact_executable "$expected")"
1133
+ [ -z "$remaining" ] && return 0
1134
+ "$SLEEP_EXECUTABLE" 0.1
1135
+ done
1136
+ while IFS=$'\t' read -r pid start_identity observed_executable; do
1137
+ [ -n "$pid" ] || continue
1138
+ signal_process_if_current KILL "$pid" "$start_identity" "$observed_executable"
1139
+ done <<< "$remaining"
1140
+ "$SLEEP_EXECUTABLE" 0.1
1141
+ [ -z "$(pids_for_exact_executable "$expected")" ] || {
1142
+ echo "Uncommitted Recordings.app process could not be stopped before recovery." >&2
1143
+ return 1
1144
+ }
1145
+ }
1146
+
1147
+ if [ -f "$JOURNAL_PATH" ]; then
1148
+ echo "Recovering incomplete Recordings.app installation transaction." >&2
1149
+ if ! RECOVER_WAS_RUNNING="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field was_running)" || \
1150
+ ! RECOVER_PHASE="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field phase)" || \
1151
+ ! RECOVER_RUNNING_APP_PATHS_JSON="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field prior_running_app_paths)"; then
1152
+ PRESERVE_MAINTENANCE_MARKER=1
1153
+ echo "Could not read startup recovery evidence; maintenance remains fail-closed." >&2
1154
+ exit 1
1155
+ fi
1156
+ case "$RECOVER_PHASE" in
1157
+ candidate-moving|candidate-installed|activated|launching)
1158
+ if ! stop_uncommitted_candidate; then
1159
+ PRESERVE_MAINTENANCE_MARKER=1
1160
+ echo "Could not stop the uncommitted candidate during startup recovery; maintenance remains fail-closed." >&2
1161
+ exit 1
1162
+ fi
1163
+ ;;
1164
+ esac
1165
+ if ! "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-recover --journal "$JOURNAL_PATH"; then
1166
+ PRESERVE_MAINTENANCE_MARKER=1
1167
+ echo "Startup recovery failed; maintenance remains fail-closed." >&2
1168
+ exit 1
1169
+ fi
1170
+ if [ "$RECOVER_WAS_RUNNING" = 1 ]; then
1171
+ # Do not relaunch the recovered app while this install still owns storage
1172
+ # exclusivity. A later safe abort relaunches it after releasing the marker;
1173
+ # a successful install carries the running intent to the new app.
1174
+ was_running=1
1175
+ STOPPED_RUNNING_APP=1
1176
+ RECOVERED_APP_RESTART_ON_ABORT=1
1177
+ while IFS= read -r recovered_app_path; do
1178
+ [ -n "$recovered_app_path" ] || continue
1179
+ RECOVERED_RUNNING_APP_PATHS+=("$recovered_app_path")
1180
+ PRIOR_RUNNING_APP_PATHS+=("$recovered_app_path")
1181
+ done < <("$BUN_EXECUTABLE" -e '
1182
+ const paths = JSON.parse(process.argv[1]);
1183
+ if (!Array.isArray(paths)) process.exit(1);
1184
+ for (const path of paths) console.log(path);
1185
+ ' "$RECOVER_RUNNING_APP_PATHS_JSON")
1186
+ fi
57
1187
  fi
1188
+ release_sqlite_barrier
58
1189
 
59
- if [ ! -d "$NATIVE_DIR" ]; then
60
- warn_or_fail "native app sources are missing from package: ${NATIVE_DIR}"
1190
+ # Recheck under the install lock. New state is private from creation; legacy
1191
+ # state remains read-only until its original mode is in the durable journal.
1192
+ verify_existing_state_root
1193
+ if [ ! -e "$DATA_PARENT" ]; then
1194
+ "$MKDIR_EXECUTABLE" -m 700 "$DATA_PARENT"
1195
+ fi
1196
+ verify_secure_parent "$DATA_PARENT"
1197
+ if [ ! -e "$DATA_DIR" ]; then
1198
+ "$MKDIR_EXECUTABLE" -m 700 "$DATA_DIR"
61
1199
  fi
1200
+ verify_existing_state_root
1201
+ ORIGINAL_STATE_MODE="$("$STAT_EXECUTABLE" -f '%Lp' "$DATA_DIR")"
62
1202
 
63
- (
64
- cd "$NATIVE_DIR" || exit 1
65
- ./build.sh "$MODE"
66
- ) || warn_or_fail "native app build failed"
1203
+ UNPACK_DIR="${WORK_DIR}/unpacked"
1204
+ ARTIFACT_SNAPSHOT="${WORK_DIR}/$("$BASENAME_EXECUTABLE" "$ARTIFACT_PATH")"
1205
+ MANIFEST_SNAPSHOT="${WORK_DIR}/$("$BASENAME_EXECUTABLE" "$MANIFEST_PATH")"
1206
+ "$MKDIR_EXECUTABLE" -p "$UNPACK_DIR"
1207
+ "$CP_EXECUTABLE" "$ARTIFACT_PATH" "$ARTIFACT_SNAPSHOT"
1208
+ "$CP_EXECUTABLE" "$MANIFEST_PATH" "$MANIFEST_SNAPSHOT"
1209
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-archive \
1210
+ --archive "$ARTIFACT_SNAPSHOT" \
1211
+ --manifest "$MANIFEST_SNAPSHOT" \
1212
+ --team-id "$EXPECTED_TEAM_ID" \
1213
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1214
+ --source-sha "$EXPECTED_SOURCE_SHA" \
1215
+ --version "$EXPECTED_VERSION" \
1216
+ --artifact-policy "$ARTIFACT_POLICY" \
1217
+ --approved-target "$APPROVED_TARGET" \
1218
+ --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1219
+ --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
1220
+ MANIFEST_BUILDER_IDENTITY_KIND="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
1221
+ --manifest "$MANIFEST_SNAPSHOT" \
1222
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1223
+ --field builder_identity_kind)"
67
1224
 
68
- if [ ! -d "$APP_SOURCE" ]; then
69
- warn_or_fail "native build did not produce ${APP_SOURCE}"
1225
+ CURRENT_MACOS="$("$SW_VERS_EXECUTABLE" -productVersion)"
1226
+ MINIMUM_MACOS="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
1227
+ --manifest "$MANIFEST_SNAPSHOT" \
1228
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1229
+ --field minimum_macos)"
1230
+ if ! "$BUN_EXECUTABLE" -e '
1231
+ const parse = (value) => value.split(".").map(Number);
1232
+ const [actual, minimum] = process.argv.slice(1).map(parse);
1233
+ for (let i = 0; i < Math.max(actual.length, minimum.length); i++) {
1234
+ if ((actual[i] ?? 0) > (minimum[i] ?? 0)) process.exit(0);
1235
+ if ((actual[i] ?? 0) < (minimum[i] ?? 0)) process.exit(1);
1236
+ }
1237
+ ' "$CURRENT_MACOS" "$MINIMUM_MACOS"; then
1238
+ echo "Recordings.app requires macOS ${MINIMUM_MACOS} or later; target is ${CURRENT_MACOS}." >&2
1239
+ exit 1
70
1240
  fi
1241
+ TARGET_ARCH="$("$UNAME_EXECUTABLE" -m)"
1242
+ MANIFEST_ARCHITECTURES="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
1243
+ --manifest "$MANIFEST_SNAPSHOT" \
1244
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1245
+ --field architectures)"
1246
+ case " ${MANIFEST_ARCHITECTURES} " in
1247
+ *" ${TARGET_ARCH} "*) ;;
1248
+ *) echo "Recordings.app artifact does not support target architecture ${TARGET_ARCH}." >&2; exit 1 ;;
1249
+ esac
71
1250
 
72
- rm -rf "$APP_DEST"
73
- mkdir -p "$DATA_DIR"
74
- cp -R "$APP_SOURCE" "$APP_DEST" || warn_or_fail "failed to copy app bundle"
1251
+ verify_secure_parent "$APP_PARENT"
1252
+ verify_existing_state_root
75
1253
 
76
- current_cdhash() {
77
- codesign -d --verbose=4 "$1" 2>&1 | awk -F= '/^CDHash=/ { print toupper($2); exit }'
1254
+ STAGING_DIR="$("$MKTEMP_EXECUTABLE" -d "${APP_PARENT}/.Recordings-install.XXXXXX")"
1255
+ STAGED_APP="${STAGING_DIR}/Recordings.app"
1256
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-archive \
1257
+ --archive "$ARTIFACT_SNAPSHOT" \
1258
+ --manifest "$MANIFEST_SNAPSHOT" \
1259
+ --team-id "$EXPECTED_TEAM_ID" \
1260
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1261
+ --source-sha "$EXPECTED_SOURCE_SHA" \
1262
+ --version "$EXPECTED_VERSION" \
1263
+ --artifact-policy "$ARTIFACT_POLICY" \
1264
+ --approved-target "$APPROVED_TARGET" \
1265
+ --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1266
+ --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
1267
+
1268
+ add_unique_app() {
1269
+ local candidate="$1"
1270
+ local existing
1271
+ if [ -e "$candidate" ] || [ -L "$candidate" ]; then
1272
+ [ -d "$candidate" ] && [ ! -L "$candidate" ] || {
1273
+ echo "Recordings.app path is not a secure directory: ${candidate}" >&2
1274
+ exit 1
1275
+ }
1276
+ fi
1277
+ [ -d "$candidate" ] || return 0
1278
+ for existing in ${DISCOVERED_APPS[@]+"${DISCOVERED_APPS[@]}"}; do
1279
+ [ "$existing" = "$candidate" ] && return 0
1280
+ done
1281
+ DISCOVERED_APPS+=("$candidate")
78
1282
  }
79
1283
 
80
- tcc_csreq_hex() {
81
- local db_path="$1"
82
- local service="$2"
83
- if [ ! -r "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
84
- return 0
1284
+ process_records_for_known_apps() {
1285
+ local pid
1286
+ local command
1287
+ local executable
1288
+ local command_matches
1289
+ local observed_executable
1290
+ local start_identity
1291
+ while read -r pid command; do
1292
+ [ -n "${pid:-}" ] || continue
1293
+ command_matches=0
1294
+ for executable in ${RUNNING_EXECUTABLES[@]+"${RUNNING_EXECUTABLES[@]}"}; do
1295
+ case "$command" in
1296
+ "$executable"|"$executable "*)
1297
+ command_matches=1
1298
+ break
1299
+ ;;
1300
+ esac
1301
+ done
1302
+ [ "$command_matches" -eq 1 ] || continue
1303
+ observed_executable=""
1304
+ for executable in ${RUNNING_EXECUTABLES[@]+"${RUNNING_EXECUTABLES[@]}"}; do
1305
+ observed_executable="$(observed_executable_for_pid "$pid" "$executable")"
1306
+ if [ -n "$observed_executable" ]; then
1307
+ break
1308
+ fi
1309
+ done
1310
+ [ -n "$observed_executable" ] || continue
1311
+ start_identity="$(process_start_identity "$pid")"
1312
+ [ -n "$start_identity" ] || continue
1313
+ printf '%s\t%s\t%s\n' "$pid" "$start_identity" "$observed_executable"
1314
+ done < <("$PS_EXECUTABLE" -axo pid=,command= 2>/dev/null || true)
1315
+ }
1316
+
1317
+ stop_old_processes() {
1318
+ local journal_phase="${1:-processes-stopping}"
1319
+ local current_records
1320
+ local app_path
1321
+ local executable
1322
+ local already_recorded
1323
+ local index
1324
+ local pid
1325
+ local start_identity
1326
+ local observed_executable
1327
+ current_records="$(process_records_for_known_apps)"
1328
+ LAST_STOPPED_PROCESS_COUNT=0
1329
+ [ -n "$current_records" ] || return 0
1330
+ while IFS=$'\t' read -r pid start_identity observed_executable; do
1331
+ [ -n "$pid" ] || continue
1332
+ LAST_STOPPED_PROCESS_COUNT=$((LAST_STOPPED_PROCESS_COUNT + 1))
1333
+ done <<< "$current_records"
1334
+ if [ -n "$OLD_PIDS" ]; then
1335
+ while IFS=$'\t' read -r pid start_identity observed_executable; do
1336
+ [ -n "$pid" ] || continue
1337
+ OLD_PIDS="${OLD_PIDS}"$'\n'"${pid}"
1338
+ done <<< "$current_records"
1339
+ else
1340
+ OLD_PIDS="$(printf '%s\n' "$current_records" | "$AWK_EXECUTABLE" -F '\t' '{ print $1 }')"
1341
+ fi
1342
+ while IFS=$'\t' read -r pid start_identity observed_executable; do
1343
+ case "$observed_executable" in
1344
+ */Contents/MacOS/Recordings) app_path="${observed_executable%/Contents/MacOS/Recordings}" ;;
1345
+ */Contents/Helpers/recordings) app_path="${observed_executable%/Contents/Helpers/recordings}" ;;
1346
+ *) continue ;;
1347
+ esac
1348
+ for ((index = 0; index < MOVED_ORIGINAL_COUNT; index++)); do
1349
+ if [ "$app_path" = "${MOVED_PATHS[$index]}" ]; then
1350
+ app_path="${MOVED_ORIGINALS[$index]}"
1351
+ break
1352
+ fi
1353
+ done
1354
+ already_recorded=0
1355
+ for recorded_path in ${PRIOR_RUNNING_APP_PATHS[@]+"${PRIOR_RUNNING_APP_PATHS[@]}"}; do
1356
+ [ "$recorded_path" != "$app_path" ] || already_recorded=1
1357
+ done
1358
+ [ "$already_recorded" -eq 1 ] || PRIOR_RUNNING_APP_PATHS+=("$app_path")
1359
+ done <<< "$current_records"
1360
+ was_running=1
1361
+ STOPPED_RUNNING_APP=1
1362
+ # Before the authoritative state backup there is intentionally no journal:
1363
+ # no app or state mutation has happened yet, so a crash can only leave the
1364
+ # prior app stopped. Once a journal exists, record every later stop phase.
1365
+ [ ! -f "$JOURNAL_PATH" ] || write_journal "$journal_phase"
1366
+ while IFS=$'\t' read -r pid start_identity observed_executable; do
1367
+ [ -n "$pid" ] || continue
1368
+ signal_process_if_current TERM "$pid" "$start_identity" "$observed_executable"
1369
+ done <<< "$current_records"
1370
+ local attempts=$((LAUNCH_TIMEOUT_SECONDS * 10))
1371
+ local remaining=0
1372
+ local attempt
1373
+ for ((attempt = 0; attempt < attempts; attempt++)); do
1374
+ remaining=0
1375
+ while IFS=$'\t' read -r pid start_identity observed_executable; do
1376
+ if process_identity_is_current "$pid" "$start_identity" "$observed_executable"; then
1377
+ remaining=1
1378
+ break
1379
+ fi
1380
+ done <<< "$current_records"
1381
+ [ "$remaining" -eq 0 ] && break
1382
+ "$SLEEP_EXECUTABLE" 0.1
1383
+ done
1384
+ if [ "$remaining" -ne 0 ]; then
1385
+ echo "Existing Recordings.app process did not stop before replacement." >&2
1386
+ exit 1
85
1387
  fi
86
- sqlite3 "$db_path" \
87
- "SELECT hex(csreq) FROM access WHERE service = '${service}' AND client = 'com.hasna.recordings' ORDER BY last_modified DESC LIMIT 1;" \
88
- 2>/dev/null || true
89
1388
  }
90
1389
 
91
- reset_stale_permission() {
92
- local service="$1"
93
- local tcc_service="$2"
94
- local db_path="$3"
95
- local cdhash="$4"
96
- local csreq_hex
97
- csreq_hex="$(tcc_csreq_hex "$db_path" "$tcc_service" | tr '[:lower:]' '[:upper:]')"
98
- if [ -n "$cdhash" ] && [ -n "$csreq_hex" ] && [[ "$csreq_hex" != *"$cdhash"* ]]; then
99
- tccutil reset "$service" com.hasna.recordings >/dev/null 2>&1 || true
100
- echo "Reset stale ${service} permission for the newly installed Recordings.app."
1390
+ quiesce_old_processes() {
1391
+ local journal_phase="$1"
1392
+ local required_empty_scans=10
1393
+ local consecutive_empty_scans=0
1394
+ local attempts=$((LAUNCH_TIMEOUT_SECONDS * 10 + required_empty_scans))
1395
+ local attempt
1396
+ for ((attempt = 0; attempt < attempts; attempt++)); do
1397
+ stop_old_processes "$journal_phase"
1398
+ if [ "$LAST_STOPPED_PROCESS_COUNT" -eq 0 ]; then
1399
+ consecutive_empty_scans=$((consecutive_empty_scans + 1))
1400
+ [ "$consecutive_empty_scans" -lt "$required_empty_scans" ] || return 0
1401
+ else
1402
+ consecutive_empty_scans=0
1403
+ fi
1404
+ "$SLEEP_EXECUTABLE" 0.1
1405
+ done
1406
+ echo "Existing Recordings.app processes did not remain quiescent before replacement." >&2
1407
+ exit 1
1408
+ }
1409
+
1410
+ restart_previous_app() {
1411
+ [ "$STOPPED_RUNNING_APP" -eq 1 ] || return 0
1412
+ restart_recorded_app_paths
1413
+ }
1414
+
1415
+ cleanup() {
1416
+ local status=$?
1417
+ local restart_after_release=0
1418
+ trap - EXIT
1419
+ if [ "$TRANSACTION_COMMITTED" -ne 1 ] && [ -f "$JOURNAL_PATH" ]; then
1420
+ STAGING_REMOVAL_DELEGATED=1
1421
+ cleanup_phase="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field phase 2>/dev/null || true)"
1422
+ case "$cleanup_phase" in
1423
+ candidate-moving|candidate-installed|activated|launching)
1424
+ if ! stop_uncommitted_candidate; then
1425
+ echo "Automatic rollback could not stop the uncommitted candidate." >&2
1426
+ PRESERVE_MAINTENANCE_MARKER=1
1427
+ RECOVERED_APP_RESTART_ON_ABORT=0
1428
+ release_install_coordination
1429
+ exit 1
1430
+ fi
1431
+ ;;
1432
+ esac
1433
+ if [ "$SQLITE_BARRIER_ACTIVE" -eq 0 ] && ! acquire_sqlite_barrier; then
1434
+ echo "Automatic rollback could not establish the SQLite writer barrier; preserved transaction evidence at ${JOURNAL_PATH}." >&2
1435
+ PRESERVE_MAINTENANCE_MARKER=1
1436
+ RECOVERED_APP_RESTART_ON_ABORT=0
1437
+ status=1
1438
+ elif ! "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-recover --journal "$JOURNAL_PATH"; then
1439
+ echo "Automatic rollback failed; preserved transaction evidence at ${JOURNAL_PATH}." >&2
1440
+ PRESERVE_MAINTENANCE_MARKER=1
1441
+ RECOVERED_APP_RESTART_ON_ABORT=0
1442
+ status=1
1443
+ else
1444
+ restart_after_release=1
1445
+ fi
1446
+ elif [ "$TRANSACTION_COMMITTED" -ne 1 ] && [ "$RECOVERED_APP_RESTART_ON_ABORT" -eq 1 ]; then
1447
+ restart_after_release=1
1448
+ fi
1449
+ if ! release_sqlite_barrier; then
1450
+ PRESERVE_MAINTENANCE_MARKER=1
1451
+ restart_after_release=0
1452
+ status=1
101
1453
  fi
1454
+ if [ "$STAGING_REMOVAL_DELEGATED" -eq 0 ]; then
1455
+ "$RM_EXECUTABLE" -rf "$STAGING_DIR"
1456
+ fi
1457
+ "$RM_EXECUTABLE" -rf "$WORK_DIR"
1458
+ if [ -n "$TRANSACTION_DIR" ] && [ ! -f "$JOURNAL_PATH" ] && [ -d "$TRANSACTION_DIR" ]; then
1459
+ if ! "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" transaction-cleanup \
1460
+ --transaction-dir "$TRANSACTION_DIR" \
1461
+ --nonce "$TRANSACTION_NONCE"; then
1462
+ echo "Could not safely isolate the pre-journal transaction; preserving maintenance evidence." >&2
1463
+ PRESERVE_MAINTENANCE_MARKER=1
1464
+ status=1
1465
+ fi
1466
+ fi
1467
+ if [ "$PRESERVE_MAINTENANCE_MARKER" -eq 0 ]; then
1468
+ if ! release_maintenance_marker; then
1469
+ restart_after_release=0
1470
+ status=1
1471
+ fi
1472
+ [ "$restart_after_release" -eq 0 ] || restart_previous_app
1473
+ fi
1474
+ release_install_lock
1475
+ exit "$status"
1476
+ }
1477
+ trap cleanup EXIT
1478
+
1479
+ maybe_crash_after_phase() {
1480
+ local phase="$1"
1481
+ if test_fault_hooks_enabled && [ "${RECORDINGS_TEST_CRASH_AFTER_PHASE:-}" = "$phase" ]; then
1482
+ kill -KILL "$$"
1483
+ fi
1484
+ }
1485
+
1486
+ write_journal() {
1487
+ local phase="$1"
1488
+ local -a arguments=(
1489
+ journal-write
1490
+ --journal "$JOURNAL_PATH"
1491
+ --phase "$phase"
1492
+ --transaction-dir "$TRANSACTION_DIR"
1493
+ --candidate-staging "$STAGING_DIR"
1494
+ --app-parent "$APP_PARENT"
1495
+ --app-destination "$APP_DEST"
1496
+ --data-dir "$DATA_DIR"
1497
+ --state-backup "$STATE_BACKUP"
1498
+ --state-backup-sha256 "$STATE_BACKUP_SHA256"
1499
+ --expected-manifest-sha256 "$EXPECTED_MANIFEST_SHA256"
1500
+ --expected-source-sha "$EXPECTED_SOURCE_SHA"
1501
+ --expected-version "$EXPECTED_VERSION"
1502
+ --artifact-policy "$ARTIFACT_POLICY"
1503
+ --approved-target "$APPROVED_TARGET"
1504
+ --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND"
1505
+ --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
1506
+ --builder-identity-kind "$MANIFEST_BUILDER_IDENTITY_KIND"
1507
+ --candidate-identity-sha256 "$candidate_identity_sha256"
1508
+ --candidate-tree-sha256 "$candidate_tree_sha256"
1509
+ --previous-identity-sha256 "$previous_identity_sha256"
1510
+ --original-state-mode "$ORIGINAL_STATE_MODE"
1511
+ )
1512
+ [ "$was_running" -eq 0 ] || arguments+=(--was-running)
1513
+ for prior_running_app_path in ${PRIOR_RUNNING_APP_PATHS[@]+"${PRIOR_RUNNING_APP_PATHS[@]}"}; do
1514
+ arguments+=(--prior-running-app-path "$prior_running_app_path")
1515
+ done
1516
+ local index
1517
+ for ((index = 0; index < MOVED_ORIGINAL_COUNT; index++)); do
1518
+ arguments+=(--original "${MOVED_ORIGINALS[$index]}" "${MOVED_PATHS[$index]}" "${MOVED_DIGESTS[$index]}")
1519
+ done
1520
+ for ((index = 0; index < ${#INSTALLER_OWNED_STATE_PATHS[@]}; index++)); do
1521
+ arguments+=(
1522
+ --installer-owned-state
1523
+ "${INSTALLER_OWNED_STATE_PATHS[$index]}"
1524
+ "${INSTALLER_OWNED_STATE_DIGESTS[$index]}"
1525
+ )
1526
+ done
1527
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" "${arguments[@]}"
1528
+ STAGING_REMOVAL_DELEGATED=1
1529
+ maybe_crash_after_phase "$phase"
102
1530
  }
103
1531
 
104
- APP_CDHASH="$(current_cdhash "$APP_DEST" || true)"
105
- reset_stale_permission "Microphone" "kTCCServiceMicrophone" "${HOME}/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
106
- reset_stale_permission "Accessibility" "kTCCServiceAccessibility" "/Library/Application Support/com.apple.TCC/TCC.db" "$APP_CDHASH"
1532
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" extract-verified-archive \
1533
+ --archive "$ARTIFACT_SNAPSHOT" \
1534
+ --manifest "$MANIFEST_SNAPSHOT" \
1535
+ --staging-target "$UNPACK_DIR" \
1536
+ --team-id "$EXPECTED_TEAM_ID" \
1537
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1538
+ --source-sha "$EXPECTED_SOURCE_SHA" \
1539
+ --version "$EXPECTED_VERSION" \
1540
+ --artifact-policy "$ARTIFACT_POLICY" \
1541
+ --approved-target "$APPROVED_TARGET" \
1542
+ --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1543
+ --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
1544
+ CANDIDATE_APP="$UNPACK_DIR/Recordings.app"
107
1545
 
108
- # A second copy in ~/Applications splits TCC permissions and leaves users running
109
- # stale builds. Keep that launch point but always point it at the fresh build.
110
- ALT_COPY="${HOME}/Applications/Recordings.app"
111
- if [ -d "$ALT_COPY" ]; then
112
- rm -rf "$ALT_COPY"
113
- cp -R "$APP_SOURCE" "$ALT_COPY" || true
114
- echo "Updated stale copy at ${ALT_COPY} to the freshly built app."
1546
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-app \
1547
+ --app "$CANDIDATE_APP" \
1548
+ --manifest "$MANIFEST_SNAPSHOT" \
1549
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1550
+ --team-id "$EXPECTED_TEAM_ID" \
1551
+ --artifact-policy "$ARTIFACT_POLICY" \
1552
+ --approved-target "$APPROVED_TARGET" \
1553
+ --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1554
+ --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
1555
+ if [ "$ARTIFACT_POLICY" = "release" ]; then
1556
+ "$XCRUN_EXECUTABLE" stapler validate "$CANDIDATE_APP"
1557
+ "$SPCTL_EXECUTABLE" --assess --type execute --verbose=2 "$CANDIDATE_APP"
1558
+ "$SYSPOLICY_CHECK_EXECUTABLE" distribution "$CANDIDATE_APP"
115
1559
  fi
1560
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$CANDIDATE_APP" --uid "$("$ID_EXECUTABLE" -u)"
116
1561
 
117
- # If an old instance is running, restart it on the new build.
118
- if pgrep -x Recordings >/dev/null 2>&1; then
119
- pkill -x Recordings || true
120
- sleep 1
121
- open "$APP_DEST" || true
122
- echo "Restarted Recordings.app on the new build."
1562
+ DISCOVERED_APPS=()
1563
+ add_unique_app "$APP_DEST"
1564
+ add_unique_app "${DATA_DIR}/Recordings.app"
1565
+ for candidate in "${HOME}"/Applications/Recordings.app.*; do
1566
+ add_unique_app "$candidate"
1567
+ done
1568
+ add_unique_app "/Applications/Recordings.app"
1569
+ if [ -x "$MDFIND_EXECUTABLE" ]; then
1570
+ while IFS= read -r candidate; do
1571
+ [ -n "$candidate" ] || continue
1572
+ add_unique_app "$candidate"
1573
+ done < <("$MDFIND_EXECUTABLE" "kMDItemCFBundleIdentifier == 'com.hasna.recordings'" 2>/dev/null || true)
1574
+ fi
1575
+
1576
+ MANAGEABLE_APPS=()
1577
+ for existing_app in ${DISCOVERED_APPS[@]+"${DISCOVERED_APPS[@]}"}; do
1578
+ case "$existing_app" in
1579
+ "$APP_DEST"|"${DATA_DIR}/Recordings.app"|"${HOME}/Applications/Recordings.app."*)
1580
+ echo "Duplicate disposition: archive transactionally, remove after activation: ${existing_app}"
1581
+ MANAGEABLE_APPS+=("$existing_app")
1582
+ ;;
1583
+ *)
1584
+ echo "Duplicate Recordings.app at ${existing_app} is outside the transactional user install paths; archive or remove it before installing." >&2
1585
+ exit 1
1586
+ ;;
1587
+ esac
1588
+ done
1589
+
1590
+ measure_kb() {
1591
+ local measured
1592
+ measured="$("$DU_EXECUTABLE" -sk "$@" 2>/dev/null | "$AWK_EXECUTABLE" '{sum += $1} END {print sum + 0}')"
1593
+ case "$measured" in ''|*[!0-9]*) echo "Could not measure transactional storage requirements." >&2; exit 1 ;; esac
1594
+ printf '%s\n' "$measured"
1595
+ }
1596
+
1597
+ require_space() {
1598
+ local path="$1"
1599
+ local required="$2"
1600
+ local available
1601
+ available="$("$DF_EXECUTABLE" -Pk "$path" | "$AWK_EXECUTABLE" 'NR == 2 {print $4}')"
1602
+ case "$available" in ''|*[!0-9]*) echo "Could not determine free space for ${path}." >&2; exit 1 ;; esac
1603
+ if [ "$available" -lt "$required" ]; then
1604
+ echo "Insufficient free space for transactional app and state backups at ${path}." >&2
1605
+ exit 1
1606
+ fi
1607
+ }
1608
+
1609
+ candidate_kb="$(measure_kb "$CANDIDATE_APP")"
1610
+ data_kb="$(measure_kb "$DATA_DIR")"
1611
+ existing_kb=0
1612
+ for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
1613
+ existing_kb=$((existing_kb + $(measure_kb "$existing_app")))
1614
+ done
1615
+ require_space "$APP_PARENT" $((candidate_kb * 3 + existing_kb * 2 + data_kb * 2 + 10240))
1616
+ require_space "$DATA_DIR" $((existing_kb * 2 + data_kb * 2 + 10240))
1617
+
1618
+ candidate_requirement="$("$CODESIGN_EXECUTABLE" -d -r- "$CANDIDATE_APP" 2>&1 | "$SED_EXECUTABLE" -n 's/^designated => //p' | "$HEAD_EXECUTABLE" -n 1 || true)"
1619
+ if [ "$ARTIFACT_POLICY" = "release" ] && [ -z "$candidate_requirement" ]; then
1620
+ echo "Candidate app has no designated requirement." >&2
1621
+ exit 1
123
1622
  fi
1623
+ identity_migration=0
1624
+ candidate_identity_sha256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" requirement-digest --app "$CANDIDATE_APP" --artifact-policy "$ARTIFACT_POLICY")"
1625
+ candidate_tree_sha256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$CANDIDATE_APP")"
1626
+ [ "$candidate_identity_sha256" = "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
1627
+ --manifest "$MANIFEST_SNAPSHOT" \
1628
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1629
+ --field identity)" ] || {
1630
+ echo "Candidate identity does not match the artifact manifest." >&2
1631
+ exit 1
1632
+ }
1633
+ previous_identity_sha256="none"
1634
+ for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
1635
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$existing_app" --uid "$("$ID_EXECUTABLE" -u)"
1636
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" assert-transition \
1637
+ --existing-app "$existing_app" \
1638
+ --manifest "$MANIFEST_SNAPSHOT" \
1639
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256"
1640
+ existing_requirement="$("$CODESIGN_EXECUTABLE" -d -r- "$existing_app" 2>&1 | "$SED_EXECUTABLE" -n 's/^designated => //p' | "$HEAD_EXECUTABLE" -n 1 || true)"
1641
+ existing_identity_policy="release"
1642
+ if [ "$ARTIFACT_POLICY" = "local_only" ] && [ -z "$existing_requirement" ]; then
1643
+ existing_identity_policy="local_only"
1644
+ fi
1645
+ existing_identity_sha256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" requirement-digest --app "$existing_app" --artifact-policy "$existing_identity_policy")"
1646
+ if [ "$previous_identity_sha256" = "none" ]; then
1647
+ previous_identity_sha256="$existing_identity_sha256"
1648
+ elif [ "$previous_identity_sha256" != "$existing_identity_sha256" ]; then
1649
+ echo "Installed duplicates have multiple signing identities; automatic migration is unsafe." >&2
1650
+ exit 1
1651
+ fi
1652
+ if [ -z "$existing_requirement" ] || \
1653
+ ! "$CODESIGN_EXECUTABLE" --verify --strict -R "$existing_requirement" "$CANDIDATE_APP" >/dev/null 2>&1 || \
1654
+ ! "$CODESIGN_EXECUTABLE" --verify --strict -R "$candidate_requirement" "$existing_app" >/dev/null 2>&1; then
1655
+ identity_migration=1
1656
+ fi
1657
+ done
1658
+ if [ "$ARTIFACT_POLICY" = "release" ] && [ "$identity_migration" -eq 1 ] && [ "$ALLOW_IDENTITY_MIGRATION" -ne 1 ]; then
1659
+ echo "Candidate and existing app designated requirements are not mutually compatible; review the signer change and rerun once with --allow-signing-identity-migration." >&2
1660
+ exit 1
1661
+ fi
1662
+ if [ "$ARTIFACT_POLICY" = "release" ] && [ "$identity_migration" -eq 1 ] && {
1663
+ [ "$previous_identity_sha256" != "$EXPECTED_OLD_IDENTITY_SHA256" ] ||
1664
+ [ "$candidate_identity_sha256" != "$EXPECTED_NEW_IDENTITY_SHA256" ];
1665
+ }; then
1666
+ echo "Signing identity migration does not match the exact operator-approved old/new identities." >&2
1667
+ exit 1
1668
+ fi
1669
+ if [ "$ARTIFACT_POLICY" = "release" ] && [ "$identity_migration" -eq 0 ] && [ "$ALLOW_IDENTITY_MIGRATION" -eq 1 ]; then
1670
+ echo "Identity migration approval was supplied but no identity migration is required." >&2
1671
+ exit 1
1672
+ fi
1673
+
1674
+ TRANSACTION_DIR="$("$MKTEMP_EXECUTABLE" -d "${APP_PARENT}/.Recordings-transaction.XXXXXX")"
1675
+ TRANSACTION_NONCE="$("$BUN_EXECUTABLE" -e 'import { randomUUID } from "node:crypto"; process.stdout.write(randomUUID())')"
1676
+ printf '%s\n' "$TRANSACTION_NONCE" >"$TRANSACTION_DIR/.Recordings-transaction-owner"
1677
+ "$CHMOD_EXECUTABLE" 600 "$TRANSACTION_DIR/.Recordings-transaction-owner"
1678
+ "$CHMOD_EXECUTABLE" 700 "$TRANSACTION_DIR"
1679
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree \
1680
+ --path "$TRANSACTION_DIR/.Recordings-transaction-owner"
1681
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR"
1682
+ "$MKDIR_EXECUTABLE" -m 700 "$TRANSACTION_DIR/apps"
1683
+ "$MKDIR_EXECUTABLE" -m 700 "$TRANSACTION_DIR/archives"
1684
+ verify_secure_parent "$TRANSACTION_DIR"
1685
+
1686
+ acquire_sqlite_barrier
1687
+ STATE_BACKUP="$TRANSACTION_DIR/state.initial"
1688
+ "$DITTO_EXECUTABLE" "$DATA_DIR" "$STATE_BACKUP"
1689
+ if ! "$DIFF_EXECUTABLE" -qr "$DATA_DIR" "$STATE_BACKUP" >/dev/null; then
1690
+ echo "Recordings state backup verification failed before replacement." >&2
1691
+ exit 1
1692
+ fi
1693
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$STATE_BACKUP" --uid "$("$ID_EXECUTABLE" -u)"
1694
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$STATE_BACKUP"
1695
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR"
1696
+ STATE_BACKUP_SHA256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$STATE_BACKUP")"
1697
+ release_sqlite_barrier
1698
+
1699
+ MOVED_ORIGINALS=()
1700
+ MOVED_PATHS=()
1701
+ MOVED_DIGESTS=()
1702
+ MOVED_ORIGINAL_COUNT=0
1703
+ move_index=0
1704
+ for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
1705
+ move_index=$((move_index + 1))
1706
+ MOVED_ORIGINALS+=("$existing_app")
1707
+ MOVED_PATHS+=("$TRANSACTION_DIR/apps/original-${move_index}")
1708
+ MOVED_DIGESTS+=("$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$existing_app")")
1709
+ MOVED_ORIGINAL_COUNT=$move_index
1710
+ done
1711
+ write_journal prepared
124
1712
 
125
- echo "Installed Recordings.app from package: ${APP_DEST}"
1713
+ ARCHIVE_SEQUENCE=0
1714
+ for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
1715
+ ARCHIVE_SEQUENCE=$((ARCHIVE_SEQUENCE + 1))
1716
+ stamp="$("$DATE_EXECUTABLE" -u +%Y%m%dT%H%M%SZ)-$$-${ARCHIVE_SEQUENCE}"
1717
+ rollback_archive="$TRANSACTION_DIR/archives/Recordings-pre-install-${stamp}.zip"
1718
+ rollback_destination="$ROLLBACK_DIR/Recordings-pre-install-${stamp}.zip"
1719
+ [ ! -e "$rollback_destination" ] && [ ! -L "$rollback_destination" ] || {
1720
+ echo "Refusing to overwrite an existing rollback archive destination." >&2
1721
+ exit 1
1722
+ }
1723
+ "$DITTO_EXECUTABLE" -c -k --sequesterRsrc --keepParent \
1724
+ "$existing_app" \
1725
+ "$rollback_archive"
1726
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$rollback_archive"
1727
+ INSTALLER_OWNED_STATE_PATHS+=("$rollback_destination")
1728
+ INSTALLER_OWNED_STATE_DIGESTS+=(
1729
+ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$rollback_archive")"
1730
+ )
1731
+ done
1732
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR/archives"
1733
+
1734
+ "$DITTO_EXECUTABLE" "$CANDIDATE_APP" "$STAGED_APP"
1735
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-app \
1736
+ --app "$STAGED_APP" \
1737
+ --manifest "$MANIFEST_SNAPSHOT" \
1738
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1739
+ --team-id "$EXPECTED_TEAM_ID" \
1740
+ --artifact-policy "$ARTIFACT_POLICY" \
1741
+ --approved-target "$APPROVED_TARGET" \
1742
+ --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1743
+ --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
1744
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$STAGED_APP" --uid "$("$ID_EXECUTABLE" -u)"
1745
+ [ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$STAGED_APP")" = \
1746
+ "$candidate_tree_sha256" ] || {
1747
+ echo "Staged candidate does not match the authenticated candidate tree." >&2
1748
+ exit 1
1749
+ }
1750
+
1751
+ # Store readers are already drained. Archive and stage while the current app
1752
+ # remains available, then stop every known app and packaged helper immediately
1753
+ # before the authoritative stopped-state snapshot. Recovery deliberately does
1754
+ # not restore state for prepared/processes-stopping; only processes-stopped and
1755
+ # later phases bind rollback to the stopped snapshot.
1756
+ for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
1757
+ RUNNING_EXECUTABLES+=("$existing_app/Contents/MacOS/Recordings")
1758
+ RUNNING_EXECUTABLES+=("$existing_app/Contents/Helpers/recordings")
1759
+ done
1760
+ stop_old_processes
1761
+ acquire_sqlite_barrier
1762
+ # A previous release may ignore the new maintenance marker and relaunch from
1763
+ # its helper. Rescan immediately on both sides of the authoritative copy while
1764
+ # the exclusive SQLite transaction prevents any surviving writer from landing.
1765
+ stop_old_processes
1766
+ NEXT_STATE_BACKUP="$TRANSACTION_DIR/state.stopped"
1767
+ "$DITTO_EXECUTABLE" "$DATA_DIR" "$NEXT_STATE_BACKUP"
1768
+ stop_old_processes
1769
+ if ! "$DIFF_EXECUTABLE" -qr "$DATA_DIR" "$NEXT_STATE_BACKUP" >/dev/null; then
1770
+ echo "Stopped-state backup verification failed before replacement." >&2
1771
+ exit 1
1772
+ fi
1773
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$NEXT_STATE_BACKUP" --uid "$("$ID_EXECUTABLE" -u)"
1774
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$NEXT_STATE_BACKUP"
1775
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR"
1776
+ NEXT_STATE_BACKUP_SHA256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$NEXT_STATE_BACKUP")"
1777
+ maybe_crash_after_phase state-refresh-copied-before-journal
1778
+ STATE_BACKUP="$NEXT_STATE_BACKUP"
1779
+ STATE_BACKUP_SHA256="$NEXT_STATE_BACKUP_SHA256"
1780
+ write_journal processes-stopped
1781
+ # The durable journal boundary can race an older helper relaunch. Quiesce every
1782
+ # known bundle, then refresh into a new immutable snapshot. If another known
1783
+ # writer appears after that journal update, repeat with a fresh generation; an
1784
+ # already-journaled backup is never changed in place. Standalone writers that
1785
+ # cannot be identified here are merged conservatively during rollback.
1786
+ STOPPED_REFRESH_STABLE=0
1787
+ for ((refresh_attempt = 1; refresh_attempt <= 5; refresh_attempt++)); do
1788
+ quiesce_old_processes processes-stopped
1789
+ REFRESHED_STATE_BACKUP="$TRANSACTION_DIR/state.stopped.${refresh_attempt}"
1790
+ "$DITTO_EXECUTABLE" "$DATA_DIR" "$REFRESHED_STATE_BACKUP"
1791
+ stop_old_processes processes-stopped
1792
+ if [ "$LAST_STOPPED_PROCESS_COUNT" -ne 0 ] || \
1793
+ ! "$DIFF_EXECUTABLE" -qr "$DATA_DIR" "$REFRESHED_STATE_BACKUP" >/dev/null; then
1794
+ continue
1795
+ fi
1796
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree \
1797
+ --path "$REFRESHED_STATE_BACKUP" --uid "$("$ID_EXECUTABLE" -u)"
1798
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$REFRESHED_STATE_BACKUP"
1799
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR"
1800
+ STATE_BACKUP="$REFRESHED_STATE_BACKUP"
1801
+ STATE_BACKUP_SHA256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$STATE_BACKUP")"
1802
+ write_journal processes-stopped
1803
+ stop_old_processes processes-stopped
1804
+ if [ "$LAST_STOPPED_PROCESS_COUNT" -eq 0 ]; then
1805
+ STOPPED_REFRESH_STABLE=1
1806
+ break
1807
+ fi
1808
+ done
1809
+ [ "$STOPPED_REFRESH_STABLE" -eq 1 ] || {
1810
+ echo "Existing Recordings.app writers did not remain quiescent around the stopped-state refresh." >&2
1811
+ exit 1
1812
+ }
1813
+
1814
+ # This phase is durable before changing state mode, creating install-owned
1815
+ # directories, or moving exact archived bundles into the state tree.
1816
+ write_journal state-mutating
1817
+
1818
+ verify_existing_state_root
1819
+ [ "$("$STAT_EXECUTABLE" -f '%Lp' "$DATA_DIR")" = "$ORIGINAL_STATE_MODE" ] || {
1820
+ echo "Recordings state mode changed after transactional backup." >&2
1821
+ exit 1
1822
+ }
1823
+ if [ "$ORIGINAL_STATE_MODE" = 755 ]; then
1824
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" state-mode-harden --path "$DATA_DIR" --uid "$("$ID_EXECUTABLE" -u)"
1825
+ maybe_crash_after_phase state-mode-hardened
1826
+ if test_fault_hooks_enabled && \
1827
+ [ "${RECORDINGS_TEST_FAIL_AFTER_STATE_MODE_HARDEN:-0}" != 0 ]; then
1828
+ exit 1
1829
+ fi
1830
+ fi
1831
+ verify_secure_parent "$DATA_DIR" 1
1832
+ "$MKDIR_EXECUTABLE" -m 700 -p "${DATA_DIR}/audio" "$ROLLBACK_DIR"
1833
+ verify_secure_parent "$ROLLBACK_DIR" 1
1834
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$ROLLBACK_DIR"
1835
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$DATA_DIR"
1836
+
1837
+ for ((archive_index = 0; archive_index < ${#INSTALLER_OWNED_STATE_PATHS[@]}; archive_index++)); do
1838
+ rollback_destination="${INSTALLER_OWNED_STATE_PATHS[$archive_index]}"
1839
+ rollback_archive="$TRANSACTION_DIR/archives/$("$BASENAME_EXECUTABLE" "$rollback_destination")"
1840
+ [ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$rollback_archive")" = \
1841
+ "${INSTALLER_OWNED_STATE_DIGESTS[$archive_index]}" ] || {
1842
+ echo "Transactional rollback archive changed before state installation." >&2
1843
+ exit 1
1844
+ }
1845
+ [ ! -e "$rollback_destination" ] && [ ! -L "$rollback_destination" ] || {
1846
+ echo "Rollback archive destination appeared during installation." >&2
1847
+ exit 1
1848
+ }
1849
+ "$MV_EXECUTABLE" "$rollback_archive" "$rollback_destination"
1850
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$rollback_destination"
1851
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$ROLLBACK_DIR"
1852
+ done
1853
+
1854
+ write_journal originals-moving
1855
+ stop_old_processes originals-moving
1856
+ for ((move_index = 0; move_index < MOVED_ORIGINAL_COUNT; move_index++)); do
1857
+ existing_app="${MOVED_ORIGINALS[$move_index]}"
1858
+ moved_path="${MOVED_PATHS[$move_index]}"
1859
+ [ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$existing_app")" = "${MOVED_DIGESTS[$move_index]}" ] || {
1860
+ echo "Installed Recordings.app changed after transactional backup planning." >&2
1861
+ exit 1
1862
+ }
1863
+ RECORDINGS_TEST_INSTALLER_PID="$$" \
1864
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" install-archive-original \
1865
+ --journal "$JOURNAL_PATH" \
1866
+ --source "$existing_app" \
1867
+ --destination "$moved_path" \
1868
+ --expected-tree-sha256 "${MOVED_DIGESTS[$move_index]}"
1869
+ RUNNING_EXECUTABLES+=("$moved_path/Contents/MacOS/Recordings")
1870
+ RUNNING_EXECUTABLES+=("$moved_path/Contents/Helpers/recordings")
1871
+ "$CHMOD_EXECUTABLE" -R go-w "$moved_path"
1872
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$moved_path" --uid "$("$ID_EXECUTABLE" -u)"
1873
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$moved_path"
1874
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$("$DIRNAME_EXECUTABLE" "$existing_app")"
1875
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$TRANSACTION_DIR/apps"
1876
+ done
1877
+ write_journal originals-moved
1878
+ # A process launched in the narrow rename window now resolves through lsof to
1879
+ # the moved bundle. Stop that exact start/executable identity before releasing
1880
+ # the SQLite barrier; PID reuse or a different executable is never signalled.
1881
+ quiesce_old_processes originals-moved
1882
+ release_sqlite_barrier
1883
+
1884
+ write_journal candidate-moving
1885
+ RECORDINGS_TEST_INSTALLER_PID="$$" \
1886
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" install-publish-candidate \
1887
+ --journal "$JOURNAL_PATH" \
1888
+ --staging "$STAGED_APP" \
1889
+ --destination "$APP_DEST" \
1890
+ --expected-tree-sha256 "$candidate_tree_sha256"
1891
+ maybe_crash_after_phase candidate-moved-before-journal
1892
+ [ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$APP_DEST")" = \
1893
+ "$candidate_tree_sha256" ] || {
1894
+ echo "Installed candidate changed during publication." >&2
1895
+ exit 1
1896
+ }
1897
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-tree --path "$APP_DEST"
1898
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" fsync-directory --path "$APP_PARENT"
1899
+ write_journal candidate-installed
1900
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-app \
1901
+ --app "$APP_DEST" \
1902
+ --manifest "$MANIFEST_SNAPSHOT" \
1903
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1904
+ --team-id "$EXPECTED_TEAM_ID" \
1905
+ --artifact-policy "$ARTIFACT_POLICY" \
1906
+ --approved-target "$APPROVED_TARGET" \
1907
+ --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1908
+ --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
1909
+ if [ "$ARTIFACT_POLICY" = "release" ]; then
1910
+ "$XCRUN_EXECUTABLE" stapler validate "$APP_DEST"
1911
+ "$SPCTL_EXECUTABLE" --assess --type execute --verbose=2 "$APP_DEST"
1912
+ "$SYSPOLICY_CHECK_EXECUTABLE" distribution "$APP_DEST"
1913
+ fi
1914
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-filesystem-tree --path "$APP_DEST" --uid "$("$ID_EXECUTABLE" -u)"
1915
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" verify-active \
1916
+ --app "$APP_DEST" \
1917
+ --manifest "$MANIFEST_SNAPSHOT" \
1918
+ --manifest-sha256 "$EXPECTED_MANIFEST_SHA256" \
1919
+ --team-id "$EXPECTED_TEAM_ID" \
1920
+ --artifact-policy "$ARTIFACT_POLICY" \
1921
+ --approved-target "$APPROVED_TARGET" \
1922
+ --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1923
+ --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256"
1924
+ SMOKE_TOOL_PATH="/usr/bin:/bin:/usr/sbin:/sbin:$("$DIRNAME_EXECUTABLE" "$BUN_EXECUTABLE")"
1925
+ PATH="$SMOKE_TOOL_PATH" "$RUNTIME_SMOKE" "$APP_DEST" "$BUN_EXECUTABLE"
1926
+ write_journal activated
1927
+
1928
+ write_journal committed
1929
+ STAGING_REMOVAL_DELEGATED=1
1930
+ "$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-recover --journal "$JOURNAL_PATH"
1931
+ TRANSACTION_COMMITTED=1
1932
+ STOPPED_RUNNING_APP=0
1933
+
1934
+ if [ "$LAUNCH_APP" -eq 1 ] || [ "$was_running" -eq 1 ]; then
1935
+ EXPECTED_EXECUTABLE="$APP_DEST/Contents/MacOS/Recordings"
1936
+ RUNNING_EXECUTABLES+=("$EXPECTED_EXECUTABLE")
1937
+ "$OPEN_EXECUTABLE" -n "$APP_DEST"
1938
+ attempts=$((LAUNCH_TIMEOUT_SECONDS * 10))
1939
+ launched_pid=""
1940
+ launched_start_identity=""
1941
+ for ((attempt = 0; attempt < attempts; attempt++)); do
1942
+ while IFS=$'\t' read -r pid start_identity observed_executable; do
1943
+ [ -n "$pid" ] || continue
1944
+ launched_pid="$pid"
1945
+ launched_start_identity="$start_identity"
1946
+ break
1947
+ done < <(pids_for_exact_executable "$EXPECTED_EXECUTABLE")
1948
+ [ -n "$launched_pid" ] && break
1949
+ "$SLEEP_EXECUTABLE" 0.1
1950
+ done
1951
+ if [ -z "$launched_pid" ]; then
1952
+ echo "Canonical app did not launch from ${EXPECTED_EXECUTABLE} within ${LAUNCH_TIMEOUT_SECONDS} seconds." >&2
1953
+ exit 1
1954
+ fi
1955
+ "$SLEEP_EXECUTABLE" 1
1956
+ if ! process_identity_is_current "$launched_pid" "$launched_start_identity" "$EXPECTED_EXECUTABLE"; then
1957
+ echo "Canonical app process exited before the stability window completed." >&2
1958
+ exit 1
1959
+ fi
1960
+ maybe_crash_after_phase candidate-launched-after-commit
1961
+ fi
1962
+
1963
+ if [ "$ARTIFACT_POLICY" = "local_only" ]; then
1964
+ echo "Installed local-only Recordings.app for ${APPROVED_TARGET}; this artifact is ad-hoc signed and non-notarized."
1965
+ echo "Microphone or Accessibility may require manual reauthorization after this code-identity change."
1966
+ elif [ "$identity_migration" -eq 1 ]; then
1967
+ echo "Installed a new signing identity; macOS will require one-time permission approval for this migration."
1968
+ else
1969
+ echo "Installed verified Recordings.app release artifact: ${APP_DEST}"
1970
+ fi