@hasna/recordings 0.3.13 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +279 -43
- package/bun.lock +37 -38
- package/contracts/v1/fixtures.json +1206 -0
- package/dist/cli/index.js +1909 -619
- package/dist/cli/macos-shortcut.d.ts +2 -2
- package/dist/contracts/hosted-v1.d.ts +105 -0
- package/dist/contracts/hosted-v1.d.ts.map +1 -0
- package/dist/contracts/hosted-v1.js +41 -0
- package/dist/contracts/stream-v1.d.ts +117 -0
- package/dist/contracts/stream-v1.d.ts.map +1 -0
- package/dist/contracts/stream-v1.js +35 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +6 -0
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/hosted/index.d.ts +58 -0
- package/dist/hosted/index.d.ts.map +1 -0
- package/dist/hosted/index.js +266 -0
- package/dist/hosted/transport.d.ts +36 -0
- package/dist/hosted/transport.d.ts.map +1 -0
- package/dist/hosted-v1-aavn7ktb.js +4114 -0
- package/dist/hosted-v1-gdr9extc.js +84 -0
- package/dist/http/client.d.ts +183 -22
- package/dist/http/client.d.ts.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2879 -2288
- package/dist/lib/audio-artifact-storage.d.ts +98 -0
- package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
- package/dist/lib/capture-probe.d.ts +3 -3
- package/dist/lib/capture-probe.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/local-opt-in.d.ts +132 -0
- package/dist/lib/local-opt-in.d.ts.map +1 -0
- package/dist/lib/macos-bundle.d.ts +1 -1
- package/dist/lib/persistence-probe.d.ts +56 -17
- package/dist/lib/persistence-probe.d.ts.map +1 -1
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1713 -459
- package/dist/sdk/index.d.ts +13 -5
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +904 -1
- package/dist/sdk/resolve.d.ts +53 -0
- package/dist/sdk/resolve.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +6 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/client-ip.d.ts +39 -0
- package/dist/server/client-ip.d.ts.map +1 -0
- package/dist/server/cloud-config.d.ts +12 -0
- 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 +20 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +590 -287
- package/dist/server/migrate-command.d.ts +1 -1
- package/dist/server/migrate-command.d.ts.map +1 -1
- package/dist/server/openapi.d.ts +21 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.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 +1358 -874
- package/dist/store.d.ts +12 -3
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +15 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/hosted-sdk.md +71 -0
- package/docs/wire-contracts.md +24 -0
- package/package.json +33 -11
- package/packaging/macos/build_release_pkg.sh +3 -3
- package/packaging/macos/managed_bootstrap.sh +4 -4
- package/packaging/macos/scripts/postinstall +2 -2
- package/packaging/macos/scripts/preinstall +2 -2
- package/scripts/build_companion_cli.sh +7 -1
- package/scripts/ci-linux-suite.ts +23 -13
- package/scripts/install_macos_app.sh +27 -24
- package/scripts/macos_artifact.ts +60 -49
- package/scripts/migrate.ts +4 -20
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +36 -4
- package/scripts/native-core-receipt.py +171 -0
- package/scripts/native_fs_guard.ts +2 -0
- package/scripts/release-suite-gate.ts +227 -150
- package/scripts/resolve_tailscale_cli.sh +24 -3
- package/scripts/smoke_macos_app.sh +21 -21
- package/src/native/Recordings/App/ContentView.swift +53 -68
- package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
- package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
- package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
- package/src/native/Recordings/App/RecordingsApp.swift +185 -9
- package/src/native/Recordings/App/RecordingsListView.swift +60 -112
- package/src/native/Recordings/App/RecordingsStore.swift +124 -109
- package/src/native/Recordings/App/Theme.swift +73 -61
- package/src/native/Recordings/Package.swift +1 -0
- package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
- package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
- package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
- package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
- package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
- package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
- package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
- package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
- package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
- package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
- package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
- package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
- package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
- package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
- package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
- package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
- package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
- package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
- package/src/native/Recordings/build.sh +8 -7
- package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
- package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
- package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
- package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
- package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
- package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
- package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
- package/dist/__tests__/preload.d.ts +0 -2
- package/dist/__tests__/preload.d.ts.map +0 -1
- package/src/native/Recordings/App/SidebarView.swift +0 -212
|
@@ -126,18 +126,18 @@ require_absolute_canonical_owned_home() {
|
|
|
126
126
|
local canonical
|
|
127
127
|
case "$candidate" in
|
|
128
128
|
/*) ;;
|
|
129
|
-
*) echo "
|
|
129
|
+
*) echo "Hasna Recordings.app installation requires HOME to be an absolute canonical path." >&2; exit 1 ;;
|
|
130
130
|
esac
|
|
131
131
|
[ -d "$candidate" ] && [ ! -L "$candidate" ] || {
|
|
132
|
-
echo "
|
|
132
|
+
echo "Hasna Recordings.app installation requires HOME to be an absolute canonical path to a non-symlink directory." >&2
|
|
133
133
|
exit 1
|
|
134
134
|
}
|
|
135
135
|
canonical="$(cd "$candidate" 2>/dev/null && builtin pwd -P)" || {
|
|
136
|
-
echo "
|
|
136
|
+
echo "Hasna Recordings.app installation could not resolve HOME canonically." >&2
|
|
137
137
|
exit 1
|
|
138
138
|
}
|
|
139
139
|
[ "$candidate" = "$canonical" ] || {
|
|
140
|
-
echo "
|
|
140
|
+
echo "Hasna Recordings.app installation requires HOME to be an absolute canonical path." >&2
|
|
141
141
|
exit 1
|
|
142
142
|
}
|
|
143
143
|
[ "$("$STAT_EXECUTABLE" -f '%u' "$candidate")" = "$("$ID_EXECUTABLE" -u)" ] || {
|
|
@@ -352,7 +352,7 @@ require_absolute_executable "BUN_EXECUTABLE" "$BUN_EXECUTABLE"
|
|
|
352
352
|
require_absolute_canonical_owned_home
|
|
353
353
|
|
|
354
354
|
if [ "$("$UNAME_EXECUTABLE" -s)" != "Darwin" ]; then
|
|
355
|
-
echo "
|
|
355
|
+
echo "Hasna Recordings.app installation is only supported on macOS." >&2
|
|
356
356
|
exit 1
|
|
357
357
|
fi
|
|
358
358
|
|
|
@@ -546,7 +546,7 @@ fi
|
|
|
546
546
|
"$BUN_EXECUTABLE" "$ARTIFACT_TOOL" native-fs-guard-check
|
|
547
547
|
DATA_DIR="${HOME}/.hasna/recordings"
|
|
548
548
|
DATA_PARENT="$("$DIRNAME_EXECUTABLE" "$DATA_DIR")"
|
|
549
|
-
MANIFEST_BUNDLE_NAME="
|
|
549
|
+
MANIFEST_BUNDLE_NAME="Hasna Recordings.app"
|
|
550
550
|
APP_DEST="${HOME}/Applications/${MANIFEST_BUNDLE_NAME}"
|
|
551
551
|
APP_PARENT="$("$DIRNAME_EXECUTABLE" "$APP_DEST")"
|
|
552
552
|
ROLLBACK_DIR="${DATA_DIR}/rollbacks"
|
|
@@ -794,7 +794,7 @@ acquire_install_lock() {
|
|
|
794
794
|
if kill -0 "$owner_pid" 2>/dev/null; then
|
|
795
795
|
actual_start="$("$PS_EXECUTABLE" -o lstart= -p "$owner_pid" 2>/dev/null | "$TR_EXECUTABLE" -s ' ' | "$SED_EXECUTABLE" 's/^ //;s/ $//' || true)"
|
|
796
796
|
if [ -z "$owner_start" ] || [ -z "$actual_start" ] || [ "$owner_start" = "$actual_start" ]; then
|
|
797
|
-
echo "Another
|
|
797
|
+
echo "Another Hasna Recordings.app installer owns the active install lock." >&2
|
|
798
798
|
exit 1
|
|
799
799
|
fi
|
|
800
800
|
fi
|
|
@@ -807,17 +807,17 @@ acquire_install_lock() {
|
|
|
807
807
|
now="$("$DATE_EXECUTABLE" +%s)"
|
|
808
808
|
lock_age=$((now - $("$STAT_EXECUTABLE" -f '%m' "$LOCK_DIR")))
|
|
809
809
|
if [ "$lock_age" -lt "$stale_seconds" ]; then
|
|
810
|
-
echo "
|
|
810
|
+
echo "Hasna Recordings.app install lock is incomplete and too recent to reclaim safely." >&2
|
|
811
811
|
exit 1
|
|
812
812
|
fi
|
|
813
813
|
fi
|
|
814
814
|
stale_claim="${LOCK_DIR}.stale.$$"
|
|
815
815
|
if ! "$MV_EXECUTABLE" "$LOCK_DIR" "$stale_claim" 2>/dev/null; then
|
|
816
|
-
echo "
|
|
816
|
+
echo "Hasna Recordings.app install lock changed during stale-owner recovery." >&2
|
|
817
817
|
exit 1
|
|
818
818
|
fi
|
|
819
819
|
"$RM_EXECUTABLE" -rf "$stale_claim"
|
|
820
|
-
"$MKDIR_EXECUTABLE" -m 700 "$LOCK_DIR" || { echo "Could not acquire
|
|
820
|
+
"$MKDIR_EXECUTABLE" -m 700 "$LOCK_DIR" || { echo "Could not acquire Hasna Recordings.app install lock." >&2; exit 1; }
|
|
821
821
|
fi
|
|
822
822
|
LOCK_OWNED=1
|
|
823
823
|
local start
|
|
@@ -1263,13 +1263,13 @@ stop_uncommitted_candidate() {
|
|
|
1263
1263
|
done <<< "$remaining"
|
|
1264
1264
|
"$SLEEP_EXECUTABLE" 0.1
|
|
1265
1265
|
[ -z "$(pids_for_exact_executable "$expected")" ] || {
|
|
1266
|
-
echo "Uncommitted
|
|
1266
|
+
echo "Uncommitted Hasna Recordings.app process could not be stopped before recovery." >&2
|
|
1267
1267
|
return 1
|
|
1268
1268
|
}
|
|
1269
1269
|
}
|
|
1270
1270
|
|
|
1271
1271
|
if [ -f "$JOURNAL_PATH" ]; then
|
|
1272
|
-
echo "Recovering incomplete
|
|
1272
|
+
echo "Recovering incomplete Hasna Recordings.app installation transaction." >&2
|
|
1273
1273
|
if ! RECOVER_WAS_RUNNING="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field was_running)" || \
|
|
1274
1274
|
! RECOVER_PHASE="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field phase)" || \
|
|
1275
1275
|
! RECOVER_RUNNING_APP_PATHS_JSON="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" journal-get --journal "$JOURNAL_PATH" --field prior_running_app_paths)"; then
|
|
@@ -1376,7 +1376,7 @@ if ! "$BUN_EXECUTABLE" -e '
|
|
|
1376
1376
|
if ((actual[i] ?? 0) < (minimum[i] ?? 0)) process.exit(1);
|
|
1377
1377
|
}
|
|
1378
1378
|
' "$CURRENT_MACOS" "$MINIMUM_MACOS"; then
|
|
1379
|
-
echo "
|
|
1379
|
+
echo "Hasna Recordings.app requires macOS ${MINIMUM_MACOS} or later; target is ${CURRENT_MACOS}." >&2
|
|
1380
1380
|
exit 1
|
|
1381
1381
|
fi
|
|
1382
1382
|
TARGET_ARCH="$("$UNAME_EXECUTABLE" -m)"
|
|
@@ -1386,7 +1386,7 @@ MANIFEST_ARCHITECTURES="$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" manifest-get \
|
|
|
1386
1386
|
--field architectures)"
|
|
1387
1387
|
case " ${MANIFEST_ARCHITECTURES} " in
|
|
1388
1388
|
*" ${TARGET_ARCH} "*) ;;
|
|
1389
|
-
*) echo "
|
|
1389
|
+
*) echo "Hasna Recordings.app artifact does not support target architecture ${TARGET_ARCH}." >&2; exit 1 ;;
|
|
1390
1390
|
esac
|
|
1391
1391
|
|
|
1392
1392
|
verify_secure_parent "$APP_PARENT"
|
|
@@ -1412,7 +1412,7 @@ add_unique_app() {
|
|
|
1412
1412
|
local existing
|
|
1413
1413
|
if [ -e "$candidate" ] || [ -L "$candidate" ]; then
|
|
1414
1414
|
[ -d "$candidate" ] && [ ! -L "$candidate" ] || {
|
|
1415
|
-
echo "
|
|
1415
|
+
echo "Hasna Recordings.app path is not a secure directory: ${candidate}" >&2
|
|
1416
1416
|
exit 1
|
|
1417
1417
|
}
|
|
1418
1418
|
fi
|
|
@@ -1524,7 +1524,7 @@ stop_old_processes() {
|
|
|
1524
1524
|
"$SLEEP_EXECUTABLE" 0.1
|
|
1525
1525
|
done
|
|
1526
1526
|
if [ "$remaining" -ne 0 ]; then
|
|
1527
|
-
echo "Existing
|
|
1527
|
+
echo "Existing Hasna Recordings.app process did not stop before replacement." >&2
|
|
1528
1528
|
exit 1
|
|
1529
1529
|
fi
|
|
1530
1530
|
}
|
|
@@ -1545,7 +1545,7 @@ quiesce_old_processes() {
|
|
|
1545
1545
|
fi
|
|
1546
1546
|
"$SLEEP_EXECUTABLE" 0.1
|
|
1547
1547
|
done
|
|
1548
|
-
echo "Existing
|
|
1548
|
+
echo "Existing Hasna Recordings.app processes did not remain quiescent before replacement." >&2
|
|
1549
1549
|
exit 1
|
|
1550
1550
|
}
|
|
1551
1551
|
|
|
@@ -1706,11 +1706,14 @@ fi
|
|
|
1706
1706
|
DISCOVERED_APPS=()
|
|
1707
1707
|
add_unique_app "$APP_DEST"
|
|
1708
1708
|
add_unique_app "${DATA_DIR}/Recordings.app"
|
|
1709
|
+
add_unique_app "${DATA_DIR}/Hasna Recordings.app"
|
|
1709
1710
|
add_unique_app "${DATA_DIR}/HasnaRecordings.app"
|
|
1710
|
-
|
|
1711
|
+
add_unique_app "${HOME}/Applications/HasnaRecordings.app"
|
|
1712
|
+
for candidate in "${HOME}"/Applications/Recordings.app.* "${HOME}/Applications/Hasna Recordings.app."* "${HOME}/Applications/HasnaRecordings.app."*; do
|
|
1711
1713
|
add_unique_app "$candidate"
|
|
1712
1714
|
done
|
|
1713
1715
|
add_unique_app "/Applications/Recordings.app"
|
|
1716
|
+
add_unique_app "/Applications/Hasna Recordings.app"
|
|
1714
1717
|
add_unique_app "/Applications/HasnaRecordings.app"
|
|
1715
1718
|
if [ -x "$MDFIND_EXECUTABLE" ]; then
|
|
1716
1719
|
while IFS= read -r candidate; do
|
|
@@ -1722,12 +1725,12 @@ fi
|
|
|
1722
1725
|
MANAGEABLE_APPS=()
|
|
1723
1726
|
for existing_app in ${DISCOVERED_APPS[@]+"${DISCOVERED_APPS[@]}"}; do
|
|
1724
1727
|
case "$existing_app" in
|
|
1725
|
-
"$APP_DEST"|"${DATA_DIR}/Recordings.app"|"${DATA_DIR}/HasnaRecordings.app"|"${HOME}/Applications/Recordings.app."*|"${HOME}/Applications/HasnaRecordings.app."*)
|
|
1728
|
+
"$APP_DEST"|"${DATA_DIR}/Recordings.app"|"${DATA_DIR}/Hasna Recordings.app"|"${DATA_DIR}/HasnaRecordings.app"|"${HOME}/Applications/HasnaRecordings.app"|"${HOME}/Applications/Recordings.app."*|"${HOME}/Applications/Hasna Recordings.app."*|"${HOME}/Applications/HasnaRecordings.app."*)
|
|
1726
1729
|
echo "Duplicate disposition: archive transactionally, remove after activation: ${existing_app}"
|
|
1727
1730
|
MANAGEABLE_APPS+=("$existing_app")
|
|
1728
1731
|
;;
|
|
1729
1732
|
*)
|
|
1730
|
-
echo "Duplicate
|
|
1733
|
+
echo "Duplicate Hasna Recordings.app at ${existing_app} is outside the transactional user install paths; archive or remove it before installing." >&2
|
|
1731
1734
|
exit 1
|
|
1732
1735
|
;;
|
|
1733
1736
|
esac
|
|
@@ -1954,7 +1957,7 @@ for ((refresh_attempt = 1; refresh_attempt <= 5; refresh_attempt++)); do
|
|
|
1954
1957
|
fi
|
|
1955
1958
|
done
|
|
1956
1959
|
[ "$STOPPED_REFRESH_STABLE" -eq 1 ] || {
|
|
1957
|
-
echo "Existing
|
|
1960
|
+
echo "Existing Hasna Recordings.app writers did not remain quiescent around the stopped-state refresh." >&2
|
|
1958
1961
|
exit 1
|
|
1959
1962
|
}
|
|
1960
1963
|
|
|
@@ -2004,7 +2007,7 @@ for ((move_index = 0; move_index < MOVED_ORIGINAL_COUNT; move_index++)); do
|
|
|
2004
2007
|
existing_app="${MOVED_ORIGINALS[$move_index]}"
|
|
2005
2008
|
moved_path="${MOVED_PATHS[$move_index]}"
|
|
2006
2009
|
[ "$("$BUN_EXECUTABLE" "$ARTIFACT_TOOL" tree-digest --path "$existing_app")" = "${MOVED_DIGESTS[$move_index]}" ] || {
|
|
2007
|
-
echo "Installed
|
|
2010
|
+
echo "Installed Hasna Recordings.app changed after transactional backup planning." >&2
|
|
2008
2011
|
exit 1
|
|
2009
2012
|
}
|
|
2010
2013
|
RECORDINGS_TEST_INSTALLER_PID="$$" \
|
|
@@ -2117,7 +2120,7 @@ if [ "$LAUNCH_APP" -eq 1 ] || [ "$was_running" -eq 1 ]; then
|
|
|
2117
2120
|
fi
|
|
2118
2121
|
|
|
2119
2122
|
if [ "$ARTIFACT_POLICY" = "local_only" ]; then
|
|
2120
|
-
echo "Installed local-only
|
|
2123
|
+
echo "Installed local-only Hasna Recordings.app for ${APPROVED_TARGET}; this artifact is ad-hoc signed and non-notarized."
|
|
2121
2124
|
if [ "$identity_migration" -eq 1 ]; then
|
|
2122
2125
|
# Not a maybe: the designated requirement did change, under an explicit approval.
|
|
2123
2126
|
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."
|
|
@@ -2127,5 +2130,5 @@ if [ "$ARTIFACT_POLICY" = "local_only" ]; then
|
|
|
2127
2130
|
elif [ "$identity_migration" -eq 1 ]; then
|
|
2128
2131
|
echo "Installed a new signing identity; macOS will require one-time permission approval for this migration."
|
|
2129
2132
|
else
|
|
2130
|
-
echo "Installed verified
|
|
2133
|
+
echo "Installed verified Hasna Recordings.app release artifact: ${APP_DEST}"
|
|
2131
2134
|
fi
|
|
@@ -183,7 +183,7 @@ export type BuildProvenance = {
|
|
|
183
183
|
|
|
184
184
|
export type MacOSArtifactManifest = BuildProvenance & {
|
|
185
185
|
artifact_type: "recordings-macos-app";
|
|
186
|
-
/// The top-level bundle directory name inside the artifact (
|
|
186
|
+
/// The top-level bundle directory name inside the artifact (Hasna Recordings.app for
|
|
187
187
|
/// both the full app and the bar variant per the fleet naming rule).
|
|
188
188
|
bundle_name: string;
|
|
189
189
|
app_sha256: string;
|
|
@@ -843,6 +843,7 @@ function manifestVariant(manifest: MacOSArtifactManifest): ArtifactVariant {
|
|
|
843
843
|
}
|
|
844
844
|
|
|
845
845
|
function manifestBundleName(manifest: MacOSArtifactManifest): string {
|
|
846
|
+
// Manifests predating explicit bundle names describe the original unspaced bundle.
|
|
846
847
|
return manifest.bundle_name ?? "HasnaRecordings.app";
|
|
847
848
|
}
|
|
848
849
|
|
|
@@ -1094,23 +1095,23 @@ function assertCanonicalZipEntries(entries: Array<{ name: string; isDirectory: b
|
|
|
1094
1095
|
const components = logicalPath.split("/");
|
|
1095
1096
|
if (
|
|
1096
1097
|
!logicalPath ||
|
|
1097
|
-
components[0] !== "
|
|
1098
|
+
components[0] !== "Hasna Recordings.app" ||
|
|
1098
1099
|
components.some((component) => !component || component === "." || component === "..")
|
|
1099
1100
|
) {
|
|
1100
|
-
throw new Error("release ZIP contains an entry outside the canonical
|
|
1101
|
+
throw new Error("release ZIP contains an entry outside the canonical Hasna Recordings.app tree");
|
|
1101
1102
|
}
|
|
1102
1103
|
const collisionKey = canonicalZipCollisionKey(logicalPath);
|
|
1103
1104
|
if (logicalPaths.has(collisionKey)) {
|
|
1104
1105
|
throw new Error("release ZIP contains duplicate, file/directory, case-fold, or Unicode-colliding entries");
|
|
1105
1106
|
}
|
|
1106
1107
|
logicalPaths.set(collisionKey, { name: logicalPath, isDirectory: entry.isDirectory });
|
|
1107
|
-
if (logicalPath === "
|
|
1108
|
-
if (!entry.isDirectory) throw new Error("release ZIP
|
|
1108
|
+
if (logicalPath === "Hasna Recordings.app") {
|
|
1109
|
+
if (!entry.isDirectory) throw new Error("release ZIP Hasna Recordings.app root is not a directory entry");
|
|
1109
1110
|
rootDirectoryCount += 1;
|
|
1110
1111
|
}
|
|
1111
1112
|
}
|
|
1112
1113
|
if (rootDirectoryCount !== 1) {
|
|
1113
|
-
throw new Error("release ZIP must contain exactly one canonical
|
|
1114
|
+
throw new Error("release ZIP must contain exactly one canonical Hasna Recordings.app root entry");
|
|
1114
1115
|
}
|
|
1115
1116
|
for (const { name } of logicalPaths.values()) {
|
|
1116
1117
|
const components = name.split("/");
|
|
@@ -1384,41 +1385,48 @@ export function verifyAndExtractArchiveDescriptors(
|
|
|
1384
1385
|
throw new Error("artifact verifier archive digest mismatch");
|
|
1385
1386
|
}
|
|
1386
1387
|
const entries = inspectZipArchiveBytes(archive);
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1388
|
+
// /dev/fd/<directory>/child is not traversable on Darwin. Keep all
|
|
1389
|
+
// extraction relative to the borrowed capability, including every ancestor.
|
|
1390
|
+
const guard = nativeFsGuard();
|
|
1391
|
+
const outputRoot = guard.duplicateDirectoryDescriptor(outputDirectoryDescriptor);
|
|
1392
|
+
try {
|
|
1393
|
+
if (guard.readDir(outputRoot).length !== 0) {
|
|
1394
|
+
throw new Error("artifact verifier output directory must be empty");
|
|
1395
|
+
}
|
|
1396
|
+
const ordered = [...entries].sort((left, right) => {
|
|
1397
|
+
const depth = left.name.split("/").length - right.name.split("/").length;
|
|
1398
|
+
if (depth !== 0) return depth;
|
|
1399
|
+
if (left.isDirectory !== right.isDirectory) return left.isDirectory ? -1 : 1;
|
|
1400
|
+
return compareUnsignedUtf8(left.name, right.name);
|
|
1401
|
+
});
|
|
1402
|
+
for (const entry of ordered) {
|
|
1403
|
+
const parts = (entry.isDirectory ? entry.name.slice(0, -1) : entry.name).split("/");
|
|
1404
|
+
const leaf = parts.pop()!;
|
|
1405
|
+
let parent = outputRoot;
|
|
1406
|
+
try {
|
|
1407
|
+
// Close each ancestor as we descend; even a large ZIP keeps only a
|
|
1408
|
+
// bounded number of descriptors open. openDirAt refuses symlinks.
|
|
1409
|
+
for (const component of parts) {
|
|
1410
|
+
const ancestor = parent;
|
|
1411
|
+
parent = guard.openDirAt(ancestor, component);
|
|
1412
|
+
if (ancestor !== outputRoot) guard.close(ancestor);
|
|
1413
|
+
}
|
|
1414
|
+
const mode = entry.unixMode & 0o777;
|
|
1415
|
+
if (entry.isDirectory) {
|
|
1416
|
+
const directory = guard.mkdirAt(parent, leaf, mode);
|
|
1417
|
+
guard.close(directory);
|
|
1418
|
+
} else {
|
|
1419
|
+
guard.writeFileAt(parent, leaf, zipEntryPayload(archive, entry), mode);
|
|
1420
|
+
}
|
|
1421
|
+
guard.fsyncHandle(parent);
|
|
1422
|
+
} finally {
|
|
1423
|
+
if (parent !== outputRoot) guard.close(parent);
|
|
1424
|
+
}
|
|
1419
1425
|
}
|
|
1426
|
+
guard.fsyncHandle(outputRoot);
|
|
1427
|
+
} finally {
|
|
1428
|
+
guard.close(outputRoot);
|
|
1420
1429
|
}
|
|
1421
|
-
fsyncSync(outputDirectoryDescriptor);
|
|
1422
1430
|
}
|
|
1423
1431
|
|
|
1424
1432
|
export function withPrivatelyExtractedArchiveApp<T>(
|
|
@@ -1426,7 +1434,7 @@ export function withPrivatelyExtractedArchiveApp<T>(
|
|
|
1426
1434
|
operation: (appPath: string) => T,
|
|
1427
1435
|
platformArchiveTool = "/usr/bin/ditto",
|
|
1428
1436
|
expectedArchiveSha256?: string,
|
|
1429
|
-
expectedBundleName = "
|
|
1437
|
+
expectedBundleName = "Hasna Recordings.app",
|
|
1430
1438
|
): T {
|
|
1431
1439
|
const privateRoot = mkdtempSync(join(tmpdir(), "recordings-artifact-extract-"));
|
|
1432
1440
|
chmodSync(privateRoot, 0o700);
|
|
@@ -1751,8 +1759,8 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
|
|
|
1751
1759
|
manifest.container?.type !== "zip" ||
|
|
1752
1760
|
JSON.stringify(manifest.container.install_locations) !== JSON.stringify(
|
|
1753
1761
|
artifactPolicy === "release"
|
|
1754
|
-
? ["/Applications/
|
|
1755
|
-
: ["~/Applications/
|
|
1762
|
+
? ["/Applications/Hasna Recordings.app"]
|
|
1763
|
+
: ["~/Applications/Hasna Recordings.app"],
|
|
1756
1764
|
)
|
|
1757
1765
|
) {
|
|
1758
1766
|
throw new Error("manifest has an unexpected container install policy");
|
|
@@ -2516,7 +2524,7 @@ export function assertVersionTransition(
|
|
|
2516
2524
|
assertManifestShape(manifest);
|
|
2517
2525
|
if (compareVersions(manifest.bundle_version, installedVersion) < 0) {
|
|
2518
2526
|
throw new Error(
|
|
2519
|
-
`refusing to downgrade
|
|
2527
|
+
`refusing to downgrade Hasna Recordings.app from ${installedVersion} to ${manifest.bundle_version}`,
|
|
2520
2528
|
);
|
|
2521
2529
|
}
|
|
2522
2530
|
if (compareVersions(manifest.bundle_version, installedVersion) === 0) {
|
|
@@ -2981,7 +2989,7 @@ function readJournal(path: string): InstallJournal {
|
|
|
2981
2989
|
if (resolve(path) !== resolve(join(expectedParent, ".Recordings-install-transaction.json"))) {
|
|
2982
2990
|
throw new Error("install transaction journal is outside the expected app parent");
|
|
2983
2991
|
}
|
|
2984
|
-
if (resolve(journal.app_destination)
|
|
2992
|
+
if (!["Hasna Recordings.app", "HasnaRecordings.app"].some((name) => resolve(journal.app_destination) === resolve(join(expectedParent, name)))) {
|
|
2985
2993
|
throw new Error("install transaction journal has an unexpected app destination");
|
|
2986
2994
|
}
|
|
2987
2995
|
const expectedDataDir = resolve(join(dirname(expectedParent), ".hasna", "recordings"));
|
|
@@ -2999,9 +3007,12 @@ function readJournal(path: string): InstallJournal {
|
|
|
2999
3007
|
return (
|
|
3000
3008
|
resolved === resolve(journal.app_destination) ||
|
|
3001
3009
|
resolved === resolve(join(journal.data_dir, "Recordings.app")) ||
|
|
3010
|
+
resolved === resolve(join(journal.data_dir, "Hasna Recordings.app")) ||
|
|
3002
3011
|
resolved === resolve(join(journal.data_dir, "HasnaRecordings.app")) ||
|
|
3012
|
+
resolved === resolve(join(journal.app_parent, "HasnaRecordings.app")) ||
|
|
3003
3013
|
resolved.startsWith(`${resolve(journal.app_parent)}/Recordings.app.`) ||
|
|
3004
|
-
resolved.startsWith(`${resolve(journal.app_parent)}/HasnaRecordings.app.`)
|
|
3014
|
+
resolved.startsWith(`${resolve(journal.app_parent)}/HasnaRecordings.app.`) ||
|
|
3015
|
+
resolved.startsWith(`${resolve(journal.app_parent)}/Hasna Recordings.app.`)
|
|
3005
3016
|
);
|
|
3006
3017
|
};
|
|
3007
3018
|
if (!journal.originals.every((entry) => allowedOriginal(entry.path))) {
|
|
@@ -3939,7 +3950,7 @@ function openRecoveryCapabilities(
|
|
|
3939
3950
|
const homePath = resolve(dirname(appParent));
|
|
3940
3951
|
if (
|
|
3941
3952
|
basename(appParent) !== "Applications" ||
|
|
3942
|
-
resolve(journal.app_destination)
|
|
3953
|
+
!["Hasna Recordings.app", "HasnaRecordings.app"].some((name) => resolve(journal.app_destination) === resolve(join(appParent, name))) ||
|
|
3943
3954
|
resolve(journal.data_dir) !== resolve(join(homePath, ".hasna", "recordings")) ||
|
|
3944
3955
|
dirname(resolve(journal.transaction_dir)) !== appParent
|
|
3945
3956
|
) {
|
|
@@ -4573,16 +4584,16 @@ function originalDestinationCapability(
|
|
|
4573
4584
|
}
|
|
4574
4585
|
if (
|
|
4575
4586
|
dirname(resolved) === resolve(journal.data_dir) &&
|
|
4576
|
-
|
|
4587
|
+
["Recordings.app", "Hasna Recordings.app", "HasnaRecordings.app"].includes(basename(resolved))
|
|
4577
4588
|
) {
|
|
4578
4589
|
// Install site inside the state data directory: the legacy pre-rename
|
|
4579
|
-
// Recordings.app and the renamed
|
|
4590
|
+
// Recordings.app and the renamed Hasna Recordings.app are both recognized and
|
|
4580
4591
|
// preserved, never treated as the canonical Applications destination.
|
|
4581
4592
|
return { parent: capabilities.data, leaf: basename(resolved) };
|
|
4582
4593
|
}
|
|
4583
4594
|
if (
|
|
4584
4595
|
dirname(resolved) === resolve(journal.app_parent) &&
|
|
4585
|
-
/^(?:Hasna)?Recordings\.app\.[A-Za-z0-9._-]+$/.test(basename(resolved))
|
|
4596
|
+
(basename(resolved) === "HasnaRecordings.app" || /^(?:Hasna ?)?Recordings\.app\.[A-Za-z0-9._-]+$/.test(basename(resolved)))
|
|
4586
4597
|
) {
|
|
4587
4598
|
return { parent: capabilities.applications, leaf: basename(resolved) };
|
|
4588
4599
|
}
|
package/scripts/migrate.ts
CHANGED
|
@@ -29,32 +29,16 @@ async function runPublishedEntrypoint(): Promise<boolean> {
|
|
|
29
29
|
async function main() {
|
|
30
30
|
// Published packages ship the compiled server but intentionally omit src/.
|
|
31
31
|
if (await runPublishedEntrypoint()) return;
|
|
32
|
-
const {
|
|
33
|
-
|
|
34
|
-
getCloudPg,
|
|
35
|
-
migrateCloudSchema,
|
|
36
|
-
pingCloudConnectivity,
|
|
37
|
-
resolveDatabaseUrl,
|
|
38
|
-
} = await import("../src/server/cloud.js");
|
|
39
|
-
const { assertCloudSchemaContract } = await import("../src/server/cloud-readiness.js");
|
|
40
|
-
const { runCloudMigration } = await import("../src/server/migrate-command.js");
|
|
41
|
-
const url = resolveDatabaseUrl();
|
|
42
|
-
if (!url) {
|
|
32
|
+
const { closeCloud, resolveMigrationDatabaseUrl, runRecordedMigration } = await import("../src/server/cloud.js");
|
|
33
|
+
if (!resolveMigrationDatabaseUrl()) {
|
|
43
34
|
console.error(
|
|
44
|
-
"migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)",
|
|
35
|
+
"migrate: no database URL (HASNA_RECORDINGS_MIGRATE_DATABASE_URL / RECORDINGS_MIGRATE_DATABASE_URL / HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)",
|
|
45
36
|
);
|
|
46
37
|
process.exitCode = 2;
|
|
47
38
|
return;
|
|
48
39
|
}
|
|
49
40
|
console.log("migrate: connecting…");
|
|
50
|
-
await
|
|
51
|
-
pingConnectivity: pingCloudConnectivity,
|
|
52
|
-
applyMigrations: async () => {
|
|
53
|
-
console.log("migrate: applying schema (recordings tables + api_keys)…");
|
|
54
|
-
await migrateCloudSchema();
|
|
55
|
-
},
|
|
56
|
-
validateContract: () => assertCloudSchemaContract(getCloudPg()),
|
|
57
|
-
});
|
|
41
|
+
await runRecordedMigration();
|
|
58
42
|
console.log("migrate: done");
|
|
59
43
|
await closeCloud();
|
|
60
44
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
#include <dirent.h>
|
|
6
6
|
#include <errno.h>
|
|
7
7
|
#include <fcntl.h>
|
|
8
|
+
#include <limits.h>
|
|
8
9
|
#include <stdint.h>
|
|
9
10
|
#include <stdbool.h>
|
|
10
11
|
#include <signal.h>
|
|
@@ -314,6 +315,32 @@ static napi_value open_trusted_home(napi_env env, napi_callback_info info) {
|
|
|
314
315
|
return make_handle(env, fd);
|
|
315
316
|
}
|
|
316
317
|
|
|
318
|
+
static napi_value duplicate_directory_descriptor(napi_env env, napi_callback_info info) {
|
|
319
|
+
napi_value argv[1];
|
|
320
|
+
size_t argc = 1;
|
|
321
|
+
double descriptor;
|
|
322
|
+
if (!check_napi(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL), "duplicateDirectoryDescriptor args") || argc != 1 ||
|
|
323
|
+
!check_napi(env, napi_get_value_double(env, argv[0], &descriptor), "read directory descriptor")) return NULL;
|
|
324
|
+
if (!(descriptor >= 0 && descriptor <= INT_MAX) || descriptor != (int)descriptor) {
|
|
325
|
+
throw_message(env, "INVALID_DESCRIPTOR", "directory descriptor must be a non-negative integer");
|
|
326
|
+
return NULL;
|
|
327
|
+
}
|
|
328
|
+
// Borrow the caller's capability, never its path or ownership of the fd.
|
|
329
|
+
int fd = fcntl((int)descriptor, F_DUPFD_CLOEXEC, 0);
|
|
330
|
+
if (fd < 0) { throw_errno(env, "duplicate directory descriptor"); return NULL; }
|
|
331
|
+
struct stat details;
|
|
332
|
+
if (fstat(fd, &details) != 0) {
|
|
333
|
+
int saved_errno = errno;
|
|
334
|
+
close(fd); errno = saved_errno; throw_errno(env, "stat duplicated directory"); return NULL;
|
|
335
|
+
}
|
|
336
|
+
if (!S_ISDIR(details.st_mode)) {
|
|
337
|
+
close(fd);
|
|
338
|
+
throw_message(env, "INVALID_DESCRIPTOR", "descriptor must reference a directory");
|
|
339
|
+
return NULL;
|
|
340
|
+
}
|
|
341
|
+
return make_handle(env, fd);
|
|
342
|
+
}
|
|
343
|
+
|
|
317
344
|
static napi_value open_dir_at(napi_env env, napi_callback_info info) {
|
|
318
345
|
napi_value argv[2];
|
|
319
346
|
size_t argc = 2;
|
|
@@ -370,8 +397,13 @@ static napi_value read_dir(napi_env env, napi_callback_info info) {
|
|
|
370
397
|
if (stream == NULL) { close(iterator_fd); throw_errno(env, "open directory stream"); return NULL; }
|
|
371
398
|
if (!check_napi(env, napi_create_array(env, &array), "create directory array")) { closedir(stream); return NULL; }
|
|
372
399
|
uint32_t index = 0;
|
|
373
|
-
|
|
374
|
-
for (
|
|
400
|
+
int saved_errno = 0;
|
|
401
|
+
for (;;) {
|
|
402
|
+
/* EOF preserves errno. Successful N-API work in the preceding iteration
|
|
403
|
+
* need not preserve it, so only the immediately following read owns it. */
|
|
404
|
+
errno = 0;
|
|
405
|
+
struct dirent *entry = readdir(stream);
|
|
406
|
+
if (entry == NULL) { saved_errno = errno; break; }
|
|
375
407
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
|
|
376
408
|
if (!valid_leaf(entry->d_name)) {
|
|
377
409
|
closedir(stream);
|
|
@@ -385,7 +417,6 @@ static napi_value read_dir(napi_env env, napi_callback_info info) {
|
|
|
385
417
|
return NULL;
|
|
386
418
|
}
|
|
387
419
|
}
|
|
388
|
-
int saved_errno = errno;
|
|
389
420
|
closedir(stream);
|
|
390
421
|
if (saved_errno != 0) { errno = saved_errno; throw_errno(env, "read directory capability"); return NULL; }
|
|
391
422
|
return array;
|
|
@@ -717,7 +748,7 @@ static napi_value write_file_at(napi_env env, napi_callback_info info) {
|
|
|
717
748
|
}
|
|
718
749
|
offset += (size_t)count;
|
|
719
750
|
}
|
|
720
|
-
if (fsync(fd) != 0) {
|
|
751
|
+
if (fchmod(fd, (mode_t)(mode & 0777)) != 0 || fsync(fd) != 0) {
|
|
721
752
|
saved_errno = errno; close(fd); errno = saved_errno; throw_errno(env, "fsync written file"); return NULL;
|
|
722
753
|
}
|
|
723
754
|
if (close(fd) != 0) { throw_errno(env, "close written file"); return NULL; }
|
|
@@ -1132,6 +1163,7 @@ static napi_value unlink_file_handle_at(napi_env env, napi_callback_info info) {
|
|
|
1132
1163
|
|
|
1133
1164
|
static napi_value init(napi_env env, napi_value exports) {
|
|
1134
1165
|
const napi_property_descriptor properties[] = {
|
|
1166
|
+
{ "duplicateDirectoryDescriptor", NULL, duplicate_directory_descriptor, NULL, NULL, NULL, napi_default, NULL },
|
|
1135
1167
|
{ "openTrustedHome", NULL, open_trusted_home, NULL, NULL, NULL, napi_default, NULL },
|
|
1136
1168
|
{ "openDirAt", NULL, open_dir_at, NULL, NULL, NULL, napi_default, NULL },
|
|
1137
1169
|
{ "openRegularAt", NULL, open_regular_at, NULL, NULL, NULL, napi_default, NULL },
|