@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.
Files changed (148) hide show
  1. package/README.md +279 -43
  2. package/bun.lock +37 -38
  3. package/contracts/v1/fixtures.json +1206 -0
  4. package/dist/cli/index.js +1909 -619
  5. package/dist/cli/macos-shortcut.d.ts +2 -2
  6. package/dist/contracts/hosted-v1.d.ts +105 -0
  7. package/dist/contracts/hosted-v1.d.ts.map +1 -0
  8. package/dist/contracts/hosted-v1.js +41 -0
  9. package/dist/contracts/stream-v1.d.ts +117 -0
  10. package/dist/contracts/stream-v1.d.ts.map +1 -0
  11. package/dist/contracts/stream-v1.js +35 -0
  12. package/dist/db/database.d.ts.map +1 -1
  13. package/dist/db/pg-migrations.d.ts.map +1 -1
  14. package/dist/db/recordings.d.ts +6 -0
  15. package/dist/db/recordings.d.ts.map +1 -1
  16. package/dist/hosted/index.d.ts +58 -0
  17. package/dist/hosted/index.d.ts.map +1 -0
  18. package/dist/hosted/index.js +266 -0
  19. package/dist/hosted/transport.d.ts +36 -0
  20. package/dist/hosted/transport.d.ts.map +1 -0
  21. package/dist/hosted-v1-aavn7ktb.js +4114 -0
  22. package/dist/hosted-v1-gdr9extc.js +84 -0
  23. package/dist/http/client.d.ts +183 -22
  24. package/dist/http/client.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2879 -2288
  28. package/dist/lib/audio-artifact-storage.d.ts +98 -0
  29. package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
  30. package/dist/lib/capture-probe.d.ts +3 -3
  31. package/dist/lib/capture-probe.d.ts.map +1 -1
  32. package/dist/lib/config.d.ts.map +1 -1
  33. package/dist/lib/local-opt-in.d.ts +132 -0
  34. package/dist/lib/local-opt-in.d.ts.map +1 -0
  35. package/dist/lib/macos-bundle.d.ts +1 -1
  36. package/dist/lib/persistence-probe.d.ts +56 -17
  37. package/dist/lib/persistence-probe.d.ts.map +1 -1
  38. package/dist/lib/transcriber.d.ts.map +1 -1
  39. package/dist/mcp/index.d.ts.map +1 -1
  40. package/dist/mcp/index.js +1713 -459
  41. package/dist/sdk/index.d.ts +13 -5
  42. package/dist/sdk/index.d.ts.map +1 -1
  43. package/dist/sdk/index.js +904 -1
  44. package/dist/sdk/resolve.d.ts +53 -0
  45. package/dist/sdk/resolve.d.ts.map +1 -0
  46. package/dist/sdk/v1.generated.d.ts +6 -0
  47. package/dist/sdk/v1.generated.d.ts.map +1 -1
  48. package/dist/server/client-ip.d.ts +39 -0
  49. package/dist/server/client-ip.d.ts.map +1 -0
  50. package/dist/server/cloud-config.d.ts +12 -0
  51. package/dist/server/cloud-config.d.ts.map +1 -1
  52. package/dist/server/cloud-readiness.d.ts.map +1 -1
  53. package/dist/server/cloud.d.ts +20 -2
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.js +590 -287
  56. package/dist/server/migrate-command.d.ts +1 -1
  57. package/dist/server/migrate-command.d.ts.map +1 -1
  58. package/dist/server/openapi.d.ts +21 -0
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/repo.d.ts.map +1 -1
  61. package/dist/server/serve.d.ts.map +1 -1
  62. package/dist/storage.d.ts +2 -2
  63. package/dist/storage.d.ts.map +1 -1
  64. package/dist/storage.js +1358 -874
  65. package/dist/store.d.ts +12 -3
  66. package/dist/store.d.ts.map +1 -1
  67. package/dist/types/index.d.ts +15 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/docs/hosted-sdk.md +71 -0
  70. package/docs/wire-contracts.md +24 -0
  71. package/package.json +33 -11
  72. package/packaging/macos/build_release_pkg.sh +3 -3
  73. package/packaging/macos/managed_bootstrap.sh +4 -4
  74. package/packaging/macos/scripts/postinstall +2 -2
  75. package/packaging/macos/scripts/preinstall +2 -2
  76. package/scripts/build_companion_cli.sh +7 -1
  77. package/scripts/ci-linux-suite.ts +23 -13
  78. package/scripts/install_macos_app.sh +27 -24
  79. package/scripts/macos_artifact.ts +60 -49
  80. package/scripts/migrate.ts +4 -20
  81. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  82. package/scripts/native/recordings_fs_guard.c +36 -4
  83. package/scripts/native-core-receipt.py +171 -0
  84. package/scripts/native_fs_guard.ts +2 -0
  85. package/scripts/release-suite-gate.ts +227 -150
  86. package/scripts/resolve_tailscale_cli.sh +24 -3
  87. package/scripts/smoke_macos_app.sh +21 -21
  88. package/src/native/Recordings/App/ContentView.swift +53 -68
  89. package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
  90. package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
  91. package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
  92. package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
  93. package/src/native/Recordings/App/RecordingsApp.swift +185 -9
  94. package/src/native/Recordings/App/RecordingsListView.swift +60 -112
  95. package/src/native/Recordings/App/RecordingsStore.swift +124 -109
  96. package/src/native/Recordings/App/Theme.swift +73 -61
  97. package/src/native/Recordings/Package.swift +1 -0
  98. package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
  99. package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  101. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
  102. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
  103. package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
  104. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
  105. package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
  106. package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
  107. package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
  108. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
  109. package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
  110. package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
  111. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
  112. package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
  113. package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
  114. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
  115. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
  116. package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
  117. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
  118. package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
  119. package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
  120. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
  121. package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
  122. package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
  123. package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
  124. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  125. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
  126. package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
  127. package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
  128. package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
  129. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  130. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  131. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  132. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  133. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  134. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  135. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  136. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  137. package/src/native/Recordings/build.sh +8 -7
  138. package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
  139. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
  140. package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
  141. package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
  142. package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
  143. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
  144. package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
  145. package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
  146. package/dist/__tests__/preload.d.ts +0 -2
  147. package/dist/__tests__/preload.d.ts.map +0 -1
  148. 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 "HasnaRecordings.app installation requires HOME to be an absolute canonical path." >&2; exit 1 ;;
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 "HasnaRecordings.app installation requires HOME to be an absolute canonical path to a non-symlink directory." >&2
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 "HasnaRecordings.app installation could not resolve HOME canonically." >&2
136
+ echo "Hasna Recordings.app installation could not resolve HOME canonically." >&2
137
137
  exit 1
