@hasna/recordings 0.3.1 → 0.3.8

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 (63) hide show
  1. package/README.md +38 -34
  2. package/bun.lock +96 -36
  3. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +1 -1
  4. package/dist/__tests__/helpers/source-assertions.d.ts +6 -4
  5. package/dist/__tests__/helpers/source-assertions.d.ts.map +1 -1
  6. package/dist/cli/index.js +197 -95
  7. package/dist/cli/macos-shortcut.d.ts +2 -2
  8. package/dist/db/pg-migrations.d.ts +1 -1
  9. package/dist/http/client.d.ts +0 -1
  10. package/dist/http/client.d.ts.map +1 -1
  11. package/dist/index.js +162 -73
  12. package/dist/lib/capture-probe.d.ts +3 -3
  13. package/dist/lib/capture-probe.d.ts.map +1 -1
  14. package/dist/lib/macos-bundle.d.ts +1 -1
  15. package/dist/lib/release-install-policy.d.ts +21 -0
  16. package/dist/lib/release-install-policy.d.ts.map +1 -1
  17. package/dist/mcp/index.js +144 -59
  18. package/dist/sdk/index.d.ts +2 -2
  19. package/dist/sdk/index.js +7 -3
  20. package/dist/sdk/v1.generated.d.ts.map +1 -1
  21. package/dist/server/cloud-config.d.ts +4 -19
  22. package/dist/server/cloud-config.d.ts.map +1 -1
  23. package/dist/server/cloud.d.ts +1 -1
  24. package/dist/server/cloud.d.ts.map +1 -1
  25. package/dist/server/index.js +168 -110
  26. package/dist/server/serve.d.ts.map +1 -1
  27. package/dist/storage.d.ts +1 -1
  28. package/dist/storage.d.ts.map +1 -1
  29. package/dist/storage.js +144 -60
  30. package/dist/store.d.ts.map +1 -1
  31. package/dist/version.d.ts +1 -1
  32. package/dist/version.d.ts.map +1 -1
  33. package/package.json +4 -3
  34. package/packaging/macos/build_release_pkg.sh +10 -4
  35. package/packaging/macos/managed_bootstrap.sh +4 -4
  36. package/packaging/macos/scripts/postinstall +2 -2
  37. package/packaging/macos/scripts/preinstall +2 -0
  38. package/scripts/generate-sdk.ts +17 -16
  39. package/scripts/install_macos_app.sh +75 -28
  40. package/scripts/macos_artifact.ts +162 -45
  41. package/scripts/migrate.ts +2 -2
  42. package/scripts/release-suite-gate.ts +181 -0
  43. package/scripts/set-version.ts +5 -5
  44. package/scripts/smoke_macos_app.sh +80 -34
  45. package/scripts/vacuity-manifests/version-sites.tsv +8 -4
  46. package/src/native/Recordings/App/MenuBarStatusView.swift +9 -4
  47. package/src/native/Recordings/App/RecordingsApp.swift +37 -1
  48. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  49. package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +21 -1
  50. package/src/native/Recordings/RecordingsTests/BarOnlyLaunchPlanTests.swift +99 -0
  51. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +2 -2
  52. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  53. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  54. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  55. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  56. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  57. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  58. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  59. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  60. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  61. package/src/native/Recordings/build.sh +152 -18
  62. package/dist/lib/retired-deployment-modes.d.ts +0 -24
  63. package/dist/lib/retired-deployment-modes.d.ts.map +0 -1
