@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
@@ -151,6 +151,7 @@ function localOnlyApprovedTargetsMessage(policyPath?: string): string {
151
151
  }
152
152
 
153
153
  export type ArtifactPolicy = "release" | "local_only";
154
+ export type ArtifactVariant = "full" | "bar";
154
155
  export type TargetIdentityKind =
155
156
  | typeof LEGACY_LOCAL_TARGET_IDENTITY_KIND
156
157
  | "tailscale_node_id_sha256";
@@ -159,6 +160,7 @@ type OperatorTargetIdentityKind = TargetIdentityKind | "none";
159
160
  export type BuildProvenance = {
160
161
  schema_version: 3 | 4;
161
162
  artifact_policy?: "local_only";
163
+ variant?: ArtifactVariant;
162
164
  approved_target?: string;
163
165
  approved_target_identity_kind?: TargetIdentityKind;
164
166
  approved_target_identity_sha256?: string;
@@ -181,6 +183,9 @@ export type BuildProvenance = {
181
183
 
182
184
  export type MacOSArtifactManifest = BuildProvenance & {
183
185
  artifact_type: "recordings-macos-app";
186
+ /// The top-level bundle directory name inside the artifact (HasnaRecordings.app for
187
+ /// both the full app and the bar variant per the fleet naming rule).
188
+ bundle_name: string;
184
189
  app_sha256: string;
185
190
  binding: {
186
191
  bundle_tree_sha256: string;
@@ -210,7 +215,7 @@ export type MacOSArtifactManifest = BuildProvenance & {
210
215
  };
211
216
  container: {
212
217
  type: "zip";
213
- install_locations: ["/Applications/Recordings.app"] | ["~/Applications/Recordings.app"];
218
+ install_locations: [string];
214
219
  };
215
220
  nested_code_policy: {
216
221
  allowlist_sha256: string;
@@ -825,6 +830,22 @@ function manifestPolicy(manifest: MacOSArtifactManifest): ArtifactPolicy {
825
830
  return manifest.schema_version === LOCAL_ARTIFACT_SCHEMA_VERSION ? "local_only" : "release";
826
831
  }
827
832
 
833
+ /**
834
+ * The variant an artifact was built as. Older manifests predate the field; they are
835
+ * full artifacts by definition ("full" was the only variant that existed).
836
+ */
837
+ function manifestVariant(manifest: MacOSArtifactManifest): ArtifactVariant {
838
+ const value = manifest.variant ?? "full";
839
+ if (value !== "full" && value !== "bar") {
840
+ throw new Error(`manifest variant is unsupported: ${value}`);
841
+ }
842
+ return value;
843
+ }
844
+
845
+ function manifestBundleName(manifest: MacOSArtifactManifest): string {
846
+ return manifest.bundle_name ?? "HasnaRecordings.app";
847
+ }
848
+
828
849
  function manifestTargetIdentityKind(manifest: MacOSArtifactManifest): OperatorTargetIdentityKind {
829
850
  if (manifestPolicy(manifest) === "release") return "none";
830
851
  return manifest.approved_target_identity_kind ?? LEGACY_LOCAL_TARGET_IDENTITY_KIND;
@@ -1073,23 +1094,23 @@ function assertCanonicalZipEntries(entries: Array<{ name: string; isDirectory: b
1073
1094
  const components = logicalPath.split("/");
1074
1095
  if (
1075
1096
  !logicalPath ||
1076
- components[0] !== "Recordings.app" ||
1097
+ components[0] !== "HasnaRecordings.app" ||
1077
1098
  components.some((component) => !component || component === "." || component === "..")
1078
1099
  ) {
1079
- throw new Error("release ZIP contains an entry outside the canonical Recordings.app tree");
1100
+ throw new Error("release ZIP contains an entry outside the canonical HasnaRecordings.app tree");
1080
1101
  }
1081
1102
  const collisionKey = canonicalZipCollisionKey(logicalPath);
1082
1103
  if (logicalPaths.has(collisionKey)) {
1083
1104
  throw new Error("release ZIP contains duplicate, file/directory, case-fold, or Unicode-colliding entries");
1084
1105
  }
1085
1106
  logicalPaths.set(collisionKey, { name: logicalPath, isDirectory: entry.isDirectory });
1086
- if (logicalPath === "Recordings.app") {
1087
- if (!entry.isDirectory) throw new Error("release ZIP Recordings.app root is not a directory entry");
1107
+ if (logicalPath === "HasnaRecordings.app") {
1108
+ if (!entry.isDirectory) throw new Error("release ZIP HasnaRecordings.app root is not a directory entry");
1088
1109
  rootDirectoryCount += 1;
1089
1110
  }
1090
1111
  }
1091
1112
  if (rootDirectoryCount !== 1) {
1092
- throw new Error("release ZIP must contain exactly one canonical Recordings.app root entry");
1113
+ throw new Error("release ZIP must contain exactly one canonical HasnaRecordings.app root entry");
1093
1114
  }
1094
1115
  for (const { name } of logicalPaths.values()) {
1095
1116
  const components = name.split("/");
@@ -1405,6 +1426,7 @@ export function withPrivatelyExtractedArchiveApp<T>(
1405
1426
  operation: (appPath: string) => T,
1406
1427
  platformArchiveTool = "/usr/bin/ditto",
1407
1428
  expectedArchiveSha256?: string,
1429
+ expectedBundleName = "HasnaRecordings.app",
1408
1430
  ): T {
1409
1431
  const privateRoot = mkdtempSync(join(tmpdir(), "recordings-artifact-extract-"));
1410
1432
  chmodSync(privateRoot, 0o700);
@@ -1449,13 +1471,17 @@ export function withPrivatelyExtractedArchiveApp<T>(
1449
1471
  inspectZipArchive(pinnedArchivePath);
1450
1472
  run(platformArchiveTool, ["-x", "-k", pinnedArchivePath, extractionRoot]);
1451
1473
  const rootEntries = readdirSync(extractionRoot);
1452
- if (rootEntries.length !== 1 || rootEntries[0] !== "Recordings.app") {
1453
- throw new Error("release archive must contain exactly one top-level Recordings.app");
1474
+ if (rootEntries.length !== 1 || rootEntries[0] !== expectedBundleName) {
1475
+ throw new Error(
1476
+ `release archive must contain exactly one top-level ${expectedBundleName}`,
1477
+ );
1454
1478
  }
1455
- const extractedAppPath = join(extractionRoot, "Recordings.app");
1479
+ const extractedAppPath = join(extractionRoot, expectedBundleName);
1456
1480
  const appDetails = lstatSync(extractedAppPath);
1457
1481
  if (appDetails.isSymbolicLink() || !appDetails.isDirectory()) {
1458
- throw new Error("release archive top-level Recordings.app must be a regular directory");
1482
+ throw new Error(
1483
+ `release archive top-level ${expectedBundleName} must be a regular directory`,
1484
+ );
1459
1485
  }
1460
1486
  assertRegularArchiveTree(extractedAppPath);
1461
1487
  return operation(extractedAppPath);
@@ -1478,6 +1504,7 @@ export function extractVerifiedArchiveToStaging(
1478
1504
  expectedApprovedTarget: string,
1479
1505
  expectedApprovedTargetIdentitySha256: string,
1480
1506
  expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind,
1507
+ expectedVariant: ArtifactVariant = "full",
1481
1508
  platformArchiveTool = "/usr/bin/ditto",
1482
1509
  ): void {
1483
1510
  if (resolve(stagingTarget) !== stagingTarget) {
@@ -1510,16 +1537,19 @@ export function extractVerifiedArchiveToStaging(
1510
1537
  withPrivatelyExtractedArchiveApp(
1511
1538
  archivePath,
1512
1539
  (appPath) => {
1513
- const targetAppPath = join(stagingTarget, "Recordings.app");
1540
+ const targetAppPath = join(stagingTarget, manifestBundleName(manifest));
1514
1541
  renameSync(appPath, targetAppPath);
1515
1542
  assertRegularArchiveTree(targetAppPath);
1516
1543
  const entries = readdirSync(stagingTarget);
1517
- if (entries.length !== 1 || entries[0] !== "Recordings.app") {
1518
- throw new Error("archive extraction did not produce exactly Recordings.app in staging");
1544
+ if (entries.length !== 1 || entries[0] !== manifestBundleName(manifest)) {
1545
+ throw new Error(
1546
+ `archive extraction did not produce exactly ${manifestBundleName(manifest)} in staging`,
1547
+ );
1519
1548
  }
1520
1549
  },
1521
1550
  platformArchiveTool,
1522
1551
  manifest.archive.sha256,
1552
+ manifestBundleName(manifest),
1523
1553
  );
1524
1554
  }
1525
1555
 
@@ -1547,6 +1577,20 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
1547
1577
  }
1548
1578
  if (manifest.artifact_type !== "recordings-macos-app") throw new Error("unexpected artifact type");
1549
1579
  if (manifest.bundle_id !== BUNDLE_ID) throw new Error("unexpected bundle identifier");
1580
+ if (
1581
+ manifest.bundle_name !== undefined &&
1582
+ (typeof manifest.bundle_name !== "string" ||
1583
+ !/^[A-Za-z0-9][A-Za-z0-9 .-]*\.app$/.test(manifest.bundle_name))
1584
+ ) {
1585
+ throw new Error("manifest bundle name must be a *.app bundle directory name");
1586
+ }
1587
+ if (
1588
+ manifest.variant !== undefined &&
1589
+ manifest.variant !== "full" &&
1590
+ manifest.variant !== "bar"
1591
+ ) {
1592
+ throw new Error(`manifest variant is unsupported: ${manifest.variant}`);
1593
+ }
1550
1594
  const artifactPolicy = manifestPolicy(manifest);
1551
1595
  for (const [label, value] of [
1552
1596
  ["bundle version", manifest.bundle_version],
@@ -1707,8 +1751,8 @@ export function assertManifestShape(manifest: MacOSArtifactManifest): void {
1707
1751
  manifest.container?.type !== "zip" ||
1708
1752
  JSON.stringify(manifest.container.install_locations) !== JSON.stringify(
1709
1753
  artifactPolicy === "release"
1710
- ? ["/Applications/Recordings.app"]
1711
- : ["~/Applications/Recordings.app"],
1754
+ ? ["/Applications/HasnaRecordings.app"]
1755
+ : ["~/Applications/HasnaRecordings.app"],
1712
1756
  )
1713
1757
  ) {
1714
1758
  throw new Error("manifest has an unexpected container install policy");
@@ -1755,6 +1799,7 @@ export function verifyArchiveManifest(
1755
1799
  expectedApprovedTarget: string = RELEASE_APPROVED_TARGET,
1756
1800
  expectedApprovedTargetIdentitySha256: string = "none",
1757
1801
  expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind = LEGACY_LOCAL_TARGET_IDENTITY_KIND,
1802
+ expectedVariant: ArtifactVariant = "full",
1758
1803
  ): MacOSArtifactManifest {
1759
1804
  if (!expectedTeamId) throw new Error("expected Team ID is required");
1760
1805
  const manifest = readAuthenticatedManifest<MacOSArtifactManifest>(
@@ -1765,6 +1810,11 @@ export function verifyArchiveManifest(
1765
1810
  if (manifestPolicy(manifest) !== expectedPolicy) {
1766
1811
  throw new Error("manifest artifact policy does not match the explicit operator selection");
1767
1812
  }
1813
+ if (manifestVariant(manifest) !== expectedVariant) {
1814
+ throw new Error(
1815
+ `manifest variant ${manifestVariant(manifest)} does not match the explicit operator selection ${expectedVariant}`,
1816
+ );
1817
+ }
1768
1818
  const actualApprovedTarget = manifest.approved_target ?? RELEASE_APPROVED_TARGET;
1769
1819
  if (actualApprovedTarget !== expectedApprovedTarget) {
1770
1820
  throw new Error("manifest approved target does not match the exact operator-approved target");
@@ -1817,6 +1867,7 @@ function assertProvenanceMatchesManifest(
1817
1867
  "builder_identity_kind",
1818
1868
  "builder_identity_sha256",
1819
1869
  "non_notarized",
1870
+ "variant",
1820
1871
  "bundle_id",
1821
1872
  "bundle_version",
1822
1873
  "git_sha",
@@ -1989,11 +2040,17 @@ export function verifyExtractedApp(
1989
2040
  expectedApprovedTarget: string = RELEASE_APPROVED_TARGET,
1990
2041
  expectedApprovedTargetIdentitySha256: string = "none",
1991
2042
  expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind = LEGACY_LOCAL_TARGET_IDENTITY_KIND,
2043
+ expectedVariant: ArtifactVariant = "full",
1992
2044
  ): MacOSArtifactManifest {
1993
2045
  const manifest = readAuthenticatedManifest<MacOSArtifactManifest>(
1994
2046
  manifestPath,
1995
2047
  expectedManifestSha256,
1996
2048
  );
2049
+ if (manifestVariant(manifest) !== expectedVariant) {
2050
+ throw new Error(
2051
+ `manifest variant ${manifestVariant(manifest)} does not match the explicit operator selection ${expectedVariant}`,
2052
+ );
2053
+ }
1997
2054
  verifyAppAgainstManifest(
1998
2055
  appPath,
1999
2056
  manifest,
@@ -2063,6 +2120,7 @@ function verifySuppliedAndArchivedApps(
2063
2120
  },
2064
2121
  "/usr/bin/ditto",
2065
2122
  manifest.archive.sha256,
2123
+ manifestBundleName(manifest),
2066
2124
  );
2067
2125
  if (sha256ArchiveFile(archivePath) !== manifest.archive.sha256) {
2068
2126
  throw new Error("release archive changed during exact-byte verification");
@@ -2080,6 +2138,7 @@ function writeProvenance(
2080
2138
  approvedTargetIdentitySha256: string,
2081
2139
  builderIdentityKind: OperatorTargetIdentityKind,
2082
2140
  builderIdentitySha256: string,
2141
+ variant: ArtifactVariant,
2083
2142
  ): void {
2084
2143
  const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
2085
2144
  const helperPath = join(appPath, "Contents", "Helpers", "recordings");
@@ -2089,6 +2148,7 @@ function writeProvenance(
2089
2148
  artifactPolicy === "release"
2090
2149
  ? RELEASE_ARTIFACT_SCHEMA_VERSION
2091
2150
  : LOCAL_ARTIFACT_SCHEMA_VERSION,
2151
+ variant,
2092
2152
  bundle_id: plistValue(appPath, "CFBundleIdentifier"),
2093
2153
  bundle_version: plistValue(appPath, "CFBundleShortVersionString"),
2094
2154
  bundle_build_version: plistValue(appPath, "CFBundleVersion"),
@@ -2151,6 +2211,7 @@ function finalizeArtifact(
2151
2211
  notaryLogPath: string,
2152
2212
  notarySubmissionId: string,
2153
2213
  submittedArchiveSha256: string,
2214
+ expectedVariant: ArtifactVariant = "full",
2154
2215
  ): void {
2155
2216
  assertCurrentSourceRevision(packageRoot, expectedSourceSha);
2156
2217
  const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
@@ -2160,6 +2221,14 @@ function finalizeArtifact(
2160
2221
  if (provenance.git_sha !== expectedSourceSha) {
2161
2222
  throw new Error("embedded provenance does not match the pinned source SHA");
2162
2223
  }
2224
+ if (expectedVariant !== "full" && expectedVariant !== "bar") {
2225
+ throw new Error(`finalize variant is unsupported: ${expectedVariant}`);
2226
+ }
2227
+ if ((provenance.variant ?? "full") !== expectedVariant) {
2228
+ throw new Error(
2229
+ `finalize variant ${expectedVariant} does not match the embedded provenance variant`,
2230
+ );
2231
+ }
2163
2232
  assertExpectedCodeLayout(appPath);
2164
2233
  const outerSigning = signingEvidence(appPath, expectedTeamId, APP_ENTITLEMENTS, executablePath);
2165
2234
  const helperSigning = signingEvidence(helperPath, expectedTeamId, HELPER_ENTITLEMENTS);
@@ -2172,9 +2241,14 @@ function finalizeArtifact(
2172
2241
  const notaryLog = JSON.parse(notaryLogSnapshot.toString("utf8")) as unknown;
2173
2242
  assertAcceptedNotaryLog(notaryLog, notarySubmissionId, submittedArchiveSha256);
2174
2243
  const items = nestedItems(appPath, expectedTeamId, "release", outerSigning, helperSigning);
2244
+ const bundleName = basename(appPath);
2245
+ if (!bundleName.endsWith(".app")) {
2246
+ throw new Error(`release app bundle must be named *.app: ${appPath}`);
2247
+ }
2175
2248
  const manifest: MacOSArtifactManifest = {
2176
2249
  ...provenance,
2177
2250
  artifact_type: "recordings-macos-app",
2251
+ bundle_name: bundleName,
2178
2252
  app_sha256: sha256File(executablePath),
2179
2253
  binding: { bundle_tree_sha256: treeDigest(appPath) },
2180
2254
  provenance_sha256: sha256File(embeddedPath),
@@ -2201,7 +2275,7 @@ function finalizeArtifact(
2201
2275
  },
2202
2276
  container: {
2203
2277
  type: "zip",
2204
- install_locations: ["/Applications/Recordings.app"],
2278
+ install_locations: [`/Applications/${bundleName}`],
2205
2279
  },
2206
2280
  nested_code_policy: {
2207
2281
  allowlist_sha256: nestedPolicyDigest(items),
@@ -2278,11 +2352,15 @@ function finalizeLocalArtifact(
2278
2352
  approvedTargetIdentityKind: TargetIdentityKind,
2279
2353
  approvedTargetIdentitySha256: string,
2280
2354
  expectedTeamId: string = "ADHOC",
2355
+ expectedVariant: ArtifactVariant = "full",
2281
2356
  ): void {
2282
2357
  assertCurrentSourceRevision(packageRoot, expectedSourceSha);
2283
2358
  if (approvedTargetIdentityKind !== "tailscale_node_id_sha256") {
2284
2359
  throw new Error("new local-only artifacts require a Tailscale node ID identity hash");
2285
2360
  }
2361
+ if (expectedVariant !== "full" && expectedVariant !== "bar") {
2362
+ throw new Error(`finalize variant is unsupported: ${expectedVariant}`);
2363
+ }
2286
2364
  const localSigningMode = localOnlySigningMode(expectedTeamId);
2287
2365
  const executablePath = join(appPath, "Contents", "MacOS", "Recordings");
2288
2366
  const helperPath = join(appPath, "Contents", "Helpers", "recordings");
@@ -2300,6 +2378,11 @@ function finalizeLocalArtifact(
2300
2378
  ) {
2301
2379
  throw new Error("embedded provenance is not approved for this local-only target");
2302
2380
  }
2381
+ if ((provenance.variant ?? "full") !== expectedVariant) {
2382
+ throw new Error(
2383
+ `finalize variant ${expectedVariant} does not match the embedded provenance variant`,
2384
+ );
2385
+ }
2303
2386
  assertExpectedCodeLayout(appPath);
2304
2387
  const outerSigning = signingEvidence(
2305
2388
  appPath,
@@ -2316,9 +2399,14 @@ function finalizeLocalArtifact(
2316
2399
  "local_only",
2317
2400
  );
2318
2401
  const items = nestedItems(appPath, expectedTeamId, "local_only", outerSigning, helperSigning);
2402
+ const bundleName = basename(appPath);
2403
+ if (!bundleName.endsWith(".app")) {
2404
+ throw new Error(`local app bundle must be named *.app: ${appPath}`);
2405
+ }
2319
2406
  const manifest: MacOSArtifactManifest = {
2320
2407
  ...provenance,
2321
2408
  artifact_type: "recordings-macos-app",
2409
+ bundle_name: bundleName,
2322
2410
  app_sha256: sha256File(executablePath),
2323
2411
  binding: { bundle_tree_sha256: treeDigest(appPath) },
2324
2412
  provenance_sha256: sha256File(embeddedPath),
@@ -2346,7 +2434,7 @@ function finalizeLocalArtifact(
2346
2434
  },
2347
2435
  container: {
2348
2436
  type: "zip",
2349
- install_locations: ["~/Applications/Recordings.app"],
2437
+ install_locations: [`~/Applications/${bundleName}`],
2350
2438
  },
2351
2439
  nested_code_policy: {
2352
2440
  allowlist_sha256: nestedPolicyDigest(items),
@@ -2428,7 +2516,7 @@ export function assertVersionTransition(
2428
2516
  assertManifestShape(manifest);
2429
2517
  if (compareVersions(manifest.bundle_version, installedVersion) < 0) {
2430
2518
  throw new Error(
2431
- `refusing to downgrade Recordings.app from ${installedVersion} to ${manifest.bundle_version}`,
2519
+ `refusing to downgrade HasnaRecordings.app from ${installedVersion} to ${manifest.bundle_version}`,
2432
2520
  );
2433
2521
  }
2434
2522
  if (compareVersions(manifest.bundle_version, installedVersion) === 0) {
@@ -2516,6 +2604,7 @@ function verifyActiveApp(
2516
2604
  expectedApprovedTarget: string,
2517
2605
  expectedApprovedTargetIdentitySha256: string,
2518
2606
  expectedApprovedTargetIdentityKind: OperatorTargetIdentityKind,
2607
+ expectedVariant: ArtifactVariant = "full",
2519
2608
  ): void {
2520
2609
  const manifest = verifyExtractedApp(
2521
2610
  appPath,
@@ -2526,6 +2615,7 @@ function verifyActiveApp(
2526
2615
  expectedApprovedTarget,
2527
2616
  expectedApprovedTargetIdentitySha256,
2528
2617
  expectedApprovedTargetIdentityKind,
2618
+ expectedVariant,
2529
2619
  );
2530
2620
  const helperPath = join(appPath, "Contents", "Helpers", "recordings");
2531
2621
  if (companionVersion(helperPath) !== manifest.companion.version) {
@@ -2536,8 +2626,6 @@ function verifyActiveApp(
2536
2626
  const environment: NodeJS.ProcessEnv = {
2537
2627
  HOME: contractHome,
2538
2628
  PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
2539
- HASNA_RECORDINGS_CLIENT_STORE: "sqlite",
2540
- RECORDINGS_CLIENT_STORE: "sqlite",
2541
2629
  HASNA_RECORDINGS_DB_PATH: join(contractHome, "recordings.db"),
2542
2630
  RECORDINGS_AUDIO_DIR: join(contractHome, "audio"),
2543
2631
  };
@@ -2893,7 +2981,7 @@ function readJournal(path: string): InstallJournal {
2893
2981
  if (resolve(path) !== resolve(join(expectedParent, ".Recordings-install-transaction.json"))) {
2894
2982
  throw new Error("install transaction journal is outside the expected app parent");
2895
2983
  }
2896
- if (resolve(journal.app_destination) !== resolve(join(expectedParent, "Recordings.app"))) {
2984
+ if (resolve(journal.app_destination) !== resolve(join(expectedParent, "HasnaRecordings.app"))) {
2897
2985
  throw new Error("install transaction journal has an unexpected app destination");
2898
2986
  }
2899
2987
  const expectedDataDir = resolve(join(dirname(expectedParent), ".hasna", "recordings"));
@@ -2911,7 +2999,9 @@ function readJournal(path: string): InstallJournal {
2911
2999
  return (
2912
3000
  resolved === resolve(journal.app_destination) ||
2913
3001
  resolved === resolve(join(journal.data_dir, "Recordings.app")) ||
2914
- resolved.startsWith(`${resolve(journal.app_parent)}/Recordings.app.`)
3002
+ resolved === resolve(join(journal.data_dir, "HasnaRecordings.app")) ||
3003
+ resolved.startsWith(`${resolve(journal.app_parent)}/Recordings.app.`) ||
3004
+ resolved.startsWith(`${resolve(journal.app_parent)}/HasnaRecordings.app.`)
2915
3005
  );
2916
3006
  };
2917
3007
  if (!journal.originals.every((entry) => allowedOriginal(entry.path))) {
@@ -3849,7 +3939,7 @@ function openRecoveryCapabilities(
3849
3939
  const homePath = resolve(dirname(appParent));
3850
3940
  if (
3851
3941
  basename(appParent) !== "Applications" ||
3852
- resolve(journal.app_destination) !== resolve(join(appParent, "Recordings.app")) ||
3942
+ resolve(journal.app_destination) !== resolve(join(appParent, "HasnaRecordings.app")) ||
3853
3943
  resolve(journal.data_dir) !== resolve(join(homePath, ".hasna", "recordings")) ||
3854
3944
  dirname(resolve(journal.transaction_dir)) !== appParent
3855
3945
  ) {
@@ -4252,9 +4342,10 @@ export function publishInstallCandidate(
4252
4342
  const destination = resolve(destinationPath);
4253
4343
  const stagingParentPath = dirname(staging);
4254
4344
  const stagingParentLeaf = basename(stagingParentPath);
4345
+ const bundleLeaf = basename(journal.app_destination);
4255
4346
  if (
4256
4347
  destination !== resolve(journal.app_destination) ||
4257
- basename(staging) !== "Recordings.app" ||
4348
+ basename(staging) !== bundleLeaf ||
4258
4349
  dirname(stagingParentPath) !== resolve(journal.app_parent) ||
4259
4350
  !/^\.Recordings-install\.[A-Za-z0-9]+$/.test(stagingParentLeaf)
4260
4351
  ) {
@@ -4277,12 +4368,12 @@ export function publishInstallCandidate(
4277
4368
  stagingParent,
4278
4369
  "candidate staging parent",
4279
4370
  );
4280
- retained = guard.openDirAt(stagingParent, "Recordings.app");
4281
- assertNativeBinding(guard, stagingParent, "Recordings.app", retained, "staged candidate");
4371
+ retained = guard.openDirAt(stagingParent, bundleLeaf);
4372
+ assertNativeBinding(guard, stagingParent, bundleLeaf, retained, "staged candidate");
4282
4373
  if (nativeTreeDigest(guard, retained) !== expectedDigest) {
4283
4374
  throw new Error("staged candidate does not match durable journal evidence");
4284
4375
  }
4285
- if (guard.statAt(capabilities.applications, "Recordings.app") !== null) {
4376
+ if (guard.statAt(capabilities.applications, bundleLeaf) !== null) {
4286
4377
  throw new Error("candidate destination already exists");
4287
4378
  }
4288
4379
  fsyncRetainedTree(guard, retained, uid, "staged candidate");
@@ -4294,8 +4385,8 @@ export function publishInstallCandidate(
4294
4385
  stagingParent,
4295
4386
  "candidate staging parent",
4296
4387
  );
4297
- assertNativeBinding(guard, stagingParent, "Recordings.app", retained, "staged candidate");
4298
- if (guard.statAt(capabilities.applications, "Recordings.app") !== null) {
4388
+ assertNativeBinding(guard, stagingParent, bundleLeaf, retained, "staged candidate");
4389
+ if (guard.statAt(capabilities.applications, bundleLeaf) !== null) {
4299
4390
  throw new Error("candidate destination appeared before publication");
4300
4391
  }
4301
4392
  installTransitionTestPoint(
@@ -4305,10 +4396,10 @@ export function publishInstallCandidate(
4305
4396
  );
4306
4397
  guard.renameHandleNoReplaceAt(
4307
4398
  stagingParent,
4308
- "Recordings.app",
4399
+ bundleLeaf,
4309
4400
  retained,
4310
4401
  capabilities.applications,
4311
- "Recordings.app",
4402
+ bundleLeaf,
4312
4403
  );
4313
4404
  installTransitionTestPoint(
4314
4405
  "publish-candidate",
@@ -4319,11 +4410,11 @@ export function publishInstallCandidate(
4319
4410
  assertNativeBinding(
4320
4411
  guard,
4321
4412
  capabilities.applications,
4322
- "Recordings.app",
4413
+ bundleLeaf,
4323
4414
  retained,
4324
4415
  "published candidate",
4325
4416
  );
4326
- if (guard.statAt(stagingParent, "Recordings.app") !== null) {
4417
+ if (guard.statAt(stagingParent, bundleLeaf) !== null) {
4327
4418
  throw new Error("candidate staging leaf was recreated during publication");
4328
4419
  }
4329
4420
  guard.fsyncHandle(capabilities.applications);
@@ -4369,7 +4460,8 @@ function cleanupInstallCandidateStaging(
4369
4460
  staging,
4370
4461
  "candidate staging root",
4371
4462
  );
4372
- const candidateMetadata = guard.statAt(staging, "Recordings.app");
4463
+ const bundleLeaf = basename(journal.app_destination);
4464
+ const candidateMetadata = guard.statAt(staging, bundleLeaf);
4373
4465
  if (candidateMetadata !== null) {
4374
4466
  if (journal.schema_version !== 9 || !journal.candidate_tree_sha256) {
4375
4467
  throw new Error("legacy journal cannot authenticate candidate staging cleanup");
@@ -4377,7 +4469,7 @@ function cleanupInstallCandidateStaging(
4377
4469
  const candidate = openProvenDirectoryAt(
4378
4470
  guard,
4379
4471
  staging,
4380
- "Recordings.app",
4472
+ bundleLeaf,
4381
4473
  journal.candidate_tree_sha256,
4382
4474
  "staged candidate",
4383
4475
  );
@@ -4385,7 +4477,7 @@ function cleanupInstallCandidateStaging(
4385
4477
  quarantineRemoveRetainedAt(
4386
4478
  guard,
4387
4479
  staging,
4388
- "Recordings.app",
4480
+ bundleLeaf,
4389
4481
  candidate,
4390
4482
  "staged candidate",
4391
4483
  );
@@ -4477,14 +4569,20 @@ function originalDestinationCapability(
4477
4569
  ): { parent: NativeHandle; leaf: string } {
4478
4570
  const resolved = resolve(path);
4479
4571
  if (resolved === resolve(journal.app_destination)) {
4480
- return { parent: capabilities.applications, leaf: "Recordings.app" };
4572
+ return { parent: capabilities.applications, leaf: basename(journal.app_destination) };
4481
4573
  }
4482
- if (resolved === resolve(join(journal.data_dir, "Recordings.app"))) {
4483
- return { parent: capabilities.data, leaf: "Recordings.app" };
4574
+ if (
4575
+ dirname(resolved) === resolve(journal.data_dir) &&
4576
+ (basename(resolved) === "Recordings.app" || basename(resolved) === "HasnaRecordings.app")
4577
+ ) {
4578
+ // Install site inside the state data directory: the legacy pre-rename
4579
+ // Recordings.app and the renamed HasnaRecordings.app are both recognized and
4580
+ // preserved, never treated as the canonical Applications destination.
4581
+ return { parent: capabilities.data, leaf: basename(resolved) };
4484
4582
  }
4485
4583
  if (
4486
4584
  dirname(resolved) === resolve(journal.app_parent) &&
4487
- /^Recordings\.app\.[A-Za-z0-9._-]+$/.test(basename(resolved))
4585
+ /^(?:Hasna)?Recordings\.app\.[A-Za-z0-9._-]+$/.test(basename(resolved))
4488
4586
  ) {
4489
4587
  return { parent: capabilities.applications, leaf: basename(resolved) };
4490
4588
  }
@@ -4915,24 +5013,25 @@ function recoverJournal(path: string): void {
4915
5013
  const canonicalOriginal = journal.originals.find(
4916
5014
  (entry) => resolve(entry.path) === resolve(journal.app_destination),
4917
5015
  );
5016
+ const destinationLeaf = basename(journal.app_destination);
4918
5017
  const canonicalDestinationDigest = canonicalOriginal
4919
- ? nativeTreeDigestAt(guard, applications, "Recordings.app")
5018
+ ? nativeTreeDigestAt(guard, applications, destinationLeaf)
4920
5019
  : null;
4921
5020
  const canonicalAlreadyRestored = canonicalOriginal !== undefined &&
4922
5021
  canonicalDestinationDigest === canonicalOriginal.sha256;
4923
5022
  if (
4924
5023
  ["candidate-moving", "candidate-installed", "activated", "launching"].includes(journal.phase) &&
4925
- !canonicalAlreadyRestored && guard.statAt(applications, "Recordings.app") !== null
5024
+ !canonicalAlreadyRestored && guard.statAt(applications, destinationLeaf) !== null
4926
5025
  ) {
4927
5026
  if (journal.schema_version !== 9 || !journal.candidate_tree_sha256) {
4928
5027
  throw new Error("legacy recovery journal cannot prove the uncommitted candidate tree");
4929
5028
  }
4930
- recoveryTestBarrier("before-candidate-remove", "Recordings.app");
5029
+ recoveryTestBarrier("before-candidate-remove", destinationLeaf);
4931
5030
  assertNativeBinding(guard, capabilities.home, "Applications", applications, "Applications");
4932
5031
  const candidate = openProvenDirectoryAt(
4933
5032
  guard,
4934
5033
  applications,
4935
- "Recordings.app",
5034
+ destinationLeaf,
4936
5035
  journal.candidate_tree_sha256,
4937
5036
  "uncommitted candidate",
4938
5037
  );
@@ -4940,7 +5039,7 @@ function recoverJournal(path: string): void {
4940
5039
  quarantineRemoveRetainedAt(
4941
5040
  guard,
4942
5041
  applications,
4943
- "Recordings.app",
5042
+ destinationLeaf,
4944
5043
  candidate,
4945
5044
  "uncommitted candidate",
4946
5045
  );
@@ -5188,6 +5287,9 @@ function manifestGet(path: string, expectedManifestSha256: string, field: string
5188
5287
  }
5189
5288
  else if (field === "approved_target_identity_sha256") console.log(manifest.approved_target_identity_sha256);
5190
5289
  else if (field === "builder_identity_kind") console.log(manifestBuilderIdentityKind(manifest));
5290
+ else if (field === "variant") console.log(manifestVariant(manifest));
5291
+ // Pre-variant manifests carry no bundle_name; they are full builds by definition.
5292
+ else if (field === "bundle_name") console.log(manifestBundleName(manifest));
5191
5293
  else throw new Error(`unsupported manifest field: ${field}`);
5192
5294
  }
5193
5295
 
@@ -5214,6 +5316,14 @@ function repeatedArguments(name: string): string[] {
5214
5316
  return values;
5215
5317
  }
5216
5318
 
5319
+ function variantArgument(defaultVariant: ArtifactVariant): ArtifactVariant {
5320
+ const value = optionalArgument("--variant") ?? defaultVariant;
5321
+ if (value !== "full" && value !== "bar") {
5322
+ throw new Error(`artifact variant must be full or bar; got: ${value}`);
5323
+ }
5324
+ return value;
5325
+ }
5326
+
5217
5327
  function artifactPolicyArgument(): ArtifactPolicy {
5218
5328
  const value = argument("--artifact-policy");
5219
5329
  if (value !== "release" && value !== "local_only") {
@@ -5276,6 +5386,7 @@ function main(): void {
5276
5386
  argument("--approved-target-identity-sha256"),
5277
5387
  builderIdentityKindArgument(policy),
5278
5388
  argument("--builder-identity-sha256"),
5389
+ variantArgument("full"),
5279
5390
  );
5280
5391
  } else if (command === "finalize") {
5281
5392
  const sourceSha = argument("--source-sha");
@@ -5290,6 +5401,7 @@ function main(): void {
5290
5401
  argument("--notary-log"),
5291
5402
  argument("--notary-submission-id"),
5292
5403
  argument("--submitted-archive-sha256"),
5404
+ variantArgument("full"),
5293
5405
  );
5294
5406
  } else if (command === "finalize-local") {
5295
5407
  const sourceSha = argument("--source-sha");
@@ -5303,6 +5415,7 @@ function main(): void {
5303
5415
  targetIdentityKindArgument(undefined, true),
5304
5416
  argument("--approved-target-identity-sha256"),
5305
5417
  optionalArgument("--expected-team-id") ?? "ADHOC",
5418
+ variantArgument("full"),
5306
5419
  );
5307
5420
  } else if (command === "verify-archive") {
5308
5421
  const teamId = argument("--team-id");
@@ -5318,6 +5431,7 @@ function main(): void {
5318
5431
  argument("--approved-target"),
5319
5432
  argument("--approved-target-identity-sha256"),
5320
5433
  targetIdentityKindArgument(policy),
5434
+ variantArgument("full"),
5321
5435
  );
5322
5436
  } else if (command === "extract-verified-archive") {
5323
5437
  const teamId = argument("--team-id");
@@ -5334,6 +5448,7 @@ function main(): void {
5334
5448
  argument("--approved-target"),
5335
5449
  argument("--approved-target-identity-sha256"),
5336
5450
  targetIdentityKindArgument(policy),
5451
+ variantArgument("full"),
5337
5452
  );
5338
5453
  } else if (command === "verify-app") {
5339
5454
  const teamId = argument("--team-id");
@@ -5347,6 +5462,7 @@ function main(): void {
5347
5462
  argument("--approved-target"),
5348
5463
  argument("--approved-target-identity-sha256"),
5349
5464
  targetIdentityKindArgument(policy),
5465
+ variantArgument("full"),
5350
5466
  );
5351
5467
  } else if (command === "verify-active") {
5352
5468
  const teamId = argument("--team-id");
@@ -5360,6 +5476,7 @@ function main(): void {
5360
5476
  argument("--approved-target"),
5361
5477
  argument("--approved-target-identity-sha256"),
5362
5478
  targetIdentityKindArgument(policy),
5479
+ variantArgument("full"),
5363
5480
  );
5364
5481
  } else if (command === "assert-release") {
5365
5482
  assertExpectedRelease(
@@ -34,11 +34,11 @@ async function main() {
34
34
  getCloudPg,
35
35
  migrateCloudSchema,
36
36
  pingCloudConnectivity,
37
- resolveCloudDatabaseUrl,
37
+ resolveDatabaseUrl,
38
38
  } = await import("../src/server/cloud.js");
39
39
  const { assertCloudSchemaContract } = await import("../src/server/cloud-readiness.js");
40
40
  const { runCloudMigration } = await import("../src/server/migrate-command.js");
41
- const url = resolveCloudDatabaseUrl();
41
+ const url = resolveDatabaseUrl();
42
42
  if (!url) {
43
43
  console.error(
44
44
  "migrate: no database URL (HASNA_RECORDINGS_DATABASE_URL / RECORDINGS_DATABASE_URL / DATABASE_URL)",