138
138
  }
139
139
  [ "$candidate" = "$canonical" ] || {
140
- echo "HasnaRecordings.app installation requires HOME to be an absolute canonical path." >&2
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 "HasnaRecordings.app installation is only supported on macOS." >&2
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="HasnaRecordings.app"
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 HasnaRecordings.app installer owns the active install lock." >&2
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 "HasnaRecordings.app install lock is incomplete and too recent to reclaim safely." >&2
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 "HasnaRecordings.app install lock changed during stale-owner recovery." >&2
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 HasnaRecordings.app install lock." >&2; exit 1; }
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 HasnaRecordings.app process could not be stopped before recovery." >&2
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 HasnaRecordings.app installation transaction." >&2
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 "HasnaRecordings.app requires macOS ${MINIMUM_MACOS} or later; target is ${CURRENT_MACOS}." >&2
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 "HasnaRecordings.app artifact does not support target architecture ${TARGET_ARCH}." >&2; exit 1 ;;
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 "HasnaRecordings.app path is not a secure directory: ${candidate}" >&2
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 HasnaRecordings.app process did not stop before replacement." >&2
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 HasnaRecordings.app processes did not remain quiescent before replacement." >&2
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
- for candidate in "${HOME}"/Applications/Recordings.app.* "${HOME}"/Applications/HasnaRecordings.app.*; do
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 HasnaRecordings.app at ${existing_app} is outside the transactional user install paths; archive or remove it before installing." >&2
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 HasnaRecordings.app writers did not remain quiescent around the stopped-state refresh." >&2
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 HasnaRecordings.app changed after transactional backup planning." >&2
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 HasnaRecordings.app for ${APPROVED_TARGET}; this artifact is ad-hoc signed and non-notarized."
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 HasnaRecordings.app release artifact: ${APP_DEST}"
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 (HasnaRecordings.app for
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] !== "HasnaRecordings.app" ||
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 HasnaRecordings.app tree");
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 === "HasnaRecordings.app") {
1108
- if (!entry.isDirectory) throw new Error("release ZIP HasnaRecordings.app root is not a directory entry");
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 HasnaRecordings.app root entry");
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
- const outputRoot = `/dev/fd/${outputDirectoryDescriptor}`;
1388
- if (readdirSync(outputRoot).length !== 0) {
1389
- throw new Error("artifact verifier output directory must be empty");
1390
- }
1391
- const ordered = [...entries].sort((left, right) => {
1392
- const depth = left.name.split("/").length - right.name.split("/").length;
1393
- if (depth !== 0) return depth;
1394
- if (left.isDirectory !== right.isDirectory) return left.isDirectory ? -1 : 1;
1395
- return compareUnsignedUtf8(left.name, right.name);
1396
- });
1397
- for (const entry of ordered) {
1398
- const leaf = entry.isDirectory ? entry.name.slice(0, -1) : entry.name;
1399
- const target = join(outputRoot, ...leaf.split("/"));
1400
- const mode = entry.unixMode & 0o777;
1401
- if (entry.isDirectory) {
1402
- mkdirSync(target, { mode });
1403
- chmodSync(target, mode);
1404
- continue;
1405
- }
1406
- const descriptor = openSync(
1407
- target,
1408
- constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
1409
- mode,
1410
- );
1411
- try {
1412
- const payload = zipEntryPayload(archive, entry);
1413
- let offset = 0;
1414
- while (offset < payload.length) offset += writeSync(descriptor, payload, offset);
1415
- fchmodSync(descriptor, mode);
1416
- fsyncSync(descriptor);
1417
- } finally {
1418
- closeSync(descriptor);
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 = "HasnaRecordings.app",
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/HasnaRecordings.app"]
1755
- : ["~/Applications/HasnaRecordings.app"],
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 HasnaRecordings.app from ${installedVersion} to ${manifest.bundle_version}`,
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) !== resolve(join(expectedParent, "HasnaRecordings.app"))) {
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) !== resolve(join(appParent, "HasnaRecordings.app")) ||
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
- (basename(resolved) === "Recordings.app" || basename(resolved) === "HasnaRecordings.app")
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 HasnaRecordings.app are both recognized and
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
  }
@@ -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
- closeCloud,
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 runCloudMigration({
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
- errno = 0;
374
- for (struct dirent *entry = readdir(stream); entry != NULL; entry = readdir(stream)) {
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 },