@@ -0,0 +1,99 @@
1
+ import Foundation
2
+ import Testing
3
+ @testable import RecordingsLib
4
+
5
+ /// Regression coverage for the bar-only successor lane (task 4ee4ebbf-9a10-4181-8e43-9307d56e684b).
6
+ ///
7
+ /// These lock the launch-plan control flow behind the terminated hasna/apps#269 lineage's P1
8
+ /// classes:
9
+ ///
10
+ /// - Cycle-1 P1: real launches of the installed bar never passed `--bar-only`, so the bar
11
+ /// binary was behaviorally identical to the full app. Fixed by making bar builds bar-only by
12
+ /// construction (`#if RECORDINGS_BAR_ONLY` compile-time default); the argument is now only a
13
+ /// self-describing launch record.
14
+ /// - Cycle-2 P1: the windowless branch keyed on `declaresMainWindow`, which excludes EVERY
15
+ /// runtime smoke, so the FULL build's smoke failed deterministically. Fixed by splitting the
16
+ /// conditions: `declaresWindow` (window creation may happen) excludes only the helper and bar
17
+ /// launches, while `declaresMainWindow` (auto-open / reopen) additionally excludes runtime
18
+ /// smoke so the smoke controls window creation deterministically.
19
+ struct BarOnlyLaunchPlanTests {
20
+ @Test("a bare launch follows the build variant")
21
+ func bareLaunchFollowsBuildVariant() {
22
+ let plan = PermissionRequestLaunchPlan(arguments: [])
23
+ #if RECORDINGS_BAR_ONLY
24
+ #expect(plan.isBarOnly, "a bar build must be bar-only by construction on a bare launch")
25
+ #expect(!plan.declaresWindow)
26
+ #expect(!plan.declaresMainWindow)
27
+ #else
28
+ #expect(!plan.isBarOnly, "a full build must launch with the workspace window")
29
+ #expect(plan.declaresWindow)
30
+ #expect(plan.declaresMainWindow)
31
+ #endif
32
+ #expect(plan.declaresMenuBar)
33
+ #expect(plan.installsGlobalHandlers)
34
+ }
35
+
36
+ @Test("the explicit bar argument makes any launch bar-only on a full build")
37
+ func explicitBarArgument() {
38
+ #if !RECORDINGS_BAR_ONLY
39
+ let plan = PermissionRequestLaunchPlan(arguments: ["--bar-only"])
40
+ #expect(plan.isBarOnly)
41
+ #expect(!plan.declaresWindow)
42
+ #expect(!plan.declaresMainWindow)
43
+ // The menu bar is still declared: the bar keeps the record controls, live
44
+ // transcription, paste delivery, and settings surfaces.
45
+ #expect(plan.declaresMenuBar)
46
+ #endif
47
+ }
48
+
49
+ @Test("a full build's normal-mode runtime smoke keeps exercising the workspace window")
50
+ func fullBuildRuntimeSmokePlan() {
51
+ let plan = PermissionRequestLaunchPlan(arguments: [
52
+ "--runtime-smoke", "normal",
53
+ "--runtime-smoke-output", "/tmp/smoke.json",
54
+ ])
55
+ #if RECORDINGS_BAR_ONLY
56
+ // On a bar build even the smoke is bar-only: the windowless branch keys on the
57
+ // build variant, not on the smoke mode.
58
+ #expect(plan.isBarOnly)
59
+ #expect(!plan.declaresWindow)
60
+ #else
61
+ // The cycle-2 P1 regression: a full build's smoke must still be able to create
62
+ // the workspace window, or every full build's smoke fails deterministically.
63
+ #expect(!plan.isBarOnly)
64
+ #expect(plan.declaresWindow, "the full-build smoke must exercise the workspace window")
65
+ #endif
66
+ // declaresMainWindow excludes runtime smoke in BOTH variants so the smoke controls
67
+ // window creation deterministically (no init auto-open, no reopen handler).
68
+ #expect(!plan.declaresMainWindow)
69
+ #expect(plan.isRuntimeSmoke)
70
+ #expect(plan.declaresMenuBar, "the smoke must observe the live menu bar surface")
71
+ }
72
+
73
+ @Test("a bar plan keeps the menu bar and global handlers but never declares a window")
74
+ func barPlanKeepsMenuBarAndGlobalHandlers() {
75
+ let plan = PermissionRequestLaunchPlan(arguments: ["--bar-only"])
76
+ #expect(plan.installsGlobalHandlers)
77
+ #expect(plan.declaresMenuBar)
78
+ #expect(plan.isBarOnly)
79
+ #expect(!plan.declaresWindow)
80
+ #expect(!plan.declaresMainWindow)
81
+ }
82
+
83
+ @Test("declaresMainWindow excludes runtime smoke, helper, and bar launches")
84
+ func declaresMainWindowExcludesRuntimeSmoke() {
85
+ for arguments in [
86
+ ["--runtime-smoke", "normal"],
87
+ ["--runtime-smoke", "permission-helper"],
88
+ ["--request-permissions"],
89
+ ["--bar-only"],
90
+ ] {
91
+ let plan = PermissionRequestLaunchPlan(arguments: arguments)
92
+ #expect(!plan.declaresMainWindow)
93
+ }
94
+ let plain = PermissionRequestLaunchPlan(arguments: [])
95
+ #if !RECORDINGS_BAR_ONLY
96
+ #expect(plain.declaresMainWindow)
97
+ #endif
98
+ }
99
+ }
@@ -8,7 +8,7 @@ struct CLIRunnerTests {
8
8
  func bundledCLIIsPreferred() throws {
9
9
  let root = FileManager.default.temporaryDirectory
10
10
  .appendingPathComponent("recordings-bundled-cli-\(UUID().uuidString)")
11
- let app = root.appendingPathComponent("Recordings.app")
11
+ let app = root.appendingPathComponent("HasnaRecordings.app")
12
12
  let helper = app.appendingPathComponent("Contents/Helpers/recordings")
13
13
  let home = root.appendingPathComponent("home")
14
14
  let external = home.appendingPathComponent(".bun/bin/recordings")
@@ -34,7 +34,7 @@ struct CLIRunnerTests {
34
34
  func packagedAppDoesNotUseGlobalFallback() throws {
35
35
  let root = FileManager.default.temporaryDirectory
36
36
  .appendingPathComponent("recordings-missing-companion-\(UUID().uuidString)")
37
- let app = root.appendingPathComponent("Recordings.app")
37
+ let app = root.appendingPathComponent("HasnaRecordings.app")
38
38
  let home = root.appendingPathComponent("home")
39
39
  let external = home.appendingPathComponent(".bun/bin/recordings")
40
40
  try FileManager.default.createDirectory(at: app, withIntermediateDirectories: true)
@@ -6,7 +6,7 @@ struct RecordingStartGateTests {
6
6
  @Test("permission-only launch skips global handlers and terminates after handling")
7
7
  func permissionHelperLaunchPlan() {
8
8
  let plan = PermissionRequestLaunchPlan(arguments: [
9
- "/Applications/Recordings.app/Contents/MacOS/Recordings",
9
+ "/Applications/HasnaRecordings.app/Contents/MacOS/Recordings",
10
10
  "--request-permissions",
11
11
  "--open-permission-settings",
12
12
  ])
@@ -51,7 +51,7 @@ enum RecordingsBootstrapPreflight {
51
51
  let manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestData)
52
52
  try validateManifest(manifest, payload: payload)
53
53
 
54
- let app = arguments.payloadRoot + "/Applications/Recordings.app"
54
+ let app = arguments.payloadRoot + "/Applications/HasnaRecordings.app"
55
55
  let client = app + "/" + RecordingsUpdateConstants.updateClientRelativePath
56
56
  let broker = arguments.payloadRoot + "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
57
57
  let artifactVerifier = arguments.payloadRoot
@@ -7,7 +7,7 @@ import RecordingsUpdateProtocol
7
7
  /// operation; first installs use exclusive rename semantics and can never replace a
8
8
  /// leaf created concurrently in `/Applications`.
9
9
  final class ApplicationNamespace {
10
- private static let liveLeaf = "Recordings.app"
10
+ private static let liveLeaf = "HasnaRecordings.app"
11
11
  private static let previousLeaf = "previous.app"
12
12
  private static let failedCandidateLeaf = "failed-candidate.app"
13
13
 
@@ -22,7 +22,7 @@ final class ApplicationNamespace {
22
22
  .deletingLastPathComponent
23
23
  guard journal.applicationPath == RecordingsUpdateConstants.applicationPath,
24
24
  journal.candidateApplicationPath == expectedTransactionDirectory
25
- + "/candidate/Recordings.app",
25
+ + "/candidate/HasnaRecordings.app",
26
26
  journal.previousApplicationPath == nil ||
27
27
  journal.previousApplicationPath == expectedTransactionDirectory + "/previous.app"
28
28
  else {
@@ -276,7 +276,7 @@ final class UpdateBrokerSession: NSObject, RecordingsUpdateXPCProtocol {
276
276
  guard mkdir(candidateRoot, 0o700) == 0 else {
277
277
  throw BrokerOperationError.candidateRejected
278
278
  }
279
- let candidateApplication = candidateRoot + "/Recordings.app"
279
+ let candidateApplication = candidateRoot + "/HasnaRecordings.app"
280
280
  let verifierIDs = try verifierAccountIDs()
281
281
  try CanonicalTreeCopier.copyApplication(
282
282
  from: verifierOutput,
@@ -22,7 +22,7 @@ enum CanonicalTreeCopier {
22
22
  }
23
23
  defer { Darwin.close(source) }
24
24
  let destinationRoot = URL(fileURLWithPath: rootCandidatePath).deletingLastPathComponent().path
25
- guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "Recordings.app" else {
25
+ guard URL(fileURLWithPath: rootCandidatePath).lastPathComponent == "HasnaRecordings.app" else {
26
26
  throw CanonicalCopyError.nonCanonicalRoot
27
27
  }
28
28
  let destination = Darwin.open(
@@ -253,7 +253,7 @@ struct DurableInstallJournal {
253
253
  let leaf = URL(fileURLWithPath: transactionDirectory).lastPathComponent
254
254
  guard leaf.hasPrefix("transaction-") else { throw InstallJournalError.invalidJournal }
255
255
  let transactionID = String(leaf.dropFirst("transaction-".count))
256
- let expectedCandidate = transactionDirectory + "/candidate/Recordings.app"
256
+ let expectedCandidate = transactionDirectory + "/candidate/HasnaRecordings.app"
257
257
  let activationPhases: Set<String> = [
258
258
  "prepared", "launch-barrier-pending", "launch-barrier-held",
259
259
  "swap-pending", "swapped", "previous-retaining",
@@ -201,8 +201,8 @@ struct ActivationRecoveryPolicyTests {
201
201
  "Contents/Helpers/recordings": 493,
202
202
  "Contents/Helpers/recordings-update-client": 493
203
203
  },
204
- "candidate_application_path": "/Library/Application Support/Hasna/Recordings/Updates/transaction-00000000-0000-0000-0000-000000000001/candidate/Recordings.app",
205
- "application_path": "/Applications/Recordings.app"
204
+ "candidate_application_path": "/Library/Application Support/Hasna/Recordings/Updates/transaction-00000000-0000-0000-0000-000000000001/candidate/HasnaRecordings.app",
205
+ "application_path": "/Applications/HasnaRecordings.app"
206
206
  }
207
207
  """#.utf8)
208
208
  let journal = try JSONDecoder().decode(BrokerInstallJournal.self, from: legacyJSON)
@@ -7,7 +7,7 @@ public enum RecordingsUpdateConstants {
7
7
  public static let rootMaintenanceSupported = false
8
8
  public static let keyRotationSupported = false
9
9
  public static let machServiceName = "com.hasna.recordings.updater"
10
- public static let applicationPath = "/Applications/Recordings.app"
10
+ public static let applicationPath = "/Applications/HasnaRecordings.app"
11
11
  public static let brokerExecutablePath = "/Library/PrivilegedHelperTools/com.hasna.recordings.updater"
12
12
  public static let stateRoot = "/Library/Application Support/Hasna/Recordings/Updates"
13
13
  public static let trustRoot = "/Library/Application Support/Hasna/Recordings/Trust"
@@ -729,7 +729,7 @@ int recordings_copy_canonical_application_tree(
729
729
  if (entry == NULL) break;
730
730
  if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue;
731
731
  visible_entries += 1;
732
- if (strcmp(entry->d_name, "Recordings.app") != 0) only_application = false;
732
+ if (strcmp(entry->d_name, "HasnaRecordings.app") != 0) only_application = false;
733
733
  }
734
734
  int status = errno;
735
735
  closedir(directory);
@@ -739,7 +739,7 @@ int recordings_copy_canonical_application_tree(
739
739
  status = copy_directory_at(
740
740
  verifier_output_directory_descriptor,
741
741
  root_candidate_directory_descriptor,
742
- "Recordings.app",
742
+ "HasnaRecordings.app",
743
743
  verifier_user_id,
744
744
  &budget
745
745
  );
@@ -1,5 +1,5 @@
1
1
  #!/bin/bash
2
- # Build, sign, notarize, and finalize a Recordings.app artifact.
2
+ # Build, sign, notarize, and finalize a HasnaRecordings.app artifact.
3
3
  # Usage: ./build.sh [debug|local] | ./build.sh release <initial-bootstrap|app-update>
4
4
 
5
5
  set -euo pipefail
@@ -83,7 +83,7 @@ fi
83
83
  readonly RELEASE_SUBTYPE
84
84
  RELEASE_ARCHITECTURES=(arm64 x86_64)
85
85
 
86
- CODESIGN_IDENTITY="${RECORDINGS_CODESIGN_IDENTITY:-}"
86
+ CODESIGN_IDENTITY="${RECORDINGS_CODESIGN_IDENTITY:-${HASNA_CODESIGN_IDENTITY:-}}"
87
87
  EXPECTED_TEAM_ID="${RECORDINGS_EXPECTED_TEAM_IDENTIFIER:-}"
88
88
  NOTARY_PROFILE="${RECORDINGS_NOTARY_KEYCHAIN_PROFILE:-}"
89
89
  INSTALLER_IDENTITY="${RECORDINGS_INSTALLER_CODESIGN_IDENTITY:-}"
@@ -96,6 +96,37 @@ COMPATIBLE_COHORT_MANIFEST="${RECORDINGS_RELEASE_COMPATIBLE_COHORT_MANIFEST:-}"
96
96
  LOCAL_APPROVED_TARGET="${RECORDINGS_LOCAL_APPROVED_TARGET:-}"
97
97
  LOCAL_APPROVED_TARGET_IDENTITY_KIND="${RECORDINGS_LOCAL_APPROVED_TARGET_IDENTITY_KIND:-}"
98
98
  LOCAL_APPROVED_TARGET_IDENTITY_SHA256="${RECORDINGS_LOCAL_APPROVED_TARGET_IDENTITY_SHA256:-}"
99
+ VARIANT="${RECORDINGS_VARIANT:-full}"
100
+ case "$VARIANT" in
101
+ full|bar) ;;
102
+ *)
103
+ echo "RECORDINGS_VARIANT must be full or bar; got: $VARIANT" >&2
104
+ exit 2
105
+ ;;
106
+ esac
107
+ if [ "$MODE" = "release" ] && [ "$VARIANT" = "bar" ]; then
108
+ # The bar variant is a deliberate product change (no workspace window, accessory
109
+ # launch); a release that carries it must be explicitly marked so a default release
110
+ # can never silently emit a bar artifact.
111
+ if [ "${RECORDINGS_RELEASE_BAR_VARIANT_MARKED:-0}" != "1" ]; then
112
+ echo "release-mode bar-variant builds require RECORDINGS_RELEASE_BAR_VARIANT_MARKED=1 to proceed explicitly." >&2
113
+ exit 2
114
+ fi
115
+ fi
116
+ readonly VARIANT
117
+ VARIANT_SWIFT_FLAGS=()
118
+ if [ "$VARIANT" = "bar" ]; then
119
+ VARIANT_SWIFT_FLAGS=(-Xswiftc -DRECORDINGS_BAR_ONLY)
120
+ fi
121
+ # Fleet naming rule (knowledge k_msxd5rz3_jfvl3i): every Hasna macOS app is
122
+ # Hasna<Name>.app with 'Hasna' at the beginning. The bundle is named
123
+ # HasnaRecordings.app for BOTH variants (full and bar); the 'bar' is a variant, never
124
+ # a separate app name. Bundle identifier stays com.hasna.recordings so TCC keeps
125
+ # keying on bundle id + signing identity, and the executable inside the bundle stays
126
+ # "Recordings".
127
+ APP_BUNDLE_NAME="HasnaRecordings.app"
128
+ readonly APP_BUNDLE_NAME
129
+ APP_BASENAME="${APP_BUNDLE_NAME%.app}"
99
130
  PLIST_BUDDY="$(select_executable "/usr/libexec/PlistBuddy" "${RECORDINGS_TEST_PLIST_BUDDY_EXECUTABLE:-${PLIST_BUDDY:-}}")"
100
131
  PLUTIL="$(select_executable "/usr/bin/plutil" "${RECORDINGS_TEST_PLUTIL_EXECUTABLE:-${PLUTIL:-}}")"
101
132
  BUN_EXECUTABLE="${BUN_EXECUTABLE:-}"
@@ -134,6 +165,48 @@ LIPO_EXECUTABLE="$(select_executable "/usr/bin/lipo" "${RECORDINGS_TEST_LIPO_EXE
134
165
  STAT_EXECUTABLE="$(select_executable "/usr/bin/stat" "${RECORDINGS_TEST_STAT_EXECUTABLE:-}")"
135
166
  TEST_GIT_EXECUTABLE="$(select_executable "" "${RECORDINGS_TEST_GIT_EXECUTABLE:-}")"
136
167
  TEST_SWIFT_EXECUTABLE="$(select_executable "" "${RECORDINGS_TEST_SWIFT_EXECUTABLE:-}")"
168
+ SECURITY_EXECUTABLE="$(select_executable "/usr/bin/security" "${RECORDINGS_TEST_SECURITY_EXECUTABLE:-}")"
169
+
170
+ # Developer ID signing identity support. The identity comes from RECORDINGS_CODESIGN_IDENTITY
171
+ # (legacy) or HASNA_CODESIGN_IDENTITY (canonical name), and when neither is supplied the
172
+ # keychain is consulted with `security find-identity -v -p codesigning` and the first
173
+ # "Developer ID Application:" identity is used. Signing NEVER silently falls back to ad-hoc
174
+ # when a Developer ID was requested: release builds always require one, and a local build
175
+ # with RECORDINGS_SIGNING_REQUIRED=1 fails loudly when none exists.
176
+ discover_developer_id_identity() {
177
+ local listing
178
+ listing="$("$SECURITY_EXECUTABLE" find-identity -v -p codesigning 2>/dev/null)" || return 0
179
+ printf '%s\n' "$listing" | "$AWK_EXECUTABLE" -F'"' '/Developer ID Application:/ { print $2; exit }'
180
+ }
181
+
182
+ derive_team_id() {
183
+ local identity="$1"
184
+ local team_id
185
+ team_id="$(printf '%s\n' "$identity" | "$SED_EXECUTABLE" -n 's/.*(\([A-Z0-9]\{10\}\))$/\1/p')"
186
+ if ! [[ "$team_id" =~ ^[A-Z0-9]{10}$ ]]; then
187
+ echo "Could not derive a 10-character Developer ID team from the signing identity: $identity" >&2
188
+ return 1
189
+ fi
190
+ printf '%s\n' "$team_id"
191
+ }
192
+
193
+ resolve_signing_identity() {
194
+ # Called when no explicit identity was supplied. Fails loudly when a Developer ID
195
+ # was requested but none can be found; the caller decides whether one was requested.
196
+ if [ "$HOST_PLATFORM" != "Darwin" ]; then
197
+ return 0
198
+ fi
199
+ local discovered
200
+ discovered="$(discover_developer_id_identity)"
201
+ if [ -z "$discovered" ]; then
202
+ echo "Developer ID signing was requested but no Developer ID Application identity found in the keychain; never silently ad-hoc when a Developer ID is requested." >&2
203
+ return 1
204
+ fi
205
+ CODESIGN_IDENTITY="$discovered"
206
+ if [ -z "$EXPECTED_TEAM_ID" ]; then
207
+ EXPECTED_TEAM_ID="$(derive_team_id "$CODESIGN_IDENTITY")" || return 1
208
+ fi
209
+ }
137
210
 
138
211
  require_executable() {
139
212
  local label="$1"
@@ -253,6 +326,7 @@ require_executable "ID_EXECUTABLE" "$ID_EXECUTABLE"
253
326
  require_executable "STAT_EXECUTABLE" "$STAT_EXECUTABLE"
254
327
  if [ "$HOST_PLATFORM" = "Darwin" ]; then
255
328
  require_executable "LIPO_EXECUTABLE" "$LIPO_EXECUTABLE"
329
+ require_executable "SECURITY_EXECUTABLE" "$SECURITY_EXECUTABLE"
256
330
  fi
257
331
  require_bun_executable "$BUN_EXECUTABLE"
258
332
  if [ "$MODE" = "release" ]; then
@@ -545,11 +619,23 @@ run_codesign() {
545
619
  }
546
620
 
547
621
  run_swift() {
622
+ # VARIANT_SWIFT_FLAGS is empty for every full build (the default). A bare
623
+ # "${arr[@]}" on an EMPTY array under `set -u` aborts bash < 4.4 — macOS
624
+ # /bin/bash is 3.2.57 — with `unbound variable`, which killed every build the
625
+ # moment the flags existed. The ${arr[0]+"${arr[@]}"} guard is the script-wide
626
+ # idiom (see run_bun, run_xcrun, run_release_sensitive_tool); do not reintroduce
627
+ # a bare expansion here.
628
+ #
629
+ # The flags are arguments TO swift (after the executable), never before it:
630
+ # placed before the executable they would be consumed by /usr/bin/env as its
631
+ # own options and the build would die with `env: '-Xswiftc': No such file`.
548
632
  "$ENV_EXECUTABLE" -i \
549
633
  HOME="$BUILD_HOME" \
550
634
  PATH="$SANITIZED_PATH" \
551
635
  TMPDIR="$BUILD_WORK_DIR" \
552
- "$SWIFT_EXECUTABLE" "$@"
636
+ "$SWIFT_EXECUTABLE" \
637
+ ${VARIANT_SWIFT_FLAGS[0]+"${VARIANT_SWIFT_FLAGS[@]}"} \
638
+ "$@"
553
639
  }
554
640
 
555
641
  run_lipo() {
@@ -939,8 +1025,19 @@ BUILDER_IDENTITY_KIND="none"
939
1025
  BUILDER_IDENTITY_SHA256="none"
940
1026
  if [ "$MODE" = "release" ]; then
941
1027
  if [ -z "$CODESIGN_IDENTITY" ] || [ "$CODESIGN_IDENTITY" = "-" ]; then
942
- echo "Release builds require RECORDINGS_CODESIGN_IDENTITY for a Developer ID Application identity." >&2
943
- exit 1
1028
+ # No explicit identity: discover the Developer ID Application identity from the
1029
+ # keychain. Release signing is never optional, so "requested but not found" fails
1030
+ # loudly instead of degrading to an ad-hoc signature.
1031
+ if [ "$HOST_PLATFORM" = "Darwin" ]; then
1032
+ resolve_signing_identity || {
1033
+ echo "Release builds require a Developer ID Application identity in RECORDINGS_CODESIGN_IDENTITY or HASNA_CODESIGN_IDENTITY." >&2
1034
+ exit 1
1035
+ }
1036
+ fi
1037
+ if [ -z "$CODESIGN_IDENTITY" ] || [ "$CODESIGN_IDENTITY" = "-" ]; then
1038
+ echo "Release builds require RECORDINGS_CODESIGN_IDENTITY or HASNA_CODESIGN_IDENTITY for a Developer ID Application identity." >&2
1039
+ exit 1
1040
+ fi
944
1041
  fi
945
1042
  if [ -z "$EXPECTED_TEAM_ID" ]; then
946
1043
  echo "Release builds require RECORDINGS_EXPECTED_TEAM_IDENTIFIER to pin the Developer ID team." >&2
@@ -1075,6 +1172,31 @@ elif [ "$MODE" = "local" ]; then
1075
1172
  # a release is notarization, the single-target binding, and the builder attestation
1076
1173
  # above, none of which are relaxed. With no identity supplied the old ad-hoc behaviour
1077
1174
  # is kept verbatim, because a Mac without the signing key must still be able to build.
1175
+ if [ -z "$CODESIGN_IDENTITY" ] || [ "$CODESIGN_IDENTITY" = "-" ]; then
1176
+ if [ "${RECORDINGS_SIGNING_REQUIRED:-0}" = "1" ]; then
1177
+ # A Developer ID was explicitly requested for this local build: discover it
1178
+ # from the keychain and fail loudly when none exists.
1179
+ resolve_signing_identity || exit 1
1180
+ elif [ "$HOST_PLATFORM" = "Darwin" ]; then
1181
+ # The DEFAULT route is the keychain: `security find-identity` and use the
1182
+ # Developer ID Application identity when one exists. That is the stable
1183
+ # signing identity TCC grants key on — an ad-hoc signature has no identity
1184
+ # to key on beyond a CDHash that changes on every rebuild, which orphans
1185
+ # the Accessibility grant and breaks the Fn hold-to-talk path. A Mac
1186
+ # without the signing key still builds ad-hoc (the old behaviour), but the
1187
+ # fallback is LOUD, never silent, and RECORDINGS_SIGNING_REQUIRED=1 above
1188
+ # turns it into a hard failure.
1189
+ discovered_identity="$(discover_developer_id_identity)"
1190
+ if [ -n "$discovered_identity" ]; then
1191
+ CODESIGN_IDENTITY="$discovered_identity"
1192
+ if [ -z "$EXPECTED_TEAM_ID" ]; then
1193
+ EXPECTED_TEAM_ID="$(derive_team_id "$CODESIGN_IDENTITY")" || exit 1
1194
+ fi
1195
+ else
1196
+ echo "WARNING: no Developer ID Application identity found in the keychain; local build falls back to ad-hoc signing (set RECORDINGS_SIGNING_REQUIRED=1 to fail instead)." >&2
1197
+ fi
1198
+ fi
1199
+ fi
1078
1200
  if [ -n "$CODESIGN_IDENTITY" ] && [ "$CODESIGN_IDENTITY" != "-" ]; then
1079
1201
  if [[ "$CODESIGN_IDENTITY" != "Developer ID Application:"* ]]; then
1080
1202
  echo "Local-station signing requires a Developer ID Application identity in RECORDINGS_CODESIGN_IDENTITY." >&2
@@ -1134,15 +1256,15 @@ fi
1134
1256
 
1135
1257
  generate_and_verify_native_fs_guard
1136
1258
 
1137
- echo "Building Recordings.app ($MODE)..."
1259
+ echo "Building HasnaRecordings.app ($MODE)..."
1138
1260
  if [ "$MODE" = "release" ]; then
1139
1261
  OUTPUT_BUILD_DIR="$BUILD_ROOT/release-output"
1140
1262
  else
1141
1263
  OUTPUT_BUILD_DIR="$SCRIPT_DIR/.build/$BUILD_CONFIGURATION"
1142
1264
  fi
1143
- OUTPUT_APP_DIR="$OUTPUT_BUILD_DIR/Recordings.app"
1265
+ OUTPUT_APP_DIR="$OUTPUT_BUILD_DIR/$APP_BUNDLE_NAME"
1144
1266
  SWIFT_PRODUCT_DIR="$SWIFT_SCRATCH_PATH/$BUILD_CONFIGURATION"
1145
- APP_DIR="$BUILD_WORK_DIR/Recordings.app"
1267
+ APP_DIR="$BUILD_WORK_DIR/$APP_BUNDLE_NAME"
1146
1268
  CONTENTS="$APP_DIR/Contents"
1147
1269
  MACOS="$CONTENTS/MacOS"
1148
1270
  RESOURCES="$CONTENTS/Resources"
@@ -1300,6 +1422,16 @@ if [ "$MODE" = "release" ] && [ "$HOST_PLATFORM" = "Darwin" ]; then
1300
1422
  fi
1301
1423
  fi
1302
1424
  "$CP_EXECUTABLE" "$SOURCE_NATIVE_DIR/RecordingsLib/Info.plist" "$CONTENTS/Info.plist"
1425
+ # Bundle display name per the fleet naming rule (knowledge k_msxd5rz3_jfvl3i):
1426
+ # "Hasna Recordings" with 'Hasna' at the beginning, for both variants.
1427
+ "$PLIST_BUDDY" -c 'Set :CFBundleDisplayName Hasna Recordings' "$CONTENTS/Info.plist"
1428
+ if [ "$VARIANT" = "bar" ]; then
1429
+ # The bar is an accessory app: LSUIElement keeps it out of the Dock and the app
1430
+ # switcher, and the app's window-declaring control flow (RECORDINGS_BAR_ONLY)
1431
+ # keeps the workspace window absent. The bundle identifier stays com.hasna.recordings
1432
+ # so TCC grants continue to key on bundle id + signing identity.
1433
+ "$PLIST_BUDDY" -c 'Add :LSUIElement bool true' "$CONTENTS/Info.plist"
1434
+ fi
1303
1435
  VERSION="$("$PLIST_BUDDY" -c 'Print :CFBundleShortVersionString' "$CONTENTS/Info.plist")"
1304
1436
 
1305
1437
  compute_release_publication_identity() {
@@ -1313,6 +1445,7 @@ compute_release_publication_identity() {
1313
1445
  --component "release_sequence=$RELEASE_SEQUENCE"
1314
1446
  --component "key_epoch=$KEY_EPOCH"
1315
1447
  --component "expires_at_utc=$ENVELOPE_EXPIRES_AT_UTC"
1448
+ --component "variant=$VARIANT"
1316
1449
  )
1317
1450
  identity_arguments+=(--component "envelope_public_key_sha256=$ENVELOPE_PUBLIC_KEY_SHA256")
1318
1451
  if [ "$RELEASE_SUBTYPE" = "initial-bootstrap" ]; then
@@ -1331,7 +1464,7 @@ compute_release_publication_identity() {
1331
1464
 
1332
1465
  reserve_release_output() {
1333
1466
  local release_set_basename reserved_output
1334
- release_set_basename="Recordings-${VERSION}-macos-${RELEASE_SUBTYPE}"
1467
+ release_set_basename="${APP_BASENAME}-${VERSION}-macos-${RELEASE_SUBTYPE}"
1335
1468
  compute_release_publication_identity
1336
1469
  RELEASE_OUTPUT_ROOT="$OUTPUT_BUILD_DIR"
1337
1470
  RELEASE_FINAL_DIR="$RELEASE_OUTPUT_ROOT/${release_set_basename}.release"
@@ -1398,7 +1531,7 @@ reserve_release_output() {
1398
1531
  RELEASE_STAGING_DIR="$($MKTEMP_EXECUTABLE -d "$RELEASE_OUTPUT_ROOT/.${release_set_basename}.staging.XXXXXX")"
1399
1532
  "$CHMOD_EXECUTABLE" 0700 "$RELEASE_STAGING_DIR"
1400
1533
  OUTPUT_BUILD_DIR="$RELEASE_STAGING_DIR"
1401
- OUTPUT_APP_DIR="$OUTPUT_BUILD_DIR/Recordings.app"
1534
+ OUTPUT_APP_DIR="$OUTPUT_BUILD_DIR/$APP_BUNDLE_NAME"
1402
1535
  }
1403
1536
 
1404
1537
  if [ "$MODE" = "release" ]; then
@@ -1533,8 +1666,6 @@ run_signed_helper_contract() {
1533
1666
  HOME="$contract_home"
1534
1667
  PATH="/usr/bin:/bin:/usr/sbin:/sbin"
1535
1668
  TMPDIR="$contract_home"
1536
- HASNA_RECORDINGS_CLIENT_STORE="sqlite"
1537
- RECORDINGS_CLIENT_STORE="sqlite"
1538
1669
  HASNA_RECORDINGS_DB_PATH="$contract_home/recordings.db"
1539
1670
  RECORDINGS_AUDIO_DIR="$contract_home/audio"
1540
1671
  )
@@ -1586,6 +1717,7 @@ if [ "$MODE" != "debug" ]; then
1586
1717
  --team-id "$EXPECTED_TEAM_ID" \
1587
1718
  --package-root "$PACKAGE_ROOT" \
1588
1719
  --artifact-policy "$ARTIFACT_POLICY" \
1720
+ --variant "$VARIANT" \
1589
1721
  --approved-target "$APPROVED_TARGET" \
1590
1722
  --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1591
1723
  --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256" \
@@ -1643,7 +1775,7 @@ run_codesign --verify --deep --strict --verbose=2 "$APP_DIR"
1643
1775
  TMPDIR="$BUILD_WORK_DIR" \
1644
1776
  SSH_CONNECTION="${SSH_CONNECTION:-}" \
1645
1777
  ${SMOKE_TEST_ENVIRONMENT[0]+"${SMOKE_TEST_ENVIRONMENT[@]}"} \
1646
- "$BASH_EXECUTABLE" "$SMOKE_SCRIPT" "$APP_DIR" "$BUN_EXECUTABLE"
1778
+ "$BASH_EXECUTABLE" "$SMOKE_SCRIPT" "$APP_DIR" "$BUN_EXECUTABLE" --variant "$VARIANT"
1647
1779
 
1648
1780
  publish_app_output() {
1649
1781
  local source_tree_digest
@@ -1807,7 +1939,7 @@ publish_complete_release_set() {
1807
1939
  RELEASE_DIRECTORY_PUBLISHED=1
1808
1940
  RELEASE_STAGING_DIR=""
1809
1941
  OUTPUT_BUILD_DIR="$RELEASE_FINAL_DIR"
1810
- OUTPUT_APP_DIR="$OUTPUT_BUILD_DIR/Recordings.app"
1942
+ OUTPUT_APP_DIR="$OUTPUT_BUILD_DIR/$APP_BUNDLE_NAME"
1811
1943
  run_bun "$SOURCE_PACKAGE_ROOT/scripts/macos_artifact.ts" complete-release-publication \
1812
1944
  --destination "$RELEASE_FINAL_DIR" \
1813
1945
  --reservation "$RELEASE_RESERVATION" \
@@ -1833,7 +1965,7 @@ if [ "$MODE" = "debug" ]; then
1833
1965
  fi
1834
1966
 
1835
1967
  if [ "$MODE" = "local" ]; then
1836
- ARTIFACT_BASENAME="Recordings-${VERSION}-macos-${APPROVED_TARGET}-local-only"
1968
+ ARTIFACT_BASENAME="${APP_BASENAME}-${VERSION}-macos-${APPROVED_TARGET}-local-only"
1837
1969
  FINAL_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}.zip"
1838
1970
  FINAL_MANIFEST="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}.manifest.json"
1839
1971
  "$RM_EXECUTABLE" -f "$FINAL_ARCHIVE" "$FINAL_MANIFEST"
@@ -1846,6 +1978,7 @@ if [ "$MODE" = "local" ]; then
1846
1978
  --archive "$FINAL_ARCHIVE" \
1847
1979
  --manifest "$FINAL_MANIFEST" \
1848
1980
  --package-root "$PACKAGE_ROOT" \
1981
+ --variant "$VARIANT" \
1849
1982
  --approved-target "$APPROVED_TARGET" \
1850
1983
  --approved-target-identity-kind "$APPROVED_TARGET_IDENTITY_KIND" \
1851
1984
  --approved-target-identity-sha256 "$APPROVED_TARGET_IDENTITY_SHA256" \
@@ -1859,8 +1992,8 @@ if [ "$MODE" = "local" ]; then
1859
1992
  fi
1860
1993
 
1861
1994
  verify_signed_code "$HELPERS/recordings" "Companion CLI"
1862
- verify_signed_code "$APP_DIR" "Recordings.app"
1863
- ARTIFACT_BASENAME="Recordings-${VERSION}-macos-${RELEASE_SUBTYPE}"
1995
+ verify_signed_code "$APP_DIR" "HasnaRecordings.app"
1996
+ ARTIFACT_BASENAME="${APP_BASENAME}-${VERSION}-macos-${RELEASE_SUBTYPE}"
1864
1997
  NOTARY_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}-notarization.zip"
1865
1998
  FINAL_ARCHIVE="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}.zip"
1866
1999
  FINAL_MANIFEST="$OUTPUT_BUILD_DIR/${ARTIFACT_BASENAME}.manifest.json"
@@ -1911,7 +2044,7 @@ run_xcrun stapler validate "$APP_DIR"
1911
2044
  run_release_sensitive_tool "$SPCTL_EXECUTABLE" --assess --type execute --verbose=2 "$APP_DIR"
1912
2045
  run_release_sensitive_tool "$SYSPOLICY_CHECK_EXECUTABLE" distribution "$APP_DIR"
1913
2046
  verify_signed_code "$HELPERS/recordings" "Companion CLI"
1914
- verify_signed_code "$APP_DIR" "Recordings.app"
2047
+ verify_signed_code "$APP_DIR" "HasnaRecordings.app"
1915
2048
  run_codesign --verify --deep --strict --verbose=2 "$APP_DIR"
1916
2049
 
1917
2050
  run_release_sensitive_tool "$DITTO_EXECUTABLE" -c -k --sequesterRsrc --keepParent "$APP_DIR" "$FINAL_ARCHIVE"
@@ -1923,6 +2056,7 @@ run_bun "$SOURCE_PACKAGE_ROOT/scripts/macos_artifact.ts" finalize \
1923
2056
  --manifest "$FINAL_MANIFEST" \
1924
2057
  --package-root "$PACKAGE_ROOT" \
1925
2058
  --team-id "$EXPECTED_TEAM_ID" \
2059
+ --variant "$VARIANT" \
1926
2060
  --notary-log "$NOTARY_LOG" \
1927
2061
  --notary-submission-id "$NOTARY_ID" \
1928
2062
  --submitted-archive-sha256 "$NOTARY_ARCHIVE_SHA256"
@@ -1,24 +0,0 @@
1
- export declare const RETIRED_DEPLOYMENT_MODES: readonly ["local", "self_hosted", "cloud", "remote", "hybrid"];
2
- export type RetiredDeploymentMode = (typeof RETIRED_DEPLOYMENT_MODES)[number];
3
- /** Fold case, surrounding space, and the `self-hosted`/`self_hosted` spelling split. */
4
- export declare function normalizeModeToken(value: string): string;
5
- /** The retired mode this value names, or null if it names something else. */
6
- export declare function asRetiredDeploymentMode(value: string): RetiredDeploymentMode | null;
7
- export interface RetiredModeReplacement {
8
- /** The variable the caller should set instead. */
9
- envKey: string;
10
- /** Value that replaces `local` — the on-this-box arm. */
11
- onBox: string;
12
- /** Value that replaces `self_hosted` / `cloud` / `remote` / `hybrid`. */
13
- offBox: string;
14
- }
15
- /**
16
- * The error a retired mode word must produce.
17
- *
18
- * It names three things a bare "unknown value" error does not: the variable that
19
- * carried the retired word, the variable to set instead, and the exact value —
20
- * because the operator reading this cannot be assumed to know the collapse
21
- * happened.
22
- */
23
- export declare function retiredDeploymentModeError(rawValue: string, sourceEnvKey: string, replacement: RetiredModeReplacement): Error;
24
- //# sourceMappingURL=retired-deployment-modes.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"retired-deployment-modes.d.ts","sourceRoot":"","sources":["../../src/lib/retired-deployment-modes.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,wBAAwB,gEAM3B,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9E,wFAAwF;AACxF,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,6EAA6E;AAC7E,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAKnF;AAED,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,MAAM,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,sBAAsB,GAClC,KAAK,CASP"}