@hasna/recordings 0.2.13 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile.package +4 -5
- package/README.md +113 -8
- package/bun.lock +2 -2
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +22 -0
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts +22 -0
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +1 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +169 -0
- package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -0
- package/dist/__tests__/preload.d.ts +1 -4
- package/dist/__tests__/preload.d.ts.map +1 -1
- package/dist/cli/desktop-snapshot.d.ts +20 -0
- package/dist/cli/desktop-snapshot.d.ts.map +1 -0
- package/dist/cli/index.js +2078 -277
- package/dist/cli/macos-permissions.d.ts +72 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -1
- package/dist/cli/macos-shortcut.d.ts +139 -0
- package/dist/cli/macos-shortcut.d.ts.map +1 -0
- package/dist/cli/trigger-probe.d.ts +172 -0
- package/dist/cli/trigger-probe.d.ts.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/http/client.d.ts +10 -7
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +634 -49
- package/dist/lib/capture-probe.d.ts +216 -0
- package/dist/lib/capture-probe.d.ts.map +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/macos-bundle.d.ts +50 -0
- package/dist/lib/macos-bundle.d.ts.map +1 -0
- package/dist/lib/persistence-probe.d.ts +208 -0
- package/dist/lib/persistence-probe.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/retired-deployment-modes.d.ts +24 -0
- package/dist/lib/retired-deployment-modes.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts +60 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.js +140 -56
- package/dist/server/cloud-config.d.ts +25 -3
- package/dist/server/cloud-config.d.ts.map +1 -1
- package/dist/server/cloud-readiness.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -7
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +187 -79
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +108 -49
- package/dist/store.d.ts +1 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/package.json +25 -6
- package/scripts/build_native_fs_guard.sh +15 -2
- package/scripts/ci-linux-suite.ts +403 -0
- package/scripts/ci-native-build.ts +402 -0
- package/scripts/enforce_identity_migration.sh +110 -0
- package/scripts/generate-sdk.ts +1 -1
- package/scripts/install_macos_app.sh +138 -24
- package/scripts/macos_artifact.ts +205 -33
- package/scripts/native/realtime-settle-probe/README.md +39 -0
- package/scripts/native/realtime-settle-probe/main.swift +135 -0
- package/scripts/policy/local-only-approved-targets.txt +21 -0
- package/scripts/read_local_only_targets.sh +203 -0
- package/scripts/scan-artifact.ts +40 -0
- package/scripts/set-version.ts +145 -0
- package/scripts/vacuity-manifest-gen.ts +96 -0
- package/scripts/vacuity-manifests/enhancement-screen-mirror.tsv +44 -0
- package/scripts/vacuity-manifests/install-chain.tsv +13 -0
- package/scripts/vacuity-manifests/must-survive-controls.tsv +10 -0
- package/scripts/vacuity-manifests/reorder.tsv +4 -0
- package/scripts/vacuity-manifests/source-side.tsv +15 -0
- package/scripts/vacuity-manifests/variable-operands.tsv +16 -0
- package/scripts/vacuity-manifests/version-sites.tsv +61 -0
- package/scripts/vacuity-mutation-battery.ts +266 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +14 -7
- package/src/native/Recordings/App/RuntimeSmoke.swift +5 -1
- package/src/native/Recordings/App/SidebarView.swift +1 -1
- package/src/native/Recordings/Package.swift +24 -5
- package/src/native/Recordings/RecordingsLib/EnhancementScreen.swift +91 -0
- package/src/native/Recordings/RecordingsLib/FnKeyMonitor.swift +27 -4
- package/src/native/Recordings/RecordingsLib/Info.plist +2 -2
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +56 -3
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +13 -0
- package/src/native/Recordings/RecordingsLib/PasteDeliveryVerification.swift +460 -0
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +41 -7
- package/src/native/Recordings/RecordingsLib/RecordingAttemptAlert.swift +36 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +1071 -75
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +47 -3
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +11 -6
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +3 -3
- package/src/native/Recordings/RecordingsTests/EnhancementScreenTests.swift +191 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +144 -4
- package/src/native/Recordings/RecordingsTests/PasteDeliveryVerificationTests.swift +463 -0
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +33 -20
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBenchmark.swift +200 -0
- package/src/native/Recordings/RecordingsTests/RealtimeSettleBudgetTests.swift +80 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +22 -4
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +12 -6
- package/src/native/Recordings/RecordingsTests/RecordingEngineHomeIsolationTests.swift +67 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +12 -3
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +207 -11
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +17 -6
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +23 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +7 -1
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +10 -10
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +5 -1
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +1 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +59 -3
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +21 -2
- package/src/native/Recordings/Updater/ProtocolTests/ExpectationIntegrityTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/UpdateXPCInterfaceTests.swift +113 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +106 -5
- package/src/native/Recordings/build.sh +75 -9
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
|
@@ -146,6 +146,63 @@ require_absolute_canonical_owned_home() {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
# Every accepted argument, because until now there was no usage output at all and an
|
|
150
|
+
# unrecognized argument printed only "Unknown argument". That mattered most for
|
|
151
|
+
# --allow-adhoc-identity-migration: the identity-migration gate refuses a local-only
|
|
152
|
+
# reinstall by default, and an operator repairing a station had no way to discover the
|
|
153
|
+
# approval flag except by reading the refusal on stderr or the script itself.
|
|
154
|
+
print_usage() {
|
|
155
|
+
cat <<'USAGE'
|
|
156
|
+
Usage: install_macos_app.sh --artifact <zip> --manifest <json> [options]
|
|
157
|
+
|
|
158
|
+
Installs a Recordings macOS app artifact transactionally. Normally invoked through
|
|
159
|
+
`recordings app install`, which forwards these arguments.
|
|
160
|
+
|
|
161
|
+
Artifact and version:
|
|
162
|
+
--artifact <path> Finalized artifact archive to install
|
|
163
|
+
--manifest <path> Artifact manifest describing it
|
|
164
|
+
--manifest-sha256 <sha256> Authenticated digest of the manifest
|
|
165
|
+
--expected-source-sha <sha> Approved 40-character source commit
|
|
166
|
+
--expected-version <version> Version the artifact must declare
|
|
167
|
+
--expected-team-id <team-id> Required Apple Developer team identifier
|
|
168
|
+
|
|
169
|
+
Target selection:
|
|
170
|
+
--expected-hostname <name> Require this live short hostname before mutating
|
|
171
|
+
--artifact-policy <release|local-only>
|
|
172
|
+
Trust policy to evaluate the artifact under
|
|
173
|
+
--approved-target <name> Approved target for a local-only artifact
|
|
174
|
+
--approved-target-identity-kind <kind>
|
|
175
|
+
hardware_uuid_sha256 or tailscale_node_id_sha256
|
|
176
|
+
--approved-target-identity-sha256 <sha256>
|
|
177
|
+
Authenticated digest of the target identity
|
|
178
|
+
|
|
179
|
+
Identity migration. Replacing an installed app with one signed by a different identity
|
|
180
|
+
voids the Microphone and Accessibility grants the installed app holds; macOS keys those
|
|
181
|
+
grants to code identity, and they cannot be restored by the installer. Both approvals are
|
|
182
|
+
one-shot and deliberately distinct: approving a release signer rotation is not approving
|
|
183
|
+
an ad-hoc replacement of a certificate-rooted install.
|
|
184
|
+
--allow-signing-identity-migration Approve one reviewed release signer change
|
|
185
|
+
--expected-old-identity-sha256 <sha256>
|
|
186
|
+
Exact installed identity approved for migration
|
|
187
|
+
--expected-new-identity-sha256 <sha256>
|
|
188
|
+
Exact candidate identity approved for migration
|
|
189
|
+
--allow-adhoc-identity-migration Approve replacing an installed app with an ad-hoc
|
|
190
|
+
signed local-only build. Required for essentially
|
|
191
|
+
every local-only reinstall, including a repair
|
|
192
|
+
install of the same version, because each ad-hoc
|
|
193
|
+
rebuild produces a new CDHash and is therefore a
|
|
194
|
+
real identity migration. Accepts that Microphone
|
|
195
|
+
and Accessibility must be granted again afterwards.
|
|
196
|
+
--acknowledge-local-signing-and-permissions
|
|
197
|
+
Acknowledge local-only ad-hoc signing up front
|
|
198
|
+
|
|
199
|
+
Launch and help:
|
|
200
|
+
--launch Launch the app after installing
|
|
201
|
+
--launch-timeout <seconds> Seconds to wait for the launch to settle
|
|
202
|
+
--help, -h Print this message and exit 0
|
|
203
|
+
USAGE
|
|
204
|
+
}
|
|
205
|
+
|
|
149
206
|
ARTIFACT_PATH=""
|
|
150
207
|
MANIFEST_PATH=""
|
|
151
208
|
EXPECTED_TEAM_ID="${RECORDINGS_EXPECTED_TEAM_IDENTIFIER:-}"
|
|
@@ -162,6 +219,7 @@ APPROVED_TARGET_IDENTITY_KIND=""
|
|
|
162
219
|
APPROVED_TARGET_IDENTITY_SHA256="none"
|
|
163
220
|
ACKNOWLEDGE_LOCAL_SIGNING_AND_PERMISSIONS=0
|
|
164
221
|
ALLOW_IDENTITY_MIGRATION=0
|
|
222
|
+
ALLOW_ADHOC_IDENTITY_MIGRATION=0
|
|
165
223
|
LAUNCH_APP=0
|
|
166
224
|
LAUNCH_TIMEOUT_SECONDS="${RECORDINGS_LAUNCH_TIMEOUT_SECONDS:-10}"
|
|
167
225
|
|
|
@@ -232,6 +290,12 @@ while [ "$#" -gt 0 ]; do
|
|
|
232
290
|
ALLOW_IDENTITY_MIGRATION=1
|
|
233
291
|
shift
|
|
234
292
|
;;
|
|
293
|
+
# Distinct from --allow-signing-identity-migration: approving a release signer
|
|
294
|
+
# rotation is not approving an ad-hoc replacement of a certificate-rooted install.
|
|
295
|
+
--allow-adhoc-identity-migration)
|
|
296
|
+
ALLOW_ADHOC_IDENTITY_MIGRATION=1
|
|
297
|
+
shift
|
|
298
|
+
;;
|
|
235
299
|
--launch)
|
|
236
300
|
LAUNCH_APP=1
|
|
237
301
|
shift
|
|
@@ -240,8 +304,13 @@ while [ "$#" -gt 0 ]; do
|
|
|
240
304
|
LAUNCH_TIMEOUT_SECONDS="${2:-}"
|
|
241
305
|
shift 2
|
|
242
306
|
;;
|
|
307
|
+
--help | -h)
|
|
308
|
+
print_usage
|
|
309
|
+
exit 0
|
|
310
|
+
;;
|
|
243
311
|
*)
|
|
244
312
|
echo "Unknown argument: $1" >&2
|
|
313
|
+
echo "Run with --help for the accepted arguments." >&2
|
|
245
314
|
exit 2
|
|
246
315
|
;;
|
|
247
316
|
esac
|
|
@@ -310,6 +379,19 @@ PACKAGE_ROOT="$(cd "$("$DIRNAME_EXECUTABLE" "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
310
379
|
ARTIFACT_TOOL="${PACKAGE_ROOT}/scripts/macos_artifact.ts"
|
|
311
380
|
RUNTIME_SMOKE="${PACKAGE_ROOT}/scripts/smoke_macos_app.sh"
|
|
312
381
|
TAILSCALE_RESOLVER="${PACKAGE_ROOT}/scripts/resolve_tailscale_cli.sh"
|
|
382
|
+
# Sourced here, before either policy branch, so a missing or unusable guard denies every
|
|
383
|
+
# install rather than only the ones that happen to reach it.
|
|
384
|
+
IDENTITY_MIGRATION_GUARD="${PACKAGE_ROOT}/scripts/enforce_identity_migration.sh"
|
|
385
|
+
[ -f "$IDENTITY_MIGRATION_GUARD" ] && [ ! -L "$IDENTITY_MIGRATION_GUARD" ] || {
|
|
386
|
+
echo "Packaged identity-migration guard is missing." >&2
|
|
387
|
+
exit 2
|
|
388
|
+
}
|
|
389
|
+
# shellcheck source=/dev/null
|
|
390
|
+
. "$IDENTITY_MIGRATION_GUARD"
|
|
391
|
+
declare -F recordings_enforce_identity_migration >/dev/null || {
|
|
392
|
+
echo "Packaged identity-migration guard does not define its enforcement function." >&2
|
|
393
|
+
exit 2
|
|
394
|
+
}
|
|
313
395
|
if [ "$ARTIFACT_POLICY" = "release" ]; then
|
|
314
396
|
if [ -z "$EXPECTED_TEAM_ID" ]; then
|
|
315
397
|
echo "Release install requires --expected-team-id or RECORDINGS_EXPECTED_TEAM_IDENTIFIER." >&2
|
|
@@ -331,13 +413,36 @@ if [ "$ARTIFACT_POLICY" = "release" ]; then
|
|
|
331
413
|
echo "Local-only acknowledgment cannot be supplied for a release artifact." >&2
|
|
332
414
|
exit 2
|
|
333
415
|
fi
|
|
416
|
+
if [ "$ALLOW_ADHOC_IDENTITY_MIGRATION" -eq 1 ]; then
|
|
417
|
+
echo "Ad-hoc identity-migration approval is not valid for a release artifact; use --allow-signing-identity-migration with the exact old/new identity digests." >&2
|
|
418
|
+
exit 2
|
|
419
|
+
fi
|
|
334
420
|
else
|
|
335
|
-
|
|
336
|
-
|
|
421
|
+
# A local-station artifact may be Developer ID signed under a pinned team, because ad-hoc
|
|
422
|
+
# signing cannot hold a macOS TCC grant: TCC keys Accessibility to bundle id plus signing
|
|
423
|
+
# identity, and an ad-hoc CDHash changes on every rebuild, which is what silently killed the
|
|
424
|
+
# Fn/Globe hold-to-talk CGEventTap. A supplied team must therefore be a real Apple team, and
|
|
425
|
+
# it is matched against the manifest below exactly as a release team is.
|
|
426
|
+
if [ -n "$EXPECTED_TEAM_ID" ] && ! [[ "$EXPECTED_TEAM_ID" =~ ^[A-Z0-9]{10}$ ]]; then
|
|
427
|
+
echo "Local-only --expected-team-id must be a 10-character Developer ID team identifier." >&2
|
|
337
428
|
exit 2
|
|
338
429
|
fi
|
|
339
|
-
|
|
340
|
-
|
|
430
|
+
# The approved local-only targets are policy data shared with the artifact tool
|
|
431
|
+
# and the builder, parsed by one sourced reader so all three agree.
|
|
432
|
+
LOCAL_TARGET_POLICY="${PACKAGE_ROOT}/scripts/policy/local-only-approved-targets.txt"
|
|
433
|
+
LOCAL_TARGET_READER="${PACKAGE_ROOT}/scripts/read_local_only_targets.sh"
|
|
434
|
+
[ -f "$LOCAL_TARGET_READER" ] && [ ! -L "$LOCAL_TARGET_READER" ] || {
|
|
435
|
+
echo "Packaged local-only approved target reader is missing." >&2
|
|
436
|
+
exit 2
|
|
437
|
+
}
|
|
438
|
+
# shellcheck source=/dev/null
|
|
439
|
+
. "$LOCAL_TARGET_READER"
|
|
440
|
+
APPROVED_TARGET_MATCHED=0
|
|
441
|
+
APPROVED_TARGET_LIST=""
|
|
442
|
+
read_local_only_targets \
|
|
443
|
+
"$LOCAL_TARGET_POLICY" APPROVED_TARGET_LIST APPROVED_TARGET_MATCHED "$APPROVED_TARGET" || exit 2
|
|
444
|
+
if [ "$APPROVED_TARGET_MATCHED" -ne 1 ]; then
|
|
445
|
+
echo "Local-only install requires an approved --approved-target (${APPROVED_TARGET_LIST})." >&2
|
|
341
446
|
exit 2
|
|
342
447
|
fi
|
|
343
448
|
if ! [[ "$APPROVED_TARGET_IDENTITY_SHA256" =~ ^[a-f0-9]{64}$ ]]; then
|
|
@@ -386,12 +491,16 @@ else
|
|
|
386
491
|
exit 1
|
|
387
492
|
fi
|
|
388
493
|
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
|
|
494
|
+
echo "Release identity-migration flags are not valid for local-only artifacts; an ad-hoc replacement is approved with --allow-adhoc-identity-migration." >&2
|
|
390
495
|
exit 2
|
|
391
496
|
fi
|
|
392
|
-
EXPECTED_TEAM_ID
|
|
497
|
+
if [ -z "$EXPECTED_TEAM_ID" ]; then
|
|
498
|
+
EXPECTED_TEAM_ID="ADHOC"
|
|
499
|
+
fi
|
|
393
500
|
echo "WARNING: installing a non-notarized local-only artifact approved only for ${APPROVED_TARGET}." >&2
|
|
394
|
-
|
|
501
|
+
if [ "$EXPECTED_TEAM_ID" = "ADHOC" ]; then
|
|
502
|
+
echo "WARNING: code identity can change; Microphone or Accessibility may require manual reauthorization." >&2
|
|
503
|
+
fi
|
|
395
504
|
fi
|
|
396
505
|
if [ "$ARTIFACT_POLICY" = "release" ] && [ "$ALLOW_IDENTITY_MIGRATION" -eq 1 ] && \
|
|
397
506
|
{ [ -z "$EXPECTED_OLD_IDENTITY_SHA256" ] || [ -z "$EXPECTED_NEW_IDENTITY_SHA256" ]; }; then
|
|
@@ -1621,7 +1730,10 @@ if [ "$ARTIFACT_POLICY" = "release" ] && [ -z "$candidate_requirement" ]; then
|
|
|
1621
1730
|
exit 1
|
|
1622
1731
|
fi
|
|
1623
1732
|
identity_migration=0
|
|
1624
|
-
|
|
1733
|
+
# Pass the SAME pinned team already given to verify-archive and verify-app. Without it the
|
|
1734
|
+
# digest verifies the candidate against the ad-hoc shape, which rejects a correctly
|
|
1735
|
+
# Developer-ID-signed local-station bundle and, under `set -euo pipefail`, aborts the install.
|
|
1736
|
+
candidate_identity_sha256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" requirement-digest --app "$CANDIDATE_APP" --artifact-policy "$ARTIFACT_POLICY" --expected-team-id "$EXPECTED_TEAM_ID")"
|
|
1625
1737
|
candidate_tree_sha256="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$CANDIDATE_APP")"
|
|
1626
1738
|
[ "$candidate_identity_sha256" = "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
|
|
1627
1739
|
--manifest "$MANIFEST_SNAPSHOT" \
|
|
@@ -1655,21 +1767,18 @@ for existing_app in ${MANAGEABLE_APPS[@]+"${MANAGEABLE_APPS[@]}"}; do
|
|
|
1655
1767
|
identity_migration=1
|
|
1656
1768
|
fi
|
|
1657
1769
|
done
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
echo "Identity migration approval was supplied but no identity migration is required." >&2
|
|
1671
|
-
exit 1
|
|
1672
|
-
fi
|
|
1770
|
+
# Enforced for every artifact policy. A local-only artifact is ad-hoc signed, so this is
|
|
1771
|
+
# the point at which an install would void the installed app's Microphone and
|
|
1772
|
+
# Accessibility grants; the printed warning further down is not a gate.
|
|
1773
|
+
recordings_enforce_identity_migration \
|
|
1774
|
+
"$ARTIFACT_POLICY" \
|
|
1775
|
+
"$identity_migration" \
|
|
1776
|
+
"$ALLOW_IDENTITY_MIGRATION" \
|
|
1777
|
+
"$ALLOW_ADHOC_IDENTITY_MIGRATION" \
|
|
1778
|
+
"$previous_identity_sha256" \
|
|
1779
|
+
"$candidate_identity_sha256" \
|
|
1780
|
+
"$EXPECTED_OLD_IDENTITY_SHA256" \
|
|
1781
|
+
"$EXPECTED_NEW_IDENTITY_SHA256" || exit 1
|
|
1673
1782
|
|
|
1674
1783
|
TRANSACTION_DIR="$("$MKTEMP_EXECUTABLE" -d "${APP_PARENT}/.Recordings-transaction.XXXXXX")"
|
|
1675
1784
|
TRANSACTION_NONCE="$("$BUN_EXECUTABLE" -e 'import { randomUUID } from "node:crypto"; process.stdout.write(randomUUID())')"
|
|
@@ -1962,7 +2071,12 @@ fi
|
|
|
1962
2071
|
|
|
1963
2072
|
if [ "$ARTIFACT_POLICY" = "local_only" ]; then
|
|
1964
2073
|
echo "Installed local-only Recordings.app for ${APPROVED_TARGET}; this artifact is ad-hoc signed and non-notarized."
|
|
1965
|
-
|
|
2074
|
+
if [ "$identity_migration" -eq 1 ]; then
|
|
2075
|
+
# Not a maybe: the designated requirement did change, under an explicit approval.
|
|
2076
|
+
echo "Code identity changed under --allow-adhoc-identity-migration; the Microphone and Accessibility grants held by the replaced app no longer apply and must be reauthorized."
|
|
2077
|
+
else
|
|
2078
|
+
echo "Microphone or Accessibility may require manual reauthorization if this code identity differs from the last authorized one."
|
|
2079
|
+
fi
|
|
1966
2080
|
elif [ "$identity_migration" -eq 1 ]; then
|
|
1967
2081
|
echo "Installed a new signing identity; macOS will require one-time permission approval for this migration."
|
|
1968
2082
|
else
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
unlinkSync,
|
|
32
32
|
} from "node:fs";
|
|
33
33
|
import { tmpdir } from "node:os";
|
|
34
|
+
import { fileURLToPath } from "node:url";
|
|
34
35
|
import { crc32, inflateRawSync } from "node:zlib";
|
|
35
36
|
import {
|
|
36
37
|
nativeFsGuard,
|
|
@@ -57,6 +58,97 @@ export const BUNDLE_ID = "com.hasna.recordings";
|
|
|
57
58
|
export const PROVENANCE_FILENAME = "recordings-build-provenance.json";
|
|
58
59
|
export const RELEASE_APPROVED_TARGET = "fleet";
|
|
59
60
|
export const LEGACY_LOCAL_TARGET_IDENTITY_KIND = "hardware_uuid_sha256";
|
|
61
|
+
export const LOCAL_ONLY_APPROVED_TARGETS_POLICY_PATH = join(
|
|
62
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
63
|
+
"policy",
|
|
64
|
+
"local-only-approved-targets.txt",
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// The approved local-only targets are policy data, not code: one file is read by
|
|
68
|
+
// this validator and, through scripts/read_local_only_targets.sh, by both shell entry
|
|
69
|
+
// points, so a target is declared once. The shell reader is a separate implementation
|
|
70
|
+
// of these rules; equivalence is enforced by an executing contract test, not by shared
|
|
71
|
+
// code. Parsing stays deliberately strict — a malformed policy fails closed rather than
|
|
72
|
+
// silently widening the allowlist.
|
|
73
|
+
//
|
|
74
|
+
// This is an operator-integrity control, NOT an authorization boundary. The file sits in
|
|
75
|
+
// the same package root as the guards that read it, so anyone able to edit it can edit
|
|
76
|
+
// those guards. Widening it still grants nothing on its own: the install additionally
|
|
77
|
+
// requires a matching `hostname -s`, a matching authenticated Tailscale node digest, and
|
|
78
|
+
// a matching authenticated manifest SHA-256.
|
|
79
|
+
export function localOnlyApprovedTargets(
|
|
80
|
+
policyPath: string = LOCAL_ONLY_APPROVED_TARGETS_POLICY_PATH,
|
|
81
|
+
): string[] {
|
|
82
|
+
// lstat, not readFileSync alone: readFileSync FOLLOWS a symlink, and the shell reader
|
|
83
|
+
// refuses one outright (`[ -L ]`). That divergence was real and it favoured the
|
|
84
|
+
// attacker — a policy symlinked at a widened allowlist was rejected by the shell gate
|
|
85
|
+
// and silently honoured here. Refusing every non-regular file matches the shell reader
|
|
86
|
+
// and keeps the wording it prints, so both sides fail closed identically.
|
|
87
|
+
let policyStats;
|
|
88
|
+
try {
|
|
89
|
+
policyStats = lstatSync(policyPath);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error("local-only approved target policy is missing");
|
|
92
|
+
}
|
|
93
|
+
if (!policyStats.isFile()) {
|
|
94
|
+
throw new Error("local-only approved target policy is missing");
|
|
95
|
+
}
|
|
96
|
+
// An unreadable (mode 000) policy passes the lstat above and then threw a raw EACCES from
|
|
97
|
+
// here, while the shell reader emitted bash's own "Permission denied" and no reader
|
|
98
|
+
// message. Same fail-closed outcome, two unrecognizable errors; both now say this.
|
|
99
|
+
let rawContents: string;
|
|
100
|
+
try {
|
|
101
|
+
rawContents = readFileSync(policyPath, "utf8");
|
|
102
|
+
} catch {
|
|
103
|
+
throw new Error("local-only approved target policy is not readable");
|
|
104
|
+
}
|
|
105
|
+
// Reject a NUL anywhere, in the same order and with the same wording as the shell
|
|
106
|
+
// reader's pre-scan. This reader had no NUL check at all and only ever refused one that
|
|
107
|
+
// happened to land inside a would-be hostname: "# comment\0" was dropped as a comment,
|
|
108
|
+
// so "# comment\0\nstation03\n" was refused by the shell gate and ACCEPTED here.
|
|
109
|
+
if (rawContents.includes("\u0000")) {
|
|
110
|
+
throw new Error("local-only approved target policy contains a NUL byte");
|
|
111
|
+
}
|
|
112
|
+
// Strip a BOM at offset 0 only. A U+FEFF anywhere else is a zero-width no-break space,
|
|
113
|
+
// not a byte-order mark; the shell reader used to strip one per line and therefore
|
|
114
|
+
// accepted a BOM on line 2 that this reader rejected.
|
|
115
|
+
const contents = rawContents.replace(/^/, "");
|
|
116
|
+
const targets = contents
|
|
117
|
+
.split("\n")
|
|
118
|
+
// Deliberately NOT String.trim(): trim() also strips U+FEFF and U+00A0, and the
|
|
119
|
+
// shell reader trims neither, so a trailing NBSP or BOM would be accepted here and
|
|
120
|
+
// rejected there. ASCII space and tab only, matching read_local_only_targets.sh
|
|
121
|
+
// under its pinned LC_ALL=C; everything else is left for the hostname shape below.
|
|
122
|
+
.map((line) => line.replace(/\r$/, "").replace(/^[\t ]+|[\t ]+$/g, ""))
|
|
123
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
124
|
+
if (targets.length === 0) {
|
|
125
|
+
throw new Error("local-only approved target policy lists no targets");
|
|
126
|
+
}
|
|
127
|
+
for (const target of targets) {
|
|
128
|
+
if (!/^[a-z][a-z0-9-]{1,30}[a-z0-9]$/.test(target)) {
|
|
129
|
+
throw new Error(`local-only approved target policy has an invalid target name: ${target}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (new Set(targets).size !== targets.length) {
|
|
133
|
+
throw new Error("local-only approved target policy has duplicate targets");
|
|
134
|
+
}
|
|
135
|
+
if (targets.includes(RELEASE_APPROVED_TARGET)) {
|
|
136
|
+
throw new Error("local-only approved target policy must not list the release fleet target");
|
|
137
|
+
}
|
|
138
|
+
return targets;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isLocalOnlyApprovedTarget(
|
|
142
|
+
target: string | undefined,
|
|
143
|
+
policyPath?: string,
|
|
144
|
+
): boolean {
|
|
145
|
+
if (target === undefined) return false;
|
|
146
|
+
return localOnlyApprovedTargets(policyPath).includes(target);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function localOnlyApprovedTargetsMessage(policyPath?: string): string {
|
|
150
|
+
return localOnlyApprovedTargets(policyPath).join(", ");
|
|
151
|
+
}
|
|
60
152
|
|
|
61
153
|
export type ArtifactPolicy = "release" | "local_only";
|
|
62
154
|
export type TargetIdentityKind =
|
|
@@ -95,7 +187,7 @@ export type MacOSArtifactManifest = BuildProvenance & {
|
|
|
95
187
|
};
|
|
96
188
|
provenance_sha256: string;
|
|
97
189
|
signing: {
|
|
98
|
-
mode?: "ad_hoc";
|
|
190
|
+
mode?: "ad_hoc" | "developer_id";
|
|
99
191
|
authority: string;
|
|
100
192
|
team_id: string;
|
|
101
193
|
trusted_timestamp: string;
|
|
@@ -460,14 +552,43 @@ export function parseDesignatedRequirement(output: string): string {
|
|
|
460
552
|
return requirement;
|
|
461
553
|
}
|
|
462
554
|
|
|
555
|
+
/**
|
|
556
|
+
* A local-station artifact is signed one of two ways, and the expected team identifier is
|
|
557
|
+
* what says which: the sentinel `ADHOC`, or a real 10-character Apple team.
|
|
558
|
+
*
|
|
559
|
+
* Developer ID signing exists on this path because ad-hoc signing cannot hold a macOS TCC
|
|
560
|
+
* grant. TCC keys Accessibility (and Microphone) to bundle id plus signing identity, and an
|
|
561
|
+
* ad-hoc signature offers only a CDHash that changes on every rebuild, so the hold-to-talk
|
|
562
|
+
* CGEventTap in FnKeyMonitor.swift lost its permission on each reinstall. Ad-hoc remains
|
|
563
|
+
* supported for builders that hold no signing key.
|
|
564
|
+
*/
|
|
565
|
+
export function isLocalOnlySigningTeam(expectedTeamId: string): boolean {
|
|
566
|
+
return expectedTeamId === "ADHOC" || /^[A-Z0-9]{10}$/.test(expectedTeamId);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function localOnlySigningMode(expectedTeamId: string): "ad_hoc" | "developer_id" {
|
|
570
|
+
if (expectedTeamId === "ADHOC") return "ad_hoc";
|
|
571
|
+
if (!/^[A-Z0-9]{10}$/.test(expectedTeamId)) {
|
|
572
|
+
throw new Error(
|
|
573
|
+
"local-only Developer ID signing requires ADHOC or a 10-character Apple team identifier",
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
return "developer_id";
|
|
577
|
+
}
|
|
578
|
+
|
|
463
579
|
export function designatedRequirementForPolicy(
|
|
464
580
|
output: string,
|
|
465
581
|
artifactPolicy: ArtifactPolicy,
|
|
466
582
|
adHocSignatureVerified = false,
|
|
583
|
+
developerIdSignatureVerified = false,
|
|
467
584
|
): string {
|
|
468
585
|
if (artifactPolicy === "release") return parseDesignatedRequirement(output);
|
|
586
|
+
// A Developer ID local-station artifact always carries a real designated requirement, and
|
|
587
|
+
// that requirement is the whole point of signing it: it is the stable identity a TCC grant
|
|
588
|
+
// attaches to. Never let it degrade to the ad-hoc sentinel.
|
|
589
|
+
if (developerIdSignatureVerified) return parseDesignatedRequirement(output);
|
|
469
590
|
if (!adHocSignatureVerified) {
|
|
470
|
-
throw new Error("local-only designated requirement evidence requires verified
|
|
591
|
+
throw new Error("local-only designated requirement evidence requires verified signing");
|
|
471
592
|
}
|
|
472
593
|
try {
|
|
473
594
|
return parseDesignatedRequirement(output);
|
|
@@ -558,11 +679,24 @@ function signingEvidence(
|
|
|
558
679
|
if (!timestamp || timestamp.toLowerCase() === "none") {
|
|
559
680
|
throw new Error(`${codePath} is missing a trusted signing timestamp`);
|
|
560
681
|
}
|
|
682
|
+
} else if (localOnlySigningMode(expectedTeamId) === "developer_id") {
|
|
683
|
+
// Developer ID signed but deliberately NOT notarized: the notarization, single-target
|
|
684
|
+
// binding and builder attestation are what still separate this from a release. Only the
|
|
685
|
+
// signing identity is shared, and only because TCC cannot key a durable Accessibility
|
|
686
|
+
// grant to anything weaker (see localOnlySigningMode).
|
|
687
|
+
if (!authority.startsWith("Developer ID Application:")) {
|
|
688
|
+
throw new Error(`${codePath} is not signed by a Developer ID Application authority`);
|
|
689
|
+
}
|
|
690
|
+
if (teamId !== expectedTeamId) {
|
|
691
|
+
throw new Error(`${codePath} TeamIdentifier ${teamId || "missing"} does not match ${expectedTeamId}`);
|
|
692
|
+
}
|
|
693
|
+
if (rawSignature.toLowerCase() === "adhoc") {
|
|
694
|
+
throw new Error(`${codePath} is ad-hoc signed but a Developer ID team was pinned`);
|
|
695
|
+
}
|
|
696
|
+
// A trusted timestamp is a release-only claim; local-station builds never pass --timestamp.
|
|
697
|
+
timestamp = rawTimestamp && rawTimestamp.toLowerCase() !== "none" ? rawTimestamp : "none";
|
|
561
698
|
} else {
|
|
562
699
|
mode = "ad_hoc";
|
|
563
|
-
if (expectedTeamId !== "ADHOC") {
|
|
564
|
-
throw new Error("local-only code verification requires the ADHOC signing identity");
|
|
565
|
-
}
|
|
566
700
|
if (rawSignature.toLowerCase() !== "adhoc" || authority) {
|
|
567
701
|
throw new Error(`${codePath} is not consistently ad-hoc signed for local-only use`);
|
|
568
702
|
}
|
|
@@ -587,6 +721,7 @@ function signingEvidence(
|
|
|
587
721
|
requirementOutput,
|
|
588
722
|
artifactPolicy,
|
|
589
723
|
mode === "ad_hoc",
|
|
724
|
+
artifactPolicy !== "release" && mode === "developer_id",
|
|
590
725
|
);
|
|
591
726
|
const entitlements = canonicalEntitlements(codePath);
|
|
592
727
|
if (entitlements !== canonicalJson(expectedEntitlements)) {
|
|
@@ -1510,7 +1645,7 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
|
|
|
1510
1645
|
} else {
|
|
1511
1646
|
if (
|
|
1512
1647
|
manifest.artifact_policy !== "local_only" ||
|
|
1513
|
-
manifest.approved_target
|
|
1648
|
+
!isLocalOnlyApprovedTarget(manifest.approved_target) ||
|
|
1514
1649
|
(manifest.approved_target_identity_kind !== undefined &&
|
|
1515
1650
|
manifest.approved_target_identity_kind !== "tailscale_node_id_sha256") ||
|
|
1516
1651
|
(manifest.approved_target_identity_kind === undefined) !==
|
|
@@ -1523,20 +1658,38 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
|
|
|
1523
1658
|
!isHex(manifest.builder_identity_sha256, 64) ||
|
|
1524
1659
|
manifest.builder_identity_sha256 === manifest.approved_target_identity_sha256
|
|
1525
1660
|
) {
|
|
1526
|
-
throw new Error(
|
|
1661
|
+
throw new Error(
|
|
1662
|
+
"local-only schema v3 requires an approved target name " +
|
|
1663
|
+
`(${localOnlyApprovedTargetsMessage()}) and machine identity`,
|
|
1664
|
+
);
|
|
1527
1665
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
manifest
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1666
|
+
// A local-station manifest is internally consistent in exactly one of two shapes: fully
|
|
1667
|
+
// ad-hoc, or fully Developer ID under one pinned team. Mixing them (e.g. a Developer ID
|
|
1668
|
+
// app with an ad-hoc helper) would leave the bundle without one stable identity for TCC
|
|
1669
|
+
// to key on, which is the failure this whole path exists to prevent.
|
|
1670
|
+
if (manifest.non_notarized !== true || !isLocalOnlySigningTeam(manifest.team_id)) {
|
|
1671
|
+
throw new Error("local-only manifest requires a non-notarized ADHOC or Developer ID team");
|
|
1672
|
+
}
|
|
1673
|
+
if (manifest.team_id === "ADHOC") {
|
|
1674
|
+
if (
|
|
1675
|
+
manifest.signing.mode !== "ad_hoc" ||
|
|
1676
|
+
manifest.signing.authority !== "adhoc" ||
|
|
1677
|
+
manifest.signing.helper_authority !== "adhoc" ||
|
|
1678
|
+
manifest.signing.team_id !== "ADHOC" ||
|
|
1679
|
+
manifest.signing.helper_team_id !== "ADHOC" ||
|
|
1680
|
+
manifest.signing.trusted_timestamp !== "none" ||
|
|
1681
|
+
manifest.signing.helper_trusted_timestamp !== "none"
|
|
1682
|
+
) {
|
|
1683
|
+
throw new Error("local-only manifest requires consistent ad-hoc signing evidence");
|
|
1684
|
+
}
|
|
1685
|
+
} else if (
|
|
1686
|
+
manifest.signing.mode !== "developer_id" ||
|
|
1687
|
+
!manifest.signing.authority.startsWith("Developer ID Application:") ||
|
|
1688
|
+
!manifest.signing.helper_authority.startsWith("Developer ID Application:") ||
|
|
1689
|
+
manifest.signing.team_id !== manifest.team_id ||
|
|
1690
|
+
manifest.signing.helper_team_id !== manifest.team_id
|
|
1538
1691
|
) {
|
|
1539
|
-
throw new Error("local-only manifest requires consistent
|
|
1692
|
+
throw new Error("local-only manifest requires consistent Developer ID signing evidence");
|
|
1540
1693
|
}
|
|
1541
1694
|
if (
|
|
1542
1695
|
manifest.notarization.status !== "Not Submitted" ||
|
|
@@ -1962,15 +2115,19 @@ function writeProvenance(
|
|
|
1962
2115
|
throw new Error("release provenance has an invalid target or Team ID");
|
|
1963
2116
|
}
|
|
1964
2117
|
} else if (
|
|
1965
|
-
|
|
1966
|
-
|
|
2118
|
+
// ADHOC or a pinned Developer ID team; localOnlySigningMode rejects anything else.
|
|
2119
|
+
!isLocalOnlySigningTeam(expectedTeamId) ||
|
|
2120
|
+
!isLocalOnlyApprovedTarget(approvedTarget) ||
|
|
1967
2121
|
approvedTargetIdentityKind !== "tailscale_node_id_sha256" ||
|
|
1968
2122
|
builderIdentityKind !== "tailscale_node_id_sha256" ||
|
|
1969
2123
|
!isHex(approvedTargetIdentitySha256, 64) ||
|
|
1970
2124
|
!isHex(builderIdentitySha256, 64) ||
|
|
1971
2125
|
approvedTargetIdentitySha256 === builderIdentitySha256
|
|
1972
2126
|
) {
|
|
1973
|
-
throw new Error(
|
|
2127
|
+
throw new Error(
|
|
2128
|
+
"new local-only provenance requires ADHOC or a pinned Developer ID team and a Tailscale " +
|
|
2129
|
+
`node-bound approved-target identity (${localOnlyApprovedTargetsMessage()})`,
|
|
2130
|
+
);
|
|
1974
2131
|
} else {
|
|
1975
2132
|
provenance.artifact_policy = "local_only";
|
|
1976
2133
|
provenance.approved_target = approvedTarget;
|
|
@@ -2120,11 +2277,13 @@ function finalizeLocalArtifact(
|
|
|
2120
2277
|
approvedTarget: string,
|
|
2121
2278
|
approvedTargetIdentityKind: TargetIdentityKind,
|
|
2122
2279
|
approvedTargetIdentitySha256: string,
|
|
2280
|
+
expectedTeamId: string = "ADHOC",
|
|
2123
2281
|
): void {
|
|
2124
2282
|
assertCurrentSourceRevision(packageRoot, expectedSourceSha);
|
|
2125
2283
|
if (approvedTargetIdentityKind !== "tailscale_node_id_sha256") {
|
|
2126
2284
|
throw new Error("new local-only artifacts require a Tailscale node ID identity hash");
|
|
2127
2285
|
}
|
|
2286
|
+
const localSigningMode = localOnlySigningMode(expectedTeamId);
|
|
2128
2287
|
const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
|
|
2129
2288
|
const helperPath = join(appPath, "Contents", "Helpers", "recordings");
|
|
2130
2289
|
const embeddedPath = provenancePath(appPath);
|
|
@@ -2137,26 +2296,26 @@ function finalizeLocalArtifact(
|
|
|
2137
2296
|
provenance.approved_target_identity_sha256 !== approvedTargetIdentitySha256 ||
|
|
2138
2297
|
provenance.builder_identity_kind !== "tailscale_node_id_sha256" ||
|
|
2139
2298
|
provenance.non_notarized !== true ||
|
|
2140
|
-
provenance.team_id !==
|
|
2299
|
+
provenance.team_id !== expectedTeamId
|
|
2141
2300
|
) {
|
|
2142
2301
|
throw new Error("embedded provenance is not approved for this local-only target");
|
|
2143
2302
|
}
|
|
2144
2303
|
assertExpectedCodeLayout(appPath);
|
|
2145
2304
|
const outerSigning = signingEvidence(
|
|
2146
2305
|
appPath,
|
|
2147
|
-
|
|
2306
|
+
expectedTeamId,
|
|
2148
2307
|
APP_ENTITLEMENTS,
|
|
2149
2308
|
executablePath,
|
|
2150
2309
|
"local_only",
|
|
2151
2310
|
);
|
|
2152
2311
|
const helperSigning = signingEvidence(
|
|
2153
2312
|
helperPath,
|
|
2154
|
-
|
|
2313
|
+
expectedTeamId,
|
|
2155
2314
|
HELPER_ENTITLEMENTS,
|
|
2156
2315
|
helperPath,
|
|
2157
2316
|
"local_only",
|
|
2158
2317
|
);
|
|
2159
|
-
const items = nestedItems(appPath,
|
|
2318
|
+
const items = nestedItems(appPath, expectedTeamId, "local_only", outerSigning, helperSigning);
|
|
2160
2319
|
const manifest: MacOSArtifactManifest = {
|
|
2161
2320
|
...provenance,
|
|
2162
2321
|
artifact_type: "recordings-macos-app",
|
|
@@ -2164,7 +2323,7 @@ function finalizeLocalArtifact(
|
|
|
2164
2323
|
binding: { bundle_tree_sha256: treeDigest(appPath) },
|
|
2165
2324
|
provenance_sha256: sha256File(embeddedPath),
|
|
2166
2325
|
signing: {
|
|
2167
|
-
mode:
|
|
2326
|
+
mode: localSigningMode,
|
|
2168
2327
|
authority: outerSigning.authority,
|
|
2169
2328
|
team_id: outerSigning.teamId,
|
|
2170
2329
|
trusted_timestamp: outerSigning.timestamp,
|
|
@@ -2208,7 +2367,7 @@ function finalizeLocalArtifact(
|
|
|
2208
2367
|
appPath,
|
|
2209
2368
|
archivePath,
|
|
2210
2369
|
manifest,
|
|
2211
|
-
|
|
2370
|
+
expectedTeamId,
|
|
2212
2371
|
"local_only",
|
|
2213
2372
|
approvedTarget,
|
|
2214
2373
|
approvedTargetIdentitySha256,
|
|
@@ -2301,11 +2460,19 @@ function assertInstallTransition(
|
|
|
2301
2460
|
assertVersionTransition(installedVersion, installedSource, manifest);
|
|
2302
2461
|
}
|
|
2303
2462
|
|
|
2304
|
-
function requirementDigest(
|
|
2463
|
+
function requirementDigest(
|
|
2464
|
+
appPath: string,
|
|
2465
|
+
artifactPolicy: ArtifactPolicy,
|
|
2466
|
+
expectedTeamId: string = "ADHOC",
|
|
2467
|
+
): void {
|
|
2305
2468
|
if (artifactPolicy === "local_only") {
|
|
2469
|
+
// The pinned team decides which local signing shape is verified. Hard-coding "ADHOC" here
|
|
2470
|
+
// rejected every correctly Developer-ID-signed local-station bundle, and because
|
|
2471
|
+
// install_macos_app.sh calls this for EVERY local install under `set -euo pipefail`, that
|
|
2472
|
+
// rejection aborted the install outright — making a properly signed build uninstallable.
|
|
2306
2473
|
const evidence = signingEvidence(
|
|
2307
2474
|
appPath,
|
|
2308
|
-
|
|
2475
|
+
expectedTeamId,
|
|
2309
2476
|
APP_ENTITLEMENTS,
|
|
2310
2477
|
join(appPath, "Contents", "MacOS", "Recordings"),
|
|
2311
2478
|
"local_only",
|
|
@@ -2369,8 +2536,8 @@ function verifyActiveApp(
|
|
|
2369
2536
|
const environment: NodeJS.ProcessEnv = {
|
|
2370
2537
|
HOME: contractHome,
|
|
2371
2538
|
PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
|
|
2372
|
-
|
|
2373
|
-
|
|
2539
|
+
HASNA_RECORDINGS_CLIENT_STORE: "sqlite",
|
|
2540
|
+
RECORDINGS_CLIENT_STORE: "sqlite",
|
|
2374
2541
|
HASNA_RECORDINGS_DB_PATH: join(contractHome, "recordings.db"),
|
|
2375
2542
|
RECORDINGS_AUDIO_DIR: join(contractHome, "audio"),
|
|
2376
2543
|
};
|
|
@@ -2702,7 +2869,7 @@ function readJournal(path: string): InstallJournal {
|
|
|
2702
2869
|
journal.approved_target_identity_sha256 !== "none" ||
|
|
2703
2870
|
journal.builder_identity_kind !== "none")) ||
|
|
2704
2871
|
(journal.artifact_policy === "local_only" &&
|
|
2705
|
-
(journal.approved_target
|
|
2872
|
+
(!isLocalOnlyApprovedTarget(journal.approved_target) ||
|
|
2706
2873
|
!isTargetIdentityKind(journal.approved_target_identity_kind) ||
|
|
2707
2874
|
!isTargetIdentityKind(journal.builder_identity_kind) ||
|
|
2708
2875
|
journal.approved_target_identity_kind !== journal.builder_identity_kind ||
|
|
@@ -5135,6 +5302,7 @@ function main(): void {
|
|
|
5135
5302
|
argument("--approved-target"),
|
|
5136
5303
|
targetIdentityKindArgument(undefined, true),
|
|
5137
5304
|
argument("--approved-target-identity-sha256"),
|
|
5305
|
+
optionalArgument("--expected-team-id") ?? "ADHOC",
|
|
5138
5306
|
);
|
|
5139
5307
|
} else if (command === "verify-archive") {
|
|
5140
5308
|
const teamId = argument("--team-id");
|
|
@@ -5207,7 +5375,11 @@ function main(): void {
|
|
|
5207
5375
|
argument("--manifest-sha256"),
|
|
5208
5376
|
);
|
|
5209
5377
|
} else if (command === "requirement-digest") {
|
|
5210
|
-
requirementDigest(
|
|
5378
|
+
requirementDigest(
|
|
5379
|
+
argument("--app"),
|
|
5380
|
+
artifactPolicyArgument(),
|
|
5381
|
+
optionalArgument("--expected-team-id") ?? "ADHOC",
|
|
5382
|
+
);
|
|
5211
5383
|
} else if (command === "tailscale-node-id-sha256") {
|
|
5212
5384
|
console.log(tailscaleNodeIdSha256(readFileSync(0, "utf8"), argument("--expected-hostname")));
|
|
5213
5385
|
} else if (command === "verify-filesystem-tree") {
|