@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
package/dist/cli/index.js CHANGED
@@ -900,10 +900,10 @@ function collectValues(value, previous) {
900
900
 
901
901
  // src/cli/index.ts
902
902
  import chalk from "chalk";
903
- import { spawnSync as spawnSync7 } from "child_process";
903
+ import { spawnSync as spawnSync9 } from "child_process";
904
904
  import {
905
905
  existsSync as existsSync8,
906
- readFileSync as readFileSync5,
906
+ readFileSync as readFileSync6,
907
907
  readdirSync as readdirSync2
908
908
  } from "fs";
909
909
  import { dirname as dirname5, join as pathJoin } from "path";
@@ -1117,6 +1117,9 @@ var DEFAULT_CONFIG = {
1117
1117
  db_path: "",
1118
1118
  audio_dir: "",
1119
1119
  max_recording_seconds: 1800,
1120
+ s3_bucket: "",
1121
+ s3_prefix: "",
1122
+ s3_region: "",
1120
1123
  config_warnings: []
1121
1124
  };
1122
1125
  function loadConfig(configPath) {
@@ -1143,6 +1146,9 @@ function loadConfig(configPath) {
1143
1146
  if (process.env.RECORDINGS_API_KEY) {
1144
1147
  config.openai_api_key = process.env.RECORDINGS_API_KEY;
1145
1148
  }
1149
+ if (process.env.RECORDINGS_OPENAI_API_KEY) {
1150
+ config.openai_api_key = process.env.RECORDINGS_OPENAI_API_KEY;
1151
+ }
1146
1152
  if (process.env.RECORDINGS_ENHANCEMENT_KEY) {
1147
1153
  config.enhancement_api_key = process.env.RECORDINGS_ENHANCEMENT_KEY;
1148
1154
  }
@@ -1186,14 +1192,26 @@ function loadConfig(configPath) {
1186
1192
  if (process.env.RECORDINGS_AUDIO_DIR) {
1187
1193
  config.audio_dir = process.env.RECORDINGS_AUDIO_DIR;
1188
1194
  }
1195
+ if (process.env.HASNA_RECORDINGS_S3_BUCKET) {
1196
+ config.s3_bucket = process.env.HASNA_RECORDINGS_S3_BUCKET;
1197
+ } else if (process.env.RECORDINGS_S3_BUCKET) {
1198
+ config.s3_bucket = process.env.RECORDINGS_S3_BUCKET;
1199
+ }
1200
+ if (process.env.HASNA_RECORDINGS_S3_PREFIX) {
1201
+ config.s3_prefix = process.env.HASNA_RECORDINGS_S3_PREFIX;
1202
+ } else if (process.env.RECORDINGS_S3_PREFIX) {
1203
+ config.s3_prefix = process.env.RECORDINGS_S3_PREFIX;
1204
+ }
1205
+ if (process.env.RECORDINGS_S3_REGION) {
1206
+ config.s3_region = process.env.RECORDINGS_S3_REGION;
1207
+ } else if (process.env.AWS_REGION) {
1208
+ config.s3_region = process.env.AWS_REGION;
1209
+ }
1189
1210
  if (process.env.RECORDINGS_MAX_SECONDS) {
1190
1211
  config.max_recording_seconds = parseInt(process.env.RECORDINGS_MAX_SECONDS, 10);
1191
1212
  }
1192
- if (!config.openai_api_key) {
1193
- config.openai_api_key = loadSecretKey("OPENAI_API_KEY");
1194
- }
1195
1213
  if (!config.enhancement_api_key) {
1196
- config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
1214
+ config.enhancement_api_key = config.openai_api_key;
1197
1215
  }
1198
1216
  if (!explicitTranscriberModel) {
1199
1217
  config.transcriber_model = config.enhancement_model;
@@ -1373,51 +1391,6 @@ function mergeDirectoryContents(sourceDir, targetDir) {
1373
1391
  }
1374
1392
  }
1375
1393
  }
1376
- function loadSecretKey(keyName) {
1377
- const secretsPath = join3(getHomeDir(), ".secrets");
1378
- if (!existsSync2(secretsPath))
1379
- return "";
1380
- for (const candidate of listSecretFiles(secretsPath)) {
1381
- try {
1382
- const content = readFileSync(candidate, "utf-8");
1383
- const match = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`));
1384
- const doubleQuotedValue = match?.[1];
1385
- if (doubleQuotedValue !== undefined)
1386
- return doubleQuotedValue;
1387
- const match2 = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`));
1388
- const singleQuotedValue = match2?.[1];
1389
- if (singleQuotedValue !== undefined)
1390
- return singleQuotedValue;
1391
- const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
1392
- const unquotedValue = match3?.[1];
1393
- if (unquotedValue !== undefined)
1394
- return unquotedValue.trim().replace(/^["']|["']$/g, "");
1395
- } catch {}
1396
- }
1397
- return "";
1398
- }
1399
- function listSecretFiles(path) {
1400
- try {
1401
- const stats = statSync(path);
1402
- if (stats.isFile())
1403
- return [path];
1404
- if (!stats.isDirectory())
1405
- return [];
1406
- return readdirSync(path).sort().flatMap((entry) => {
1407
- const child = join3(path, entry);
1408
- try {
1409
- const childStats = statSync(child);
1410
- if (childStats.isDirectory())
1411
- return listSecretFiles(child);
1412
- if (childStats.isFile() && child.endsWith(".env"))
1413
- return [child];
1414
- } catch {}
1415
- return [];
1416
- });
1417
- } catch {
1418
- return [];
1419
- }
1420
- }
1421
1394
  function getHomeDir() {
1422
1395
  return process.env["HOME"] || process.env["USERPROFILE"] || homedir3();
1423
1396
  }
@@ -1547,6 +1520,11 @@ var MIGRATIONS = [
1547
1520
  `,
1548
1521
  `
1549
1522
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
1523
+ `,
1524
+ `
1525
+ ALTER TABLE recordings ADD COLUMN audio_object_key TEXT;
1526
+ ALTER TABLE recordings ADD COLUMN audio_sha256 TEXT;
1527
+ ALTER TABLE recordings ADD COLUMN audio_bytes INTEGER;
1550
1528
  `
1551
1529
  ];
1552
1530
  var CURRENT_MIGRATION_LEVEL = MIGRATIONS.length - 1;
@@ -1602,6 +1580,9 @@ function repairSchemaDrift(db) {
1602
1580
  ensureColumn(db, "recordings", "task_list_id", "TEXT");
1603
1581
  ensureColumn(db, "recordings", "machine_id", "TEXT");
1604
1582
  ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
1583
+ ensureColumn(db, "recordings", "audio_object_key", "TEXT");
1584
+ ensureColumn(db, "recordings", "audio_sha256", "TEXT");
1585
+ ensureColumn(db, "recordings", "audio_bytes", "INTEGER");
1605
1586
  ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
1606
1587
  }
1607
1588
  function ensureColumn(db, table, column, definition) {
@@ -1677,6 +1658,9 @@ function parseRow(row) {
1677
1658
  return {
1678
1659
  id: row["id"],
1679
1660
  audio_path: row["audio_path"] || null,
1661
+ audio_object_key: row["audio_object_key"] || null,
1662
+ audio_sha256: row["audio_sha256"] || null,
1663
+ audio_bytes: Number(row["audio_bytes"] ?? 0) || null,
1680
1664
  raw_text: row["raw_text"],
1681
1665
  processed_text: row["processed_text"] || null,
1682
1666
  processing_mode: row["processing_mode"] || "raw",
@@ -1708,9 +1692,9 @@ function createRecording(input, db, idempotencyKey) {
1708
1692
  }
1709
1693
  const tagsJson = JSON.stringify(input.tags || []);
1710
1694
  const metadataJson = JSON.stringify(input.metadata || {});
1711
- const insertResult = d.query(`INSERT INTO recordings (id, audio_path, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
1712
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1713
- ON CONFLICT(id) DO NOTHING`).run(id, input.audio_path || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, tagsJson, input.agent_id || null, input.project_id || null, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, metadataJson);
1695
+ const insertResult = d.query(`INSERT INTO recordings (id, audio_path, audio_object_key, audio_sha256, audio_bytes, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
1696
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1697
+ ON CONFLICT(id) DO NOTHING`).run(id, input.audio_path || null, input.audio_object_key || null, input.audio_sha256 || null, input.audio_bytes || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, tagsJson, input.agent_id || null, input.project_id || null, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, metadataJson);
1714
1698
  if (insertResult.changes === 0) {
1715
1699
  const existing = getRecording(id, d);
1716
1700
  if (existing?.id === id)
@@ -1727,6 +1711,13 @@ function createRecording(input, db, idempotencyKey) {
1727
1711
  });
1728
1712
  return create();
1729
1713
  }
1714
+ function updateRecordingAudio(id, audioObjectKey, audioSha256, audioBytes, db) {
1715
+ const d = db || getDatabase();
1716
+ const result = d.query(`UPDATE recordings SET audio_object_key = ?, audio_sha256 = ?, audio_bytes = ? WHERE id = ?`).run(audioObjectKey, audioSha256, audioBytes, id);
1717
+ if (result.changes === 0)
1718
+ return null;
1719
+ return getRecording(id, d);
1720
+ }
1730
1721
  function getRecording(id, db) {
1731
1722
  const d = db || getDatabase();
1732
1723
  let row = d.query("SELECT * FROM recordings WHERE id = ?").get(id);
@@ -1928,7 +1919,7 @@ function setAgentFocus(idOrName, projectId, db) {
1928
1919
  // package.json
1929
1920
  var package_default = {
1930
1921
  name: "@hasna/recordings",
1931
- version: "0.3.13",
1922
+ version: "0.5.0",
1932
1923
  type: "module",
1933
1924
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
1934
1925
  repository: {
@@ -1954,6 +1945,19 @@ var package_default = {
1954
1945
  "./sdk": {
1955
1946
  import: "./dist/sdk/index.js",
1956
1947
  types: "./dist/sdk/index.d.ts"
1948
+ },
1949
+ "./contracts/hosted-v1": {
1950
+ import: "./dist/contracts/hosted-v1.js",
1951
+ types: "./dist/contracts/hosted-v1.d.ts"
1952
+ },
1953
+ "./contracts/stream-v1": {
1954
+ import: "./dist/contracts/stream-v1.js",
1955
+ types: "./dist/contracts/stream-v1.d.ts"
1956
+ },
1957
+ "./contracts/fixtures/v1": "./contracts/v1/fixtures.json",
1958
+ "./hosted": {
1959
+ import: "./dist/hosted/index.js",
1960
+ types: "./dist/hosted/index.d.ts"
1957
1961
  }
1958
1962
  },
1959
1963
  engines: {
@@ -1962,20 +1966,20 @@ var package_default = {
1962
1966
  scripts: {
1963
1967
  clean: "rm -rf dist",
1964
1968
  build: "bun run clean && bun run build:cli && bun run build:mcp && bun run build:serve && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
1965
- "build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
1966
- "build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
1967
- "build:serve": "bun build src/server/index.ts --target=bun --outfile=dist/server/index.js --external=@modelcontextprotocol/sdk --external=@hasna/contracts --external=openai --external=pg",
1968
- "build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai",
1969
+ "build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai --external=@aws-sdk/client-s3",
1970
+ "build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai --external=@aws-sdk/client-s3",
1971
+ "build:serve": "bun build src/server/index.ts --target=bun --outfile=dist/server/index.js --external=@modelcontextprotocol/sdk --external=@hasna/contracts --external=openai --external=pg --external=@aws-sdk/client-s3",
1972
+ "build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai --external=@aws-sdk/client-s3 && bun run build:contracts",
1969
1973
  "build:native-fs-guard": "/bin/bash scripts/build_native_fs_guard.sh",
1970
1974
  "generate:sdk": "bun run scripts/generate-sdk.ts",
1971
1975
  migrate: "bun run scripts/migrate.ts",
1972
1976
  typecheck: "tsc --noEmit",
1973
1977
  "typecheck:tcc-contract": "tsc --noEmit -p tsconfig.tcc-contract.json",
1974
- test: "bun test",
1978
+ test: "bun scripts/release-suite-gate.ts --all",
1975
1979
  "verify:ci-suite": "bun scripts/ci-linux-suite.ts --check",
1976
1980
  "verify:ci-suite:run": "bun scripts/ci-linux-suite.ts --verify-run",
1977
1981
  "verify:ci-native": "bun scripts/ci-native-build.ts",
1978
- "test:gated": 'bun scripts/ci-linux-suite.ts --check && RECORDINGS_TEST_TIMEOUT_MS="${RECORDINGS_TEST_TIMEOUT_MS:-120000}" bun test --timeout "$RECORDINGS_TEST_TIMEOUT_MS" $(bun scripts/ci-linux-suite.ts --gated)',
1982
+ "test:gated": "bun scripts/release-suite-gate.ts",
1979
1983
  "test:vacuity-battery": 'bun scripts/vacuity-manifest-gen.ts > "${TMPDIR:-/tmp}/vacuity-corrupt-sites.tsv" && bun scripts/vacuity-mutation-battery.ts "${TMPDIR:-/tmp}/vacuity-corrupt-sites.tsv"',
1980
1984
  "test:vacuity-battery:source": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/source-side.tsv",
1981
1985
  "test:vacuity-battery:chain": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/install-chain.tsv",
@@ -1993,11 +1997,16 @@ var package_default = {
1993
1997
  "prepack:platform-gate": `bash -c 'if [ "$(uname -s)" = Darwin ]; then bun run build:native-fs-guard; else echo "WARN: native fs-guard build skipped on $(uname -s) \u2014 dry-run/CI pack; publish recordings from macOS"; fi'`,
1994
1998
  prepublishOnly: "bun run typecheck && bun run release-suite-gate",
1995
1999
  "release-suite-gate": "bun run scripts/release-suite-gate.ts",
1996
- "typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json"
2000
+ "typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json",
2001
+ "build:contracts": "bun build src/contracts/hosted-v1.ts src/contracts/stream-v1.ts src/hosted/index.ts --target=bun --root src --outdir=dist --splitting --external=@hasna/contracts"
1997
2002
  },
1998
2003
  files: [
1999
2004
  "dist/",
2005
+ "!dist/__tests__/",
2000
2006
  "scripts/",
2007
+ "!scripts/test-recorder-fixtures.ts",
2008
+ "!scripts/**/__pycache__/",
2009
+ "!scripts/**/*.pyc",
2001
2010
  "Dockerfile.package",
2002
2011
  "bun.lock",
2003
2012
  "src/native/Recordings/App/",
@@ -2009,10 +2018,14 @@ var package_default = {
2009
2018
  "src/native/Recordings/build.sh",
2010
2019
  "packaging/macos/",
2011
2020
  "README.md",
2012
- "LICENSE"
2021
+ "LICENSE",
2022
+ "contracts/v1/fixtures.json",
2023
+ "docs/wire-contracts.md",
2024
+ "docs/hosted-sdk.md"
2013
2025
  ],
2014
2026
  dependencies: {
2015
- "@hasna/contracts": "0.14.1",
2027
+ "@aws-sdk/client-s3": "^3.1007.0",
2028
+ "@hasna/contracts": "1.0.2",
2016
2029
  "@hasna/events": "0.1.11",
2017
2030
  "@modelcontextprotocol/sdk": "^1.12.1",
2018
2031
  chalk: "^5.4.1",
@@ -2022,7 +2035,7 @@ var package_default = {
2022
2035
  zod: "^3.24.2"
2023
2036
  },
2024
2037
  devDependencies: {
2025
- "@types/bun": "^1.2.5",
2038
+ "@types/bun": "1.3.14",
2026
2039
  "@types/pg": "^8.11.11",
2027
2040
  "node-api-headers": "1.9.0",
2028
2041
  typescript: "^5.8.2"
@@ -2046,8 +2059,12 @@ function saveFeedback(input) {
2046
2059
 
2047
2060
  // ../contracts/dist/client/transport.js
2048
2061
  import { isIP } from "net";
2049
- import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
2050
- import { join as join4 } from "path";
2062
+ import { spawnSync as spawnSync2 } from "child_process";
2063
+ import { closeSync, fstatSync, openSync, readFileSync as readFileSync2 } from "fs";
2064
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
2065
+ import { createRequire } from "module";
2066
+ import { hostname as osHostname } from "os";
2067
+ import { isAbsolute as isAbsolute2, join as join4 } from "path";
2051
2068
  function envToken(name) {
2052
2069
  return name.toUpperCase().replace(/-/g, "_");
2053
2070
  }
@@ -2062,6 +2079,9 @@ function credentialOverrideEnvKey(name) {
2062
2079
  return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
2063
2080
  }
2064
2081
  var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
2082
+ function credentialPointerEnvKey(name) {
2083
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
2084
+ }
2065
2085
 
2066
2086
  class CredentialResolutionError extends Error {
2067
2087
  appName;
@@ -2073,34 +2093,70 @@ class CredentialResolutionError extends Error {
2073
2093
  this.attempted = attempted;
2074
2094
  }
2075
2095
  }
2076
- var HASNA_STATE_DIR = ".hasna";
2077
- var FLEET_CREDENTIAL_DIR = "cloud";
2078
- var CONFIG_DIR = ".config";
2079
- var CONFIG_NAMESPACE = "hasna";
2096
+
2097
+ class CredentialFileUnsafeError extends Error {
2098
+ path;
2099
+ constructor(path, reason) {
2100
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
2101
+ this.name = "CredentialFileUnsafeError";
2102
+ this.path = path;
2103
+ }
2104
+ }
2105
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
2106
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
2107
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
2108
+ var HASNA_HOME_DIR = ".hasna";
2109
+ var CONFIG_SUBDIR = "config";
2110
+ var CREDENTIALS_FILE = "credentials";
2111
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
2112
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
2113
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
2114
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
2080
2115
  var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
2081
2116
  var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
2082
2117
  var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
2083
2118
  var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
2119
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
2084
2120
  function homeDir(env) {
2085
2121
  const home = env.HOME?.trim();
2086
2122
  return home ? home : null;
2087
2123
  }
2088
- function credentialDiskSources(name, env) {
2089
- return profileDiskSources(name, env, null);
2124
+ function absoluteOverride(env, key) {
2125
+ const value = env[key]?.trim();
2126
+ return value && isAbsolute2(value) ? value : null;
2090
2127
  }
2091
- function profileDiskSources(name, env, profile) {
2128
+ function hasnaHomeDir(env) {
2129
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
2130
+ if (override)
2131
+ return override;
2092
2132
  const home = homeDir(env);
2093
- if (!home || !SAFE_APP_SLUG.test(name))
2133
+ return home ? join4(home, HASNA_HOME_DIR) : null;
2134
+ }
2135
+ function appConfigDir(name, env) {
2136
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
2137
+ if (configRoot)
2138
+ return join4(configRoot, name);
2139
+ const root = hasnaHomeDir(env);
2140
+ return root ? join4(root, name, CONFIG_SUBDIR) : null;
2141
+ }
2142
+ function credentialDiskSourceList(name, env, profile = null) {
2143
+ if (!SAFE_APP_SLUG.test(name))
2094
2144
  return [];
2095
- const stem = profile ? `${name}.${profile}` : name;
2096
- const configStem = profile ? `${name}-${profile}` : name;
2097
- return [
2098
- join4(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
2099
- join4(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
2100
- ];
2145
+ const directory = appConfigDir(name, env);
2146
+ if (!directory)
2147
+ return [];
2148
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
2149
+ return [{ path: join4(directory, file), tier: "disk" }];
2150
+ }
2151
+ function credentialDiskSources(name, env) {
2152
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
2153
+ }
2154
+ function profileDiskSources(name, env, profile) {
2155
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
2101
2156
  }
2102
2157
  function parseEnvFile(text) {
2103
2158
  const values = new Map;
2159
+ const unusable = new Set;
2104
2160
  for (const rawLine of text.split(/\r?\n/)) {
2105
2161
  const line = rawLine.trim();
2106
2162
  if (line.length === 0 || line.startsWith("#"))
@@ -2115,38 +2171,81 @@ function parseEnvFile(text) {
2115
2171
  let value = withoutExport.slice(equals + 1).trim();
2116
2172
  const quote = value[0];
2117
2173
  if (quote === '"' || quote === "'") {
2118
- if (value.length < 2 || !value.endsWith(quote))
2174
+ if (value.length < 2 || !value.endsWith(quote)) {
2175
+ unusable.add(key);
2119
2176
  continue;
2177
+ }
2120
2178
  value = value.slice(1, -1);
2121
2179
  }
2122
- if (value.length === 0)
2180
+ if (value.trim().length === 0) {
2181
+ unusable.add(key);
2123
2182
  continue;
2183
+ }
2184
+ if (values.has(key) && values.get(key) !== value)
2185
+ unusable.add(key);
2124
2186
  values.set(key, value);
2125
2187
  }
2126
- return values;
2188
+ return { values, unusable };
2189
+ }
2190
+ function configFileModeAllowed(mode) {
2191
+ const permissions = mode & 4095;
2192
+ return permissions === 256 || permissions === 384;
2193
+ }
2194
+ function configFileReadsCoherent(before, after) {
2195
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
2127
2196
  }
2128
2197
  function readAppConfigFile(path) {
2129
- let text;
2198
+ const unsafe = (reason) => {
2199
+ throw new CredentialFileUnsafeError(path, reason);
2200
+ };
2201
+ let fd = -1;
2130
2202
  try {
2131
- const stats = statSync2(path);
2132
- if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
2203
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
2204
+ } catch (error) {
2205
+ const code = error.code;
2206
+ if (code === "ENOENT" || code === "ENOTDIR")
2133
2207
  return null;
2134
- text = readFileSync2(path, "utf8");
2135
- } catch {
2136
- return null;
2208
+ if (code === "ELOOP")
2209
+ unsafe("the path is a symlink");
2210
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
2211
+ }
2212
+ try {
2213
+ const before = fstatSync(fd);
2214
+ if (!before.isFile())
2215
+ unsafe("the path is not a regular file");
2216
+ if (!configFileModeAllowed(before.mode)) {
2217
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
2218
+ }
2219
+ const uid = process.getuid?.() ?? process.geteuid?.();
2220
+ if (uid !== undefined && before.uid !== uid)
2221
+ unsafe("the file is not owned by the current user");
2222
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
2223
+ unsafe("the file exceeds the size limit");
2224
+ const bytes = readFileSync2(fd);
2225
+ const after = fstatSync(fd);
2226
+ if (!configFileReadsCoherent(before, after)) {
2227
+ unsafe("the file changed while being read");
2228
+ }
2229
+ return parseEnvFile(bytes.toString("utf8"));
2230
+ } finally {
2231
+ if (fd !== -1)
2232
+ closeSync(fd);
2137
2233
  }
2138
- return parseEnvFile(text);
2139
2234
  }
2140
2235
  function readCredentialFile(path, apiKeyKeys) {
2141
- const values = readAppConfigFile(path);
2142
- if (!values)
2236
+ const parsed = readAppConfigFile(path);
2237
+ if (!parsed)
2143
2238
  return null;
2144
2239
  for (const key of apiKeyKeys) {
2145
- const value = values.get(key)?.trim();
2146
- if (value)
2147
- return value;
2240
+ if (parsed.unusable.has(key)) {
2241
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
2242
+ }
2148
2243
  }
2149
- return null;
2244
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
2245
+ if (new Set(values).size > 1) {
2246
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
2247
+ }
2248
+ return values[0] ?? null;
2150
2249
  }
2151
2250
  var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
2152
2251
  function appConfigDiskValue(name, env, keys) {
@@ -2154,11 +2253,19 @@ function appConfigDiskValue(name, env, keys) {
2154
2253
  if (wanted.length === 0)
2155
2254
  return null;
2156
2255
  for (const path of credentialDiskSources(name, env)) {
2157
- const values = readAppConfigFile(path);
2158
- if (!values)
2256
+ const parsed = readAppConfigFile(path);
2257
+ if (!parsed)
2159
2258
  continue;
2259
+ if (wanted.some((key) => parsed.unusable.has(key))) {
2260
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
2261
+ }
2262
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
2263
+ if (new Set(values).size > 1)
2264
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
2160
2265
  for (const key of wanted) {
2161
- const value = values.get(key)?.trim();
2266
+ if (parsed.unusable.has(key))
2267
+ return { key, value: "", path, unusable: true };
2268
+ const value = parsed.values.get(key)?.trim();
2162
2269
  if (value)
2163
2270
  return { key, value, path };
2164
2271
  }
@@ -2166,20 +2273,21 @@ function appConfigDiskValue(name, env, keys) {
2166
2273
  return null;
2167
2274
  }
2168
2275
  function assertUsableCredential(appName, source, value) {
2276
+ if (VAULT_POINTER_SHAPE.test(value)) {
2277
+ throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
2278
+ }
2169
2279
  if (!ILLEGAL_IN_HEADER_VALUE.test(value))
2170
2280
  return;
2171
2281
  throw new CredentialResolutionError(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
2172
2282
  }
2173
2283
  var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
2174
2284
  var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
2175
- var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
2176
2285
  function sealCredential(fields) {
2177
2286
  const { apiKey } = fields;
2178
2287
  const visible = {
2179
2288
  tier: fields.tier,
2180
2289
  source: fields.source,
2181
2290
  deliberate: fields.deliberate,
2182
- deprecated: fields.deprecated,
2183
2291
  diskCandidates: Object.freeze([...fields.diskCandidates]),
2184
2292
  warning: fields.warning
2185
2293
  };
@@ -2190,6 +2298,14 @@ function sealCredential(fields) {
2190
2298
  writable: false,
2191
2299
  configurable: false
2192
2300
  });
2301
+ if (fields.pointerVaultKey !== undefined) {
2302
+ Object.defineProperty(sealed, "pointerVaultKey", {
2303
+ value: fields.pointerVaultKey,
2304
+ enumerable: false,
2305
+ writable: false,
2306
+ configurable: false
2307
+ });
2308
+ }
2193
2309
  Object.defineProperty(sealed, INSPECT_CUSTOM, {
2194
2310
  value: () => ({ ...visible, apiKey: "[redacted]" }),
2195
2311
  enumerable: false,
@@ -2204,88 +2320,142 @@ function sealCredential(fields) {
2204
2320
  });
2205
2321
  return Object.freeze(sealed);
2206
2322
  }
2207
- function isSealedCredential(credential) {
2208
- return credential[CREDENTIAL_SEAL] === true;
2209
- }
2210
- function explicitCredential(appName, apiKey) {
2211
- const source = "explicit apiKey option";
2212
- assertUsableCredential(appName, source, apiKey);
2213
- return sealCredential({
2214
- apiKey,
2215
- tier: "argument",
2216
- source,
2217
- deliberate: true,
2218
- deprecated: false,
2219
- diskCandidates: [],
2220
- warning: null
2221
- });
2222
- }
2223
- function validateAndSealResolvedCredential(appName, credential) {
2224
- const apiKey = credential.apiKey;
2225
- assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
2226
- if (!isSealedCredential(credential)) {
2227
- return sealCredential({
2228
- apiKey,
2229
- tier: "argument",
2230
- source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
2231
- deliberate: true,
2232
- deprecated: false,
2233
- diskCandidates: [],
2234
- warning: null
2235
- });
2236
- }
2237
- return sealCredential({
2238
- apiKey,
2239
- tier: credential.tier,
2240
- source: credential.source,
2241
- deliberate: credential.deliberate,
2242
- deprecated: credential.deprecated,
2243
- diskCandidates: credential.diskCandidates,
2244
- warning: credential.warning
2245
- });
2246
- }
2247
2323
  function firstEnvValue(env, keys) {
2248
2324
  for (const key of keys) {
2325
+ if (!Object.prototype.hasOwnProperty.call(env, key))
2326
+ continue;
2249
2327
  const value = env[key]?.trim();
2250
2328
  if (value)
2251
2329
  return { key, value };
2252
2330
  }
2253
2331
  return null;
2254
2332
  }
2255
- var DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
2256
- function deprecationNotified() {
2257
- const host = globalThis;
2258
- const existing = host[DEPRECATION_REGISTRY];
2259
- if (existing instanceof Set)
2260
- return existing;
2261
- const created = new Set;
2262
- host[DEPRECATION_REGISTRY] = created;
2263
- return created;
2264
- }
2265
- function defaultDeprecationSink(message) {
2266
- if (typeof process !== "undefined" && process.stderr) {
2267
- process.stderr.write(`${message}
2268
- `);
2333
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
2334
+ function isAmbientEnvironment(env) {
2335
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
2336
+ }
2337
+ function defaultKeychainRunner(argv) {
2338
+ const result = spawnSync2(KEYCHAIN_SECURITY_BIN, [...argv], {
2339
+ encoding: "utf8",
2340
+ stdio: ["ignore", "pipe", "pipe"],
2341
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
2342
+ });
2343
+ return {
2344
+ status: result.status,
2345
+ stdout: result.stdout ?? "",
2346
+ stderr: result.error ? result.error.message : result.stderr ?? ""
2347
+ };
2348
+ }
2349
+ function keychainTierEnabled(env, options) {
2350
+ if ((options.platform ?? process.platform) !== "darwin")
2351
+ return false;
2352
+ if (options.enabled !== undefined)
2353
+ return options.enabled;
2354
+ return options.run !== undefined || isAmbientEnvironment(env);
2355
+ }
2356
+ function keychainAccount(env, options) {
2357
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
2358
+ if (station)
2359
+ return station;
2360
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
2361
+ if (host)
2362
+ return host;
2363
+ const user = env.USER?.trim();
2364
+ return user || null;
2365
+ }
2366
+ function keychainFailureHint(text) {
2367
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
2368
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
2369
+ return clean ? `: ${clean}` : "";
2370
+ }
2371
+ function readKeychainItem(name, env, kind, options) {
2372
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
2373
+ return null;
2374
+ const account = keychainAccount(env, options);
2375
+ if (!account)
2376
+ return null;
2377
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
2378
+ const source = `keychain:${service}@${account}`;
2379
+ const run = options.run ?? defaultKeychainRunner;
2380
+ let result;
2381
+ try {
2382
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
2383
+ } catch (error) {
2384
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
2385
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
2269
2386
  }
2387
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
2388
+ return null;
2389
+ if (result.status !== 0) {
2390
+ throw new CredentialResolutionError(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
2391
+ }
2392
+ const value = result.stdout.trim();
2393
+ if (!value) {
2394
+ throw new CredentialResolutionError(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
2395
+ }
2396
+ return { value, source };
2397
+ }
2398
+ function keychainConfigValue(name, env, options = {}) {
2399
+ return readKeychainItem(name, env, "api-url", options);
2400
+ }
2401
+ function snapshotClientEnvironment(name, env) {
2402
+ const keys = clientTransportEnvKeys(name);
2403
+ const ambient = isAmbientEnvironment(env);
2404
+ const snapshot = Object.create(null);
2405
+ for (const key of [
2406
+ ...keys.apiUrlKeys,
2407
+ ...keys.apiKeyKeys,
2408
+ credentialOverrideEnvKey(name),
2409
+ credentialPointerEnvKey(name),
2410
+ CREDENTIAL_PROFILE_ENV_KEY,
2411
+ "HOME",
2412
+ HASNA_HOME_ENV_KEY,
2413
+ HASNA_CONFIG_HOME_ENV_KEY,
2414
+ KEYCHAIN_STATION_ENV_KEY,
2415
+ "USER"
2416
+ ]) {
2417
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
2418
+ if (!descriptor)
2419
+ continue;
2420
+ if (!("value" in descriptor)) {
2421
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
2422
+ }
2423
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
2424
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
2425
+ }
2426
+ snapshot[key] = descriptor.value;
2427
+ }
2428
+ if (ambient) {
2429
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
2430
+ value: true,
2431
+ enumerable: false,
2432
+ writable: false,
2433
+ configurable: false
2434
+ });
2435
+ }
2436
+ return Object.freeze(snapshot);
2270
2437
  }
2271
2438
  function resolveCredential(name, env, options = {}) {
2439
+ env = snapshotClientEnvironment(name, env);
2272
2440
  const { apiKeyKeys } = clientTransportEnvKeys(name);
2273
2441
  const diskPaths = credentialDiskSources(name, env);
2274
- const explicitKey = options.apiKey?.trim();
2275
- if (explicitKey) {
2442
+ if (options.apiKey !== undefined) {
2443
+ const explicitKey = options.apiKey.trim();
2444
+ if (!explicitKey) {
2445
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
2446
+ }
2276
2447
  assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
2277
2448
  return sealCredential({
2278
2449
  apiKey: explicitKey,
2279
2450
  tier: "argument",
2280
2451
  source: "explicit apiKey argument",
2281
2452
  deliberate: true,
2282
- deprecated: false,
2283
2453
  diskCandidates: diskPaths,
2284
2454
  warning: null
2285
2455
  });
2286
2456
  }
2287
2457
  const overrideKeyName = credentialOverrideEnvKey(name);
2288
- const overrideRaw = env[overrideKeyName];
2458
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
2289
2459
  if (overrideRaw !== undefined) {
2290
2460
  const override = overrideRaw.trim();
2291
2461
  if (!override) {
@@ -2297,12 +2467,38 @@ function resolveCredential(name, env, options = {}) {
2297
2467
  tier: "override",
2298
2468
  source: overrideKeyName,
2299
2469
  deliberate: true,
2300
- deprecated: false,
2301
2470
  diskCandidates: diskPaths,
2302
2471
  warning: null
2303
2472
  });
2304
2473
  }
2305
- const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
2474
+ const pointerKeyName = credentialPointerEnvKey(name);
2475
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
2476
+ if (pointerRaw !== undefined) {
2477
+ const pointer = pointerRaw.trim();
2478
+ if (!pointer) {
2479
+ throw new CredentialResolutionError(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
2480
+ }
2481
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
2482
+ throw new CredentialResolutionError(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
2483
+ }
2484
+ return sealCredential({
2485
+ apiKey: "",
2486
+ pointerVaultKey: pointer,
2487
+ tier: "pointer",
2488
+ source: pointerKeyName,
2489
+ deliberate: true,
2490
+ diskCandidates: diskPaths,
2491
+ warning: null
2492
+ });
2493
+ }
2494
+ if (options.profile !== undefined && !options.profile.trim()) {
2495
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
2496
+ }
2497
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
2498
+ if (profileRaw !== undefined && !profileRaw.trim()) {
2499
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
2500
+ }
2501
+ const profile = options.profile?.trim() || profileRaw?.trim();
2306
2502
  if (profile) {
2307
2503
  const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
2308
2504
  if (!SAFE_PROFILE.test(profile)) {
@@ -2318,7 +2514,6 @@ function resolveCredential(name, env, options = {}) {
2318
2514
  tier: "profile",
2319
2515
  source: path,
2320
2516
  deliberate: true,
2321
- deprecated: false,
2322
2517
  diskCandidates: paths,
2323
2518
  warning: null
2324
2519
  });
@@ -2326,51 +2521,67 @@ function resolveCredential(name, env, options = {}) {
2326
2521
  }
2327
2522
  throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
2328
2523
  }
2329
- const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
2524
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
2525
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
2526
+ if (blankEnv) {
2527
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
2528
+ }
2529
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
2530
+ throw new CredentialResolutionError(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
2531
+ }
2532
+ const envHit = firstEnvValue(env, apiKeyKeys);
2533
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
2534
+ if (keychainHit) {
2535
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
2536
+ const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
2537
+ return sealCredential({
2538
+ apiKey: keychainHit.value,
2539
+ tier: "keychain",
2540
+ source: keychainHit.source,
2541
+ deliberate: false,
2542
+ diskCandidates: diskPaths,
2543
+ warning
2544
+ });
2545
+ }
2546
+ const diskSourceList = credentialDiskSourceList(name, env, null);
2547
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
2330
2548
  if (diskHits.length > 0) {
2331
2549
  const winner = diskHits[0];
2332
- assertUsableCredential(name, winner.path, winner.value);
2550
+ assertUsableCredential(name, winner.src.path, winner.value);
2333
2551
  const divergentSources = [
2334
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
2335
- ...(() => {
2336
- const legacyHit = firstEnvValue(env, apiKeyKeys);
2337
- return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
2338
- })()
2552
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
2553
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
2339
2554
  ];
2340
- const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
2555
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
2341
2556
  return sealCredential({
2342
2557
  apiKey: winner.value,
2343
- tier: "disk",
2344
- source: winner.path,
2558
+ tier: winner.src.tier,
2559
+ source: winner.src.path,
2345
2560
  deliberate: false,
2346
- deprecated: false,
2347
2561
  diskCandidates: diskPaths,
2348
2562
  warning
2349
2563
  });
2350
2564
  }
2351
- const legacy = firstEnvValue(env, apiKeyKeys);
2352
- if (legacy) {
2353
- assertUsableCredential(name, legacy.key, legacy.value);
2354
- const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
2355
- const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
2356
- const sink = options.onDeprecation ?? defaultDeprecationSink;
2357
- const notified = deprecationNotified();
2358
- if (!notified.has(name)) {
2359
- notified.add(name);
2360
- sink(message);
2361
- }
2565
+ if (envHit) {
2566
+ assertUsableCredential(name, envHit.key, envHit.value);
2362
2567
  return sealCredential({
2363
- apiKey: legacy.value,
2364
- tier: "legacy-env",
2365
- source: legacy.key,
2568
+ apiKey: envHit.value,
2569
+ tier: "env",
2570
+ source: envHit.key,
2366
2571
  deliberate: false,
2367
- deprecated: true,
2368
2572
  diskCandidates: diskPaths,
2369
- warning: message
2573
+ warning: null
2370
2574
  });
2371
2575
  }
2372
2576
  return null;
2373
2577
  }
2578
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
2579
+ var requireSecretsSdk = createRequire(import.meta.url);
2580
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
2581
+ var DEFAULT_AUTHORITY_SOURCE = "default";
2582
+ function defaultFleetGatewayBaseUrl(name) {
2583
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
2584
+ }
2374
2585
  var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
2375
2586
  var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
2376
2587
  function isValidDnsDomain(value) {
@@ -2379,23 +2590,835 @@ function isValidDnsDomain(value) {
2379
2590
  }
2380
2591
  return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
2381
2592
  }
2382
- function firstEnv(env, keys, options = {}) {
2383
- for (const key of keys) {
2384
- const raw = env[key];
2385
- const value = raw?.trim();
2386
- if (value)
2387
- return { key, value: options.preserveRaw ? raw : value };
2593
+ function validateAppSlug(name) {
2594
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
2595
+ throw new Error("App name must be one lowercase DNS label.");
2596
+ }
2597
+ return name;
2598
+ }
2599
+ function rawAuthority(value) {
2600
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
2601
+ if (!match)
2602
+ throw new Error("API URL must be absolute.");
2603
+ const afterScheme = value.slice(match[0].length);
2604
+ const boundary = afterScheme.search(/[/?#]/);
2605
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
2606
+ if (!authority)
2607
+ throw new Error("API URL must include a hostname.");
2608
+ return authority;
2609
+ }
2610
+ function assertCanonicalPort(port) {
2611
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
2612
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2613
+ }
2614
+ const numericPort = Number(port);
2615
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
2616
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2617
+ }
2618
+ }
2619
+ function canonicalAuthorityHostname(authority) {
2620
+ let rawHostname;
2621
+ if (authority.startsWith("[")) {
2622
+ const closingBracket = authority.indexOf("]");
2623
+ if (closingBracket === -1) {
2624
+ throw new Error("API URL authority must contain a canonical hostname.");
2625
+ }
2626
+ rawHostname = authority.slice(0, closingBracket + 1);
2627
+ const portSuffix = authority.slice(closingBracket + 1);
2628
+ if (portSuffix) {
2629
+ if (!portSuffix.startsWith(":")) {
2630
+ throw new Error("API URL authority must contain a canonical hostname and port.");
2631
+ }
2632
+ assertCanonicalPort(portSuffix.slice(1));
2633
+ }
2634
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
2635
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
2636
+ }
2637
+ } else {
2638
+ const firstColon = authority.indexOf(":");
2639
+ const lastColon = authority.lastIndexOf(":");
2640
+ if (firstColon !== lastColon) {
2641
+ throw new Error("IPv6 API URL authorities must use brackets.");
2642
+ }
2643
+ if (lastColon !== -1) {
2644
+ const port = authority.slice(lastColon + 1);
2645
+ assertCanonicalPort(port);
2646
+ rawHostname = authority.slice(0, lastColon);
2647
+ } else {
2648
+ rawHostname = authority;
2649
+ }
2650
+ const ipVersion = isIP(rawHostname);
2651
+ const numericAddressParts = rawHostname.split(".");
2652
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
2653
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
2654
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
2655
+ }
2656
+ }
2657
+ return rawHostname.toLowerCase();
2658
+ }
2659
+ function isDeliberateLoopbackHttpAuthority(authority) {
2660
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
2661
+ }
2662
+ function toV1BaseUrl(apiUrl) {
2663
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
2664
+ throw new Error("API URL must not contain ASCII control characters.");
2665
+ }
2666
+ const input = apiUrl.trim();
2667
+ const authority = rawAuthority(input);
2668
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
2669
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
2670
+ }
2671
+ const canonicalHostname = canonicalAuthorityHostname(authority);
2672
+ const url = new URL(input);
2673
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2674
+ throw new Error("API URL must use http or https.");
2675
+ }
2676
+ if (url.username || url.password) {
2677
+ throw new Error("API URL must not include credentials.");
2678
+ }
2679
+ if (!url.hostname || url.hostname.endsWith(".")) {
2680
+ throw new Error("API URL must include a canonical hostname.");
2681
+ }
2682
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
2683
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
2684
+ }
2685
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
2686
+ throw new Error("API URL must not use IDN or punycode hostnames.");
2687
+ }
2688
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
2689
+ throw new Error("API URL may use http only for an exact loopback authority.");
2690
+ }
2691
+ if (url.search || url.hash) {
2692
+ throw new Error("API URL must not include a query string or fragment.");
2693
+ }
2694
+ let path = url.pathname.replace(/\/+$/, "");
2695
+ if (path.endsWith("/v1"))
2696
+ path = path.slice(0, -"/v1".length);
2697
+ url.pathname = `${path}/v1`;
2698
+ return url.toString().replace(/\/+$/, "");
2699
+ }
2700
+ class ClientTransportConfigurationError extends Error {
2701
+ appName;
2702
+ sources;
2703
+ constructor(appName, message, sources = []) {
2704
+ super(message);
2705
+ this.name = "ClientTransportConfigurationError";
2706
+ this.appName = appName;
2707
+ this.sources = Object.freeze([...sources]);
2708
+ }
2709
+ }
2710
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
2711
+ env = snapshotClientEnvironment(name, env);
2712
+ const keys = clientTransportEnvKeys(name);
2713
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
2714
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
2715
+ if (blankUrl) {
2716
+ throw new ClientTransportConfigurationError(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
2717
+ }
2718
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
2719
+ if (controlledUrl) {
2720
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
2721
+ }
2722
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
2723
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
2724
+ throw new ClientTransportConfigurationError(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
2725
+ }
2726
+ const envUrlHit = usableUrlEntries[0] ?? null;
2727
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
2728
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
2729
+ if (diskConfigUrlHit?.unusable) {
2730
+ throw new ClientTransportConfigurationError(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
2731
+ }
2732
+ const urlCandidates = [
2733
+ ...envUrlHit ? [envUrlHit] : [],
2734
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
2735
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
2736
+ ];
2737
+ const configuredUrl = urlCandidates[0] ?? null;
2738
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
2739
+ if (configuredUrl && divergentUrls.length > 0) {
2740
+ throw new ClientTransportConfigurationError(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
2741
+ }
2742
+ const warnings = [];
2743
+ if (configuredUrl && !envUrlHit) {
2744
+ warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
2745
+ }
2746
+ const credential = resolveCredential(name, env, options.credentials);
2747
+ if (!credential) {
2748
+ const diskHint = credentialDiskSourcesForMessage(name, env);
2749
+ const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
2750
+ warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
2751
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
2752
+ }
2753
+ if (credential.warning)
2754
+ warnings.push(credential.warning);
2755
+ let urlHit;
2756
+ if (configuredUrl) {
2757
+ urlHit = configuredUrl;
2758
+ } else {
2759
+ try {
2760
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
2761
+ } catch (error) {
2762
+ const message = error instanceof Error ? error.message : String(error);
2763
+ throw new ClientTransportConfigurationError(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
2764
+ }
2765
+ }
2766
+ const apiUrlSource = urlHit.key;
2767
+ let baseUrl;
2768
+ try {
2769
+ baseUrl = toV1BaseUrl(urlHit.value);
2770
+ } catch (error) {
2771
+ const message = error instanceof Error ? error.message : String(error);
2772
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
2773
+ }
2774
+ return {
2775
+ resolution: {
2776
+ transport: "http",
2777
+ transportSource: urlHit.key,
2778
+ baseUrl,
2779
+ apiUrlSource,
2780
+ apiKeyPresent: true,
2781
+ apiKeySource: credential.source,
2782
+ apiKeyTier: credential.tier,
2783
+ misconfigured: false,
2784
+ warning: warnings.length > 0 ? warnings.join(" ") : null
2785
+ },
2786
+ credential
2787
+ };
2788
+ }
2789
+ function resolveClientTransport(name, env = process.env, options = {}) {
2790
+ return resolveClientTransportSnapshot(name, env, options).resolution;
2791
+ }
2792
+ function credentialDiskSourcesForMessage(name, env) {
2793
+ const paths = credentialDiskSources(name, env);
2794
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
2795
+ }
2796
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
2797
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
2798
+ "host",
2799
+ ":authority",
2800
+ "forwarded",
2801
+ "x-forwarded-host",
2802
+ "x-original-host"
2803
+ ]);
2804
+
2805
+ // ../contracts/dist/client/storage.js
2806
+ import { isIP as isIP2 } from "net";
2807
+ import { spawnSync as spawnSync3 } from "child_process";
2808
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync3 } from "fs";
2809
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
2810
+ import { createRequire as createRequire2 } from "module";
2811
+ import { hostname as osHostname2 } from "os";
2812
+ import { isAbsolute as isAbsolute3, join as join5 } from "path";
2813
+ function envToken2(name) {
2814
+ return name.toUpperCase().replace(/-/g, "_");
2815
+ }
2816
+ function clientTransportEnvKeys2(name) {
2817
+ const envSegment = envToken2(name);
2818
+ return {
2819
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
2820
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
2821
+ };
2822
+ }
2823
+ function credentialOverrideEnvKey2(name) {
2824
+ return `HASNA_${envToken2(name)}_API_KEY_OVERRIDE`;
2825
+ }
2826
+ var CREDENTIAL_PROFILE_ENV_KEY2 = "HASNA_PROFILE";
2827
+ function credentialPointerEnvKey2(name) {
2828
+ return `HASNA_${envToken2(name)}_API_KEY_REF`;
2829
+ }
2830
+
2831
+ class CredentialResolutionError2 extends Error {
2832
+ appName;
2833
+ attempted;
2834
+ constructor(appName, message, attempted) {
2835
+ super(message);
2836
+ this.name = "CredentialResolutionError";
2837
+ this.appName = appName;
2838
+ this.attempted = attempted;
2839
+ }
2840
+ }
2841
+
2842
+ class CredentialFileUnsafeError2 extends Error {
2843
+ path;
2844
+ constructor(path, reason) {
2845
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
2846
+ this.name = "CredentialFileUnsafeError";
2847
+ this.path = path;
2848
+ }
2849
+ }
2850
+ var HASNA_HOME_ENV_KEY2 = "HASNA_HOME";
2851
+ var HASNA_CONFIG_HOME_ENV_KEY2 = "HASNA_CONFIG_HOME";
2852
+ var KEYCHAIN_STATION_ENV_KEY2 = "HASNA_STATION";
2853
+ var HASNA_HOME_DIR2 = ".hasna";
2854
+ var CONFIG_SUBDIR2 = "config";
2855
+ var CREDENTIALS_FILE2 = "credentials";
2856
+ var KEYCHAIN_SECURITY_BIN2 = "/usr/bin/security";
2857
+ var KEYCHAIN_SERVICE_PREFIX2 = "hasna.credentials";
2858
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS2 = 44;
2859
+ var KEYCHAIN_SPAWN_TIMEOUT_MS2 = 1e4;
2860
+ var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
2861
+ var SAFE_APP_SLUG2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
2862
+ var SAFE_PROFILE2 = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
2863
+ var ILLEGAL_IN_HEADER_VALUE2 = /[^\t\x20-\x7e]/;
2864
+ var VAULT_POINTER_SHAPE2 = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
2865
+ function homeDir2(env) {
2866
+ const home = env.HOME?.trim();
2867
+ return home ? home : null;
2868
+ }
2869
+ function absoluteOverride2(env, key) {
2870
+ const value = env[key]?.trim();
2871
+ return value && isAbsolute3(value) ? value : null;
2872
+ }
2873
+ function hasnaHomeDir2(env) {
2874
+ const override = absoluteOverride2(env, HASNA_HOME_ENV_KEY2);
2875
+ if (override)
2876
+ return override;
2877
+ const home = homeDir2(env);
2878
+ return home ? join5(home, HASNA_HOME_DIR2) : null;
2879
+ }
2880
+ function appConfigDir2(name, env) {
2881
+ const configRoot = absoluteOverride2(env, HASNA_CONFIG_HOME_ENV_KEY2);
2882
+ if (configRoot)
2883
+ return join5(configRoot, name);
2884
+ const root = hasnaHomeDir2(env);
2885
+ return root ? join5(root, name, CONFIG_SUBDIR2) : null;
2886
+ }
2887
+ function credentialDiskSourceList2(name, env, profile = null) {
2888
+ if (!SAFE_APP_SLUG2.test(name))
2889
+ return [];
2890
+ const directory = appConfigDir2(name, env);
2891
+ if (!directory)
2892
+ return [];
2893
+ const file = profile ? `${CREDENTIALS_FILE2}-${profile}` : CREDENTIALS_FILE2;
2894
+ return [{ path: join5(directory, file), tier: "disk" }];
2895
+ }
2896
+ function credentialDiskSources2(name, env) {
2897
+ return credentialDiskSourceList2(name, env, null).map((s) => s.path);
2898
+ }
2899
+ function profileDiskSources2(name, env, profile) {
2900
+ return credentialDiskSourceList2(name, env, profile).map((s) => s.path);
2901
+ }
2902
+ function parseEnvFile2(text) {
2903
+ const values = new Map;
2904
+ const unusable = new Set;
2905
+ for (const rawLine of text.split(/\r?\n/)) {
2906
+ const line = rawLine.trim();
2907
+ if (line.length === 0 || line.startsWith("#"))
2908
+ continue;
2909
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
2910
+ const equals = withoutExport.indexOf("=");
2911
+ if (equals <= 0)
2912
+ continue;
2913
+ const key = withoutExport.slice(0, equals).trim();
2914
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
2915
+ continue;
2916
+ let value = withoutExport.slice(equals + 1).trim();
2917
+ const quote = value[0];
2918
+ if (quote === '"' || quote === "'") {
2919
+ if (value.length < 2 || !value.endsWith(quote)) {
2920
+ unusable.add(key);
2921
+ continue;
2922
+ }
2923
+ value = value.slice(1, -1);
2924
+ }
2925
+ if (value.trim().length === 0) {
2926
+ unusable.add(key);
2927
+ continue;
2928
+ }
2929
+ if (values.has(key) && values.get(key) !== value)
2930
+ unusable.add(key);
2931
+ values.set(key, value);
2932
+ }
2933
+ return { values, unusable };
2934
+ }
2935
+ function configFileModeAllowed2(mode) {
2936
+ const permissions = mode & 4095;
2937
+ return permissions === 256 || permissions === 384;
2938
+ }
2939
+ function configFileReadsCoherent2(before, after) {
2940
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
2941
+ }
2942
+ function readAppConfigFile2(path) {
2943
+ const unsafe = (reason) => {
2944
+ throw new CredentialFileUnsafeError2(path, reason);
2945
+ };
2946
+ let fd = -1;
2947
+ try {
2948
+ fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
2949
+ } catch (error) {
2950
+ const code = error.code;
2951
+ if (code === "ENOENT" || code === "ENOTDIR")
2952
+ return null;
2953
+ if (code === "ELOOP")
2954
+ unsafe("the path is a symlink");
2955
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
2956
+ }
2957
+ try {
2958
+ const before = fstatSync2(fd);
2959
+ if (!before.isFile())
2960
+ unsafe("the path is not a regular file");
2961
+ if (!configFileModeAllowed2(before.mode)) {
2962
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
2963
+ }
2964
+ const uid = process.getuid?.() ?? process.geteuid?.();
2965
+ if (uid !== undefined && before.uid !== uid)
2966
+ unsafe("the file is not owned by the current user");
2967
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
2968
+ unsafe("the file exceeds the size limit");
2969
+ const bytes = readFileSync3(fd);
2970
+ const after = fstatSync2(fd);
2971
+ if (!configFileReadsCoherent2(before, after)) {
2972
+ unsafe("the file changed while being read");
2973
+ }
2974
+ return parseEnvFile2(bytes.toString("utf8"));
2975
+ } finally {
2976
+ if (fd !== -1)
2977
+ closeSync2(fd);
2978
+ }
2979
+ }
2980
+ function readCredentialFile2(path, apiKeyKeys) {
2981
+ const parsed = readAppConfigFile2(path);
2982
+ if (!parsed)
2983
+ return null;
2984
+ for (const key of apiKeyKeys) {
2985
+ if (parsed.unusable.has(key)) {
2986
+ throw new CredentialFileUnsafeError2(path, `${key} is declared but blank or malformed`);
2987
+ }
2988
+ }
2989
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
2990
+ if (new Set(values).size > 1) {
2991
+ throw new CredentialFileUnsafeError2(path, "credential aliases disagree");
2992
+ }
2993
+ return values[0] ?? null;
2994
+ }
2995
+ var CREDENTIAL_SHAPED_KEY2 = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
2996
+ function appConfigDiskValue2(name, env, keys) {
2997
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY2.test(key));
2998
+ if (wanted.length === 0)
2999
+ return null;
3000
+ for (const path of credentialDiskSources2(name, env)) {
3001
+ const parsed = readAppConfigFile2(path);
3002
+ if (!parsed)
3003
+ continue;
3004
+ if (wanted.some((key) => parsed.unusable.has(key))) {
3005
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
3006
+ }
3007
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
3008
+ if (new Set(values).size > 1)
3009
+ throw new CredentialFileUnsafeError2(path, "configuration aliases disagree");
3010
+ for (const key of wanted) {
3011
+ if (parsed.unusable.has(key))
3012
+ return { key, value: "", path, unusable: true };
3013
+ const value = parsed.values.get(key)?.trim();
3014
+ if (value)
3015
+ return { key, value, path };
3016
+ }
3017
+ }
3018
+ return null;
3019
+ }
3020
+ function assertUsableCredential2(appName, source, value) {
3021
+ if (VAULT_POINTER_SHAPE2.test(value)) {
3022
+ throw new CredentialResolutionError2(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey2(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
3023
+ }
3024
+ if (!ILLEGAL_IN_HEADER_VALUE2.test(value))
3025
+ return;
3026
+ throw new CredentialResolutionError2(appName, `The credential from ${source} contains characters that cannot be sent in an HTTP header ` + `(a control character or non-ASCII byte). A file written with CR-only line endings is the usual ` + `cause. Rewrite that credential file with one LF-terminated KEY=value line. ` + `The value is not shown here, and is deliberately never logged.`, [source]);
3027
+ }
3028
+ var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
3029
+ var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
3030
+ var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
3031
+ function sealCredential2(fields) {
3032
+ const { apiKey } = fields;
3033
+ const visible = {
3034
+ tier: fields.tier,
3035
+ source: fields.source,
3036
+ deliberate: fields.deliberate,
3037
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
3038
+ warning: fields.warning
3039
+ };
3040
+ const sealed = { ...visible };
3041
+ Object.defineProperty(sealed, "apiKey", {
3042
+ value: apiKey,
3043
+ enumerable: false,
3044
+ writable: false,
3045
+ configurable: false
3046
+ });
3047
+ if (fields.pointerVaultKey !== undefined) {
3048
+ Object.defineProperty(sealed, "pointerVaultKey", {
3049
+ value: fields.pointerVaultKey,
3050
+ enumerable: false,
3051
+ writable: false,
3052
+ configurable: false
3053
+ });
3054
+ }
3055
+ Object.defineProperty(sealed, INSPECT_CUSTOM2, {
3056
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
3057
+ enumerable: false,
3058
+ writable: false,
3059
+ configurable: false
3060
+ });
3061
+ Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
3062
+ value: true,
3063
+ enumerable: false,
3064
+ writable: false,
3065
+ configurable: false
3066
+ });
3067
+ return Object.freeze(sealed);
3068
+ }
3069
+ function isSealedCredential(credential) {
3070
+ return credential[CREDENTIAL_SEAL2] === true;
3071
+ }
3072
+ function explicitCredential(appName, apiKey) {
3073
+ const source = "explicit apiKey option";
3074
+ assertUsableCredential2(appName, source, apiKey);
3075
+ return sealCredential2({
3076
+ apiKey,
3077
+ tier: "argument",
3078
+ source,
3079
+ deliberate: true,
3080
+ diskCandidates: [],
3081
+ warning: null
3082
+ });
3083
+ }
3084
+ function validateAndSealResolvedCredential(appName, credential) {
3085
+ const apiKey = credential.apiKey;
3086
+ assertUsableCredential2(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
3087
+ if (!isSealedCredential(credential)) {
3088
+ return sealCredential2({
3089
+ apiKey,
3090
+ tier: "argument",
3091
+ source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
3092
+ deliberate: true,
3093
+ diskCandidates: [],
3094
+ warning: null
3095
+ });
3096
+ }
3097
+ return sealCredential2({
3098
+ apiKey,
3099
+ tier: credential.tier,
3100
+ source: credential.source,
3101
+ deliberate: credential.deliberate,
3102
+ diskCandidates: credential.diskCandidates,
3103
+ warning: credential.warning,
3104
+ ...credential.pointerVaultKey !== undefined ? { pointerVaultKey: credential.pointerVaultKey } : {}
3105
+ });
3106
+ }
3107
+ function firstEnvValue2(env, keys) {
3108
+ for (const key of keys) {
3109
+ if (!Object.prototype.hasOwnProperty.call(env, key))
3110
+ continue;
3111
+ const value = env[key]?.trim();
3112
+ if (value)
3113
+ return { key, value };
3114
+ }
3115
+ return null;
3116
+ }
3117
+ var AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
3118
+ function isAmbientEnvironment2(env) {
3119
+ return env === process.env || env[AMBIENT_ENVIRONMENT2] === true;
3120
+ }
3121
+ function defaultKeychainRunner2(argv) {
3122
+ const result = spawnSync3(KEYCHAIN_SECURITY_BIN2, [...argv], {
3123
+ encoding: "utf8",
3124
+ stdio: ["ignore", "pipe", "pipe"],
3125
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS2
3126
+ });
3127
+ return {
3128
+ status: result.status,
3129
+ stdout: result.stdout ?? "",
3130
+ stderr: result.error ? result.error.message : result.stderr ?? ""
3131
+ };
3132
+ }
3133
+ function keychainTierEnabled2(env, options) {
3134
+ if ((options.platform ?? process.platform) !== "darwin")
3135
+ return false;
3136
+ if (options.enabled !== undefined)
3137
+ return options.enabled;
3138
+ return options.run !== undefined || isAmbientEnvironment2(env);
3139
+ }
3140
+ function keychainAccount2(env, options) {
3141
+ const station = env[KEYCHAIN_STATION_ENV_KEY2]?.trim();
3142
+ if (station)
3143
+ return station;
3144
+ const host = (options.hostname ?? osHostname2)().split(".")[0]?.trim() ?? "";
3145
+ if (host)
3146
+ return host;
3147
+ const user = env.USER?.trim();
3148
+ return user || null;
3149
+ }
3150
+ function keychainFailureHint2(text) {
3151
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
3152
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
3153
+ return clean ? `: ${clean}` : "";
3154
+ }
3155
+ function readKeychainItem2(name, env, kind, options) {
3156
+ if (!SAFE_APP_SLUG2.test(name) || !keychainTierEnabled2(env, options))
3157
+ return null;
3158
+ const account = keychainAccount2(env, options);
3159
+ if (!account)
3160
+ return null;
3161
+ const service = `${KEYCHAIN_SERVICE_PREFIX2}.${name}.${kind}`;
3162
+ const source = `keychain:${service}@${account}`;
3163
+ const run = options.run ?? defaultKeychainRunner2;
3164
+ let result;
3165
+ try {
3166
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
3167
+ } catch (error) {
3168
+ const reason = keychainFailureHint2(error instanceof Error ? error.message : String(error));
3169
+ throw new CredentialResolutionError2(name, `The Keychain lookup for ${source} could not run${reason}. A Keychain failure is never resolved ` + `around: fix the keychain, or delete the item to fall through to the credential on disk.`, [source]);
3170
+ }
3171
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS2)
3172
+ return null;
3173
+ if (result.status !== 0) {
3174
+ throw new CredentialResolutionError2(name, `The Keychain lookup for ${source} failed (security exited ` + `${result.status ?? "without a status"}${keychainFailureHint2(result.stderr)}). A Keychain item that ` + `exists but cannot be read is never resolved around: unlock the keychain, run from a session that ` + `may use it, or delete the item to fall through to the credential on disk.`, [source]);
3175
+ }
3176
+ const value = result.stdout.trim();
3177
+ if (!value) {
3178
+ throw new CredentialResolutionError2(name, `${source} exists but holds an empty value; a declared item never falls through to another ` + `identity. Store a value in it or delete the item.`, [source]);
3179
+ }
3180
+ return { value, source };
3181
+ }
3182
+ function keychainConfigValue2(name, env, options = {}) {
3183
+ return readKeychainItem2(name, env, "api-url", options);
3184
+ }
3185
+ function snapshotClientEnvironment2(name, env) {
3186
+ const keys = clientTransportEnvKeys2(name);
3187
+ const ambient = isAmbientEnvironment2(env);
3188
+ const snapshot = Object.create(null);
3189
+ for (const key of [
3190
+ ...keys.apiUrlKeys,
3191
+ ...keys.apiKeyKeys,
3192
+ credentialOverrideEnvKey2(name),
3193
+ credentialPointerEnvKey2(name),
3194
+ CREDENTIAL_PROFILE_ENV_KEY2,
3195
+ "HOME",
3196
+ HASNA_HOME_ENV_KEY2,
3197
+ HASNA_CONFIG_HOME_ENV_KEY2,
3198
+ KEYCHAIN_STATION_ENV_KEY2,
3199
+ "USER"
3200
+ ]) {
3201
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
3202
+ if (!descriptor)
3203
+ continue;
3204
+ if (!("value" in descriptor)) {
3205
+ throw new CredentialResolutionError2(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
3206
+ }
3207
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
3208
+ throw new CredentialResolutionError2(name, `${key} must be a string data property.`, [key]);
3209
+ }
3210
+ snapshot[key] = descriptor.value;
3211
+ }
3212
+ if (ambient) {
3213
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT2, {
3214
+ value: true,
3215
+ enumerable: false,
3216
+ writable: false,
3217
+ configurable: false
3218
+ });
3219
+ }
3220
+ return Object.freeze(snapshot);
3221
+ }
3222
+ function resolveCredential2(name, env, options = {}) {
3223
+ env = snapshotClientEnvironment2(name, env);
3224
+ const { apiKeyKeys } = clientTransportEnvKeys2(name);
3225
+ const diskPaths = credentialDiskSources2(name, env);
3226
+ if (options.apiKey !== undefined) {
3227
+ const explicitKey = options.apiKey.trim();
3228
+ if (!explicitKey) {
3229
+ throw new CredentialResolutionError2(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
3230
+ }
3231
+ assertUsableCredential2(name, "the explicit apiKey argument", explicitKey);
3232
+ return sealCredential2({
3233
+ apiKey: explicitKey,
3234
+ tier: "argument",
3235
+ source: "explicit apiKey argument",
3236
+ deliberate: true,
3237
+ diskCandidates: diskPaths,
3238
+ warning: null
3239
+ });
3240
+ }
3241
+ const overrideKeyName = credentialOverrideEnvKey2(name);
3242
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
3243
+ if (overrideRaw !== undefined) {
3244
+ const override = overrideRaw.trim();
3245
+ if (!override) {
3246
+ throw new CredentialResolutionError2(name, `${overrideKeyName} is set but empty. It is a deliberate override, so it is not resolved around: ` + `either give it a real key or unset it to fall back to the credential on disk.`, [overrideKeyName]);
3247
+ }
3248
+ assertUsableCredential2(name, overrideKeyName, override);
3249
+ return sealCredential2({
3250
+ apiKey: override,
3251
+ tier: "override",
3252
+ source: overrideKeyName,
3253
+ deliberate: true,
3254
+ diskCandidates: diskPaths,
3255
+ warning: null
3256
+ });
3257
+ }
3258
+ const pointerKeyName = credentialPointerEnvKey2(name);
3259
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
3260
+ if (pointerRaw !== undefined) {
3261
+ const pointer = pointerRaw.trim();
3262
+ if (!pointer) {
3263
+ throw new CredentialResolutionError2(name, `${pointerKeyName} is set but empty. It is a deliberate vault pointer, so it is not resolved around: ` + `either give it a vault item key or unset it to fall back to the credential on disk.`, [pointerKeyName]);
3264
+ }
3265
+ if (!VAULT_POINTER_SHAPE2.test(pointer)) {
3266
+ throw new CredentialResolutionError2(name, `${pointerKeyName} must name a vault ITEM KEY (a path-shaped reference like ` + `'namespace/app/live/api_key'), not a credential value. A pointer that carries a literal is refused.`, [pointerKeyName]);
3267
+ }
3268
+ return sealCredential2({
3269
+ apiKey: "",
3270
+ pointerVaultKey: pointer,
3271
+ tier: "pointer",
3272
+ source: pointerKeyName,
3273
+ deliberate: true,
3274
+ diskCandidates: diskPaths,
3275
+ warning: null
3276
+ });
3277
+ }
3278
+ if (options.profile !== undefined && !options.profile.trim()) {
3279
+ throw new CredentialResolutionError2(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
3280
+ }
3281
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY2) ? env[CREDENTIAL_PROFILE_ENV_KEY2] : undefined;
3282
+ if (profileRaw !== undefined && !profileRaw.trim()) {
3283
+ throw new CredentialResolutionError2(name, `${CREDENTIAL_PROFILE_ENV_KEY2} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY2]);
3284
+ }
3285
+ const profile = options.profile?.trim() || profileRaw?.trim();
3286
+ if (profile) {
3287
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY2;
3288
+ if (!SAFE_PROFILE2.test(profile)) {
3289
+ throw new CredentialResolutionError2(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
3290
+ }
3291
+ const paths = profileDiskSources2(name, env, profile);
3292
+ for (const path of paths) {
3293
+ const value = readCredentialFile2(path, apiKeyKeys);
3294
+ if (value) {
3295
+ assertUsableCredential2(name, path, value);
3296
+ return sealCredential2({
3297
+ apiKey: value,
3298
+ tier: "profile",
3299
+ source: path,
3300
+ deliberate: true,
3301
+ diskCandidates: paths,
3302
+ warning: null
3303
+ });
3304
+ }
3305
+ }
3306
+ throw new CredentialResolutionError2(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY2}.`, paths);
3307
+ }
3308
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
3309
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
3310
+ if (blankEnv) {
3311
+ throw new CredentialResolutionError2(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
3312
+ }
3313
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
3314
+ throw new CredentialResolutionError2(name, `${definedEnvEntries.map((entry) => entry.key).join(" and ")} disagree; credential aliases must be identical or only one may be set.`, definedEnvEntries.map((entry) => entry.key));
3315
+ }
3316
+ const envHit = firstEnvValue2(env, apiKeyKeys);
3317
+ const keychainHit = readKeychainItem2(name, env, "api-key", options.keychain ?? {});
3318
+ if (keychainHit) {
3319
+ assertUsableCredential2(name, keychainHit.source, keychainHit.value);
3320
+ const warning = envHit && envHit.value !== keychainHit.value ? `Credential sources disagree for '${name}': ${keychainHit.source} and ${envHit.key} hold ` + `different keys. ${keychainHit.source} wins, because the Keychain is re-read on every call while ` + `an environment variable is a snapshot. Reconcile them \u2014 a rotation that updated only one leaves ` + `the other to fail 401 wherever it is loaded first.` : null;
3321
+ return sealCredential2({
3322
+ apiKey: keychainHit.value,
3323
+ tier: "keychain",
3324
+ source: keychainHit.source,
3325
+ deliberate: false,
3326
+ diskCandidates: diskPaths,
3327
+ warning
3328
+ });
3329
+ }
3330
+ const diskSourceList = credentialDiskSourceList2(name, env, null);
3331
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile2(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
3332
+ if (diskHits.length > 0) {
3333
+ const winner = diskHits[0];
3334
+ assertUsableCredential2(name, winner.src.path, winner.value);
3335
+ const divergentSources = [
3336
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
3337
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
3338
+ ];
3339
+ const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
3340
+ return sealCredential2({
3341
+ apiKey: winner.value,
3342
+ tier: winner.src.tier,
3343
+ source: winner.src.path,
3344
+ deliberate: false,
3345
+ diskCandidates: diskPaths,
3346
+ warning
3347
+ });
3348
+ }
3349
+ if (envHit) {
3350
+ assertUsableCredential2(name, envHit.key, envHit.value);
3351
+ return sealCredential2({
3352
+ apiKey: envHit.value,
3353
+ tier: "env",
3354
+ source: envHit.key,
3355
+ deliberate: false,
3356
+ diskCandidates: diskPaths,
3357
+ warning: null
3358
+ });
3359
+ }
3360
+ return null;
3361
+ }
3362
+ var SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
3363
+ var requireSecretsSdk2 = createRequire2(import.meta.url);
3364
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
3365
+ const vaultKey = pointerResolution.pointerVaultKey;
3366
+ const pointerEnvKey = pointerResolution.source;
3367
+ if (!vaultKey) {
3368
+ throw new CredentialResolutionError2(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
3369
+ }
3370
+ let secretsSdk;
3371
+ try {
3372
+ secretsSdk = requireSecretsSdk2(SECRETS_PACKAGE_SPECIFIER2);
3373
+ } catch {
3374
+ throw new CredentialResolutionError2(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
3375
+ }
3376
+ let client;
3377
+ try {
3378
+ client = secretsSdk.createSecretsClientFromEnv(env);
3379
+ } catch {
3380
+ throw new CredentialResolutionError2(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
3381
+ }
3382
+ let secret;
3383
+ try {
3384
+ secret = await client.getSecret({ key: vaultKey });
3385
+ } catch {
3386
+ throw new CredentialResolutionError2(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
3387
+ }
3388
+ const value = secret.value;
3389
+ if (!value) {
3390
+ throw new CredentialResolutionError2(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
3391
+ }
3392
+ assertUsableCredential2(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
3393
+ return sealCredential2({
3394
+ apiKey: value,
3395
+ tier: "pointer",
3396
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
3397
+ deliberate: true,
3398
+ diskCandidates: pointerResolution.diskCandidates,
3399
+ warning: null
3400
+ });
3401
+ }
3402
+ var DEFAULT_FLEET_GATEWAY_ORIGIN2 = "https://api.hasna.com";
3403
+ var DEFAULT_AUTHORITY_SOURCE2 = "default";
3404
+ function defaultFleetGatewayBaseUrl2(name) {
3405
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN2}/${validateAppSlug2(name)}`;
3406
+ }
3407
+ var ASCII_CONTROL_PATTERN2 = /[\u0000-\u001f\u007f]/;
3408
+ var DNS_LABEL_PATTERN2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
3409
+ function isValidDnsDomain2(value) {
3410
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN2.test(value) || /[^\x00-\x7f]/.test(value)) {
3411
+ return false;
2388
3412
  }
2389
- return null;
3413
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN2.test(label));
2390
3414
  }
2391
- function firstEnvDefinedKey(env, keys) {
2392
- for (const key of keys) {
2393
- if (env[key] !== undefined)
2394
- return key;
3415
+ function validateAppSlug2(name) {
3416
+ if (name.length > 63 || !DNS_LABEL_PATTERN2.test(name)) {
3417
+ throw new Error("App name must be one lowercase DNS label.");
2395
3418
  }
2396
- return null;
3419
+ return name;
2397
3420
  }
2398
- function rawAuthority(value) {
3421
+ function rawAuthority2(value) {
2399
3422
  const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
2400
3423
  if (!match)
2401
3424
  throw new Error("API URL must be absolute.");
@@ -2406,7 +3429,7 @@ function rawAuthority(value) {
2406
3429
  throw new Error("API URL must include a hostname.");
2407
3430
  return authority;
2408
3431
  }
2409
- function assertCanonicalPort(port) {
3432
+ function assertCanonicalPort2(port) {
2410
3433
  if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
2411
3434
  throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2412
3435
  }
@@ -2415,7 +3438,7 @@ function assertCanonicalPort(port) {
2415
3438
  throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2416
3439
  }
2417
3440
  }
2418
- function canonicalAuthorityHostname(authority) {
3441
+ function canonicalAuthorityHostname2(authority) {
2419
3442
  let rawHostname;
2420
3443
  if (authority.startsWith("[")) {
2421
3444
  const closingBracket = authority.indexOf("]");
@@ -2428,9 +3451,9 @@ function canonicalAuthorityHostname(authority) {
2428
3451
  if (!portSuffix.startsWith(":")) {
2429
3452
  throw new Error("API URL authority must contain a canonical hostname and port.");
2430
3453
  }
2431
- assertCanonicalPort(portSuffix.slice(1));
3454
+ assertCanonicalPort2(portSuffix.slice(1));
2432
3455
  }
2433
- if (isIP(rawHostname.slice(1, -1)) !== 6) {
3456
+ if (isIP2(rawHostname.slice(1, -1)) !== 6) {
2434
3457
  throw new Error("API URL authority must contain a canonical IPv6 literal.");
2435
3458
  }
2436
3459
  } else {
@@ -2441,33 +3464,33 @@ function canonicalAuthorityHostname(authority) {
2441
3464
  }
2442
3465
  if (lastColon !== -1) {
2443
3466
  const port = authority.slice(lastColon + 1);
2444
- assertCanonicalPort(port);
3467
+ assertCanonicalPort2(port);
2445
3468
  rawHostname = authority.slice(0, lastColon);
2446
3469
  } else {
2447
3470
  rawHostname = authority;
2448
3471
  }
2449
- const ipVersion = isIP(rawHostname);
3472
+ const ipVersion = isIP2(rawHostname);
2450
3473
  const numericAddressParts = rawHostname.split(".");
2451
3474
  const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
2452
- if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
3475
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain2(rawHostname.toLowerCase())) {
2453
3476
  throw new Error("API URL authority must contain a canonical ASCII hostname.");
2454
3477
  }
2455
3478
  }
2456
3479
  return rawHostname.toLowerCase();
2457
3480
  }
2458
- function isDeliberateLoopbackHttpAuthority(authority) {
3481
+ function isDeliberateLoopbackHttpAuthority2(authority) {
2459
3482
  return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
2460
3483
  }
2461
- function toV1BaseUrl(apiUrl) {
2462
- if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
3484
+ function toV1BaseUrl2(apiUrl) {
3485
+ if (ASCII_CONTROL_PATTERN2.test(apiUrl)) {
2463
3486
  throw new Error("API URL must not contain ASCII control characters.");
2464
3487
  }
2465
3488
  const input = apiUrl.trim();
2466
- const authority = rawAuthority(input);
3489
+ const authority = rawAuthority2(input);
2467
3490
  if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
2468
3491
  throw new Error("API URL authority must be canonical ASCII without credentials.");
2469
3492
  }
2470
- const canonicalHostname = canonicalAuthorityHostname(authority);
3493
+ const canonicalHostname = canonicalAuthorityHostname2(authority);
2471
3494
  const url = new URL(input);
2472
3495
  if (url.protocol !== "http:" && url.protocol !== "https:") {
2473
3496
  throw new Error("API URL must use http or https.");
@@ -2484,7 +3507,7 @@ function toV1BaseUrl(apiUrl) {
2484
3507
  if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
2485
3508
  throw new Error("API URL must not use IDN or punycode hostnames.");
2486
3509
  }
2487
- if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
3510
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority2(authority)) {
2488
3511
  throw new Error("API URL may use http only for an exact loopback authority.");
2489
3512
  }
2490
3513
  if (url.search || url.hash) {
@@ -2496,106 +3519,104 @@ function toV1BaseUrl(apiUrl) {
2496
3519
  url.pathname = `${path}/v1`;
2497
3520
  return url.toString().replace(/\/+$/, "");
2498
3521
  }
2499
- function resolveClientTransport(name, env = process.env, options = {}) {
2500
- const keys = clientTransportEnvKeys(name);
2501
- const envUrlHit = firstEnv(env, keys.apiUrlKeys, { preserveRaw: true });
2502
- const explicitLocalKey = envUrlHit ? null : firstEnvDefinedKey(env, keys.apiUrlKeys);
2503
- const diskUrlHit = envUrlHit || explicitLocalKey ? null : appConfigDiskValue(name, env, keys.apiUrlKeys);
2504
- const urlHit = envUrlHit ?? (diskUrlHit ? { key: diskUrlHit.path, value: diskUrlHit.value } : null);
2505
- const keyHit = firstEnv(env, keys.apiKeyKeys);
2506
- const warnings = [];
2507
- if (!urlHit) {
2508
- if (explicitLocalKey) {
2509
- const overriddenPointer = appConfigDiskValue(name, env, keys.apiUrlKeys);
2510
- if (overriddenPointer) {
2511
- warnings.push(`${explicitLocalKey} is defined but blank, which selects the local store. ` + `The server URL in ${overriddenPointer.path} was NOT selected: an explicit blank wins over a disk pointer.`);
2512
- }
2513
- return {
2514
- transport: "sqlite",
2515
- transportSource: explicitLocalKey,
2516
- baseUrl: null,
2517
- apiUrlSource: null,
2518
- apiKeyPresent: Boolean(keyHit),
2519
- apiKeySource: keyHit ? keyHit.key : null,
2520
- apiKeyTier: null,
2521
- misconfigured: false,
2522
- warning: warnings.length > 0 ? warnings.join(" ") : null
2523
- };
2524
- }
2525
- return {
2526
- transport: "sqlite",
2527
- transportSource: "default",
2528
- baseUrl: null,
2529
- apiUrlSource: null,
2530
- apiKeyPresent: Boolean(keyHit),
2531
- apiKeySource: keyHit ? keyHit.key : null,
2532
- apiKeyTier: null,
2533
- misconfigured: false,
2534
- warning: null
2535
- };
3522
+ class ClientTransportConfigurationError2 extends Error {
3523
+ appName;
3524
+ sources;
3525
+ constructor(appName, message, sources = []) {
3526
+ super(message);
3527
+ this.name = "ClientTransportConfigurationError";
3528
+ this.appName = appName;
3529
+ this.sources = Object.freeze([...sources]);
3530
+ }
3531
+ }
3532
+ function resolveClientTransportSnapshot2(name, env = process.env, options = {}) {
3533
+ env = snapshotClientEnvironment2(name, env);
3534
+ const keys = clientTransportEnvKeys2(name);
3535
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
3536
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
3537
+ if (blankUrl) {
3538
+ throw new ClientTransportConfigurationError2(name, `${blankUrl.key} is set but blank; public clients require an explicit HTTPS API URL and never select local storage.`, [blankUrl.key]);
3539
+ }
3540
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN2.test(entry.raw));
3541
+ if (controlledUrl) {
3542
+ throw new ClientTransportConfigurationError2(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
3543
+ }
3544
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
3545
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
3546
+ throw new ClientTransportConfigurationError2(name, `${usableUrlEntries.map((entry) => entry.key).join(" and ")} disagree; client authority aliases must be identical or only one may be set.`, usableUrlEntries.map((entry) => entry.key));
3547
+ }
3548
+ const envUrlHit = usableUrlEntries[0] ?? null;
3549
+ const keychainUrlHit = keychainConfigValue2(name, env, options.credentials?.keychain);
3550
+ const diskConfigUrlHit = appConfigDiskValue2(name, env, keys.apiUrlKeys);
3551
+ if (diskConfigUrlHit?.unusable) {
3552
+ throw new ClientTransportConfigurationError2(name, `${diskConfigUrlHit.key} in ${diskConfigUrlHit.path} is declared but blank or malformed; public clients require a valid HTTPS service authority.`, [diskConfigUrlHit.path]);
3553
+ }
3554
+ const urlCandidates = [
3555
+ ...envUrlHit ? [envUrlHit] : [],
3556
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
3557
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
3558
+ ];
3559
+ const configuredUrl = urlCandidates[0] ?? null;
3560
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
3561
+ if (configuredUrl && divergentUrls.length > 0) {
3562
+ throw new ClientTransportConfigurationError2(name, `${configuredUrl.key} and ${divergentUrls.map((candidate) => candidate.key).join(" and ")} select different service authorities; refusing to send a credential written for one authority to the other.`, urlCandidates.map((candidate) => candidate.key));
2536
3563
  }
2537
- if (diskUrlHit) {
2538
- warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${diskUrlHit.path} was used, so this client connects to the server. ` + `Unset the pointer or remove the file to stay on the local store.`);
3564
+ const warnings = [];
3565
+ if (configuredUrl && !envUrlHit) {
3566
+ warnings.push(`No ${keys.apiUrlKeys[0]} in the environment; the server URL in ${configuredUrl.key} was used, so this client connects to the server. ` + `Keep that entry aligned with the intended service authority.`);
2539
3567
  }
2540
- const credential = resolveCredential(name, env, options.credentials);
3568
+ const credential = resolveCredential2(name, env, options.credentials);
2541
3569
  if (!credential) {
2542
- const diskHint = credentialDiskSourcesForMessage(name, env);
2543
- warnings.push(`${urlHit.key} selects the HTTP server for '${name}', but no API key could be resolved; ` + `refusing to route and leaving the local sqlite store selected. ` + `Looked for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
2544
- return {
2545
- transport: "sqlite",
2546
- transportSource: urlHit.key,
2547
- baseUrl: null,
2548
- apiUrlSource: urlHit.key,
2549
- apiKeyPresent: false,
2550
- apiKeySource: null,
2551
- apiKeyTier: null,
2552
- misconfigured: true,
2553
- warning: warnings.join(" ")
2554
- };
3570
+ const diskHint = credentialDiskSourcesForMessage2(name, env);
3571
+ const lead = configuredUrl ? `${configuredUrl.key} selects the HTTP server for '${name}', but no API key could be resolved` : `${keys.apiUrlKeys[0]} is not set and no API key could be resolved for '${name}'; a credential is required before the default fleet gateway authority applies`;
3572
+ warnings.push(`${lead}; refusing to create an unauthenticated client \u2014 public clients never fall back to SQLite or another local store. ` + `Looked in the Keychain (macOS only), then for a credential file at ${diskHint}, then for ${keys.apiKeyKeys[0]} in the environment.`);
3573
+ throw new ClientTransportConfigurationError2(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
2555
3574
  }
2556
3575
  if (credential.warning)
2557
3576
  warnings.push(credential.warning);
3577
+ let urlHit;
3578
+ if (configuredUrl) {
3579
+ urlHit = configuredUrl;
3580
+ } else {
3581
+ try {
3582
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE2, value: defaultFleetGatewayBaseUrl2(name) };
3583
+ } catch (error) {
3584
+ const message = error instanceof Error ? error.message : String(error);
3585
+ throw new ClientTransportConfigurationError2(name, `No ${keys.apiUrlKeys[0]} is configured and the default fleet gateway authority cannot be composed for '${name}': ${message}`, [keys.apiUrlKeys[0]]);
3586
+ }
3587
+ }
2558
3588
  const apiUrlSource = urlHit.key;
2559
3589
  let baseUrl;
2560
3590
  try {
2561
- baseUrl = toV1BaseUrl(urlHit.value);
3591
+ baseUrl = toV1BaseUrl2(urlHit.value);
2562
3592
  } catch (error) {
2563
3593
  const message = error instanceof Error ? error.message : String(error);
2564
- warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
2565
- return {
2566
- transport: "sqlite",
3594
+ throw new ClientTransportConfigurationError2(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
3595
+ }
3596
+ return {
3597
+ resolution: {
3598
+ transport: "http",
2567
3599
  transportSource: urlHit.key,
2568
- baseUrl: null,
2569
- apiUrlSource: urlHit.key,
3600
+ baseUrl,
3601
+ apiUrlSource,
2570
3602
  apiKeyPresent: true,
2571
3603
  apiKeySource: credential.source,
2572
3604
  apiKeyTier: credential.tier,
2573
- misconfigured: true,
2574
- warning: warnings.join(" ")
2575
- };
2576
- }
2577
- return {
2578
- transport: "http",
2579
- transportSource: urlHit.key,
2580
- baseUrl,
2581
- apiUrlSource,
2582
- apiKeyPresent: true,
2583
- apiKeySource: credential.source,
2584
- apiKeyTier: credential.tier,
2585
- misconfigured: false,
2586
- warning: warnings.length > 0 ? warnings.join(" ") : null
3605
+ misconfigured: false,
3606
+ warning: warnings.length > 0 ? warnings.join(" ") : null
3607
+ },
3608
+ credential
2587
3609
  };
2588
3610
  }
2589
- function credentialDiskSourcesForMessage(name, env) {
2590
- const paths = credentialDiskSources(name, env);
2591
- return paths.length > 0 ? paths.join(" or ") : "<no HOME set in this environment, so no credential file was consulted>";
3611
+ function credentialDiskSourcesForMessage2(name, env) {
3612
+ const paths = credentialDiskSources2(name, env);
3613
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
2592
3614
  }
2593
3615
 
2594
3616
  class HasnaHttpError extends Error {
2595
3617
  status;
2596
3618
  method;
2597
3619
  path;
2598
- body;
2599
3620
  credentialSource;
2600
3621
  credentialTier;
2601
3622
  constructor(method, path, status, body, credential) {
@@ -2605,7 +3626,12 @@ class HasnaHttpError extends Error {
2605
3626
  this.status = status;
2606
3627
  this.method = method;
2607
3628
  this.path = path;
2608
- this.body = body;
3629
+ Object.defineProperty(this, "body", {
3630
+ value: body,
3631
+ enumerable: status !== 401 && status !== 403,
3632
+ writable: false,
3633
+ configurable: false
3634
+ });
2609
3635
  this.credentialSource = credential?.source ?? null;
2610
3636
  this.credentialTier = credential?.tier ?? null;
2611
3637
  }
@@ -2616,22 +3642,32 @@ function currentCredential(name, apiKey) {
2616
3642
  }
2617
3643
  return explicitCredential(name, apiKey);
2618
3644
  }
3645
+ async function resolveRequestCredential(name, apiKey, env = process.env) {
3646
+ const resolved = currentCredential(name, apiKey);
3647
+ if (resolved.tier === "pointer") {
3648
+ return completePointerCredential(name, resolved, env);
3649
+ }
3650
+ return resolved;
3651
+ }
2619
3652
  function authFailureGuidance(credential) {
2620
3653
  const origin = `The API key for this request came from ${credential.source}`;
2621
3654
  if (credential.deliberate) {
2622
3655
  const remedy = credential.source === CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE ? `Fix that provider so it returns the current key, or replace it with resolveCredential() ` + `so diagnostics can name the original source.` : `Rotate that key, or unset the override to use the credential on disk.`;
2623
3656
  return `${origin} \u2014 a credential you selected deliberately. It was NOT substituted with any other key: ` + `falling back here would authenticate as a different principal than the one you named, which is ` + `exactly the failure an override exists to prevent. ${remedy}`;
2624
3657
  }
2625
- if (credential.deprecated) {
3658
+ if (credential.tier === "env") {
2626
3659
  const target = credential.diskCandidates[0];
2627
- const remedy = target ? `Write the CURRENT key to ${target} \u2014 that file is re-read on every call, so rotations take ` + `effect immediately and in every shell. Do not simply unset ${credential.source}: nothing was ` + `found on disk, so that would leave this client with no credential at all.` : `This environment has no HOME, so no credential file could be consulted; the disk tier is ` + `unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.`;
2628
- return `${origin}, a variable in this process's environment \u2014 which is a snapshot taken when the process ` + `started. A STALE SHELL is the most common cause of this error: this shell exported the key before ` + `it was rotated, and will keep sending the old one until it exits. ${remedy}`;
3660
+ const remedy = target ? `Store the CURRENT key in the Keychain or write it to ${target} \u2014 both are re-read on every call, so ` + `rotations take effect immediately and in every shell. Do not simply unset ${credential.source}: ` + `nothing was found in the Keychain or on disk, so that would leave this client with no credential at all.` : `This environment has no HOME or HASNA_HOME, so no credential file could be consulted; the disk tier is ` + `unavailable here and there is nothing to fall back to. Set HOME, or supply the key explicitly.`;
3661
+ return `${origin}, a variable in this process's environment. If a wrapper injected it for this one process, the ` + `wrapper re-reads its store on every invocation and the stored key itself is being rejected \u2014 rotate it. ` + `If this SHELL exported it, the export is a snapshot taken when the shell started: a STALE SHELL that ` + `exported the key before it was rotated keeps sending the old one until it exits. ${remedy}`;
3662
+ }
3663
+ if (credential.tier === "keychain") {
3664
+ return `${origin}, which was re-read from the Keychain on this very call \u2014 so a stale shell is NOT the cause ` + `here. The stored item is genuinely being rejected: update it with the current key, or re-run the fleet ` + `key distribution so this machine gets the current key.`;
2629
3665
  }
2630
3666
  return `${origin}, which was re-read from disk on this very call \u2014 so a stale shell is NOT the cause here. ` + `The stored credential is genuinely being rejected: rotate it, or re-run the fleet key distribution ` + `so this machine gets the current key.`;
2631
3667
  }
2632
3668
  var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
2633
- var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
2634
- var AUTHORITY_OVERRIDE_HEADERS = new Set([
3669
+ var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
3670
+ var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
2635
3671
  "host",
2636
3672
  ":authority",
2637
3673
  "forwarded",
@@ -2641,7 +3677,7 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
2641
3677
  function assertNoAuthorityOverrideHeaders(headers, source) {
2642
3678
  if (!headers)
2643
3679
  return;
2644
- const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS.has(name.trim().toLowerCase()));
3680
+ const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS2.has(name.trim().toLowerCase()));
2645
3681
  if (forbidden) {
2646
3682
  throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
2647
3683
  }
@@ -2668,9 +3704,9 @@ function appendQuery(path, query) {
2668
3704
  return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
2669
3705
  }
2670
3706
  var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
2671
- function createHasnaHttpTransport(options) {
3707
+ function createHasnaHttpTransportInternal(options, requestBindingProvider) {
2672
3708
  const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
2673
- const base = toV1BaseUrl(options.baseUrl);
3709
+ const base = toV1BaseUrl2(options.baseUrl);
2674
3710
  const timeoutMs = options.timeoutMs ?? 30000;
2675
3711
  const sleep = options.sleepImpl ?? defaultSleep;
2676
3712
  const defaultRetry = options.retry;
@@ -2730,13 +3766,20 @@ function createHasnaHttpTransport(options) {
2730
3766
  if (opts.signal)
2731
3767
  opts.signal.removeEventListener("abort", onAbort);
2732
3768
  }
2733
- const text = await response.text();
3769
+ const authenticationFailure = response.status === 401 || response.status === 403;
2734
3770
  let parsed = undefined;
2735
- if (text.length > 0) {
3771
+ if (authenticationFailure) {
2736
3772
  try {
2737
- parsed = JSON.parse(text);
2738
- } catch {
2739
- parsed = text;
3773
+ await response.body?.cancel();
3774
+ } catch {}
3775
+ } else {
3776
+ const text = await response.text();
3777
+ if (text.length > 0) {
3778
+ try {
3779
+ parsed = JSON.parse(text);
3780
+ } catch {
3781
+ parsed = text;
3782
+ }
2740
3783
  }
2741
3784
  }
2742
3785
  if (!response.ok) {
@@ -2747,11 +3790,11 @@ function createHasnaHttpTransport(options) {
2747
3790
  error: new HasnaHttpError(method, rel, response.status, parsed)
2748
3791
  };
2749
3792
  }
2750
- if (response.status === 401 || response.status === 403) {
3793
+ if (authenticationFailure) {
2751
3794
  return {
2752
3795
  ok: false,
2753
3796
  retryable: false,
2754
- error: new HasnaHttpError(method, rel, response.status, parsed, {
3797
+ error: new HasnaHttpError(method, rel, response.status, undefined, {
2755
3798
  source: credential.source,
2756
3799
  tier: credential.tier,
2757
3800
  guidance: authFailureGuidance(credential)
@@ -2767,11 +3810,15 @@ function createHasnaHttpTransport(options) {
2767
3810
  async function request(method, path, body, opts = {}) {
2768
3811
  const upper = method.toUpperCase();
2769
3812
  const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
2770
- const url = `${base}${rel}`;
2771
3813
  const retry = resolveRetry(opts.retry);
2772
- const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
3814
+ const methodRetryable = IDEMPOTENT_METHODS2.has(upper) || Boolean(opts.idempotencyKey);
2773
3815
  const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
2774
- const credential = currentCredential(options.name, options.apiKey);
3816
+ const binding = requestBindingProvider ? await requestBindingProvider() : {
3817
+ baseUrl: base,
3818
+ credential: await resolveRequestCredential(options.name, options.apiKey)
3819
+ };
3820
+ const url = `${binding.baseUrl}${rel}`;
3821
+ const credential = binding.credential;
2775
3822
  let last = null;
2776
3823
  for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2777
3824
  const result = await once(upper, rel, url, body, opts, credential);
@@ -2799,49 +3846,44 @@ function createHasnaHttpTransport(options) {
2799
3846
  }
2800
3847
  function createClientTransport(name, env = process.env, overrides) {
2801
3848
  const credentialOptions = overrides?.credentials;
2802
- const resolution = resolveClientTransport(name, env, { ...credentialOptions ? { credentials: credentialOptions } : {} });
2803
- if (resolution.misconfigured) {
2804
- throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the API client.`);
2805
- }
2806
- if (resolution.transport === "sqlite" || !resolution.baseUrl) {
2807
- return { transport: "sqlite", client: null, resolution };
2808
- }
2809
- const credentialProvider = () => {
2810
- const resolved = resolveCredential(name, env, credentialOptions);
2811
- if (!resolved) {
2812
- throw new Error(`Client for '${name}' resolved to the http transport but no API key is available any more. ` + `Looked at ${credentialDiskSourcesForMessage(name, env)}, then the environment. ` + `A credential file that was removed after this client was built is the usual cause.`);
2813
- }
2814
- return resolved;
3849
+ const snapshotOptions = { ...credentialOptions ? { credentials: credentialOptions } : {} };
3850
+ const resolution = resolveClientTransportSnapshot2(name, env, snapshotOptions).resolution;
3851
+ const sameBinding = (left, right) => left.resolution.baseUrl === right.resolution.baseUrl && left.credential.apiKey === right.credential.apiKey && left.credential.pointerVaultKey === right.credential.pointerVaultKey && left.credential.source === right.credential.source && left.credential.tier === right.credential.tier;
3852
+ const unstableConfiguration = () => new ClientTransportConfigurationError2(name, "The configured service authority or credential changed while a request was being prepared; no authenticated request was sent.");
3853
+ const requestBindingProvider = async () => {
3854
+ const first = resolveClientTransportSnapshot2(name, env, snapshotOptions);
3855
+ const reviewed = resolveClientTransportSnapshot2(name, env, snapshotOptions);
3856
+ if (!sameBinding(first, reviewed))
3857
+ throw unstableConfiguration();
3858
+ if (reviewed.resolution.baseUrl !== resolution.baseUrl) {
3859
+ throw new ClientTransportConfigurationError2(name, "The configured service authority changed; rebuild the client before sending credentials.");
3860
+ }
3861
+ const credential = await resolveRequestCredential(name, () => reviewed.credential, env);
3862
+ const immediatelyBeforeDispatch = resolveClientTransportSnapshot2(name, env, snapshotOptions);
3863
+ if (!sameBinding(reviewed, immediatelyBeforeDispatch))
3864
+ throw unstableConfiguration();
3865
+ if (immediatelyBeforeDispatch.resolution.baseUrl !== resolution.baseUrl) {
3866
+ throw new ClientTransportConfigurationError2(name, "The configured service authority changed; rebuild the client before sending credentials.");
3867
+ }
3868
+ return { baseUrl: immediatelyBeforeDispatch.resolution.baseUrl, credential };
2815
3869
  };
2816
3870
  return {
2817
3871
  transport: "http",
2818
- client: createHasnaHttpTransport({
3872
+ client: createHasnaHttpTransportInternal({
2819
3873
  name,
2820
3874
  baseUrl: resolution.baseUrl,
2821
- apiKey: credentialProvider,
3875
+ apiKey: () => {
3876
+ throw new Error("The authenticated request binding provider was not invoked.");
3877
+ },
2822
3878
  ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
2823
3879
  ...overrides?.headers ? { headers: overrides.headers } : {},
2824
3880
  ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
2825
3881
  ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
2826
3882
  ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
2827
- }),
3883
+ }, requestBindingProvider),
2828
3884
  resolution
2829
3885
  };
2830
3886
  }
2831
-
2832
- // ../contracts/dist/client/storage.js
2833
- var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
2834
- var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
2835
- var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
2836
- var DEPRECATION_REGISTRY2 = Symbol.for("hasna:contracts:credentialDeprecationNotices");
2837
- var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
2838
- var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
2839
- "host",
2840
- ":authority",
2841
- "forwarded",
2842
- "x-forwarded-host",
2843
- "x-original-host"
2844
- ]);
2845
3887
  function resourcePath(resource) {
2846
3888
  const trimmed = resource.replace(/^\/+|\/+$/g, "");
2847
3889
  if (!trimmed)
@@ -2941,145 +3983,336 @@ function createHasnaStorageClient(name, transport) {
2941
3983
  }
2942
3984
  };
2943
3985
  }
3986
+ function resolveStorageClient(name, env = process.env, overrides) {
3987
+ const wired = createClientTransport(name, env, overrides);
3988
+ return { transport: "http", client: createHasnaStorageClient(name, wired.client) };
3989
+ }
3990
+
3991
+ // src/lib/local-opt-in.ts
3992
+ var RECORDINGS_LOCAL_OPT_IN_ENV_KEYS = [
3993
+ "HASNA_RECORDINGS_LOCAL",
3994
+ "RECORDINGS_LOCAL"
3995
+ ];
3996
+ function isRecordingsLocalOptIn(env = process.env) {
3997
+ return RECORDINGS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
3998
+ }
3999
+ var RECORDINGS_CARVED_LEGACY_ENV_KEYS = [
4000
+ "RECORDINGS_API_URL",
4001
+ "RECORDINGS_API_KEY"
4002
+ ];
4003
+ function recordingsAuthorityEnvKeys() {
4004
+ const keys = clientTransportEnvKeys("recordings");
4005
+ return [
4006
+ ...keys.apiUrlKeys,
4007
+ ...keys.apiKeyKeys,
4008
+ credentialOverrideEnvKey("recordings"),
4009
+ credentialPointerEnvKey("recordings"),
4010
+ CREDENTIAL_PROFILE_ENV_KEY
4011
+ ].filter((key) => !RECORDINGS_CARVED_LEGACY_ENV_KEYS.includes(key));
4012
+ }
4013
+ function hasRecordingsEnvAuthorityIntent(env = process.env) {
4014
+ return recordingsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
4015
+ }
4016
+ function selectsRecordingsLocalStore(env = process.env) {
4017
+ return !hasRecordingsEnvAuthorityIntent(env) && isRecordingsLocalOptIn(env);
4018
+ }
4019
+ function recordingsResolverEnv(env) {
4020
+ const carved = RECORDINGS_CARVED_LEGACY_ENV_KEYS.filter((key) => (key in env));
4021
+ const blanks = recordingsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
4022
+ const removed = new Set([...carved, ...blanks]);
4023
+ if (removed.size === 0)
4024
+ return env;
4025
+ const next = { ...env };
4026
+ for (const key of removed)
4027
+ delete next[key];
4028
+ return next;
4029
+ }
4030
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
4031
+ function isAmbientRecordingsEnv(env) {
4032
+ if (typeof process !== "undefined" && env === process.env) {
4033
+ return true;
4034
+ }
4035
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
4036
+ }
4037
+ function recordingsResolverInputs(env, credentials = {}) {
4038
+ const normalised = recordingsResolverEnv(env);
4039
+ if (normalised === env)
4040
+ return { env: normalised, credentials };
4041
+ const keychain = { ...credentials.keychain };
4042
+ if (keychain.enabled === undefined && keychain.run === undefined) {
4043
+ keychain.enabled = isAmbientRecordingsEnv(env);
4044
+ }
4045
+ return { env: normalised, credentials: { ...credentials, keychain } };
4046
+ }
2944
4047
 
2945
4048
  // src/http/client.ts
2946
- function envToken2(name) {
2947
- return name.toUpperCase().replace(/-/g, "_");
4049
+ var RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
4050
+ var IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4051
+ function throwRecordingsAuthorityFailure(error) {
4052
+ const message = error instanceof Error ? error.message : String(error);
4053
+ const name = error instanceof Error ? error.name : "";
4054
+ if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
4055
+ throw new Error(`REMOTE_API_CREDENTIAL_INVALID: ${message} There is no local fallback: ` + "local SQLite is opt-in only (HASNA_RECORDINGS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
4056
+ }
4057
+ if (/no API key could be resolved/.test(message)) {
4058
+ if (/is not set and no API key could be resolved/.test(message)) {
4059
+ throw new Error("REMOTE_API_CONFIG_MISSING: no Recordings credential resolved from the Keychain item " + `hasna.credentials.recordings.api-key, ~/.hasna/recordings/config/credentials, or HASNA_RECORDINGS_API_KEY. ${message} ` + "There is no local fallback: local SQLite is opt-in only (HASNA_RECORDINGS_LOCAL=1, alias RECORDINGS_LOCAL=1) " + "and is disabled by default \u2014 failing closed instead of serving the local store", { cause: error });
4060
+ }
4061
+ throw new Error("REMOTE_API_KEY_MISSING: remote Recordings storage requires HASNA_RECORDINGS_API_KEY, the Keychain item " + `hasna.credentials.recordings.api-key, or ~/.hasna/recordings/config/credentials. ${message} ` + "There is no local fallback: local SQLite is opt-in only (HASNA_RECORDINGS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
4062
+ }
4063
+ throw new Error(`REMOTE_API_URL_INVALID: ${message} local SQLite fallback is disabled`, { cause: error });
2948
4064
  }
2949
- function envKeys(name) {
2950
- const token = envToken2(name);
4065
+ function toRecordsAuthority(resolution) {
2951
4066
  return {
2952
- storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
2953
- apiUrlKeys: [`HASNA_${token}_API_URL`],
2954
- apiKeyKeys: [`HASNA_${token}_API_KEY`]
4067
+ transport: resolution.transport,
4068
+ transportSource: resolution.transportSource,
4069
+ baseUrl: resolution.baseUrl,
4070
+ apiUrlSource: resolution.apiUrlSource,
4071
+ apiKeyPresent: resolution.apiKeyPresent,
4072
+ apiKeySource: resolution.apiKeySource,
4073
+ apiKeyTier: resolution.apiKeyTier,
4074
+ misconfigured: resolution.misconfigured,
4075
+ warning: resolution.warning
2955
4076
  };
2956
4077
  }
2957
- function normalizeClientStore(value) {
2958
- const normalized = value.trim().toLowerCase();
2959
- if (normalized === "sqlite")
2960
- return "sqlite";
2961
- if (normalized === "http" || normalized === "https")
2962
- return "http";
2963
- throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
2964
- }
2965
- function firstEnv2(env, keys) {
2966
- for (const key of keys) {
2967
- const value = env[key]?.trim();
2968
- if (value)
2969
- return { key, value };
4078
+ function resolveRecordingsTransport(env = process.env, options = {}) {
4079
+ if (selectsRecordingsLocalStore(env)) {
4080
+ return { transport: "sqlite", selected: false, source: "local-opt-in", authority: null };
2970
4081
  }
2971
- return null;
2972
- }
2973
- function toV1BaseUrl2(apiUrl) {
2974
- const url = new URL(apiUrl);
2975
- if (url.protocol !== "http:" && url.protocol !== "https:") {
2976
- throw new Error("API URL must use http or https.");
4082
+ const resolverInputs = recordingsResolverInputs(env, options.credentials);
4083
+ let resolution;
4084
+ try {
4085
+ resolution = resolveClientTransport("recordings", resolverInputs.env, {
4086
+ credentials: resolverInputs.credentials
4087
+ });
4088
+ } catch (error) {
4089
+ throwRecordingsAuthorityFailure(error);
2977
4090
  }
2978
- let path = url.pathname.replace(/\/+$/, "");
2979
- if (path.endsWith("/v1"))
2980
- path = path.slice(0, -"/v1".length);
2981
- url.pathname = `${path}/v1`;
2982
- url.search = "";
2983
- url.hash = "";
2984
- return url.toString().replace(/\/+$/, "");
4091
+ return {
4092
+ transport: "http",
4093
+ selected: true,
4094
+ source: `${resolution.apiKeySource ?? resolution.apiKeyTier}+${resolution.apiUrlSource ?? "default"}`,
4095
+ authority: toRecordsAuthority(resolution)
4096
+ };
2985
4097
  }
2986
- function resolveTransport(name, env = process.env) {
2987
- const keys = envKeys(name);
2988
- const storeHit = firstEnv2(env, keys.storeKeys);
2989
- const urlHit = firstEnv2(env, keys.apiUrlKeys);
2990
- const keyHit = firstEnv2(env, keys.apiKeyKeys);
2991
- let requested = "sqlite";
2992
- let modeSource = "default";
2993
- if (storeHit) {
2994
- requested = normalizeClientStore(storeHit.value);
2995
- modeSource = storeHit.key;
2996
- } else if (urlHit && keyHit) {
2997
- requested = "http";
2998
- modeSource = "auto:api-url+api-key";
2999
- } else if (urlHit || keyHit) {
3000
- const missing = urlHit ? keys.apiKeyKeys[0] : keys.apiUrlKeys[0];
3001
- const present = urlHit ? keys.apiUrlKeys[0] : keys.apiKeyKeys[0];
4098
+ function getRecordingsTransportStatus(env = process.env, options = {}) {
4099
+ let resolution;
4100
+ try {
4101
+ resolution = resolveRecordingsTransport(env, options);
4102
+ } catch (error) {
4103
+ const issue = error instanceof Error ? error.message : String(error);
4104
+ const envKeys = clientTransportEnvKeys("recordings");
4105
+ const declared = (keys) => keys.some((key) => (env[key] ?? "").trim() !== "");
3002
4106
  return {
3003
- transport: "sqlite",
3004
- requested,
3005
- modeSource,
3006
- baseUrl: null,
3007
- apiKeyPresent: Boolean(keyHit),
3008
- misconfigured: true,
3009
- warning: `${present} is set but ${missing} is not: the hosted API is only ` + `selected when BOTH are present. Set ${missing}, or unset ${present} to ` + `use the on-box store.`
4107
+ selected: true,
4108
+ ok: false,
4109
+ transport: "invalid",
4110
+ api_url_configured: declared(envKeys.apiUrlKeys),
4111
+ api_key_configured: declared(envKeys.apiKeyKeys),
4112
+ api_url_source: null,
4113
+ api_key_source: null,
4114
+ api_key_tier: null,
4115
+ v1_base_url: null,
4116
+ issues: [issue],
4117
+ local_fallback: false
3010
4118
  };
3011
4119
  }
3012
- if (requested === "sqlite") {
3013
- return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
3014
- }
3015
- if (!urlHit) {
4120
+ if (!resolution.selected) {
3016
4121
  return {
3017
- transport: "sqlite",
3018
- requested,
3019
- modeSource,
3020
- baseUrl: null,
3021
- apiKeyPresent: Boolean(keyHit),
3022
- misconfigured: true,
3023
- warning: `${modeSource}=http but no API URL is set (${keys.apiUrlKeys[0]}). Refusing to route to the API.`
4122
+ selected: false,
4123
+ ok: true,
4124
+ transport: resolution.transport,
4125
+ api_url_configured: false,
4126
+ api_key_configured: false,
4127
+ api_url_source: null,
4128
+ api_key_source: null,
4129
+ api_key_tier: null,
4130
+ v1_base_url: null,
4131
+ issues: [],
4132
+ local_fallback: false
3024
4133
  };
3025
4134
  }
3026
- if (!keyHit) {
3027
- return {
3028
- transport: "sqlite",
3029
- requested,
3030
- modeSource,
3031
- baseUrl: null,
3032
- apiKeyPresent: false,
3033
- misconfigured: true,
3034
- warning: `${modeSource}=http but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to the API.`
3035
- };
4135
+ const authority = resolution.authority;
4136
+ return {
4137
+ selected: true,
4138
+ ok: true,
4139
+ transport: resolution.transport,
4140
+ api_url_configured: authority.apiUrlSource !== null && authority.apiUrlSource !== "default",
4141
+ api_key_configured: authority.apiKeyPresent,
4142
+ api_url_source: authority.apiUrlSource,
4143
+ api_key_source: authority.apiKeySource,
4144
+ api_key_tier: authority.apiKeyTier,
4145
+ v1_base_url: authority.baseUrl,
4146
+ issues: [],
4147
+ local_fallback: false
4148
+ };
4149
+ }
4150
+ function resolveRecordingsCloudClient(env = process.env, options = {}) {
4151
+ if (selectsRecordingsLocalStore(env))
4152
+ return null;
4153
+ const resolverInputs = recordingsResolverInputs(env, options.credentials);
4154
+ let resolved;
4155
+ try {
4156
+ resolved = resolveStorageClient("recordings", resolverInputs.env, {
4157
+ fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
4158
+ credentials: resolverInputs.credentials
4159
+ });
4160
+ } catch (error) {
4161
+ throwRecordingsAuthorityFailure(error);
3036
4162
  }
3037
- const rawUrl = urlHit.value;
3038
- let baseUrl;
4163
+ const client = resolved.client;
4164
+ return client;
4165
+ }
4166
+
4167
+ // src/store.ts
4168
+ import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
4169
+
4170
+ // src/lib/audio-artifact-storage.ts
4171
+ import { GetObjectCommand, PutObjectCommand, S3Client as AwsS3Client } from "@aws-sdk/client-s3";
4172
+ import { createHash } from "crypto";
4173
+ import { readFile as readFile2 } from "fs/promises";
4174
+ import { extname } from "path";
4175
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
4176
+ var DEFAULT_S3_PREFIX = "recordings";
4177
+ var AUDIO_CONTENT_TYPES = {
4178
+ wav: "audio/wav",
4179
+ mp3: "audio/mpeg",
4180
+ m4a: "audio/mp4",
4181
+ aac: "audio/aac",
4182
+ webm: "audio/webm",
4183
+ ogg: "audio/ogg",
4184
+ flac: "audio/flac"
4185
+ };
4186
+ function contentTypeForExtension(extension) {
4187
+ return AUDIO_CONTENT_TYPES[extension.toLowerCase()] ?? "application/octet-stream";
4188
+ }
4189
+
4190
+ class AudioArtifactStorage {
4191
+ bucket;
4192
+ prefix;
4193
+ s3;
4194
+ constructor(options = {}) {
4195
+ this.bucket = options.bucket;
4196
+ this.prefix = (options.prefix || DEFAULT_S3_PREFIX).replace(/^\/+|\/+$/g, "");
4197
+ this.s3 = this.bucket ? options.client ?? new AwsS3Client({ region: options.region || process.env.AWS_REGION || "us-east-1" }) : undefined;
4198
+ }
4199
+ get usesS3() {
4200
+ return Boolean(this.bucket);
4201
+ }
4202
+ objectKeyFor(recordingId, sha256, extension) {
4203
+ assertSha256(sha256);
4204
+ const safeExtension = extension.toLowerCase().match(/^[a-z0-9]+/)?.[0] || "bin";
4205
+ return `${this.prefix}/${recordingId}/${sha256}.${safeExtension}`;
4206
+ }
4207
+ async uploadAudio(recordingId, audioPath) {
4208
+ if (!this.bucket || !this.s3)
4209
+ return null;
4210
+ const bytes = await readFileBytes(audioPath);
4211
+ if (!bytes)
4212
+ return null;
4213
+ const sha256 = sha256Hex(bytes);
4214
+ const extension = extensionOf(audioPath);
4215
+ const key = this.objectKeyFor(recordingId, sha256, extension);
4216
+ await this.s3.send(new PutObjectCommand({
4217
+ Bucket: this.bucket,
4218
+ Key: key,
4219
+ Body: bytes,
4220
+ ContentType: contentTypeForExtension(extension)
4221
+ }));
4222
+ return { objectKey: key, sha256, bytes: bytes.byteLength };
4223
+ }
4224
+ async readAudio(objectKey) {
4225
+ if (!this.bucket || !this.s3)
4226
+ return null;
4227
+ try {
4228
+ const response = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }));
4229
+ return await bodyToBytes(response.Body);
4230
+ } catch {
4231
+ return null;
4232
+ }
4233
+ }
4234
+ }
4235
+ function resolveAudioArtifactStorage(env = process.env, client) {
4236
+ const bucket = env.HASNA_RECORDINGS_S3_BUCKET || env.RECORDINGS_S3_BUCKET || undefined;
4237
+ const prefix = env.HASNA_RECORDINGS_S3_PREFIX || env.RECORDINGS_S3_PREFIX || undefined;
4238
+ const region = env.RECORDINGS_S3_REGION || env.AWS_REGION || undefined;
4239
+ return new AudioArtifactStorage({ bucket, prefix, region, client });
4240
+ }
4241
+ async function uploadAudioAtCreation(recordingId, audioPath, storage = resolveAudioArtifactStorage()) {
4242
+ if (!audioPath)
4243
+ return null;
4244
+ if (!storage.usesS3)
4245
+ return null;
3039
4246
  try {
3040
- baseUrl = toV1BaseUrl2(rawUrl);
4247
+ const uploaded = await storage.uploadAudio(recordingId, audioPath);
4248
+ if (!uploaded)
4249
+ return null;
4250
+ console.warn(`recordings: uploaded audio for ${recordingId} to ${uploaded.objectKey}`);
4251
+ return uploaded;
3041
4252
  } catch (error) {
3042
- const message = error instanceof Error ? error.message : String(error);
3043
- return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
4253
+ console.warn(`recordings: audio upload failed for ${recordingId} (${String(error instanceof Error ? error.message : error)}); keeping local audio_path only`);
4254
+ return null;
3044
4255
  }
3045
- return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
3046
4256
  }
3047
- var RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
3048
- var IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
3049
- function resolveStoreClient(name, env = process.env) {
3050
- const resolution = resolveTransport(name, env);
3051
- if (resolution.misconfigured) {
3052
- const wired2 = createClientTransport(name, env);
3053
- if (wired2.transport === "http") {
3054
- return {
3055
- transport: "http",
3056
- client: createHasnaStorageClient(name, wired2.client),
3057
- resolution: {
3058
- transport: "http",
3059
- requested: "http",
3060
- modeSource: resolution.modeSource === "default" ? "auto:api-url+seam-credential" : resolution.modeSource,
3061
- baseUrl: wired2.resolution.baseUrl,
3062
- apiKeyPresent: true,
3063
- misconfigured: false,
3064
- warning: null
3065
- }
3066
- };
4257
+ function assertSha256(value) {
4258
+ if (!SHA256_HEX.test(value)) {
4259
+ throw new Error("audio digest must be a lowercase hex sha-256; refusing to build a storage key from it");
4260
+ }
4261
+ }
4262
+ function sha256Hex(bytes) {
4263
+ const hash = createHash("sha256");
4264
+ hash.update(bytes);
4265
+ return hash.digest("hex");
4266
+ }
4267
+ async function readFileBytes(path) {
4268
+ try {
4269
+ return new Uint8Array(await readFile2(path));
4270
+ } catch {
4271
+ return null;
4272
+ }
4273
+ }
4274
+ function extensionOf(path) {
4275
+ const extension = extname(path).replace(/^\./, "").toLowerCase();
4276
+ return extension || "bin";
4277
+ }
4278
+ async function bodyToBytes(body) {
4279
+ if (!body)
4280
+ return new Uint8Array(0);
4281
+ if (body instanceof Uint8Array)
4282
+ return body;
4283
+ if (typeof body.transformToByteArray === "function") {
4284
+ return new Uint8Array(await body.transformToByteArray());
4285
+ }
4286
+ if (typeof body.getReader === "function") {
4287
+ const reader = body.getReader();
4288
+ const chunks2 = [];
4289
+ for (;; ) {
4290
+ const { value, done } = await reader.read();
4291
+ if (done)
4292
+ break;
4293
+ if (value)
4294
+ chunks2.push(value);
3067
4295
  }
3068
- throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
4296
+ return concat(chunks2);
3069
4297
  }
3070
- if (resolution.transport === "sqlite" || !resolution.baseUrl) {
3071
- return { transport: "sqlite", client: null, resolution };
4298
+ const chunks = [];
4299
+ for await (const chunk of body) {
4300
+ chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
3072
4301
  }
3073
- const wired = createClientTransport(name, env);
3074
- if (wired.transport !== "http") {
3075
- throw new Error(`Client for '${name}' resolved to the /v1 API without an API key.`);
4302
+ return concat(chunks);
4303
+ }
4304
+ function concat(chunks) {
4305
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
4306
+ const merged = new Uint8Array(new ArrayBuffer(total));
4307
+ let offset = 0;
4308
+ for (const chunk of chunks) {
4309
+ merged.set(chunk, offset);
4310
+ offset += chunk.byteLength;
3076
4311
  }
3077
- return { transport: "http", client: createHasnaStorageClient(name, wired.client), resolution };
4312
+ return merged;
3078
4313
  }
3079
4314
 
3080
4315
  // src/store.ts
3081
- import { createHash, randomUUID as randomUUID5 } from "crypto";
3082
- var APP = "recordings";
3083
4316
  function listQuery(filter) {
3084
4317
  if (!filter)
3085
4318
  return {};
@@ -3109,7 +4342,15 @@ var localStore = {
3109
4342
  mode: "sqlite",
3110
4343
  baseUrl: null,
3111
4344
  async createRecording(input, idempotencyKey) {
3112
- return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
4345
+ return withLocalStoreReaderLease(async () => {
4346
+ const recording = createRecording(input, undefined, idempotencyKey);
4347
+ if (!recording.audio_path)
4348
+ return recording;
4349
+ const uploaded = await uploadAudioAtCreation(recording.id, recording.audio_path, localArtifactStorageFor());
4350
+ if (!uploaded)
4351
+ return recording;
4352
+ return updateRecordingAudio(recording.id, uploaded.objectKey, uploaded.sha256, uploaded.bytes) ?? recording;
4353
+ });
3113
4354
  },
3114
4355
  async getRecording(id) {
3115
4356
  return withLocalStoreReaderLease(() => getRecording(id));
@@ -3303,7 +4544,7 @@ function apiStore(client) {
3303
4544
  };
3304
4545
  }
3305
4546
  function recordingPageFingerprint(items) {
3306
- const hash = createHash("sha256");
4547
+ const hash = createHash2("sha256");
3307
4548
  const ids = items.map((item) => String(item.id ?? "")).sort();
3308
4549
  for (const id of ids) {
3309
4550
  hash.update(String(id.length));
@@ -3339,19 +4580,23 @@ async function countStoreRecordings(store, filter) {
3339
4580
  throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
3340
4581
  }
3341
4582
  var cached = null;
3342
- function getStore(env = process.env) {
3343
- if (env === process.env && cached)
4583
+ var localArtifactStorageOverride;
4584
+ function localArtifactStorageFor() {
4585
+ return localArtifactStorageOverride ?? resolveAudioArtifactStorage();
4586
+ }
4587
+ function getStore(env = process.env, options = {}) {
4588
+ if (env === process.env && cached && options.credentials === undefined)
3344
4589
  return cached;
3345
- const resolved = resolveStoreClient(APP, env);
3346
- const store = resolved.transport === "http" ? apiStore(resolved.client) : localStore;
3347
- if (env === process.env)
4590
+ const client = resolveRecordingsCloudClient(env, options);
4591
+ const store = client ? apiStore(client) : localStore;
4592
+ if (env === process.env && options.credentials === undefined)
3348
4593
  cached = store;
3349
4594
  return store;
3350
4595
  }
3351
4596
 
3352
4597
  // src/lib/recorder.ts
3353
4598
  import { spawn as spawn2 } from "child_process";
3354
- import { join as join5 } from "path";
4599
+ import { join as join6 } from "path";
3355
4600
  import { existsSync as existsSync3 } from "fs";
3356
4601
 
3357
4602
  // src/types/index.ts
@@ -3402,7 +4647,7 @@ function startRecording(config) {
3402
4647
  }
3403
4648
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
3404
4649
  const filename = `recording-${timestamp}.${config.audio_format}`;
3405
- const filepath = join5(config.audio_dir, filename);
4650
+ const filepath = join6(config.audio_dir, filename);
3406
4651
  const args = buildRecordArgs(filepath, config);
3407
4652
  const [command, ...commandArgs] = args;
3408
4653
  if (command === undefined) {
@@ -3474,7 +4719,7 @@ function buildRecordArgs(filepath, config) {
3474
4719
  async function recordDuration(seconds, config) {
3475
4720
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
3476
4721
  const filename = `recording-${timestamp}.${config.audio_format}`;
3477
- const filepath = join5(config.audio_dir, filename);
4722
+ const filepath = join6(config.audio_dir, filename);
3478
4723
  const args = [
3479
4724
  "rec",
3480
4725
  "-r",
@@ -3512,7 +4757,7 @@ var _client = null;
3512
4757
  var _clientApiKey = null;
3513
4758
  function getClient(config) {
3514
4759
  if (!config.openai_api_key) {
3515
- throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
4760
+ throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var (e.g. " + "'secrets exec <key> --as OPENAI_API_KEY -- recordings \u2026')");
3516
4761
  }
3517
4762
  if (_client && _clientApiKey === config.openai_api_key)
3518
4763
  return _client;
@@ -3537,7 +4782,7 @@ async function verifyTranscriptionCredential(config, model, options = {}) {
3537
4782
  status: null,
3538
4783
  model,
3539
4784
  role,
3540
- message: `no API key configured for the ${role} role. ` + "Set OPENAI_API_KEY env var or add to ~/.secrets"
4785
+ message: `no API key configured for the ${role} role. ` + "Set OPENAI_API_KEY env var (e.g. 'secrets exec <key> --as OPENAI_API_KEY -- recordings \u2026')"
3541
4786
  };
3542
4787
  }
3543
4788
  try {
@@ -3646,9 +4891,9 @@ ${trimmed}`;
3646
4891
  }
3647
4892
 
3648
4893
  // src/lib/capture-probe.ts
3649
- import { spawnSync as spawnSync2 } from "child_process";
3650
- import { existsSync as existsSync4, readFileSync as readFileSync3, rmSync as rmSync2 } from "fs";
3651
- import { join as join6 } from "path";
4894
+ import { spawnSync as spawnSync4 } from "child_process";
4895
+ import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
4896
+ import { join as join7 } from "path";
3652
4897
  import { tmpdir } from "os";
3653
4898
 
3654
4899
  // src/lib/macos-bundle.ts
@@ -3664,7 +4909,7 @@ var WAVE_FORMAT_PCM = 1;
3664
4909
  var WAVE_FORMAT_EXTENSIBLE = 65534;
3665
4910
  var SUBFORMAT_OFFSET_IN_EXTENSION = 8;
3666
4911
  function readWavPeak(filepath) {
3667
- const buf = readFileSync3(filepath);
4912
+ const buf = readFileSync4(filepath);
3668
4913
  if (buf.length < RIFF_HEADER_BYTES) {
3669
4914
  throw new Error(`not a RIFF file (${buf.length} bytes): ${filepath}`);
3670
4915
  }
@@ -3739,9 +4984,9 @@ function probeMicrophoneCapture(config, options = {}) {
3739
4984
  peak: 0,
3740
4985
  silent: null
3741
4986
  };
3742
- const filepath = join6(tmpdir(), `recordings-capture-probe-${process.pid}-${Date.now()}.wav`);
4987
+ const filepath = join7(tmpdir(), `recordings-capture-probe-${process.pid}-${Date.now()}.wav`);
3743
4988
  try {
3744
- const result = spawnSync2(executable, [
4989
+ const result = spawnSync4(executable, [
3745
4990
  "-q",
3746
4991
  "-r",
3747
4992
  String(config.sample_rate),
@@ -3839,37 +5084,37 @@ function captureProbeSubject(env = process.env, options = {}) {
3839
5084
  const termProgram = env.TERM_PROGRAM?.trim();
3840
5085
  const hasTty = options.hasTty ?? Boolean(process.stdin.isTTY || process.stdout.isTTY);
3841
5086
  if (overSsh) {
3842
- const subject2 = "the SSH session (sshd), not HasnaRecordings.app";
5087
+ const subject2 = "the SSH session (sshd), not Hasna Recordings.app";
3843
5088
  return {
3844
5089
  headless: true,
3845
5090
  subject_known: true,
3846
5091
  subject: subject2,
3847
- note: "Running over SSH: macOS cannot display a consent prompt to a session with no GUI, " + "so Microphone stays not_determined and a silent capture here says NOTHING about " + "whether HasnaRecordings.app can record. Judge the app by its own TCC entry and its log."
5092
+ note: "Running over SSH: macOS cannot display a consent prompt to a session with no GUI, " + "so Microphone stays not_determined and a silent capture here says NOTHING about " + "whether Hasna Recordings.app can record. Judge the app by its own TCC entry and its log."
3848
5093
  };
3849
5094
  }
3850
5095
  if (inTmux) {
3851
- const subject2 = "the tmux server, not HasnaRecordings.app and not this pane's shell";
5096
+ const subject2 = "the tmux server, not Hasna Recordings.app and not this pane's shell";
3852
5097
  return {
3853
5098
  headless: false,
3854
5099
  subject_known: true,
3855
5100
  subject: subject2,
3856
- note: "Running inside tmux: TCC attributes this capture to the tmux binary, which holds its " + "own grant. tmux also snapshots the environment at pane creation, so SSH variables may " + "be missing even in a remote session \u2014 treat a silent result as inconclusive about both " + "HasnaRecordings.app and about whether anyone could have been prompted."
5101
+ note: "Running inside tmux: TCC attributes this capture to the tmux binary, which holds its " + "own grant. tmux also snapshots the environment at pane creation, so SSH variables may " + "be missing even in a remote session \u2014 treat a silent result as inconclusive about both " + "Hasna Recordings.app and about whether anyone could have been prompted."
3857
5102
  };
3858
5103
  }
3859
5104
  if (!termProgram && !hasTty) {
3860
5105
  return {
3861
5106
  headless: true,
3862
5107
  subject_known: false,
3863
- subject: "an unidentified responsible process (not HasnaRecordings.app)",
3864
- note: "The responsible process could not be identified: no SSH variables, no TERM_PROGRAM and " + "no tty, which is what launchd, cron, CI and sudo look like. Whatever holds the grant, it " + "is not HasnaRecordings.app, and nothing here can be shown a consent prompt. Inconclusive."
5108
+ subject: "an unidentified responsible process (not Hasna Recordings.app)",
5109
+ note: "The responsible process could not be identified: no SSH variables, no TERM_PROGRAM and " + "no tty, which is what launchd, cron, CI and sudo look like. Whatever holds the grant, it " + "is not Hasna Recordings.app, and nothing here can be shown a consent prompt. Inconclusive."
3865
5110
  };
3866
5111
  }
3867
- const subject = `${termProgram || "the terminal application running this command"}, not HasnaRecordings.app`;
5112
+ const subject = `${termProgram || "the terminal application running this command"}, not Hasna Recordings.app`;
3868
5113
  return {
3869
5114
  headless: false,
3870
5115
  subject_known: Boolean(termProgram),
3871
5116
  subject,
3872
- note: `Grants are per responsible process: this probe exercises ${subject}. ` + "A pass proves the microphone hardware and the input device work; it does not " + "transfer to HasnaRecordings.app, which needs its own grant."
5117
+ note: `Grants are per responsible process: this probe exercises ${subject}. ` + "A pass proves the microphone hardware and the input device work; it does not " + "transfer to Hasna Recordings.app, which needs its own grant."
3873
5118
  };
3874
5119
  }
3875
5120
  var TCC_UNREADABLE_STATE = TCC_DATABASE_UNREADABLE_STATE;
@@ -3899,7 +5144,7 @@ function microphoneGrantInstruction(options) {
3899
5144
  const bundlePath = candidates[0] ?? null;
3900
5145
  const steps = [];
3901
5146
  if (!bundlePath) {
3902
- steps.push("No HasnaRecordings.app bundle was found on disk, so there is nothing to grant Microphone to yet. " + "Install the app first ('recordings app install').");
5147
+ steps.push("No Hasna Recordings.app bundle was found on disk, so there is nothing to grant Microphone to yet. " + "Install the app first ('recordings app install').");
3903
5148
  return {
3904
5149
  bundle_path: null,
3905
5150
  bundle_identifier: RECORDINGS_BUNDLE_IDENTIFIER,
@@ -3908,10 +5153,10 @@ function microphoneGrantInstruction(options) {
3908
5153
  };
3909
5154
  }
3910
5155
  if (candidates.length > 1) {
3911
- steps.push(`AMBIGUOUS: ${candidates.length} HasnaRecordings.app bundles exist (${candidates.join(", ")}). ` + "A TCC grant is bound to the bundle's code signature, so granting one does not grant the " + "other, and the toggle in Settings does not say which is which. Remove the bundles you are " + "not running before granting, or the grant may attach to the wrong one.");
5156
+ steps.push(`AMBIGUOUS: ${candidates.length} Hasna Recordings.app bundles exist (${candidates.join(", ")}). ` + "A TCC grant is bound to the bundle's code signature, so granting one does not grant the " + "other, and the toggle in Settings does not say which is which. Remove the bundles you are " + "not running before granting, or the grant may attach to the wrong one.");
3912
5157
  }
3913
5158
  steps.push(`At the keyboard on the machine itself (not over SSH), launch ${bundlePath} and start a ` + "recording once. macOS shows the consent sheet titled " + `"\u201CRecordings\u201D would like to access the microphone" \u2014 click Allow.`);
3914
- steps.push("If no sheet appears, open System Settings \u2192 Privacy & Security \u2192 Microphone " + "and switch ON the row named \u201CRecordings\u201D. " + `That row is bundle ${RECORDINGS_BUNDLE_IDENTIFIER} at ${bundlePath}; the binary that ` + `receives the grant is ${join6(bundlePath, "Contents", "MacOS", "Recordings")}.`);
5159
+ steps.push("If no sheet appears, open System Settings \u2192 Privacy & Security \u2192 Microphone " + "and switch ON the row named \u201CRecordings\u201D. " + `That row is bundle ${RECORDINGS_BUNDLE_IDENTIFIER} at ${bundlePath}; the binary that ` + `receives the grant is ${join7(bundlePath, "Contents", "MacOS", "Recordings")}.`);
3915
5160
  if (options.requestState === "never_requested") {
3916
5161
  steps.push("Note: the app has never requested microphone access on this machine (no TCC entry exists), " + "so the Microphone list will NOT contain a \u201CRecordings\u201D row until the app asks once. " + "Do the launch-and-record step first; the Settings toggle only exists afterwards.");
3917
5162
  } else if (options.requestState === "unknown") {
@@ -3930,7 +5175,7 @@ function microphoneGrantInstruction(options) {
3930
5175
  // src/lib/persistence-probe.ts
3931
5176
  import { Database as Database2 } from "bun:sqlite";
3932
5177
  import { existsSync as existsSync5 } from "fs";
3933
- var AUTO_FLIP_MODE_SOURCE = "auto:api-url+api-key";
5178
+ var ENV_CREDENTIAL_MODE_SOURCE = "env";
3934
5179
  function safeBaseUrl(baseUrl) {
3935
5180
  if (!baseUrl)
3936
5181
  return baseUrl;
@@ -3974,42 +5219,51 @@ function localStoreIsBehindSchema(dbPath) {
3974
5219
  return null;
3975
5220
  }
3976
5221
  }
3977
- function describeActiveStore(config, env = process.env) {
5222
+ function describeActiveStore(config, env = process.env, options = {}) {
3978
5223
  const localDbPath = config.db_path;
3979
5224
  const localDbPresent = existsSync5(localDbPath);
3980
- const localDbRecordings = localDbPresent ? readLocalRecordingCount(localDbPath) : null;
3981
- let resolution;
3982
- try {
3983
- resolution = resolveTransport(APP, env);
3984
- } catch (error) {
5225
+ const status = getRecordingsTransportStatus(env, options);
5226
+ if (!status.ok) {
5227
+ const issues = status.issues.length > 0 ? status.issues.join(" ") : null;
3985
5228
  return {
3986
- transport: "sqlite",
5229
+ transport: "none",
3987
5230
  mode_source: "unresolved",
3988
5231
  base_url: null,
3989
5232
  local_db_path: localDbPath,
3990
5233
  local_db_present: localDbPresent,
3991
- local_db_recordings: localDbRecordings,
5234
+ local_db_recordings: null,
5235
+ divergent: false,
5236
+ warning: issues ? redactKeyMaterial(issues) : null
5237
+ };
5238
+ }
5239
+ if (!status.selected) {
5240
+ return {
5241
+ transport: "sqlite",
5242
+ mode_source: "local-opt-in",
5243
+ base_url: null,
5244
+ local_db_path: localDbPath,
5245
+ local_db_present: localDbPresent,
5246
+ local_db_recordings: localDbPresent ? readLocalRecordingCount(localDbPath) : null,
3992
5247
  divergent: false,
3993
- warning: redactKeyMaterial(`could not resolve the active store: ${error.message}`)
5248
+ warning: null
3994
5249
  };
3995
5250
  }
3996
- const divergent = resolution.transport === "http" && (localDbRecordings ?? 0) > 0;
5251
+ const localDbRecordings = localDbPresent ? readLocalRecordingCount(localDbPath) : null;
5252
+ const divergent = (localDbRecordings ?? 0) > 0;
3997
5253
  const warnings = [];
3998
- if (resolution.warning)
3999
- warnings.push(resolution.warning);
4000
- if (resolution.transport === "http" && localDbPresent && localDbRecordings === null) {
4001
- warnings.push(`writes go to ${safeBaseUrl(resolution.baseUrl)}, and ${localDbPath} exists but could not be counted, ` + "so whether a second dataset is sitting there is UNKNOWN \u2014 not 'none'.");
5254
+ if (status.api_key_tier === ENV_CREDENTIAL_MODE_SOURCE) {
5255
+ warnings.push("the credential came from the process environment (HASNA_RECORDINGS_API_KEY): a shell export is a " + "snapshot taken when the shell started, so a rotation stays invisible until that shell exits. " + "Check `launchctl getenv HASNA_RECORDINGS_API_KEY` too: a launchd session variable " + "is inherited by the GUI app as well as by shells, and is invisible in a login profile. " + "Prefer the Keychain item hasna.credentials.recordings.api-key or " + "~/.hasna/recordings/config/credentials, both re-read on every call.");
4002
5256
  }
4003
- if (divergent) {
4004
- warnings.push(`writes go to ${safeBaseUrl(resolution.baseUrl)}, but ${localDbPath} still holds ` + `${localDbRecordings} recordings from an earlier on-box-only period. ` + "That file is NOT the live store \u2014 auditing it undercounts and looks like data loss.");
5257
+ if (localDbPresent && localDbRecordings === null) {
5258
+ warnings.push(`writes go to ${safeBaseUrl(status.v1_base_url)}, and ${localDbPath} exists but could not be counted, ` + "so whether a second dataset is sitting there is UNKNOWN \u2014 not 'none'.");
4005
5259
  }
4006
- if (resolution.transport === "http" && resolution.modeSource === AUTO_FLIP_MODE_SOURCE) {
4007
- warnings.push("the API transport was selected by the mere PRESENCE of " + "HASNA_RECORDINGS_API_URL + HASNA_RECORDINGS_API_KEY. " + "Check `launchctl getenv HASNA_RECORDINGS_API_URL` too: a launchd session variable " + "is inherited by the GUI app as well as by shells, and is invisible in a login profile.");
5260
+ if (divergent) {
5261
+ warnings.push(`writes go to ${safeBaseUrl(status.v1_base_url)}, but ${localDbPath} still holds ` + `${localDbRecordings} recordings from an earlier on-box-only period. ` + "That file is NOT the live store \u2014 auditing it undercounts and looks like data loss.");
4008
5262
  }
4009
5263
  return {
4010
- transport: resolution.transport,
4011
- mode_source: resolution.modeSource,
4012
- base_url: safeBaseUrl(resolution.baseUrl),
5264
+ transport: "http",
5265
+ mode_source: `${status.api_key_source ?? status.api_key_tier}+${status.api_url_source ?? "default"}`,
5266
+ base_url: safeBaseUrl(status.v1_base_url),
4013
5267
  local_db_path: localDbPath,
4014
5268
  local_db_present: localDbPresent,
4015
5269
  local_db_recordings: localDbRecordings,
@@ -4017,6 +5271,20 @@ function describeActiveStore(config, env = process.env) {
4017
5271
  warning: warnings.length > 0 ? warnings.join(" ") : null
4018
5272
  };
4019
5273
  }
5274
+ function activeStoreFailClosed(description) {
5275
+ return description.transport === "none";
5276
+ }
5277
+ function describeActiveStoreLine(description, paint) {
5278
+ if (activeStoreFailClosed(description)) {
5279
+ const refusal = description.warning ? ` (${description.warning})` : "";
5280
+ const file = description.local_db_present ? `${description.local_db_path} is present but NOT opened` : `${description.local_db_path} absent`;
5281
+ return paint.fail(`\u2717 Active store: none \u2014 fail-closed${refusal}; ${file}`);
5282
+ }
5283
+ if (description.transport === "sqlite") {
5284
+ return paint.pass(`\u2713 Active store: sqlite \u2192 ${description.local_db_path} (selected by ${description.mode_source})`);
5285
+ }
5286
+ return paint.pass(`\u2713 Active store: http \u2192 ${description.base_url ?? "?"} (selected by ${description.mode_source})`);
5287
+ }
4020
5288
  var PROBE_SEVERITY_GLYPHS = {
4021
5289
  pass: "\u2713",
4022
5290
  warn: "?",
@@ -4443,13 +5711,13 @@ ${block}` : block;
4443
5711
  }
4444
5712
 
4445
5713
  // src/cli/macos-permissions.ts
4446
- import { spawnSync as spawnSync3 } from "child_process";
4447
- import { existsSync as existsSync6, mkdtempSync, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
5714
+ import { spawnSync as spawnSync5 } from "child_process";
5715
+ import { existsSync as existsSync6, mkdtempSync, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
4448
5716
  import { tmpdir as tmpdir2 } from "os";
4449
- import { join as join7 } from "path";
4450
- var defaultPermissionHelperRunner = (executable, arguments_, options) => spawnSync3(executable, arguments_, options);
5717
+ import { join as join8 } from "path";
5718
+ var defaultPermissionHelperRunner = (executable, arguments_, options) => spawnSync5(executable, arguments_, options);
4451
5719
  function runMacOSPermissionRequest(appPath, runner = defaultPermissionHelperRunner) {
4452
- const executable = join7(appPath, "Contents", "MacOS", "Recordings");
5720
+ const executable = join8(appPath, "Contents", "MacOS", "Recordings");
4453
5721
  const result = runner(executable, ["--request-permissions", "--open-permission-settings"], { stdio: "inherit" });
4454
5722
  return {
4455
5723
  exitCode: result.error ? 1 : result.status ?? 1,
@@ -4460,8 +5728,8 @@ var CODESIGN_REQUIREMENT_SATISFIED_STATUS = 0;
4460
5728
  var CODESIGN_REQUIREMENT_UNSATISFIED_STATUS = 3;
4461
5729
  function tccDatabasePaths(home) {
4462
5730
  return [
4463
- join7(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
4464
- join7("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
5731
+ join8(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
5732
+ join8("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
4465
5733
  ];
4466
5734
  }
4467
5735
  function tccAuthValueLabel(value) {
@@ -4478,7 +5746,7 @@ function tccAuthValueLabel(value) {
4478
5746
  return `unknown(${value})`;
4479
5747
  }
4480
5748
  }
4481
- var defaultCodesignRequirementRunner = (requirementPath, appPath) => spawnSync3("/usr/bin/codesign", ["--verify", "--verbose=2", "-R", requirementPath, appPath], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
5749
+ var defaultCodesignRequirementRunner = (requirementPath, appPath) => spawnSync5("/usr/bin/codesign", ["--verify", "--verbose=2", "-R", requirementPath, appPath], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
4482
5750
  function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = defaultCodesignRequirementRunner, bundleExists = existsSync6) {
4483
5751
  const normalized = csreqHex.trim();
4484
5752
  if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(normalized)) {
@@ -4488,8 +5756,8 @@ function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = default
4488
5756
  return "unverifiable";
4489
5757
  let scratchDirectory = null;
4490
5758
  try {
4491
- scratchDirectory = mkdtempSync(join7(tmpdir2(), "recordings-tcc-csreq-"));
4492
- const requirementPath = join7(scratchDirectory, "tcc-requirement.bin");
5759
+ scratchDirectory = mkdtempSync(join8(tmpdir2(), "recordings-tcc-csreq-"));
5760
+ const requirementPath = join8(scratchDirectory, "tcc-requirement.bin");
4493
5761
  writeFileSync2(requirementPath, Buffer.from(normalized, "hex"));
4494
5762
  const result = runner(requirementPath, appPath);
4495
5763
  if (result.error)
@@ -4509,7 +5777,7 @@ function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = default
4509
5777
  var defaultTccPermissionProbe = {
4510
5778
  databasePresence: (dbPath) => {
4511
5779
  try {
4512
- statSync3(dbPath);
5780
+ statSync2(dbPath);
4513
5781
  return "present";
4514
5782
  } catch (error) {
4515
5783
  return error.code === "ENOENT" ? "absent" : "indeterminate";
@@ -4517,7 +5785,7 @@ var defaultTccPermissionProbe = {
4517
5785
  },
4518
5786
  readAccessRow: (dbPath, service) => {
4519
5787
  const sql = "select ifnull(auth_value, '') || '|' || ifnull(hex(csreq), '') from access where service = '" + service.replace(/'/g, "''") + "' and client = '" + RECORDINGS_BUNDLE_IDENTIFIER + "' and client_type = 0 order by last_modified desc limit 1;";
4520
- const result = spawnSync3("/usr/bin/sqlite3", [dbPath, sql], {
5788
+ const result = spawnSync5("/usr/bin/sqlite3", [dbPath, sql], {
4521
5789
  encoding: "utf8",
4522
5790
  stdio: ["ignore", "pipe", "pipe"]
4523
5791
  });
@@ -4538,7 +5806,7 @@ var defaultTccPermissionProbe = {
4538
5806
  verifyStoredRequirement: (csreqHex, appPath) => verifyStoredRequirementWithCodesign(csreqHex, appPath),
4539
5807
  describeStoredRequirement: (csreqHex) => describeStoredRequirementWithCsreq(csreqHex)
4540
5808
  };
4541
- function describeStoredRequirementWithCsreq(csreqHex, runner = (requirementPath) => spawnSync3("/usr/bin/csreq", ["-r", requirementPath, "-t"], {
5809
+ function describeStoredRequirementWithCsreq(csreqHex, runner = (requirementPath) => spawnSync5("/usr/bin/csreq", ["-r", requirementPath, "-t"], {
4542
5810
  encoding: "utf8",
4543
5811
  stdio: ["ignore", "pipe", "ignore"]
4544
5812
  })) {
@@ -4548,8 +5816,8 @@ function describeStoredRequirementWithCsreq(csreqHex, runner = (requirementPath)
4548
5816
  }
4549
5817
  let scratchDirectory = null;
4550
5818
  try {
4551
- scratchDirectory = mkdtempSync(join7(tmpdir2(), "recordings-tcc-decode-"));
4552
- const requirementPath = join7(scratchDirectory, "tcc-requirement.bin");
5819
+ scratchDirectory = mkdtempSync(join8(tmpdir2(), "recordings-tcc-decode-"));
5820
+ const requirementPath = join8(scratchDirectory, "tcc-requirement.bin");
4553
5821
  writeFileSync2(requirementPath, Buffer.from(normalized, "hex"));
4554
5822
  const result = runner(requirementPath);
4555
5823
  if (result.error || result.status !== 0)
@@ -4638,7 +5906,7 @@ function describeTccAuthorizationSubject(appPath) {
4638
5906
  }
4639
5907
 
4640
5908
  // src/cli/macos-shortcut.ts
4641
- import { spawnSync as spawnSync4 } from "child_process";
5909
+ import { spawnSync as spawnSync6 } from "child_process";
4642
5910
  import { existsSync as existsSync7 } from "fs";
4643
5911
  var TRIGGER_DEFAULTS_EXECUTABLE = process.platform === "darwin" ? "/usr/bin/defaults" : process.env.RECORDINGS_TEST_DEFAULTS_EXECUTABLE ?? null;
4644
5912
  var RECORDINGS_BUNDLE_ID = RECORDINGS_BUNDLE_IDENTIFIER;
@@ -4845,14 +6113,14 @@ var defaultBundleIdentifierReader = (bundlePath) => {
4845
6113
  if (!existsSync7(`${bundlePath}/Contents/Info.plist`) && !existsSync7(`${bundlePath}/Contents/Info`)) {
4846
6114
  return null;
4847
6115
  }
4848
- const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["read", `${bundlePath}/Contents/Info`, "CFBundleIdentifier"], { encoding: "utf8" });
6116
+ const result = spawnSync6(TRIGGER_DEFAULTS_EXECUTABLE, ["read", `${bundlePath}/Contents/Info`, "CFBundleIdentifier"], { encoding: "utf8" });
4849
6117
  if (result.status !== 0)
4850
6118
  return null;
4851
6119
  return result.stdout.trim() || null;
4852
6120
  };
4853
6121
  var PROCESS_LISTER_EXECUTABLE = process.platform === "darwin" ? "/bin/ps" : process.env.RECORDINGS_TEST_PS_EXECUTABLE ?? "/bin/ps";
4854
6122
  var defaultProcessLister = () => {
4855
- const result = spawnSync4(PROCESS_LISTER_EXECUTABLE, ["-Awwo", "comm="], { encoding: "utf8" });
6123
+ const result = spawnSync6(PROCESS_LISTER_EXECUTABLE, ["-Awwo", "comm="], { encoding: "utf8" });
4856
6124
  if (result.status !== 0)
4857
6125
  return null;
4858
6126
  return result.stdout;
@@ -4897,7 +6165,7 @@ function isThisApp(candidate, readBundleIdentifier) {
4897
6165
  function readDefault(key) {
4898
6166
  if (TRIGGER_DEFAULTS_EXECUTABLE === null)
4899
6167
  return null;
4900
- const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["read", RECORDINGS_BUNDLE_ID, key], {
6168
+ const result = spawnSync6(TRIGGER_DEFAULTS_EXECUTABLE, ["read", RECORDINGS_BUNDLE_ID, key], {
4901
6169
  encoding: "utf8"
4902
6170
  });
4903
6171
  if (result.status !== 0)
@@ -4908,7 +6176,7 @@ function writeDefault(key, value) {
4908
6176
  if (TRIGGER_DEFAULTS_EXECUTABLE === null) {
4909
6177
  throw new Error(`cannot write ${key}: this machine has no readable app UserDefaults domain`);
4910
6178
  }
4911
- const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, key, "-string", value], {
6179
+ const result = spawnSync6(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, key, "-string", value], {
4912
6180
  encoding: "utf8"
4913
6181
  });
4914
6182
  if (result.status !== 0) {
@@ -4944,14 +6212,14 @@ function writeUseFnKey(enabled) {
4944
6212
  if (TRIGGER_DEFAULTS_EXECUTABLE === null) {
4945
6213
  throw new Error(`cannot write ${USE_FN_KEY_DEFAULTS_KEY}: this machine has no readable app UserDefaults domain`);
4946
6214
  }
4947
- const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, USE_FN_KEY_DEFAULTS_KEY, "-bool", enabled ? "true" : "false"], { encoding: "utf8" });
6215
+ const result = spawnSync6(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, USE_FN_KEY_DEFAULTS_KEY, "-bool", enabled ? "true" : "false"], { encoding: "utf8" });
4948
6216
  if (result.status !== 0) {
4949
6217
  throw new Error(`defaults write ${USE_FN_KEY_DEFAULTS_KEY} failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
4950
6218
  }
4951
6219
  }
4952
6220
 
4953
6221
  // src/cli/trigger-probe.ts
4954
- import { closeSync, openSync, readSync, statSync as statSync4 } from "fs";
6222
+ import { closeSync as closeSync3, openSync as openSync3, readSync, statSync as statSync3 } from "fs";
4955
6223
  var FN_BLOCKING_ACCESSIBILITY_STATES = [
4956
6224
  "denied",
4957
6225
  "stale_allowed_for_previous_app_build"
@@ -5070,12 +6338,12 @@ function readAppLogTail(logPath, maxBytes = APP_LOG_TAIL_BYTES) {
5070
6338
  return null;
5071
6339
  let handle = null;
5072
6340
  try {
5073
- const size = statSync4(logPath).size;
6341
+ const size = statSync3(logPath).size;
5074
6342
  if (size === 0)
5075
6343
  return null;
5076
6344
  const length = Math.min(size, maxBytes);
5077
6345
  const buffer = Buffer.allocUnsafe(length);
5078
- handle = openSync(logPath, "r");
6346
+ handle = openSync3(logPath, "r");
5079
6347
  const read = readSync(handle, buffer, 0, length, size - length);
5080
6348
  return buffer.subarray(0, read).toString("utf8");
5081
6349
  } catch {
@@ -5083,7 +6351,7 @@ function readAppLogTail(logPath, maxBytes = APP_LOG_TAIL_BYTES) {
5083
6351
  } finally {
5084
6352
  if (handle !== null) {
5085
6353
  try {
5086
- closeSync(handle);
6354
+ closeSync3(handle);
5087
6355
  } catch {}
5088
6356
  }
5089
6357
  }
@@ -5190,10 +6458,10 @@ function currentMachineId(env = process.env, hostName = hostname()) {
5190
6458
  }
5191
6459
 
5192
6460
  // src/lib/bun-runtime.ts
5193
- import { accessSync, constants as fsConstants, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
5194
- import { isAbsolute as isAbsolute2 } from "path";
6461
+ import { accessSync, constants as fsConstants, realpathSync as realpathSync3, statSync as statSync4 } from "fs";
6462
+ import { isAbsolute as isAbsolute4 } from "path";
5195
6463
  import { randomBytes } from "crypto";
5196
- import { spawnSync as spawnSync5 } from "child_process";
6464
+ import { spawnSync as spawnSync7 } from "child_process";
5197
6465
  var BUN_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
5198
6466
  var INSTALLER_ENVIRONMENT_KEYS = [
5199
6467
  "HOME",
@@ -5207,19 +6475,19 @@ var INSTALLER_ENVIRONMENT_KEYS = [
5207
6475
  ];
5208
6476
  var INSTALLER_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
5209
6477
  function validateBunExecutable(candidate) {
5210
- if (!isAbsolute2(candidate))
6478
+ if (!isAbsolute4(candidate))
5211
6479
  return { reason: "path is not absolute" };
5212
6480
  let executable;
5213
6481
  try {
5214
6482
  executable = realpathSync3(candidate);
5215
- if (!statSync5(executable).isFile())
6483
+ if (!statSync4(executable).isFile())
5216
6484
  return { reason: "resolved path is not a regular file" };
5217
6485
  accessSync(executable, fsConstants.X_OK);
5218
6486
  } catch {
5219
6487
  return { reason: "path is missing, inaccessible, or not executable" };
5220
6488
  }
5221
6489
  const nonce = randomBytes(24).toString("hex");
5222
- const probe = spawnSync5(executable, [
6490
+ const probe = spawnSync7(executable, [
5223
6491
  "-e",
5224
6492
  `
5225
6493
  import { realpathSync, statSync } from "node:fs";
@@ -5283,20 +6551,20 @@ function createInstallerEnvironment(environment, bunExecutable) {
5283
6551
  }
5284
6552
 
5285
6553
  // src/lib/release-install-policy.ts
5286
- import { createHash as createHash2 } from "crypto";
6554
+ import { createHash as createHash3 } from "crypto";
5287
6555
  import {
5288
6556
  chmodSync as chmodSync2,
5289
- closeSync as closeSync2,
6557
+ closeSync as closeSync4,
5290
6558
  constants,
5291
- fstatSync,
6559
+ fstatSync as fstatSync3,
5292
6560
  fsyncSync,
5293
6561
  mkdtempSync as mkdtempSync2,
5294
- openSync as openSync2,
5295
- readFileSync as readFileSync4,
6562
+ openSync as openSync4,
6563
+ readFileSync as readFileSync5,
5296
6564
  rmSync as rmSync4,
5297
6565
  writeFileSync as writeFileSync3
5298
6566
  } from "fs";
5299
- import { isAbsolute as isAbsolute3, join as join8 } from "path";
6567
+ import { isAbsolute as isAbsolute5, join as join9 } from "path";
5300
6568
  var LOWER_SHA256 = /^[a-f0-9]{64}$/;
5301
6569
  var LOWER_SOURCE_SHA = /^[a-f0-9]{40}$/;
5302
6570
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
@@ -5356,7 +6624,7 @@ function prepareReleaseInstallInputs(input) {
5356
6624
  throw new Error("Team ID must be 10 uppercase alphanumeric characters");
5357
6625
  }
5358
6626
  const manifestBytes = readBoundedRegularFile(input.manifestPath, "manifest", MAX_MANIFEST_BYTES);
5359
- const actualManifestSha256 = createHash2("sha256").update(manifestBytes).digest("hex");
6627
+ const actualManifestSha256 = createHash3("sha256").update(manifestBytes).digest("hex");
5360
6628
  if (actualManifestSha256 !== input.manifestSha256) {
5361
6629
  throw new Error("manifest does not match the operator-approved SHA-256");
5362
6630
  }
@@ -5366,10 +6634,10 @@ function prepareReleaseInstallInputs(input) {
5366
6634
  assertReleaseManifest(manifest, input);
5367
6635
  assertReleaseEnvelope(envelope, manifest, manifestBytes.byteLength, input);
5368
6636
  const snapshotRoot = input.snapshotRoot ?? "/private/tmp";
5369
- if (!isAbsolute3(snapshotRoot)) {
6637
+ if (!isAbsolute5(snapshotRoot)) {
5370
6638
  throw new Error("release snapshot root must be absolute");
5371
6639
  }
5372
- const snapshotDirectory = mkdtempSync2(join8(snapshotRoot, "recordings-release-install."));
6640
+ const snapshotDirectory = mkdtempSync2(join9(snapshotRoot, "recordings-release-install."));
5373
6641
  let cleaned = false;
5374
6642
  const cleanup = () => {
5375
6643
  if (cleaned)
@@ -5381,9 +6649,9 @@ function prepareReleaseInstallInputs(input) {
5381
6649
  rmSync4(snapshotDirectory, { recursive: true, force: true });
5382
6650
  };
5383
6651
  try {
5384
- const manifestSnapshot = join8(snapshotDirectory, `${input.manifestSha256}.manifest.json`);
5385
- const envelopeDigest = createHash2("sha256").update(envelopeBytes).digest("hex");
5386
- const envelopeSnapshot = join8(snapshotDirectory, `${envelopeDigest}.envelope.json`);
6652
+ const manifestSnapshot = join9(snapshotDirectory, `${input.manifestSha256}.manifest.json`);
6653
+ const envelopeDigest = createHash3("sha256").update(envelopeBytes).digest("hex");
6654
+ const envelopeSnapshot = join9(snapshotDirectory, `${envelopeDigest}.envelope.json`);
5387
6655
  writeSnapshot(manifestSnapshot, manifestBytes);
5388
6656
  writeSnapshot(envelopeSnapshot, envelopeBytes);
5389
6657
  chmodSync2(snapshotDirectory, 320);
@@ -5394,19 +6662,19 @@ function prepareReleaseInstallInputs(input) {
5394
6662
  }
5395
6663
  }
5396
6664
  function assertAbsolutePath(value, label) {
5397
- if (!isAbsolute3(value))
6665
+ if (!isAbsolute5(value))
5398
6666
  throw new Error(`${label} path must be absolute`);
5399
6667
  }
5400
6668
  function readBoundedRegularFile(path, label, maximum) {
5401
- const descriptor = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
6669
+ const descriptor = openSync4(path, constants.O_RDONLY | constants.O_NOFOLLOW);
5402
6670
  try {
5403
- const metadata = fstatSync(descriptor);
6671
+ const metadata = fstatSync3(descriptor);
5404
6672
  if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximum) {
5405
6673
  throw new Error(`${label} must be a non-empty bounded regular file`);
5406
6674
  }
5407
- return readFileSync4(descriptor);
6675
+ return readFileSync5(descriptor);
5408
6676
  } finally {
5409
- closeSync2(descriptor);
6677
+ closeSync4(descriptor);
5410
6678
  }
5411
6679
  }
5412
6680
  function parseObject(bytes, label) {
@@ -5486,22 +6754,22 @@ function isObject(value) {
5486
6754
  }
5487
6755
  function writeSnapshot(path, bytes) {
5488
6756
  writeFileSync3(path, bytes, { flag: "wx", mode: 256 });
5489
- const descriptor = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
6757
+ const descriptor = openSync4(path, constants.O_RDONLY | constants.O_NOFOLLOW);
5490
6758
  try {
5491
6759
  fsyncSync(descriptor);
5492
6760
  } finally {
5493
- closeSync2(descriptor);
6761
+ closeSync4(descriptor);
5494
6762
  }
5495
6763
  }
5496
6764
 
5497
6765
  // src/cli/desktop-snapshot.ts
5498
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync3, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync6 } from "fs";
5499
- import { dirname as dirname4, join as join9, resolve as resolve3 } from "path";
5500
- import { spawnSync as spawnSync6 } from "child_process";
6766
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync3, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync5 } from "fs";
6767
+ import { dirname as dirname4, join as join10, resolve as resolve3 } from "path";
6768
+ import { spawnSync as spawnSync8 } from "child_process";
5501
6769
  var SCREEN_CAPTURE_EXECUTABLE = "/usr/sbin/screencapture";
5502
6770
  var DEFAULT_DESKTOP_SNAPSHOT = "desktop-snapshot.png";
5503
6771
  function runScreenCapture(executable, arguments_) {
5504
- const result = spawnSync6(executable, arguments_, {
6772
+ const result = spawnSync8(executable, arguments_, {
5505
6773
  encoding: "utf8",
5506
6774
  stdio: ["ignore", "ignore", "pipe"]
5507
6775
  });
@@ -5518,9 +6786,9 @@ function exportDesktopSnapshot(output = DEFAULT_DESKTOP_SNAPSHOT, dependencies =
5518
6786
  const destination = resolve3(dependencies.cwd ?? process.cwd(), output);
5519
6787
  const destinationDirectory = dirname4(destination);
5520
6788
  mkdirSync4(destinationDirectory, { recursive: true });
5521
- const stagingDirectory = mkdtempSync3(join9(destinationDirectory, ".recordings-desktop-"));
6789
+ const stagingDirectory = mkdtempSync3(join10(destinationDirectory, ".recordings-desktop-"));
5522
6790
  chmodSync3(stagingDirectory, 448);
5523
- const stagingPath = join9(stagingDirectory, "snapshot.png");
6791
+ const stagingPath = join10(stagingDirectory, "snapshot.png");
5524
6792
  try {
5525
6793
  const capture = dependencies.capture ?? runScreenCapture;
5526
6794
  const result = capture(SCREEN_CAPTURE_EXECUTABLE, [
@@ -5540,7 +6808,7 @@ function exportDesktopSnapshot(output = DEFAULT_DESKTOP_SNAPSHOT, dependencies =
5540
6808
  }
5541
6809
  let size = 0;
5542
6810
  try {
5543
- const snapshot = statSync6(stagingPath);
6811
+ const snapshot = statSync5(stagingPath);
5544
6812
  if (snapshot.isFile())
5545
6813
  size = snapshot.size;
5546
6814
  } catch {}
@@ -5643,7 +6911,7 @@ Output:`));
5643
6911
  })
5644
6912
  });
5645
6913
  if (parentOpts.json) {
5646
- console.log(JSON.stringify(recording, null, 2));
6914
+ await printJSON(recording);
5647
6915
  } else {
5648
6916
  console.log(chalk.dim(`
5649
6917
  Saved as ${recording.id.slice(0, 8)}`));
@@ -5695,7 +6963,7 @@ program.command("transcribe <file>").description("Transcribe an existing audio f
5695
6963
  })
5696
6964
  }, recordingId);
5697
6965
  if (parentOpts.json) {
5698
- console.log(JSON.stringify(recording, null, 2));
6966
+ await printJSON(recording);
5699
6967
  } else if (processed.mode === "enhanced") {
5700
6968
  console.log(chalk.green("Enhanced:"));
5701
6969
  console.log(processed.text);
@@ -5750,7 +7018,7 @@ program.command("save-text [text]").description("Save already-transcribed text a
5750
7018
  metadata
5751
7019
  }, recordingId);
5752
7020
  if (parentOpts.json) {
5753
- console.log(JSON.stringify(recording, null, 2));
7021
+ await printJSON(recording);
5754
7022
  } else if (processed.mode === "enhanced") {
5755
7023
  console.log(processed.text);
5756
7024
  } else {
@@ -5772,11 +7040,11 @@ ${text}`;
5772
7040
  try {
5773
7041
  const result = await enhanceText(text, instruction, config);
5774
7042
  if (parentOpts.json) {
5775
- console.log(JSON.stringify({
7043
+ await printJSON({
5776
7044
  raw_text: text,
5777
7045
  processed_text: result.enhanced,
5778
7046
  model_used: result.model
5779
- }, null, 2));
7047
+ });
5780
7048
  } else {
5781
7049
  console.log(result.enhanced);
5782
7050
  }
@@ -5815,7 +7083,7 @@ program.command("save <text>").description("Save raw text as a recording (no aud
5815
7083
  machine_id: currentMachineId()
5816
7084
  });
5817
7085
  if (parentOpts.json) {
5818
- console.log(JSON.stringify(recording, null, 2));
7086
+ await printJSON(recording);
5819
7087
  } else {
5820
7088
  console.log(chalk.green(`\u2713 Saved recording ${recording.id}`));
5821
7089
  }
@@ -5842,7 +7110,7 @@ program.command("list").description("List recordings in compact form").option("-
5842
7110
  const store = getStore();
5843
7111
  const recordings = await store.listRecordings(filter);
5844
7112
  if (parentOpts.json) {
5845
- console.log(JSON.stringify(recordings, null, 2));
7113
+ await printJSON(recordings);
5846
7114
  return;
5847
7115
  }
5848
7116
  const total = await countStoreRecordings(store, withoutPagination(filter));
@@ -5874,7 +7142,7 @@ program.command("search <query>").description("Search recordings by text content
5874
7142
  const store = getStore();
5875
7143
  const results = await store.searchRecordings(query, filter);
5876
7144
  if (parentOpts.json) {
5877
- console.log(JSON.stringify(results, null, 2));
7145
+ await printJSON(results);
5878
7146
  return;
5879
7147
  }
5880
7148
  const total = await countStoreRecordings(store, withoutPagination({ ...filter, search: query }));
@@ -5900,7 +7168,7 @@ program.command("stats").description("Show recording statistics").action(async (
5900
7168
  const parentOpts = program.opts();
5901
7169
  const stats = await getStore().getRecordingStats();
5902
7170
  if (parentOpts.json) {
5903
- console.log(JSON.stringify(stats, null, 2));
7171
+ await printJSON(stats);
5904
7172
  return;
5905
7173
  }
5906
7174
  console.log(chalk.bold(`Recording Statistics
@@ -5926,7 +7194,7 @@ program.command("agents").description("List registered agents").option("-n, --li
5926
7194
  const agents = await getStore().listAgents();
5927
7195
  const page = parentOpts.json ? maybePageJson(agents, pagination, opts) : pageItems(agents, pagination);
5928
7196
  if (parentOpts.json) {
5929
- console.log(JSON.stringify(page, null, 2));
7197
+ await printJSON(page);
5930
7198
  return;
5931
7199
  }
5932
7200
  if (page.length === 0) {
@@ -5953,7 +7221,7 @@ projectCommand.command("register").description("Register a project in the active
5953
7221
  const parentOpts = program.opts();
5954
7222
  const project = await getStore().registerProject(opts.name, opts.path, opts.description);
5955
7223
  if (parentOpts.json) {
5956
- console.log(JSON.stringify(project, null, 2));
7224
+ await printJSON(project);
5957
7225
  return;
5958
7226
  }
5959
7227
  console.log(`${chalk.cyan(truncateText(project.id, 80))} ${chalk.bold(truncateText(project.name, 80))} \u2014 ${truncatePath(project.path, 120)}`);
@@ -5964,7 +7232,7 @@ program.command("projects").description("List registered projects").option("-n,
5964
7232
  const projects = await getStore().listProjects();
5965
7233
  const page = parentOpts.json ? maybePageJson(projects, pagination, opts) : pageItems(projects, pagination);
5966
7234
  if (parentOpts.json) {
5967
- console.log(JSON.stringify(page, null, 2));
7235
+ await printJSON(page);
5968
7236
  return;
5969
7237
  }
5970
7238
  if (page.length === 0) {
@@ -5989,10 +7257,10 @@ program.command("projects").description("List registered projects").option("-n,
5989
7257
  });
5990
7258
  program.command("init").description("Initialize .recordings/ in current directory").action(() => {
5991
7259
  const { mkdirSync: mkdirSync5, writeFileSync: writeFileSync4, existsSync: existsSync9 } = __require("fs");
5992
- const { join: join10 } = __require("path");
5993
- const dir = join10(process.cwd(), ".recordings");
5994
- const audioDir = join10(dir, "audio");
5995
- const configFile = join10(dir, "config.json");
7260
+ const { join: join11 } = __require("path");
7261
+ const dir = join11(process.cwd(), ".recordings");
7262
+ const audioDir = join11(dir, "audio");
7263
+ const configFile = join11(dir, "config.json");
5996
7264
  mkdirSync5(audioDir, { recursive: true });
5997
7265
  if (!existsSync9(configFile)) {
5998
7266
  const defaultConf = {
@@ -6015,9 +7283,9 @@ program.command("init").description("Initialize .recordings/ in current director
6015
7283
  console.log(chalk.dim(" db: .recordings/recordings.db"));
6016
7284
  });
6017
7285
  var appCommand = program.command("app").description("Manage the macOS app installed from this package");
6018
- appCommand.command("install").description("Install a release or explicitly approved local-only HasnaRecordings.app artifact").requiredOption("--artifact <path>", "Finalized HasnaRecordings.app ZIP artifact").requiredOption("--manifest <path>", "Artifact provenance manifest").option("--envelope <path>", "Signed release envelope (required for release artifacts)").option("--expected-team-id <team>", "Required Developer ID TeamIdentifier for release artifacts").requiredOption("--manifest-sha256 <sha256>", "Authenticated release-manifest SHA-256").requiredOption("--expected-source-sha <sha>", "Exact approved 40-character source commit").requiredOption("--expected-version <version>", "Exact approved release version").option("--expected-hostname <hostname>", "Exact deployment hostname to verify before any install mutation").option("--artifact-policy <policy>", "Artifact policy: release or local-only", "release").option("--approved-target <station>", "Exact approved target; fleet for release artifacts", "fleet").option("--variant <variant>", "Artifact variant the operator intends to install: full or bar (default: full)", "full").option("--approved-target-identity-kind <kind>", "Target identity kind: hardware_uuid_sha256 or tailscale_node_id_sha256").option("--approved-target-identity-sha256 <sha256>", "Authenticated SHA-256 of the approved target identity; none for release artifacts", "none").option("--acknowledge-local-signing-and-permissions", "Acknowledge local-only ad-hoc identity and possible permission reauthorization").option("--expected-old-identity-sha256 <sha256>", "Exact installed identity approved for migration").option("--expected-new-identity-sha256 <sha256>", "Exact candidate identity approved for migration").option("--allow-signing-identity-migration", "Allow one reviewed signer change that requires new macOS permission approval").option("--allow-adhoc-identity-migration", "Accept that replacing an ad-hoc signed local-only app voids its Microphone and Accessibility grants").option("--launch", "Launch and verify the canonical app after installation").option("--launch-timeout <seconds>", "Canonical process launch timeout").action((opts) => {
7286
+ appCommand.command("install").description("Install a release or explicitly approved local-only Hasna Recordings.app artifact").requiredOption("--artifact <path>", "Finalized Hasna Recordings.app ZIP artifact").requiredOption("--manifest <path>", "Artifact provenance manifest").option("--envelope <path>", "Signed release envelope (required for release artifacts)").option("--expected-team-id <team>", "Required Developer ID TeamIdentifier for release artifacts").requiredOption("--manifest-sha256 <sha256>", "Authenticated release-manifest SHA-256").requiredOption("--expected-source-sha <sha>", "Exact approved 40-character source commit").requiredOption("--expected-version <version>", "Exact approved release version").option("--expected-hostname <hostname>", "Exact deployment hostname to verify before any install mutation").option("--artifact-policy <policy>", "Artifact policy: release or local-only", "release").option("--approved-target <station>", "Exact approved target; fleet for release artifacts", "fleet").option("--variant <variant>", "Artifact variant the operator intends to install: full or bar (default: full)", "full").option("--approved-target-identity-kind <kind>", "Target identity kind: hardware_uuid_sha256 or tailscale_node_id_sha256").option("--approved-target-identity-sha256 <sha256>", "Authenticated SHA-256 of the approved target identity; none for release artifacts", "none").option("--acknowledge-local-signing-and-permissions", "Acknowledge local-only ad-hoc identity and possible permission reauthorization").option("--expected-old-identity-sha256 <sha256>", "Exact installed identity approved for migration").option("--expected-new-identity-sha256 <sha256>", "Exact candidate identity approved for migration").option("--allow-signing-identity-migration", "Allow one reviewed signer change that requires new macOS permission approval").option("--allow-adhoc-identity-migration", "Accept that replacing an ad-hoc signed local-only app voids its Microphone and Accessibility grants").option("--launch", "Launch and verify the canonical app after installation").option("--launch-timeout <seconds>", "Canonical process launch timeout").action((opts) => {
6019
7287
  if (process.platform !== "darwin") {
6020
- console.error(chalk.red("HasnaRecordings.app installation is only supported on macOS"));
7288
+ console.error(chalk.red("Hasna Recordings.app installation is only supported on macOS"));
6021
7289
  process.exit(1);
6022
7290
  }
6023
7291
  if (opts.variant !== "full" && opts.variant !== "bar") {
@@ -6037,7 +7305,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6037
7305
  try {
6038
7306
  assertReleaseOnlyOptions(opts);
6039
7307
  if (opts.expectedHostname) {
6040
- const hostnameResult = spawnSync7("/bin/hostname", ["-s"], {
7308
+ const hostnameResult = spawnSync9("/bin/hostname", ["-s"], {
6041
7309
  encoding: "utf8",
6042
7310
  stdio: ["ignore", "pipe", "ignore"],
6043
7311
  env: {
@@ -6066,7 +7334,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6066
7334
  process.exit(1);
6067
7335
  }
6068
7336
  const home = process.env.HOME || process.env.USERPROFILE || "";
6069
- const canonicalAppPath = pathJoin(home, "Applications", "HasnaRecordings.app");
7337
+ const canonicalAppPath = pathJoin(home, "Applications", "Hasna Recordings.app");
6070
7338
  const legacyInstallPaths = findLegacyMacOSAppPaths(home, canonicalAppPath);
6071
7339
  const installedAppPath = resolveInstalledAppPath(home, canonicalAppPath, legacyInstallPaths);
6072
7340
  const updateClientPath = pathJoin(installedAppPath, "Contents", "Helpers", "recordings-update-client");
@@ -6077,7 +7345,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6077
7345
  }
6078
7346
  const result2 = (() => {
6079
7347
  try {
6080
- return spawnSync7(updateClientPath, [
7348
+ return spawnSync9(updateClientPath, [
6081
7349
  "install",
6082
7350
  "--artifact",
6083
7351
  opts.artifact,
@@ -6176,7 +7444,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6176
7444
  if (opts.launch)
6177
7445
  installerArgs.push("--launch");
6178
7446
  const installerEnvironment = createInstallerEnvironment(process.env, bunExecutable);
6179
- const result = spawnSync7("/bin/bash", installerArgs, {
7447
+ const result = spawnSync9("/bin/bash", installerArgs, {
6180
7448
  stdio: "inherit",
6181
7449
  env: installerEnvironment
6182
7450
  });
@@ -6186,7 +7454,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6186
7454
  }
6187
7455
  process.exit(result.status ?? 1);
6188
7456
  });
6189
- appCommand.command("status").description("Show installed HasnaRecordings.app status").option("--verbose", "Show package paths, code hash, and log path").action((opts) => {
7457
+ appCommand.command("status").description("Show installed Hasna Recordings.app status").option("--verbose", "Show package paths, code hash, and log path").action((opts) => {
6190
7458
  const status = getMacOSAppStatus();
6191
7459
  const trigger = probeTriggerDiagnostics({
6192
7460
  accessibilityPermission: process.platform === "darwin" ? status.accessibility_permission : null,
@@ -6226,7 +7494,7 @@ appCommand.command("status").description("Show installed HasnaRecordings.app sta
6226
7494
  console.log(chalk.dim("Use --verbose for paths/code hash/log, or --json for the full status object."));
6227
7495
  }
6228
7496
  });
6229
- appCommand.command("permissions").description("Show macOS permission state for HasnaRecordings.app").action(() => {
7497
+ appCommand.command("permissions").description("Show macOS permission state for Hasna Recordings.app").action(() => {
6230
7498
  const status = getMacOSAppStatus();
6231
7499
  const permissions = {
6232
7500
  platform: status.platform,
@@ -6234,7 +7502,7 @@ appCommand.command("permissions").description("Show macOS permission state for H
6234
7502
  installed_app_path: status.installed_app_path,
6235
7503
  installed: status.installed,
6236
7504
  legacy_install_paths: status.legacy_install_paths,
6237
- permission_subject: describeTccAuthorizationSubject(status.installed_app_path),
7505
+ permission_subject: describeTccAuthorizationSubject(status.installed ? status.installed_app_path : null),
6238
7506
  microphone: status.microphone_permission,
6239
7507
  accessibility: status.accessibility_permission,
6240
7508
  app_code_hash: status.app_code_hash,
@@ -6262,21 +7530,21 @@ appCommand.command("permissions").description("Show macOS permission state for H
6262
7530
  }
6263
7531
  console.log(`Log: ${permissions.log_path}`);
6264
7532
  });
6265
- appCommand.command("reset-permissions").description("Reset macOS Microphone and Accessibility permissions for HasnaRecordings.app").action(() => {
7533
+ appCommand.command("reset-permissions").description("Reset macOS Microphone and Accessibility permissions for Hasna Recordings.app").action(() => {
6266
7534
  if (process.platform !== "darwin") {
6267
7535
  console.error(chalk.red("Permission reset is only available on macOS"));
6268
7536
  process.exit(1);
6269
7537
  }
6270
7538
  resetMacOSPermissions();
6271
7539
  });
6272
- appCommand.command("request-permissions").description("Open HasnaRecordings.app and trigger macOS Microphone and Accessibility permission prompts").option("--reset", "Reset existing Microphone and Accessibility decisions before requesting").action((opts) => {
7540
+ appCommand.command("request-permissions").description("Open Hasna Recordings.app and trigger macOS Microphone and Accessibility permission prompts").option("--reset", "Reset existing Microphone and Accessibility decisions before requesting").action((opts) => {
6273
7541
  if (process.platform !== "darwin") {
6274
7542
  console.error(chalk.red("Permission prompts are only available on macOS"));
6275
7543
  process.exit(1);
6276
7544
  }
6277
7545
  const status = getMacOSAppStatus();
6278
7546
  if (!status.installed) {
6279
- console.error(chalk.red("HasnaRecordings.app is not installed. Run: recordings app install"));
7547
+ console.error(chalk.red("Hasna Recordings.app is not installed. Run: recordings app install"));
6280
7548
  process.exit(1);
6281
7549
  }
6282
7550
  if (opts.reset) {
@@ -6288,14 +7556,14 @@ appCommand.command("request-permissions").description("Open HasnaRecordings.app
6288
7556
  }
6289
7557
  process.exit(result.exitCode);
6290
7558
  });
6291
- appCommand.command("log").description("Show the HasnaRecordings.app diagnostic log").option("-n, --lines <lines>", "Number of lines to print", String(DEFAULT_LOG_LINES)).action((opts) => {
7559
+ appCommand.command("log").description("Show the Hasna Recordings.app diagnostic log").option("-n, --lines <lines>", "Number of lines to print", String(DEFAULT_LOG_LINES)).action((opts) => {
6292
7560
  const status = getMacOSAppStatus();
6293
7561
  if (!existsSync8(status.log_path)) {
6294
7562
  console.log("");
6295
7563
  return;
6296
7564
  }
6297
7565
  const lines = Math.max(1, parseInt(opts.lines, 10) || DEFAULT_LOG_LINES);
6298
- const result = spawnSync7("tail", ["-n", String(lines), status.log_path], {
7566
+ const result = spawnSync9("tail", ["-n", String(lines), status.log_path], {
6299
7567
  encoding: "utf8"
6300
7568
  });
6301
7569
  if (result.error) {
@@ -6314,17 +7582,17 @@ appCommand.command("snapshot [output]").description("Write the current main desk
6314
7582
  console.log(chalk.green(`Desktop snapshot written: ${path}`));
6315
7583
  }
6316
7584
  });
6317
- appCommand.command("open").description("Open the installed HasnaRecordings.app").action(() => {
7585
+ appCommand.command("open").description("Open the installed Hasna Recordings.app").action(() => {
6318
7586
  const status = getMacOSAppStatus();
6319
7587
  if (process.platform !== "darwin") {
6320
- console.error(chalk.red("HasnaRecordings.app can only be opened on macOS"));
7588
+ console.error(chalk.red("Hasna Recordings.app can only be opened on macOS"));
6321
7589
  process.exit(1);
6322
7590
  }
6323
7591
  if (!status.installed) {
6324
- console.error(chalk.red("HasnaRecordings.app is not installed. Run: recordings app install"));
7592
+ console.error(chalk.red("Hasna Recordings.app is not installed. Run: recordings app install"));
6325
7593
  process.exit(1);
6326
7594
  }
6327
- const result = spawnSync7("open", [status.installed_app_path], { stdio: "inherit" });
7595
+ const result = spawnSync9("open", [status.installed_app_path], { stdio: "inherit" });
6328
7596
  if (result.error) {
6329
7597
  console.error(chalk.red(result.error.message));
6330
7598
  process.exit(1);
@@ -6410,8 +7678,9 @@ program.command("check").description("Check system dependencies (sox, API keys)
6410
7678
  persistence_probe: persistence,
6411
7679
  trigger
6412
7680
  }, null, 2));
6413
- if (probeFailed || triggerFailed)
7681
+ if (probeFailed || triggerFailed || activeStoreFailClosed(activeStore)) {
6414
7682
  process.exitCode = 1;
7683
+ }
6415
7684
  return;
6416
7685
  }
6417
7686
  if (deps.available) {
@@ -6422,14 +7691,18 @@ program.command("check").description("Check system dependencies (sox, API keys)
6422
7691
  if (config.openai_api_key) {
6423
7692
  console.log(chalk.green(`\u2713 OpenAI API key present`) + chalk.dim(opts.probe ? "" : " (presence only \u2014 run 'recordings check --probe' to verify it is accepted)"));
6424
7693
  } else {
6425
- console.log(chalk.red(`\u2717 OpenAI API key not found. Set OPENAI_API_KEY env var or add to ~/.secrets`));
7694
+ console.log(chalk.red(`\u2717 OpenAI API key not found. Set OPENAI_API_KEY (e.g. ` + `'secrets exec <key> --as OPENAI_API_KEY -- recordings \u2026')`));
6426
7695
  }
6427
7696
  if (enhKey) {
6428
7697
  console.log(chalk.green(`\u2713 Enhancement API key present (model: ${transcriberModel})`) + chalk.dim(opts.probe ? "" : " (presence only \u2014 'recordings check --probe' verifies it is accepted)"));
6429
7698
  } else {
6430
7699
  console.log(chalk.yellow(`\u26A0 Enhancement API key not configured \u2014 enhancement disabled`));
6431
7700
  }
6432
- console.log(chalk.green("\u2713") + ` Active store: ${activeStore.transport}` + (activeStore.base_url ? ` \u2192 ${activeStore.base_url}` : ` \u2192 ${activeStore.local_db_path}`) + chalk.dim(` (selected by ${activeStore.mode_source})`));
7701
+ console.log(describeActiveStoreLine(activeStore, {
7702
+ pass: (text) => chalk.green(text),
7703
+ warn: (text) => chalk.yellow(text),
7704
+ fail: (text) => chalk.red(text)
7705
+ }));
6433
7706
  if (activeStore.divergent) {
6434
7707
  console.log(chalk.yellow("\u26A0") + ` Two datasets present: ${activeStore.local_db_recordings} recordings sit in ` + `${activeStore.local_db_path}, which is NOT the live store.`);
6435
7708
  }
@@ -6453,7 +7726,7 @@ program.command("check").description("Check system dependencies (sox, API keys)
6453
7726
  console.log(chalk.dim(" Could not read the TCC database. This is NOT a denial and NOT proof the app never " + "asked. Reading it needs Full Disk Access, which is held by the session's " + "RESPONSIBLE process and inherited by its children \u2014 not granted per-tool: on " + "a fleet Mac `bun` is explicitly denied and still reads the database over SSH, " + "because it inherits sshd's grant. So re-run from a plain ssh shell rather than " + "granting Full Disk Access to tmux or bun, and not under sudo, which changes the " + "responsible process. A missing sqlite3, a locked database or a corrupt file " + "produce this same state, so the cause is not established either."));
6454
7727
  }
6455
7728
  if (!micOk && !micUnreadable) {
6456
- console.log(chalk.dim(" HasnaRecordings.app cannot capture audio without this. macOS does not error when it is " + "missing \u2014 it delivers silent audio. This grant CANNOT be set remotely; it needs a " + "human at the keyboard:"));
7729
+ console.log(chalk.dim(" Hasna Recordings.app cannot capture audio without this. macOS does not error when it is " + "missing \u2014 it delivers silent audio. This grant CANNOT be set remotely; it needs a " + "human at the keyboard:"));
6457
7730
  const instruction = microphoneGrantInstruction({
6458
7731
  installedAppPath: macStatus.installed_app_path,
6459
7732
  otherAppPaths: macStatus.legacy_install_paths,
@@ -6486,7 +7759,7 @@ program.command("check").description("Check system dependencies (sox, API keys)
6486
7759
  });
6487
7760
  console.log(persistenceMarker + ` Persistence round-trip: ${persistence.message}`);
6488
7761
  }
6489
- if (probeFailed || triggerFailed)
7762
+ if (probeFailed || triggerFailed || activeStoreFailClosed(activeStore))
6490
7763
  process.exitCode = 1;
6491
7764
  });
6492
7765
  function reportTriggerDiagnosis(trigger) {
@@ -6612,7 +7885,7 @@ Bye.`));
6612
7885
  }
6613
7886
  });
6614
7887
  });
6615
- program.command("shortcut").description("Show or change the global recording trigger (macOS). Exits non-zero when a change was " + "written while HasnaRecordings.app was running, because the running instance keeps the " + "trigger it registered with \u2014 the write is stored but not armed until it is reopened.").option("--set <chord>", 'Set the app hotkey, e.g. "f13" or "ctrl+opt+r"').option("--reset", "Reset the app hotkey to the app's built-in default").option("--fn <state>", "Use fn/Globe as push-to-talk: on|off").option("--keys", "List the key names accepted by --set").option("--script", "Write the app-less toggle script and print its path").option("--raycast", "Generate a Raycast script command (requires Raycast)").option("--karabiner", "Generate a Karabiner-Elements rule (requires Karabiner-Elements)").option("--skhd", "Print an skhd hotkey config (requires skhd)").option("--hammerspoon", "Print a Hammerspoon config (requires Hammerspoon)").action((opts) => {
7888
+ program.command("shortcut").description("Show or change the global recording trigger (macOS). Exits non-zero when a change was " + "written while Hasna Recordings.app was running, because the running instance keeps the " + "trigger it registered with \u2014 the write is stored but not armed until it is reopened.").option("--set <chord>", 'Set the app hotkey, e.g. "f13" or "ctrl+opt+r"').option("--reset", "Reset the app hotkey to the app's built-in default").option("--fn <state>", "Use fn/Globe as push-to-talk: on|off").option("--keys", "List the key names accepted by --set").option("--script", "Write the app-less toggle script and print its path").option("--raycast", "Generate a Raycast script command (requires Raycast)").option("--karabiner", "Generate a Karabiner-Elements rule (requires Karabiner-Elements)").option("--skhd", "Print an skhd hotkey config (requires skhd)").option("--hammerspoon", "Print a Hammerspoon config (requires Hammerspoon)").action((opts) => {
6616
7889
  const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync5, chmodSync: chmodSync4, existsSync: existsSync9 } = __require("fs");
6617
7890
  const { join: pathJoin2 } = __require("path");
6618
7891
  const { homedir: getHome } = __require("os");
@@ -6726,10 +7999,10 @@ program.command("shortcut").description("Show or change the global recording tri
6726
7999
  function reportPickup() {
6727
8000
  const pickup = describeTriggerPickup(runningBundles());
6728
8001
  if (pickup.armed) {
6729
- console.log(chalk.dim(" HasnaRecordings.app is not running; it will register this on next launch."));
8002
+ console.log(chalk.dim(" Hasna Recordings.app is not running; it will register this on next launch."));
6730
8003
  return;
6731
8004
  }
6732
- console.log(chalk.yellow(" HasnaRecordings.app is running and still holds the previous trigger.") + `
8005
+ console.log(chalk.yellow(" Hasna Recordings.app is running and still holds the previous trigger.") + `
6733
8006
  Quit and reopen it to arm this one:`);
6734
8007
  for (const path of pickup.runningBundlePaths)
6735
8008
  console.log(chalk.dim(` ${path}`));
@@ -6788,7 +8061,7 @@ program.command("shortcut").description("Show or change the global recording tri
6788
8061
  ` + ` ${fnGrant.settingsPath} > enable Recordings`);
6789
8062
  const target = grantTargetPaths();
6790
8063
  if (target.paths.length === 0) {
6791
- console.log(chalk.dim(" (no installed HasnaRecordings.app found to grant it to)"));
8064
+ console.log(chalk.dim(" (no installed Hasna Recordings.app found to grant it to)"));
6792
8065
  }
6793
8066
  for (const path of target.paths) {
6794
8067
  console.log(chalk.dim(` grant it to: ${path}${target.running ? "" : " (not running \u2014 installed copy)"}`));
@@ -6815,7 +8088,7 @@ program.command("shortcut").description("Show or change the global recording tri
6815
8088
  # Toggle recording on/off. Run this from a global hotkey.
6816
8089
  # Each press toggles: start recording -> stop + transcribe + copy to clipboard
6817
8090
  #
6818
- # This is the app-less path. If HasnaRecordings.app is running, prefer its own hotkey
8091
+ # This is the app-less path. If Hasna Recordings.app is running, prefer its own hotkey
6819
8092
  # ("recordings shortcut --set ..."): the app streams transcription and pastes into
6820
8093
  # the focused field, which this script cannot do.
6821
8094
  set -e
@@ -7012,7 +8285,7 @@ async function readSaveTextInput(text, opts) {
7012
8285
  }
7013
8286
  let rawText;
7014
8287
  if (opts.textFile !== undefined) {
7015
- rawText = readFileSync5(opts.textFile, "utf8");
8288
+ rawText = readFileSync6(opts.textFile, "utf8");
7016
8289
  } else if (opts.stdin) {
7017
8290
  rawText = await Bun.stdin.text();
7018
8291
  } else {
@@ -7106,7 +8379,7 @@ async function printRecordingDetail(id) {
7106
8379
  return;
7107
8380
  }
7108
8381
  if (parentOpts.json) {
7109
- console.log(JSON.stringify(recording, null, 2));
8382
+ await printJSON(recording);
7110
8383
  return;
7111
8384
  }
7112
8385
  console.log(formatRecordingDetail(recording));
@@ -7226,7 +8499,7 @@ function relativeHint(value) {
7226
8499
  return `${Math.floor(hours / 24)}d ago`;
7227
8500
  }
7228
8501
  program.command("mcp").description("Install recordings MCP server into Claude Code, Codex, or Gemini").option("--claude", "Install into Claude Code (via `claude mcp add`)").option("--codex", "Install into Codex (~/.codex/config.toml)").option("--gemini", "Install into Gemini (~/.gemini/settings.json)").option("--all", "Install into all supported agents").option("--uninstall", "Remove recordings MCP from config").action(async (opts) => {
7229
- const { readFileSync: readFileSync6, writeFileSync: writeFileSync4, existsSync: fileExists } = __require("fs");
8502
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync4, existsSync: fileExists } = __require("fs");
7230
8503
  const { join: pathJoin2 } = __require("path");
7231
8504
  const { homedir: getHome } = __require("os");
7232
8505
  const { execSync } = __require("child_process");
@@ -7259,7 +8532,7 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
7259
8532
  if (target === "codex") {
7260
8533
  const configPath = pathJoin2(home, ".codex", "config.toml");
7261
8534
  if (fileExists(configPath)) {
7262
- const content = readFileSync6(configPath, "utf-8");
8535
+ const content = readFileSync7(configPath, "utf-8");
7263
8536
  if (opts.uninstall) {
7264
8537
  const { content: next, removed } = removeCodexServerBlock(content, "recordings");
7265
8538
  writeFileSync4(configPath, next, "utf-8");
@@ -7277,7 +8550,7 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
7277
8550
  const configPath = pathJoin2(home, ".gemini", "settings.json");
7278
8551
  let config = {};
7279
8552
  if (fileExists(configPath)) {
7280
- config = JSON.parse(readFileSync6(configPath, "utf-8"));
8553
+ config = JSON.parse(readFileSync7(configPath, "utf-8"));
7281
8554
  }
7282
8555
  const servers = config["mcpServers"] || {};
7283
8556
  if (opts.uninstall) {
@@ -7316,7 +8589,7 @@ program.command("feedback <message>").description("Send feedback").option("--ema
7316
8589
  function getMacOSAppStatus() {
7317
8590
  const packageRoot = findPackageRoot();
7318
8591
  const home = process.env.HOME || process.env.USERPROFILE || "";
7319
- const canonicalAppPath = pathJoin(home, "Applications", "HasnaRecordings.app");
8592
+ const canonicalAppPath = pathJoin(home, "Applications", "Hasna Recordings.app");
7320
8593
  const legacyInstallPaths = findLegacyMacOSAppPaths(home, canonicalAppPath);
7321
8594
  const installedAppPath = resolveInstalledAppPath(home, canonicalAppPath, legacyInstallPaths);
7322
8595
  const installedAppPathForGrants = existsSync8(installedAppPath) ? installedAppPath : null;
@@ -7364,7 +8637,7 @@ function buildPermissionWarnings(status) {
7364
8637
  warnings.push(`no app bundle exists at ${status.installed_app_path}, so the states above describe no ` + "installed code \u2014 install the app before trusting them");
7365
8638
  }
7366
8639
  if (status.ambiguous_installations) {
7367
- warnings.push("more than one HasnaRecordings.app is installed, so the states above may describe a bundle " + `other than the one macOS granted: reporting on ${status.installed_app_path}, also ` + `present are ${status.legacy_install_paths.join(", ")}`);
8640
+ warnings.push("more than one Hasna Recordings.app is installed, so the states above may describe a bundle " + `other than the one macOS granted: reporting on ${status.installed_app_path}, also ` + `present are ${status.legacy_install_paths.join(", ")}`);
7368
8641
  }
7369
8642
  for (const [service, durability] of [
7370
8643
  ["Microphone", status.microphone_grant_durability],
@@ -7387,8 +8660,11 @@ function buildPermissionWarnings(status) {
7387
8660
  function resolveInstalledAppPath(home, canonicalPath, legacyPaths) {
7388
8661
  const preference = [
7389
8662
  canonicalPath,
8663
+ pathJoin("/", "Applications", "Hasna Recordings.app"),
8664
+ pathJoin(home, "Applications", "HasnaRecordings.app"),
7390
8665
  pathJoin("/", "Applications", "HasnaRecordings.app"),
7391
8666
  pathJoin("/", "Applications", "Recordings.app"),
8667
+ pathJoin(home, ".hasna", "recordings", "Hasna Recordings.app"),
7392
8668
  pathJoin(home, ".hasna", "recordings", "HasnaRecordings.app"),
7393
8669
  pathJoin(home, ".hasna", "recordings", "Recordings.app")
7394
8670
  ];
@@ -7400,15 +8676,18 @@ function resolveInstalledAppPath(home, canonicalPath, legacyPaths) {
7400
8676
  }
7401
8677
  function findLegacyMacOSAppPaths(home, canonicalPath) {
7402
8678
  const candidates = [
7403
- pathJoin(home, ".hasna", "recordings", "Recordings.app"),
8679
+ pathJoin(home, "Applications", "HasnaRecordings.app"),
7404
8680
  pathJoin(home, ".hasna", "recordings", "HasnaRecordings.app"),
8681
+ pathJoin("/", "Applications", "HasnaRecordings.app"),
8682
+ pathJoin(home, ".hasna", "recordings", "Recordings.app"),
8683
+ pathJoin(home, ".hasna", "recordings", "Hasna Recordings.app"),
7405
8684
  pathJoin("/", "Applications", "Recordings.app"),
7406
- pathJoin("/", "Applications", "HasnaRecordings.app")
8685
+ pathJoin("/", "Applications", "Hasna Recordings.app")
7407
8686
  ];
7408
8687
  const userApplications = pathJoin(home, "Applications");
7409
8688
  if (existsSync8(userApplications)) {
7410
8689
  for (const entry of readdirSync2(userApplications, { withFileTypes: true })) {
7411
- if (entry.isDirectory() && (entry.name.startsWith("Recordings.app.") || entry.name.startsWith("HasnaRecordings.app."))) {
8690
+ if (entry.isDirectory() && (entry.name.startsWith("Recordings.app.") || entry.name.startsWith("Hasna Recordings.app.") || entry.name.startsWith("HasnaRecordings.app."))) {
7412
8691
  candidates.push(pathJoin(userApplications, entry.name));
7413
8692
  }
7414
8693
  }
@@ -7418,7 +8697,7 @@ function findLegacyMacOSAppPaths(home, canonicalPath) {
7418
8697
  function resetMacOSPermissions() {
7419
8698
  const services = ["Microphone", "Accessibility"];
7420
8699
  for (const service of services) {
7421
- const result = spawnSync7("/usr/bin/tccutil", ["reset", service, RECORDINGS_BUNDLE_IDENTIFIER], { stdio: "inherit" });
8700
+ const result = spawnSync9("/usr/bin/tccutil", ["reset", service, RECORDINGS_BUNDLE_IDENTIFIER], { stdio: "inherit" });
7422
8701
  if (result.error) {
7423
8702
  console.error(chalk.red(result.error.message));
7424
8703
  process.exit(1);
@@ -7436,7 +8715,7 @@ function getCodeSigningInfo(appPath) {
7436
8715
  authorities: []
7437
8716
  };
7438
8717
  }
7439
- const result = spawnSync7("/usr/bin/codesign", ["-d", "-r-", "--verbose=4", appPath], {
8718
+ const result = spawnSync9("/usr/bin/codesign", ["-d", "-r-", "--verbose=4", appPath], {
7440
8719
  encoding: "utf8",
7441
8720
  stdio: ["ignore", "pipe", "pipe"]
7442
8721
  });
@@ -7465,7 +8744,7 @@ function findPackageRoot() {
7465
8744
  const packagePath = pathJoin(current, "package.json");
7466
8745
  if (existsSync8(packagePath)) {
7467
8746
  try {
7468
- const pkg = JSON.parse(readFileSync5(packagePath, "utf8"));
8747
+ const pkg = JSON.parse(readFileSync6(packagePath, "utf8"));
7469
8748
  if (pkg.name === "@hasna/recordings") {
7470
8749
  return current;
7471
8750
  }
@@ -7481,6 +8760,17 @@ function findPackageRoot() {
7481
8760
  function getMacOSInstallerPath(packageRoot = findPackageRoot()) {
7482
8761
  return pathJoin(packageRoot, "scripts", "install_macos_app.sh");
7483
8762
  }
8763
+ async function printJSON(value) {
8764
+ await new Promise((resolve4, reject) => {
8765
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
8766
+ `, (error) => {
8767
+ if (error)
8768
+ reject(error);
8769
+ else
8770
+ resolve4();
8771
+ });
8772
+ });
8773
+ }
7484
8774
  program.parseAsync().catch((error) => {
7485
8775
  const msg = error instanceof Error ? error.message : String(error);
7486
8776
  console.error(`ERROR: ${msg}`);