@hasna/recordings 0.3.13 → 0.4.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 (105) hide show
  1. package/README.md +223 -37
  2. package/bun.lock +37 -38
  3. package/dist/cli/index.js +1884 -615
  4. package/dist/cli/macos-shortcut.d.ts +2 -2
  5. package/dist/db/database.d.ts.map +1 -1
  6. package/dist/db/pg-migrations.d.ts.map +1 -1
  7. package/dist/db/recordings.d.ts +6 -0
  8. package/dist/db/recordings.d.ts.map +1 -1
  9. package/dist/http/client.d.ts +183 -22
  10. package/dist/http/client.d.ts.map +1 -1
  11. package/dist/index.d.ts +3 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2858 -2288
  14. package/dist/lib/audio-artifact-storage.d.ts +98 -0
  15. package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
  16. package/dist/lib/capture-probe.d.ts +3 -3
  17. package/dist/lib/capture-probe.d.ts.map +1 -1
  18. package/dist/lib/config.d.ts.map +1 -1
  19. package/dist/lib/local-opt-in.d.ts +132 -0
  20. package/dist/lib/local-opt-in.d.ts.map +1 -0
  21. package/dist/lib/macos-bundle.d.ts +1 -1
  22. package/dist/lib/persistence-probe.d.ts +56 -17
  23. package/dist/lib/persistence-probe.d.ts.map +1 -1
  24. package/dist/lib/transcriber.d.ts.map +1 -1
  25. package/dist/mcp/index.d.ts.map +1 -1
  26. package/dist/mcp/index.js +1689 -456
  27. package/dist/sdk/index.d.ts +13 -5
  28. package/dist/sdk/index.d.ts.map +1 -1
  29. package/dist/sdk/index.js +904 -1
  30. package/dist/sdk/resolve.d.ts +53 -0
  31. package/dist/sdk/resolve.d.ts.map +1 -0
  32. package/dist/sdk/v1.generated.d.ts +6 -0
  33. package/dist/sdk/v1.generated.d.ts.map +1 -1
  34. package/dist/server/client-ip.d.ts +39 -0
  35. package/dist/server/client-ip.d.ts.map +1 -0
  36. package/dist/server/cloud-config.d.ts +12 -0
  37. package/dist/server/cloud-config.d.ts.map +1 -1
  38. package/dist/server/cloud-readiness.d.ts.map +1 -1
  39. package/dist/server/cloud.d.ts +20 -2
  40. package/dist/server/cloud.d.ts.map +1 -1
  41. package/dist/server/index.js +566 -284
  42. package/dist/server/migrate-command.d.ts +1 -1
  43. package/dist/server/migrate-command.d.ts.map +1 -1
  44. package/dist/server/openapi.d.ts +21 -0
  45. package/dist/server/openapi.d.ts.map +1 -1
  46. package/dist/server/repo.d.ts.map +1 -1
  47. package/dist/server/serve.d.ts.map +1 -1
  48. package/dist/storage.d.ts +2 -2
  49. package/dist/storage.d.ts.map +1 -1
  50. package/dist/storage.js +1334 -871
  51. package/dist/store.d.ts +12 -3
  52. package/dist/store.d.ts.map +1 -1
  53. package/dist/types/index.d.ts +15 -0
  54. package/dist/types/index.d.ts.map +1 -1
  55. package/package.json +9 -8
  56. package/packaging/macos/build_release_pkg.sh +3 -3
  57. package/packaging/macos/managed_bootstrap.sh +4 -4
  58. package/packaging/macos/scripts/postinstall +2 -2
  59. package/packaging/macos/scripts/preinstall +2 -2
  60. package/scripts/build_companion_cli.sh +7 -1
  61. package/scripts/install_macos_app.sh +27 -24
  62. package/scripts/macos_artifact.ts +20 -16
  63. package/scripts/migrate.ts +4 -20
  64. package/scripts/smoke_macos_app.sh +21 -21
  65. package/src/native/Recordings/App/ContentView.swift +53 -68
  66. package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
  67. package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
  68. package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
  69. package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
  70. package/src/native/Recordings/App/RecordingsApp.swift +185 -9
  71. package/src/native/Recordings/App/RecordingsListView.swift +60 -112
  72. package/src/native/Recordings/App/RecordingsStore.swift +124 -109
  73. package/src/native/Recordings/App/Theme.swift +73 -61
  74. package/src/native/Recordings/Package.swift +1 -0
  75. package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
  76. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  77. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
  78. package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
  79. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +316 -34
  80. package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
  81. package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +102 -0
  82. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
  83. package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
  84. package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
  85. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
  86. package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
  87. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +13 -4
  88. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
  89. package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
  90. package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
  91. package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +195 -0
  92. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  93. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +30 -0
  94. package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
  95. package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
  96. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  97. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  98. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  99. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  100. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  101. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  102. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  103. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  104. package/src/native/Recordings/build.sh +6 -6
  105. 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.4.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: {
@@ -1962,10 +1953,10 @@ var package_default = {
1962
1953
  scripts: {
1963
1954
  clean: "rm -rf dist",
1964
1955
  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",
1956
+ "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",
1957
+ "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",
1958
+ "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",
1959
+ "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",
1969
1960
  "build:native-fs-guard": "/bin/bash scripts/build_native_fs_guard.sh",
1970
1961
  "generate:sdk": "bun run scripts/generate-sdk.ts",
1971
1962
  migrate: "bun run scripts/migrate.ts",
@@ -1975,7 +1966,7 @@ var package_default = {
1975
1966
  "verify:ci-suite": "bun scripts/ci-linux-suite.ts --check",
1976
1967
  "verify:ci-suite:run": "bun scripts/ci-linux-suite.ts --verify-run",
1977
1968
  "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)',
1969
+ "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:-120000}" $(bun scripts/ci-linux-suite.ts --gated)',
1979
1970
  "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
1971
  "test:vacuity-battery:source": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/source-side.tsv",
1981
1972
  "test:vacuity-battery:chain": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/install-chain.tsv",
@@ -2012,7 +2003,8 @@ var package_default = {
2012
2003
  "LICENSE"
2013
2004
  ],
2014
2005
  dependencies: {
2015
- "@hasna/contracts": "0.14.1",
2006
+ "@aws-sdk/client-s3": "^3.1007.0",
2007
+ "@hasna/contracts": "1.0.2",
2016
2008
  "@hasna/events": "0.1.11",
2017
2009
  "@modelcontextprotocol/sdk": "^1.12.1",
2018
2010
  chalk: "^5.4.1",
@@ -2022,7 +2014,7 @@ var package_default = {
2022
2014
  zod: "^3.24.2"
2023
2015
  },
2024
2016
  devDependencies: {
2025
- "@types/bun": "^1.2.5",
2017
+ "@types/bun": "1.3.14",
2026
2018
  "@types/pg": "^8.11.11",
2027
2019
  "node-api-headers": "1.9.0",
2028
2020
  typescript: "^5.8.2"
@@ -2046,8 +2038,12 @@ function saveFeedback(input) {
2046
2038
 
2047
2039
  // ../contracts/dist/client/transport.js
2048
2040
  import { isIP } from "net";
2049
- import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
2050
- import { join as join4 } from "path";
2041
+ import { spawnSync as spawnSync2 } from "child_process";
2042
+ import { closeSync, fstatSync, openSync, readFileSync as readFileSync2 } from "fs";
2043
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
2044
+ import { createRequire } from "module";
2045
+ import { hostname as osHostname } from "os";
2046
+ import { isAbsolute as isAbsolute2, join as join4 } from "path";
2051
2047
  function envToken(name) {
2052
2048
  return name.toUpperCase().replace(/-/g, "_");
2053
2049
  }
@@ -2062,6 +2058,9 @@ function credentialOverrideEnvKey(name) {
2062
2058
  return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
2063
2059
  }
2064
2060
  var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
2061
+ function credentialPointerEnvKey(name) {
2062
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
2063
+ }
2065
2064
 
2066
2065
  class CredentialResolutionError extends Error {
2067
2066
  appName;
@@ -2073,34 +2072,70 @@ class CredentialResolutionError extends Error {
2073
2072
  this.attempted = attempted;
2074
2073
  }
2075
2074
  }
2076
- var HASNA_STATE_DIR = ".hasna";
2077
- var FLEET_CREDENTIAL_DIR = "cloud";
2078
- var CONFIG_DIR = ".config";
2079
- var CONFIG_NAMESPACE = "hasna";
2075
+
2076
+ class CredentialFileUnsafeError extends Error {
2077
+ path;
2078
+ constructor(path, reason) {
2079
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
2080
+ this.name = "CredentialFileUnsafeError";
2081
+ this.path = path;
2082
+ }
2083
+ }
2084
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
2085
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
2086
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
2087
+ var HASNA_HOME_DIR = ".hasna";
2088
+ var CONFIG_SUBDIR = "config";
2089
+ var CREDENTIALS_FILE = "credentials";
2090
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
2091
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
2092
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
2093
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
2080
2094
  var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
2081
2095
  var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
2082
2096
  var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
2083
2097
  var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
2098
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
2084
2099
  function homeDir(env) {
2085
2100
  const home = env.HOME?.trim();
2086
2101
  return home ? home : null;
2087
2102
  }
2088
- function credentialDiskSources(name, env) {
2089
- return profileDiskSources(name, env, null);
2103
+ function absoluteOverride(env, key) {
2104
+ const value = env[key]?.trim();
2105
+ return value && isAbsolute2(value) ? value : null;
2090
2106
  }
2091
- function profileDiskSources(name, env, profile) {
2107
+ function hasnaHomeDir(env) {
2108
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
2109
+ if (override)
2110
+ return override;
2092
2111
  const home = homeDir(env);
2093
- if (!home || !SAFE_APP_SLUG.test(name))
2112
+ return home ? join4(home, HASNA_HOME_DIR) : null;
2113
+ }
2114
+ function appConfigDir(name, env) {
2115
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
2116
+ if (configRoot)
2117
+ return join4(configRoot, name);
2118
+ const root = hasnaHomeDir(env);
2119
+ return root ? join4(root, name, CONFIG_SUBDIR) : null;
2120
+ }
2121
+ function credentialDiskSourceList(name, env, profile = null) {
2122
+ if (!SAFE_APP_SLUG.test(name))
2094
2123
  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
- ];
2124
+ const directory = appConfigDir(name, env);
2125
+ if (!directory)
2126
+ return [];
2127
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
2128
+ return [{ path: join4(directory, file), tier: "disk" }];
2129
+ }
2130
+ function credentialDiskSources(name, env) {
2131
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
2132
+ }
2133
+ function profileDiskSources(name, env, profile) {
2134
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
2101
2135
  }
2102
2136
  function parseEnvFile(text) {
2103
2137
  const values = new Map;
2138
+ const unusable = new Set;
2104
2139
  for (const rawLine of text.split(/\r?\n/)) {
2105
2140
  const line = rawLine.trim();
2106
2141
  if (line.length === 0 || line.startsWith("#"))
@@ -2115,38 +2150,81 @@ function parseEnvFile(text) {
2115
2150
  let value = withoutExport.slice(equals + 1).trim();
2116
2151
  const quote = value[0];
2117
2152
  if (quote === '"' || quote === "'") {
2118
- if (value.length < 2 || !value.endsWith(quote))
2153
+ if (value.length < 2 || !value.endsWith(quote)) {
2154
+ unusable.add(key);
2119
2155
  continue;
2156
+ }
2120
2157
  value = value.slice(1, -1);
2121
2158
  }
2122
- if (value.length === 0)
2159
+ if (value.trim().length === 0) {
2160
+ unusable.add(key);
2123
2161
  continue;
2162
+ }
2163
+ if (values.has(key) && values.get(key) !== value)
2164
+ unusable.add(key);
2124
2165
  values.set(key, value);
2125
2166
  }
2126
- return values;
2167
+ return { values, unusable };
2168
+ }
2169
+ function configFileModeAllowed(mode) {
2170
+ const permissions = mode & 4095;
2171
+ return permissions === 256 || permissions === 384;
2172
+ }
2173
+ function configFileReadsCoherent(before, after) {
2174
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
2127
2175
  }
2128
2176
  function readAppConfigFile(path) {
2129
- let text;
2177
+ const unsafe = (reason) => {
2178
+ throw new CredentialFileUnsafeError(path, reason);
2179
+ };
2180
+ let fd = -1;
2130
2181
  try {
2131
- const stats = statSync2(path);
2132
- if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
2182
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
2183
+ } catch (error) {
2184
+ const code = error.code;
2185
+ if (code === "ENOENT" || code === "ENOTDIR")
2133
2186
  return null;
2134
- text = readFileSync2(path, "utf8");
2135
- } catch {
2136
- return null;
2187
+ if (code === "ELOOP")
2188
+ unsafe("the path is a symlink");
2189
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
2190
+ }
2191
+ try {
2192
+ const before = fstatSync(fd);
2193
+ if (!before.isFile())
2194
+ unsafe("the path is not a regular file");
2195
+ if (!configFileModeAllowed(before.mode)) {
2196
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
2197
+ }
2198
+ const uid = process.getuid?.() ?? process.geteuid?.();
2199
+ if (uid !== undefined && before.uid !== uid)
2200
+ unsafe("the file is not owned by the current user");
2201
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
2202
+ unsafe("the file exceeds the size limit");
2203
+ const bytes = readFileSync2(fd);
2204
+ const after = fstatSync(fd);
2205
+ if (!configFileReadsCoherent(before, after)) {
2206
+ unsafe("the file changed while being read");
2207
+ }
2208
+ return parseEnvFile(bytes.toString("utf8"));
2209
+ } finally {
2210
+ if (fd !== -1)
2211
+ closeSync(fd);
2137
2212
  }
2138
- return parseEnvFile(text);
2139
2213
  }
2140
2214
  function readCredentialFile(path, apiKeyKeys) {
2141
- const values = readAppConfigFile(path);
2142
- if (!values)
2215
+ const parsed = readAppConfigFile(path);
2216
+ if (!parsed)
2143
2217
  return null;
2144
2218
  for (const key of apiKeyKeys) {
2145
- const value = values.get(key)?.trim();
2146
- if (value)
2147
- return value;
2219
+ if (parsed.unusable.has(key)) {
2220
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
2221
+ }
2148
2222
  }
2149
- return null;
2223
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
2224
+ if (new Set(values).size > 1) {
2225
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
2226
+ }
2227
+ return values[0] ?? null;
2150
2228
  }
2151
2229
  var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
2152
2230
  function appConfigDiskValue(name, env, keys) {
@@ -2154,11 +2232,19 @@ function appConfigDiskValue(name, env, keys) {
2154
2232
  if (wanted.length === 0)
2155
2233
  return null;
2156
2234
  for (const path of credentialDiskSources(name, env)) {
2157
- const values = readAppConfigFile(path);
2158
- if (!values)
2235
+ const parsed = readAppConfigFile(path);
2236
+ if (!parsed)
2159
2237
  continue;
2238
+ if (wanted.some((key) => parsed.unusable.has(key))) {
2239
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
2240
+ }
2241
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
2242
+ if (new Set(values).size > 1)
2243
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
2160
2244
  for (const key of wanted) {
2161
- const value = values.get(key)?.trim();
2245
+ if (parsed.unusable.has(key))
2246
+ return { key, value: "", path, unusable: true };
2247
+ const value = parsed.values.get(key)?.trim();
2162
2248
  if (value)
2163
2249
  return { key, value, path };
2164
2250
  }
@@ -2166,20 +2252,21 @@ function appConfigDiskValue(name, env, keys) {
2166
2252
  return null;
2167
2253
  }
2168
2254
  function assertUsableCredential(appName, source, value) {
2255
+ if (VAULT_POINTER_SHAPE.test(value)) {
2256
+ 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]);
2257
+ }
2169
2258
  if (!ILLEGAL_IN_HEADER_VALUE.test(value))
2170
2259
  return;
2171
2260
  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
2261
  }
2173
2262
  var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
2174
2263
  var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
2175
- var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
2176
2264
  function sealCredential(fields) {
2177
2265
  const { apiKey } = fields;
2178
2266
  const visible = {
2179
2267
  tier: fields.tier,
2180
2268
  source: fields.source,
2181
2269
  deliberate: fields.deliberate,
2182
- deprecated: fields.deprecated,
2183
2270
  diskCandidates: Object.freeze([...fields.diskCandidates]),
2184
2271
  warning: fields.warning
2185
2272
  };
@@ -2190,6 +2277,14 @@ function sealCredential(fields) {
2190
2277
  writable: false,
2191
2278
  configurable: false
2192
2279
  });
2280
+ if (fields.pointerVaultKey !== undefined) {
2281
+ Object.defineProperty(sealed, "pointerVaultKey", {
2282
+ value: fields.pointerVaultKey,
2283
+ enumerable: false,
2284
+ writable: false,
2285
+ configurable: false
2286
+ });
2287
+ }
2193
2288
  Object.defineProperty(sealed, INSPECT_CUSTOM, {
2194
2289
  value: () => ({ ...visible, apiKey: "[redacted]" }),
2195
2290
  enumerable: false,
@@ -2204,88 +2299,142 @@ function sealCredential(fields) {
2204
2299
  });
2205
2300
  return Object.freeze(sealed);
2206
2301
  }
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
2302
  function firstEnvValue(env, keys) {
2248
2303
  for (const key of keys) {
2304
+ if (!Object.prototype.hasOwnProperty.call(env, key))
2305
+ continue;
2249
2306
  const value = env[key]?.trim();
2250
2307
  if (value)
2251
2308
  return { key, value };
2252
2309
  }
2253
2310
  return null;
2254
2311
  }
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
- `);
2312
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
2313
+ function isAmbientEnvironment(env) {
2314
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
2315
+ }
2316
+ function defaultKeychainRunner(argv) {
2317
+ const result = spawnSync2(KEYCHAIN_SECURITY_BIN, [...argv], {
2318
+ encoding: "utf8",
2319
+ stdio: ["ignore", "pipe", "pipe"],
2320
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
2321
+ });
2322
+ return {
2323
+ status: result.status,
2324
+ stdout: result.stdout ?? "",
2325
+ stderr: result.error ? result.error.message : result.stderr ?? ""
2326
+ };
2327
+ }
2328
+ function keychainTierEnabled(env, options) {
2329
+ if ((options.platform ?? process.platform) !== "darwin")
2330
+ return false;
2331
+ if (options.enabled !== undefined)
2332
+ return options.enabled;
2333
+ return options.run !== undefined || isAmbientEnvironment(env);
2334
+ }
2335
+ function keychainAccount(env, options) {
2336
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
2337
+ if (station)
2338
+ return station;
2339
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
2340
+ if (host)
2341
+ return host;
2342
+ const user = env.USER?.trim();
2343
+ return user || null;
2344
+ }
2345
+ function keychainFailureHint(text) {
2346
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
2347
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
2348
+ return clean ? `: ${clean}` : "";
2349
+ }
2350
+ function readKeychainItem(name, env, kind, options) {
2351
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
2352
+ return null;
2353
+ const account = keychainAccount(env, options);
2354
+ if (!account)
2355
+ return null;
2356
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
2357
+ const source = `keychain:${service}@${account}`;
2358
+ const run = options.run ?? defaultKeychainRunner;
2359
+ let result;
2360
+ try {
2361
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
2362
+ } catch (error) {
2363
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
2364
+ 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]);
2365
+ }
2366
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
2367
+ return null;
2368
+ if (result.status !== 0) {
2369
+ 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]);
2269
2370
  }
2371
+ const value = result.stdout.trim();
2372
+ if (!value) {
2373
+ 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]);
2374
+ }
2375
+ return { value, source };
2376
+ }
2377
+ function keychainConfigValue(name, env, options = {}) {
2378
+ return readKeychainItem(name, env, "api-url", options);
2379
+ }
2380
+ function snapshotClientEnvironment(name, env) {
2381
+ const keys = clientTransportEnvKeys(name);
2382
+ const ambient = isAmbientEnvironment(env);
2383
+ const snapshot = Object.create(null);
2384
+ for (const key of [
2385
+ ...keys.apiUrlKeys,
2386
+ ...keys.apiKeyKeys,
2387
+ credentialOverrideEnvKey(name),
2388
+ credentialPointerEnvKey(name),
2389
+ CREDENTIAL_PROFILE_ENV_KEY,
2390
+ "HOME",
2391
+ HASNA_HOME_ENV_KEY,
2392
+ HASNA_CONFIG_HOME_ENV_KEY,
2393
+ KEYCHAIN_STATION_ENV_KEY,
2394
+ "USER"
2395
+ ]) {
2396
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
2397
+ if (!descriptor)
2398
+ continue;
2399
+ if (!("value" in descriptor)) {
2400
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
2401
+ }
2402
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
2403
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
2404
+ }
2405
+ snapshot[key] = descriptor.value;
2406
+ }
2407
+ if (ambient) {
2408
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
2409
+ value: true,
2410
+ enumerable: false,
2411
+ writable: false,
2412
+ configurable: false
2413
+ });
2414
+ }
2415
+ return Object.freeze(snapshot);
2270
2416
  }
2271
2417
  function resolveCredential(name, env, options = {}) {
2418
+ env = snapshotClientEnvironment(name, env);
2272
2419
  const { apiKeyKeys } = clientTransportEnvKeys(name);
2273
2420
  const diskPaths = credentialDiskSources(name, env);
2274
- const explicitKey = options.apiKey?.trim();
2275
- if (explicitKey) {
2421
+ if (options.apiKey !== undefined) {
2422
+ const explicitKey = options.apiKey.trim();
2423
+ if (!explicitKey) {
2424
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
2425
+ }
2276
2426
  assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
2277
2427
  return sealCredential({
2278
2428
  apiKey: explicitKey,
2279
2429
  tier: "argument",
2280
2430
  source: "explicit apiKey argument",
2281
2431
  deliberate: true,
2282
- deprecated: false,
2283
2432
  diskCandidates: diskPaths,
2284
2433
  warning: null
2285
2434
  });
2286
2435
  }
2287
2436
  const overrideKeyName = credentialOverrideEnvKey(name);
2288
- const overrideRaw = env[overrideKeyName];
2437
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
2289
2438
  if (overrideRaw !== undefined) {
2290
2439
  const override = overrideRaw.trim();
2291
2440
  if (!override) {
@@ -2297,12 +2446,38 @@ function resolveCredential(name, env, options = {}) {
2297
2446
  tier: "override",
2298
2447
  source: overrideKeyName,
2299
2448
  deliberate: true,
2300
- deprecated: false,
2301
2449
  diskCandidates: diskPaths,
2302
2450
  warning: null
2303
2451
  });
2304
2452
  }
2305
- const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
2453
+ const pointerKeyName = credentialPointerEnvKey(name);
2454
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
2455
+ if (pointerRaw !== undefined) {
2456
+ const pointer = pointerRaw.trim();
2457
+ if (!pointer) {
2458
+ 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]);
2459
+ }
2460
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
2461
+ 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]);
2462
+ }
2463
+ return sealCredential({
2464
+ apiKey: "",
2465
+ pointerVaultKey: pointer,
2466
+ tier: "pointer",
2467
+ source: pointerKeyName,
2468
+ deliberate: true,
2469
+ diskCandidates: diskPaths,
2470
+ warning: null
2471
+ });
2472
+ }
2473
+ if (options.profile !== undefined && !options.profile.trim()) {
2474
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
2475
+ }
2476
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
2477
+ if (profileRaw !== undefined && !profileRaw.trim()) {
2478
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
2479
+ }
2480
+ const profile = options.profile?.trim() || profileRaw?.trim();
2306
2481
  if (profile) {
2307
2482
  const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
2308
2483
  if (!SAFE_PROFILE.test(profile)) {
@@ -2318,7 +2493,6 @@ function resolveCredential(name, env, options = {}) {
2318
2493
  tier: "profile",
2319
2494
  source: path,
2320
2495
  deliberate: true,
2321
- deprecated: false,
2322
2496
  diskCandidates: paths,
2323
2497
  warning: null
2324
2498
  });
@@ -2326,51 +2500,67 @@ function resolveCredential(name, env, options = {}) {
2326
2500
  }
2327
2501
  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
2502
  }
2329
- const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
2503
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
2504
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
2505
+ if (blankEnv) {
2506
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
2507
+ }
2508
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
2509
+ 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));
2510
+ }
2511
+ const envHit = firstEnvValue(env, apiKeyKeys);
2512
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
2513
+ if (keychainHit) {
2514
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
2515
+ 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;
2516
+ return sealCredential({
2517
+ apiKey: keychainHit.value,
2518
+ tier: "keychain",
2519
+ source: keychainHit.source,
2520
+ deliberate: false,
2521
+ diskCandidates: diskPaths,
2522
+ warning
2523
+ });
2524
+ }
2525
+ const diskSourceList = credentialDiskSourceList(name, env, null);
2526
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
2330
2527
  if (diskHits.length > 0) {
2331
2528
  const winner = diskHits[0];
2332
- assertUsableCredential(name, winner.path, winner.value);
2529
+ assertUsableCredential(name, winner.src.path, winner.value);
2333
2530
  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
- })()
2531
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
2532
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
2339
2533
  ];
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;
2534
+ 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
2535
  return sealCredential({
2342
2536
  apiKey: winner.value,
2343
- tier: "disk",
2344
- source: winner.path,
2537
+ tier: winner.src.tier,
2538
+ source: winner.src.path,
2345
2539
  deliberate: false,
2346
- deprecated: false,
2347
2540
  diskCandidates: diskPaths,
2348
2541
  warning
2349
2542
  });
2350
2543
  }
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
- }
2544
+ if (envHit) {
2545
+ assertUsableCredential(name, envHit.key, envHit.value);
2362
2546
  return sealCredential({
2363
- apiKey: legacy.value,
2364
- tier: "legacy-env",
2365
- source: legacy.key,
2547
+ apiKey: envHit.value,
2548
+ tier: "env",
2549
+ source: envHit.key,
2366
2550
  deliberate: false,
2367
- deprecated: true,
2368
2551
  diskCandidates: diskPaths,
2369
- warning: message
2552
+ warning: null
2370
2553
  });
2371
2554
  }
2372
2555
  return null;
2373
2556
  }
2557
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
2558
+ var requireSecretsSdk = createRequire(import.meta.url);
2559
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
2560
+ var DEFAULT_AUTHORITY_SOURCE = "default";
2561
+ function defaultFleetGatewayBaseUrl(name) {
2562
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
2563
+ }
2374
2564
  var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
2375
2565
  var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
2376
2566
  function isValidDnsDomain(value) {
@@ -2379,23 +2569,835 @@ function isValidDnsDomain(value) {
2379
2569
  }
2380
2570
  return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
2381
2571
  }
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 };
2572
+ function validateAppSlug(name) {
2573
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
2574
+ throw new Error("App name must be one lowercase DNS label.");
2575
+ }
2576
+ return name;
2577
+ }
2578
+ function rawAuthority(value) {
2579
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
2580
+ if (!match)
2581
+ throw new Error("API URL must be absolute.");
2582
+ const afterScheme = value.slice(match[0].length);
2583
+ const boundary = afterScheme.search(/[/?#]/);
2584
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
2585
+ if (!authority)
2586
+ throw new Error("API URL must include a hostname.");
2587
+ return authority;
2588
+ }
2589
+ function assertCanonicalPort(port) {
2590
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
2591
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2592
+ }
2593
+ const numericPort = Number(port);
2594
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
2595
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2596
+ }
2597
+ }
2598
+ function canonicalAuthorityHostname(authority) {
2599
+ let rawHostname;
2600
+ if (authority.startsWith("[")) {
2601
+ const closingBracket = authority.indexOf("]");
2602
+ if (closingBracket === -1) {
2603
+ throw new Error("API URL authority must contain a canonical hostname.");
2604
+ }
2605
+ rawHostname = authority.slice(0, closingBracket + 1);
2606
+ const portSuffix = authority.slice(closingBracket + 1);
2607
+ if (portSuffix) {
2608
+ if (!portSuffix.startsWith(":")) {
2609
+ throw new Error("API URL authority must contain a canonical hostname and port.");
2610
+ }
2611
+ assertCanonicalPort(portSuffix.slice(1));
2612
+ }
2613
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
2614
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
2615
+ }
2616
+ } else {
2617
+ const firstColon = authority.indexOf(":");
2618
+ const lastColon = authority.lastIndexOf(":");
2619
+ if (firstColon !== lastColon) {
2620
+ throw new Error("IPv6 API URL authorities must use brackets.");
2621
+ }
2622
+ if (lastColon !== -1) {
2623
+ const port = authority.slice(lastColon + 1);
2624
+ assertCanonicalPort(port);
2625
+ rawHostname = authority.slice(0, lastColon);
2626
+ } else {
2627
+ rawHostname = authority;
2628
+ }
2629
+ const ipVersion = isIP(rawHostname);
2630
+ const numericAddressParts = rawHostname.split(".");
2631
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
2632
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
2633
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
2634
+ }
2635
+ }
2636
+ return rawHostname.toLowerCase();
2637
+ }
2638
+ function isDeliberateLoopbackHttpAuthority(authority) {
2639
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
2640
+ }
2641
+ function toV1BaseUrl(apiUrl) {
2642
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
2643
+ throw new Error("API URL must not contain ASCII control characters.");
2644
+ }
2645
+ const input = apiUrl.trim();
2646
+ const authority = rawAuthority(input);
2647
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
2648
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
2649
+ }
2650
+ const canonicalHostname = canonicalAuthorityHostname(authority);
2651
+ const url = new URL(input);
2652
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2653
+ throw new Error("API URL must use http or https.");
2654
+ }
2655
+ if (url.username || url.password) {
2656
+ throw new Error("API URL must not include credentials.");
2657
+ }
2658
+ if (!url.hostname || url.hostname.endsWith(".")) {
2659
+ throw new Error("API URL must include a canonical hostname.");
2660
+ }
2661
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
2662
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
2663
+ }
2664
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
2665
+ throw new Error("API URL must not use IDN or punycode hostnames.");
2666
+ }
2667
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
2668
+ throw new Error("API URL may use http only for an exact loopback authority.");
2669
+ }
2670
+ if (url.search || url.hash) {
2671
+ throw new Error("API URL must not include a query string or fragment.");
2672
+ }
2673
+ let path = url.pathname.replace(/\/+$/, "");
2674
+ if (path.endsWith("/v1"))
2675
+ path = path.slice(0, -"/v1".length);
2676
+ url.pathname = `${path}/v1`;
2677
+ return url.toString().replace(/\/+$/, "");
2678
+ }
2679
+ class ClientTransportConfigurationError extends Error {
2680
+ appName;
2681
+ sources;
2682
+ constructor(appName, message, sources = []) {
2683
+ super(message);
2684
+ this.name = "ClientTransportConfigurationError";
2685
+ this.appName = appName;
2686
+ this.sources = Object.freeze([...sources]);
2687
+ }
2688
+ }
2689
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
2690
+ env = snapshotClientEnvironment(name, env);
2691
+ const keys = clientTransportEnvKeys(name);
2692
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
2693
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
2694
+ if (blankUrl) {
2695
+ 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]);
2696
+ }
2697
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
2698
+ if (controlledUrl) {
2699
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
2700
+ }
2701
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
2702
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
2703
+ 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));
2704
+ }
2705
+ const envUrlHit = usableUrlEntries[0] ?? null;
2706
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
2707
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
2708
+ if (diskConfigUrlHit?.unusable) {
2709
+ 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]);
2710
+ }
2711
+ const urlCandidates = [
2712
+ ...envUrlHit ? [envUrlHit] : [],
2713
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
2714
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
2715
+ ];
2716
+ const configuredUrl = urlCandidates[0] ?? null;
2717
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
2718
+ if (configuredUrl && divergentUrls.length > 0) {
2719
+ 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));
2720
+ }
2721
+ const warnings = [];
2722
+ if (configuredUrl && !envUrlHit) {
2723
+ 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.`);
2724
+ }
2725
+ const credential = resolveCredential(name, env, options.credentials);
2726
+ if (!credential) {
2727
+ const diskHint = credentialDiskSourcesForMessage(name, env);
2728
+ 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`;
2729
+ 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.`);
2730
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
2731
+ }
2732
+ if (credential.warning)
2733
+ warnings.push(credential.warning);
2734
+ let urlHit;
2735
+ if (configuredUrl) {
2736
+ urlHit = configuredUrl;
2737
+ } else {
2738
+ try {
2739
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
2740
+ } catch (error) {
2741
+ const message = error instanceof Error ? error.message : String(error);
2742
+ 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]]);
2743
+ }
2744
+ }
2745
+ const apiUrlSource = urlHit.key;
2746
+ let baseUrl;
2747
+ try {
2748
+ baseUrl = toV1BaseUrl(urlHit.value);
2749
+ } catch (error) {
2750
+ const message = error instanceof Error ? error.message : String(error);
2751
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
2752
+ }
2753
+ return {
2754
+ resolution: {
2755
+ transport: "http",
2756
+ transportSource: urlHit.key,
2757
+ baseUrl,
2758
+ apiUrlSource,
2759
+ apiKeyPresent: true,
2760
+ apiKeySource: credential.source,
2761
+ apiKeyTier: credential.tier,
2762
+ misconfigured: false,
2763
+ warning: warnings.length > 0 ? warnings.join(" ") : null
2764
+ },
2765
+ credential
2766
+ };
2767
+ }
2768
+ function resolveClientTransport(name, env = process.env, options = {}) {
2769
+ return resolveClientTransportSnapshot(name, env, options).resolution;
2770
+ }
2771
+ function credentialDiskSourcesForMessage(name, env) {
2772
+ const paths = credentialDiskSources(name, env);
2773
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
2774
+ }
2775
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
2776
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
2777
+ "host",
2778
+ ":authority",
2779
+ "forwarded",
2780
+ "x-forwarded-host",
2781
+ "x-original-host"
2782
+ ]);
2783
+
2784
+ // ../contracts/dist/client/storage.js
2785
+ import { isIP as isIP2 } from "net";
2786
+ import { spawnSync as spawnSync3 } from "child_process";
2787
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync3 } from "fs";
2788
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
2789
+ import { createRequire as createRequire2 } from "module";
2790
+ import { hostname as osHostname2 } from "os";
2791
+ import { isAbsolute as isAbsolute3, join as join5 } from "path";
2792
+ function envToken2(name) {
2793
+ return name.toUpperCase().replace(/-/g, "_");
2794
+ }
2795
+ function clientTransportEnvKeys2(name) {
2796
+ const envSegment = envToken2(name);
2797
+ return {
2798
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
2799
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
2800
+ };
2801
+ }
2802
+ function credentialOverrideEnvKey2(name) {
2803
+ return `HASNA_${envToken2(name)}_API_KEY_OVERRIDE`;
2804
+ }
2805
+ var CREDENTIAL_PROFILE_ENV_KEY2 = "HASNA_PROFILE";
2806
+ function credentialPointerEnvKey2(name) {
2807
+ return `HASNA_${envToken2(name)}_API_KEY_REF`;
2808
+ }
2809
+
2810
+ class CredentialResolutionError2 extends Error {
2811
+ appName;
2812
+ attempted;
2813
+ constructor(appName, message, attempted) {
2814
+ super(message);
2815
+ this.name = "CredentialResolutionError";
2816
+ this.appName = appName;
2817
+ this.attempted = attempted;
2818
+ }
2819
+ }
2820
+
2821
+ class CredentialFileUnsafeError2 extends Error {
2822
+ path;
2823
+ constructor(path, reason) {
2824
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
2825
+ this.name = "CredentialFileUnsafeError";
2826
+ this.path = path;
2827
+ }
2828
+ }
2829
+ var HASNA_HOME_ENV_KEY2 = "HASNA_HOME";
2830
+ var HASNA_CONFIG_HOME_ENV_KEY2 = "HASNA_CONFIG_HOME";
2831
+ var KEYCHAIN_STATION_ENV_KEY2 = "HASNA_STATION";
2832
+ var HASNA_HOME_DIR2 = ".hasna";
2833
+ var CONFIG_SUBDIR2 = "config";
2834
+ var CREDENTIALS_FILE2 = "credentials";
2835
+ var KEYCHAIN_SECURITY_BIN2 = "/usr/bin/security";
2836
+ var KEYCHAIN_SERVICE_PREFIX2 = "hasna.credentials";
2837
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS2 = 44;
2838
+ var KEYCHAIN_SPAWN_TIMEOUT_MS2 = 1e4;
2839
+ var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
2840
+ var SAFE_APP_SLUG2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
2841
+ var SAFE_PROFILE2 = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
2842
+ var ILLEGAL_IN_HEADER_VALUE2 = /[^\t\x20-\x7e]/;
2843
+ var VAULT_POINTER_SHAPE2 = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
2844
+ function homeDir2(env) {
2845
+ const home = env.HOME?.trim();
2846
+ return home ? home : null;
2847
+ }
2848
+ function absoluteOverride2(env, key) {
2849
+ const value = env[key]?.trim();
2850
+ return value && isAbsolute3(value) ? value : null;
2851
+ }
2852
+ function hasnaHomeDir2(env) {
2853
+ const override = absoluteOverride2(env, HASNA_HOME_ENV_KEY2);
2854
+ if (override)
2855
+ return override;
2856
+ const home = homeDir2(env);
2857
+ return home ? join5(home, HASNA_HOME_DIR2) : null;
2858
+ }
2859
+ function appConfigDir2(name, env) {
2860
+ const configRoot = absoluteOverride2(env, HASNA_CONFIG_HOME_ENV_KEY2);
2861
+ if (configRoot)
2862
+ return join5(configRoot, name);
2863
+ const root = hasnaHomeDir2(env);
2864
+ return root ? join5(root, name, CONFIG_SUBDIR2) : null;
2865
+ }
2866
+ function credentialDiskSourceList2(name, env, profile = null) {
2867
+ if (!SAFE_APP_SLUG2.test(name))
2868
+ return [];
2869
+ const directory = appConfigDir2(name, env);
2870
+ if (!directory)
2871
+ return [];
2872
+ const file = profile ? `${CREDENTIALS_FILE2}-${profile}` : CREDENTIALS_FILE2;
2873
+ return [{ path: join5(directory, file), tier: "disk" }];
2874
+ }
2875
+ function credentialDiskSources2(name, env) {
2876
+ return credentialDiskSourceList2(name, env, null).map((s) => s.path);
2877
+ }
2878
+ function profileDiskSources2(name, env, profile) {
2879
+ return credentialDiskSourceList2(name, env, profile).map((s) => s.path);
2880
+ }
2881
+ function parseEnvFile2(text) {
2882
+ const values = new Map;
2883
+ const unusable = new Set;
2884
+ for (const rawLine of text.split(/\r?\n/)) {
2885
+ const line = rawLine.trim();
2886
+ if (line.length === 0 || line.startsWith("#"))
2887
+ continue;
2888
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
2889
+ const equals = withoutExport.indexOf("=");
2890
+ if (equals <= 0)
2891
+ continue;
2892
+ const key = withoutExport.slice(0, equals).trim();
2893
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
2894
+ continue;
2895
+ let value = withoutExport.slice(equals + 1).trim();
2896
+ const quote = value[0];
2897
+ if (quote === '"' || quote === "'") {
2898
+ if (value.length < 2 || !value.endsWith(quote)) {
2899
+ unusable.add(key);
2900
+ continue;
2901
+ }
2902
+ value = value.slice(1, -1);
2903
+ }
2904
+ if (value.trim().length === 0) {
2905
+ unusable.add(key);
2906
+ continue;
2907
+ }
2908
+ if (values.has(key) && values.get(key) !== value)
2909
+ unusable.add(key);
2910
+ values.set(key, value);
2911
+ }
2912
+ return { values, unusable };
2913
+ }
2914
+ function configFileModeAllowed2(mode) {
2915
+ const permissions = mode & 4095;
2916
+ return permissions === 256 || permissions === 384;
2917
+ }
2918
+ function configFileReadsCoherent2(before, after) {
2919
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
2920
+ }
2921
+ function readAppConfigFile2(path) {
2922
+ const unsafe = (reason) => {
2923
+ throw new CredentialFileUnsafeError2(path, reason);
2924
+ };
2925
+ let fd = -1;
2926
+ try {
2927
+ fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
2928
+ } catch (error) {
2929
+ const code = error.code;
2930
+ if (code === "ENOENT" || code === "ENOTDIR")
2931
+ return null;
2932
+ if (code === "ELOOP")
2933
+ unsafe("the path is a symlink");
2934
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
2935
+ }
2936
+ try {
2937
+ const before = fstatSync2(fd);
2938
+ if (!before.isFile())
2939
+ unsafe("the path is not a regular file");
2940
+ if (!configFileModeAllowed2(before.mode)) {
2941
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
2942
+ }
2943
+ const uid = process.getuid?.() ?? process.geteuid?.();
2944
+ if (uid !== undefined && before.uid !== uid)
2945
+ unsafe("the file is not owned by the current user");
2946
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
2947
+ unsafe("the file exceeds the size limit");
2948
+ const bytes = readFileSync3(fd);
2949
+ const after = fstatSync2(fd);
2950
+ if (!configFileReadsCoherent2(before, after)) {
2951
+ unsafe("the file changed while being read");
2952
+ }
2953
+ return parseEnvFile2(bytes.toString("utf8"));
2954
+ } finally {
2955
+ if (fd !== -1)
2956
+ closeSync2(fd);
2957
+ }
2958
+ }
2959
+ function readCredentialFile2(path, apiKeyKeys) {
2960
+ const parsed = readAppConfigFile2(path);
2961
+ if (!parsed)
2962
+ return null;
2963
+ for (const key of apiKeyKeys) {
2964
+ if (parsed.unusable.has(key)) {
2965
+ throw new CredentialFileUnsafeError2(path, `${key} is declared but blank or malformed`);
2966
+ }
2967
+ }
2968
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
2969
+ if (new Set(values).size > 1) {
2970
+ throw new CredentialFileUnsafeError2(path, "credential aliases disagree");
2971
+ }
2972
+ return values[0] ?? null;
2973
+ }
2974
+ var CREDENTIAL_SHAPED_KEY2 = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
2975
+ function appConfigDiskValue2(name, env, keys) {
2976
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY2.test(key));
2977
+ if (wanted.length === 0)
2978
+ return null;
2979
+ for (const path of credentialDiskSources2(name, env)) {
2980
+ const parsed = readAppConfigFile2(path);
2981
+ if (!parsed)
2982
+ continue;
2983
+ if (wanted.some((key) => parsed.unusable.has(key))) {
2984
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
2985
+ }
2986
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
2987
+ if (new Set(values).size > 1)
2988
+ throw new CredentialFileUnsafeError2(path, "configuration aliases disagree");
2989
+ for (const key of wanted) {
2990
+ if (parsed.unusable.has(key))
2991
+ return { key, value: "", path, unusable: true };
2992
+ const value = parsed.values.get(key)?.trim();
2993
+ if (value)
2994
+ return { key, value, path };
2995
+ }
2996
+ }
2997
+ return null;
2998
+ }
2999
+ function assertUsableCredential2(appName, source, value) {
3000
+ if (VAULT_POINTER_SHAPE2.test(value)) {
3001
+ 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]);
3002
+ }
3003
+ if (!ILLEGAL_IN_HEADER_VALUE2.test(value))
3004
+ return;
3005
+ 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]);
3006
+ }
3007
+ var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
3008
+ var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
3009
+ var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
3010
+ function sealCredential2(fields) {
3011
+ const { apiKey } = fields;
3012
+ const visible = {
3013
+ tier: fields.tier,
3014
+ source: fields.source,
3015
+ deliberate: fields.deliberate,
3016
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
3017
+ warning: fields.warning
3018
+ };
3019
+ const sealed = { ...visible };
3020
+ Object.defineProperty(sealed, "apiKey", {
3021
+ value: apiKey,
3022
+ enumerable: false,
3023
+ writable: false,
3024
+ configurable: false
3025
+ });
3026
+ if (fields.pointerVaultKey !== undefined) {
3027
+ Object.defineProperty(sealed, "pointerVaultKey", {
3028
+ value: fields.pointerVaultKey,
3029
+ enumerable: false,
3030
+ writable: false,
3031
+ configurable: false
3032
+ });
3033
+ }
3034
+ Object.defineProperty(sealed, INSPECT_CUSTOM2, {
3035
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
3036
+ enumerable: false,
3037
+ writable: false,
3038
+ configurable: false
3039
+ });
3040
+ Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
3041
+ value: true,
3042
+ enumerable: false,
3043
+ writable: false,
3044
+ configurable: false
3045
+ });
3046
+ return Object.freeze(sealed);
3047
+ }
3048
+ function isSealedCredential(credential) {
3049
+ return credential[CREDENTIAL_SEAL2] === true;
3050
+ }
3051
+ function explicitCredential(appName, apiKey) {
3052
+ const source = "explicit apiKey option";
3053
+ assertUsableCredential2(appName, source, apiKey);
3054
+ return sealCredential2({
3055
+ apiKey,
3056
+ tier: "argument",
3057
+ source,
3058
+ deliberate: true,
3059
+ diskCandidates: [],
3060
+ warning: null
3061
+ });
3062
+ }
3063
+ function validateAndSealResolvedCredential(appName, credential) {
3064
+ const apiKey = credential.apiKey;
3065
+ assertUsableCredential2(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
3066
+ if (!isSealedCredential(credential)) {
3067
+ return sealCredential2({
3068
+ apiKey,
3069
+ tier: "argument",
3070
+ source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
3071
+ deliberate: true,
3072
+ diskCandidates: [],
3073
+ warning: null
3074
+ });
3075
+ }
3076
+ return sealCredential2({
3077
+ apiKey,
3078
+ tier: credential.tier,
3079
+ source: credential.source,
3080
+ deliberate: credential.deliberate,
3081
+ diskCandidates: credential.diskCandidates,
3082
+ warning: credential.warning,
3083
+ ...credential.pointerVaultKey !== undefined ? { pointerVaultKey: credential.pointerVaultKey } : {}
3084
+ });
3085
+ }
3086
+ function firstEnvValue2(env, keys) {
3087
+ for (const key of keys) {
3088
+ if (!Object.prototype.hasOwnProperty.call(env, key))
3089
+ continue;
3090
+ const value = env[key]?.trim();
3091
+ if (value)
3092
+ return { key, value };
3093
+ }
3094
+ return null;
3095
+ }
3096
+ var AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
3097
+ function isAmbientEnvironment2(env) {
3098
+ return env === process.env || env[AMBIENT_ENVIRONMENT2] === true;
3099
+ }
3100
+ function defaultKeychainRunner2(argv) {
3101
+ const result = spawnSync3(KEYCHAIN_SECURITY_BIN2, [...argv], {
3102
+ encoding: "utf8",
3103
+ stdio: ["ignore", "pipe", "pipe"],
3104
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS2
3105
+ });
3106
+ return {
3107
+ status: result.status,
3108
+ stdout: result.stdout ?? "",
3109
+ stderr: result.error ? result.error.message : result.stderr ?? ""
3110
+ };
3111
+ }
3112
+ function keychainTierEnabled2(env, options) {
3113
+ if ((options.platform ?? process.platform) !== "darwin")
3114
+ return false;
3115
+ if (options.enabled !== undefined)
3116
+ return options.enabled;
3117
+ return options.run !== undefined || isAmbientEnvironment2(env);
3118
+ }
3119
+ function keychainAccount2(env, options) {
3120
+ const station = env[KEYCHAIN_STATION_ENV_KEY2]?.trim();
3121
+ if (station)
3122
+ return station;
3123
+ const host = (options.hostname ?? osHostname2)().split(".")[0]?.trim() ?? "";
3124
+ if (host)
3125
+ return host;
3126
+ const user = env.USER?.trim();
3127
+ return user || null;
3128
+ }
3129
+ function keychainFailureHint2(text) {
3130
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
3131
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
3132
+ return clean ? `: ${clean}` : "";
3133
+ }
3134
+ function readKeychainItem2(name, env, kind, options) {
3135
+ if (!SAFE_APP_SLUG2.test(name) || !keychainTierEnabled2(env, options))
3136
+ return null;
3137
+ const account = keychainAccount2(env, options);
3138
+ if (!account)
3139
+ return null;
3140
+ const service = `${KEYCHAIN_SERVICE_PREFIX2}.${name}.${kind}`;
3141
+ const source = `keychain:${service}@${account}`;
3142
+ const run = options.run ?? defaultKeychainRunner2;
3143
+ let result;
3144
+ try {
3145
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
3146
+ } catch (error) {
3147
+ const reason = keychainFailureHint2(error instanceof Error ? error.message : String(error));
3148
+ 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]);
3149
+ }
3150
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS2)
3151
+ return null;
3152
+ if (result.status !== 0) {
3153
+ 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]);
3154
+ }
3155
+ const value = result.stdout.trim();
3156
+ if (!value) {
3157
+ 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]);
3158
+ }
3159
+ return { value, source };
3160
+ }
3161
+ function keychainConfigValue2(name, env, options = {}) {
3162
+ return readKeychainItem2(name, env, "api-url", options);
3163
+ }
3164
+ function snapshotClientEnvironment2(name, env) {
3165
+ const keys = clientTransportEnvKeys2(name);
3166
+ const ambient = isAmbientEnvironment2(env);
3167
+ const snapshot = Object.create(null);
3168
+ for (const key of [
3169
+ ...keys.apiUrlKeys,
3170
+ ...keys.apiKeyKeys,
3171
+ credentialOverrideEnvKey2(name),
3172
+ credentialPointerEnvKey2(name),
3173
+ CREDENTIAL_PROFILE_ENV_KEY2,
3174
+ "HOME",
3175
+ HASNA_HOME_ENV_KEY2,
3176
+ HASNA_CONFIG_HOME_ENV_KEY2,
3177
+ KEYCHAIN_STATION_ENV_KEY2,
3178
+ "USER"
3179
+ ]) {
3180
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
3181
+ if (!descriptor)
3182
+ continue;
3183
+ if (!("value" in descriptor)) {
3184
+ throw new CredentialResolutionError2(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
3185
+ }
3186
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
3187
+ throw new CredentialResolutionError2(name, `${key} must be a string data property.`, [key]);
3188
+ }
3189
+ snapshot[key] = descriptor.value;
3190
+ }
3191
+ if (ambient) {
3192
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT2, {
3193
+ value: true,
3194
+ enumerable: false,
3195
+ writable: false,
3196
+ configurable: false
3197
+ });
3198
+ }
3199
+ return Object.freeze(snapshot);
3200
+ }
3201
+ function resolveCredential2(name, env, options = {}) {
3202
+ env = snapshotClientEnvironment2(name, env);
3203
+ const { apiKeyKeys } = clientTransportEnvKeys2(name);
3204
+ const diskPaths = credentialDiskSources2(name, env);
3205
+ if (options.apiKey !== undefined) {
3206
+ const explicitKey = options.apiKey.trim();
3207
+ if (!explicitKey) {
3208
+ throw new CredentialResolutionError2(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
3209
+ }
3210
+ assertUsableCredential2(name, "the explicit apiKey argument", explicitKey);
3211
+ return sealCredential2({
3212
+ apiKey: explicitKey,
3213
+ tier: "argument",
3214
+ source: "explicit apiKey argument",
3215
+ deliberate: true,
3216
+ diskCandidates: diskPaths,
3217
+ warning: null
3218
+ });
3219
+ }
3220
+ const overrideKeyName = credentialOverrideEnvKey2(name);
3221
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
3222
+ if (overrideRaw !== undefined) {
3223
+ const override = overrideRaw.trim();
3224
+ if (!override) {
3225
+ 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]);
3226
+ }
3227
+ assertUsableCredential2(name, overrideKeyName, override);
3228
+ return sealCredential2({
3229
+ apiKey: override,
3230
+ tier: "override",
3231
+ source: overrideKeyName,
3232
+ deliberate: true,
3233
+ diskCandidates: diskPaths,
3234
+ warning: null
3235
+ });
3236
+ }
3237
+ const pointerKeyName = credentialPointerEnvKey2(name);
3238
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
3239
+ if (pointerRaw !== undefined) {
3240
+ const pointer = pointerRaw.trim();
3241
+ if (!pointer) {
3242
+ 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]);
3243
+ }
3244
+ if (!VAULT_POINTER_SHAPE2.test(pointer)) {
3245
+ 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]);
3246
+ }
3247
+ return sealCredential2({
3248
+ apiKey: "",
3249
+ pointerVaultKey: pointer,
3250
+ tier: "pointer",
3251
+ source: pointerKeyName,
3252
+ deliberate: true,
3253
+ diskCandidates: diskPaths,
3254
+ warning: null
3255
+ });
3256
+ }
3257
+ if (options.profile !== undefined && !options.profile.trim()) {
3258
+ throw new CredentialResolutionError2(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
3259
+ }
3260
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY2) ? env[CREDENTIAL_PROFILE_ENV_KEY2] : undefined;
3261
+ if (profileRaw !== undefined && !profileRaw.trim()) {
3262
+ throw new CredentialResolutionError2(name, `${CREDENTIAL_PROFILE_ENV_KEY2} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY2]);
3263
+ }
3264
+ const profile = options.profile?.trim() || profileRaw?.trim();
3265
+ if (profile) {
3266
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY2;
3267
+ if (!SAFE_PROFILE2.test(profile)) {
3268
+ throw new CredentialResolutionError2(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
3269
+ }
3270
+ const paths = profileDiskSources2(name, env, profile);
3271
+ for (const path of paths) {
3272
+ const value = readCredentialFile2(path, apiKeyKeys);
3273
+ if (value) {
3274
+ assertUsableCredential2(name, path, value);
3275
+ return sealCredential2({
3276
+ apiKey: value,
3277
+ tier: "profile",
3278
+ source: path,
3279
+ deliberate: true,
3280
+ diskCandidates: paths,
3281
+ warning: null
3282
+ });
3283
+ }
3284
+ }
3285
+ 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);
3286
+ }
3287
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
3288
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
3289
+ if (blankEnv) {
3290
+ throw new CredentialResolutionError2(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
3291
+ }
3292
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
3293
+ 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));
3294
+ }
3295
+ const envHit = firstEnvValue2(env, apiKeyKeys);
3296
+ const keychainHit = readKeychainItem2(name, env, "api-key", options.keychain ?? {});
3297
+ if (keychainHit) {
3298
+ assertUsableCredential2(name, keychainHit.source, keychainHit.value);
3299
+ 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;
3300
+ return sealCredential2({
3301
+ apiKey: keychainHit.value,
3302
+ tier: "keychain",
3303
+ source: keychainHit.source,
3304
+ deliberate: false,
3305
+ diskCandidates: diskPaths,
3306
+ warning
3307
+ });
3308
+ }
3309
+ const diskSourceList = credentialDiskSourceList2(name, env, null);
3310
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile2(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
3311
+ if (diskHits.length > 0) {
3312
+ const winner = diskHits[0];
3313
+ assertUsableCredential2(name, winner.src.path, winner.value);
3314
+ const divergentSources = [
3315
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
3316
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
3317
+ ];
3318
+ 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;
3319
+ return sealCredential2({
3320
+ apiKey: winner.value,
3321
+ tier: winner.src.tier,
3322
+ source: winner.src.path,
3323
+ deliberate: false,
3324
+ diskCandidates: diskPaths,
3325
+ warning
3326
+ });
3327
+ }
3328
+ if (envHit) {
3329
+ assertUsableCredential2(name, envHit.key, envHit.value);
3330
+ return sealCredential2({
3331
+ apiKey: envHit.value,
3332
+ tier: "env",
3333
+ source: envHit.key,
3334
+ deliberate: false,
3335
+ diskCandidates: diskPaths,
3336
+ warning: null
3337
+ });
3338
+ }
3339
+ return null;
3340
+ }
3341
+ var SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
3342
+ var requireSecretsSdk2 = createRequire2(import.meta.url);
3343
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
3344
+ const vaultKey = pointerResolution.pointerVaultKey;
3345
+ const pointerEnvKey = pointerResolution.source;
3346
+ if (!vaultKey) {
3347
+ throw new CredentialResolutionError2(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
3348
+ }
3349
+ let secretsSdk;
3350
+ try {
3351
+ secretsSdk = requireSecretsSdk2(SECRETS_PACKAGE_SPECIFIER2);
3352
+ } catch {
3353
+ 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]);
3354
+ }
3355
+ let client;
3356
+ try {
3357
+ client = secretsSdk.createSecretsClientFromEnv(env);
3358
+ } catch {
3359
+ 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]);
3360
+ }
3361
+ let secret;
3362
+ try {
3363
+ secret = await client.getSecret({ key: vaultKey });
3364
+ } catch {
3365
+ 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]);
3366
+ }
3367
+ const value = secret.value;
3368
+ if (!value) {
3369
+ throw new CredentialResolutionError2(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
3370
+ }
3371
+ assertUsableCredential2(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
3372
+ return sealCredential2({
3373
+ apiKey: value,
3374
+ tier: "pointer",
3375
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
3376
+ deliberate: true,
3377
+ diskCandidates: pointerResolution.diskCandidates,
3378
+ warning: null
3379
+ });
3380
+ }
3381
+ var DEFAULT_FLEET_GATEWAY_ORIGIN2 = "https://api.hasna.com";
3382
+ var DEFAULT_AUTHORITY_SOURCE2 = "default";
3383
+ function defaultFleetGatewayBaseUrl2(name) {
3384
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN2}/${validateAppSlug2(name)}`;
3385
+ }
3386
+ var ASCII_CONTROL_PATTERN2 = /[\u0000-\u001f\u007f]/;
3387
+ var DNS_LABEL_PATTERN2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
3388
+ function isValidDnsDomain2(value) {
3389
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN2.test(value) || /[^\x00-\x7f]/.test(value)) {
3390
+ return false;
2388
3391
  }
2389
- return null;
3392
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN2.test(label));
2390
3393
  }
2391
- function firstEnvDefinedKey(env, keys) {
2392
- for (const key of keys) {
2393
- if (env[key] !== undefined)
2394
- return key;
3394
+ function validateAppSlug2(name) {
3395
+ if (name.length > 63 || !DNS_LABEL_PATTERN2.test(name)) {
3396
+ throw new Error("App name must be one lowercase DNS label.");
2395
3397
  }
2396
- return null;
3398
+ return name;
2397
3399
  }
2398
- function rawAuthority(value) {
3400
+ function rawAuthority2(value) {
2399
3401
  const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
2400
3402
  if (!match)
2401
3403
  throw new Error("API URL must be absolute.");
@@ -2406,7 +3408,7 @@ function rawAuthority(value) {
2406
3408
  throw new Error("API URL must include a hostname.");
2407
3409
  return authority;
2408
3410
  }
2409
- function assertCanonicalPort(port) {
3411
+ function assertCanonicalPort2(port) {
2410
3412
  if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
2411
3413
  throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2412
3414
  }
@@ -2415,7 +3417,7 @@ function assertCanonicalPort(port) {
2415
3417
  throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
2416
3418
  }
2417
3419
  }
2418
- function canonicalAuthorityHostname(authority) {
3420
+ function canonicalAuthorityHostname2(authority) {
2419
3421
  let rawHostname;
2420
3422
  if (authority.startsWith("[")) {
2421
3423
  const closingBracket = authority.indexOf("]");
@@ -2428,9 +3430,9 @@ function canonicalAuthorityHostname(authority) {
2428
3430
  if (!portSuffix.startsWith(":")) {
2429
3431
  throw new Error("API URL authority must contain a canonical hostname and port.");
2430
3432
  }
2431
- assertCanonicalPort(portSuffix.slice(1));
3433
+ assertCanonicalPort2(portSuffix.slice(1));
2432
3434
  }
2433
- if (isIP(rawHostname.slice(1, -1)) !== 6) {
3435
+ if (isIP2(rawHostname.slice(1, -1)) !== 6) {
2434
3436
  throw new Error("API URL authority must contain a canonical IPv6 literal.");
2435
3437
  }
2436
3438
  } else {
@@ -2441,33 +3443,33 @@ function canonicalAuthorityHostname(authority) {
2441
3443
  }
2442
3444
  if (lastColon !== -1) {
2443
3445
  const port = authority.slice(lastColon + 1);
2444
- assertCanonicalPort(port);
3446
+ assertCanonicalPort2(port);
2445
3447
  rawHostname = authority.slice(0, lastColon);
2446
3448
  } else {
2447
3449
  rawHostname = authority;
2448
3450
  }
2449
- const ipVersion = isIP(rawHostname);
3451
+ const ipVersion = isIP2(rawHostname);
2450
3452
  const numericAddressParts = rawHostname.split(".");
2451
3453
  const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
2452
- if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
3454
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain2(rawHostname.toLowerCase())) {
2453
3455
  throw new Error("API URL authority must contain a canonical ASCII hostname.");
2454
3456
  }
2455
3457
  }
2456
3458
  return rawHostname.toLowerCase();
2457
3459
  }
2458
- function isDeliberateLoopbackHttpAuthority(authority) {
3460
+ function isDeliberateLoopbackHttpAuthority2(authority) {
2459
3461
  return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
2460
3462
  }
2461
- function toV1BaseUrl(apiUrl) {
2462
- if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
3463
+ function toV1BaseUrl2(apiUrl) {
3464
+ if (ASCII_CONTROL_PATTERN2.test(apiUrl)) {
2463
3465
  throw new Error("API URL must not contain ASCII control characters.");
2464
3466
  }
2465
3467
  const input = apiUrl.trim();
2466
- const authority = rawAuthority(input);
3468
+ const authority = rawAuthority2(input);
2467
3469
  if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
2468
3470
  throw new Error("API URL authority must be canonical ASCII without credentials.");
2469
3471
  }
2470
- const canonicalHostname = canonicalAuthorityHostname(authority);
3472
+ const canonicalHostname = canonicalAuthorityHostname2(authority);
2471
3473
  const url = new URL(input);
2472
3474
  if (url.protocol !== "http:" && url.protocol !== "https:") {
2473
3475
  throw new Error("API URL must use http or https.");
@@ -2484,7 +3486,7 @@ function toV1BaseUrl(apiUrl) {
2484
3486
  if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
2485
3487
  throw new Error("API URL must not use IDN or punycode hostnames.");
2486
3488
  }
2487
- if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
3489
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority2(authority)) {
2488
3490
  throw new Error("API URL may use http only for an exact loopback authority.");
2489
3491
  }
2490
3492
  if (url.search || url.hash) {
@@ -2496,106 +3498,104 @@ function toV1BaseUrl(apiUrl) {
2496
3498
  url.pathname = `${path}/v1`;
2497
3499
  return url.toString().replace(/\/+$/, "");
2498
3500
  }
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
- };
3501
+ class ClientTransportConfigurationError2 extends Error {
3502
+ appName;
3503
+ sources;
3504
+ constructor(appName, message, sources = []) {
3505
+ super(message);
3506
+ this.name = "ClientTransportConfigurationError";
3507
+ this.appName = appName;
3508
+ this.sources = Object.freeze([...sources]);
3509
+ }
3510
+ }
3511
+ function resolveClientTransportSnapshot2(name, env = process.env, options = {}) {
3512
+ env = snapshotClientEnvironment2(name, env);
3513
+ const keys = clientTransportEnvKeys2(name);
3514
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
3515
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
3516
+ if (blankUrl) {
3517
+ 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]);
3518
+ }
3519
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN2.test(entry.raw));
3520
+ if (controlledUrl) {
3521
+ throw new ClientTransportConfigurationError2(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
3522
+ }
3523
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
3524
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
3525
+ 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));
3526
+ }
3527
+ const envUrlHit = usableUrlEntries[0] ?? null;
3528
+ const keychainUrlHit = keychainConfigValue2(name, env, options.credentials?.keychain);
3529
+ const diskConfigUrlHit = appConfigDiskValue2(name, env, keys.apiUrlKeys);
3530
+ if (diskConfigUrlHit?.unusable) {
3531
+ 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]);
3532
+ }
3533
+ const urlCandidates = [
3534
+ ...envUrlHit ? [envUrlHit] : [],
3535
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
3536
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
3537
+ ];
3538
+ const configuredUrl = urlCandidates[0] ?? null;
3539
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
3540
+ if (configuredUrl && divergentUrls.length > 0) {
3541
+ 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
3542
  }
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.`);
3543
+ const warnings = [];
3544
+ if (configuredUrl && !envUrlHit) {
3545
+ 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
3546
  }
2540
- const credential = resolveCredential(name, env, options.credentials);
3547
+ const credential = resolveCredential2(name, env, options.credentials);
2541
3548
  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
- };
3549
+ const diskHint = credentialDiskSourcesForMessage2(name, env);
3550
+ 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`;
3551
+ 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.`);
3552
+ throw new ClientTransportConfigurationError2(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
2555
3553
  }
2556
3554
  if (credential.warning)
2557
3555
  warnings.push(credential.warning);
3556
+ let urlHit;
3557
+ if (configuredUrl) {
3558
+ urlHit = configuredUrl;
3559
+ } else {
3560
+ try {
3561
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE2, value: defaultFleetGatewayBaseUrl2(name) };
3562
+ } catch (error) {
3563
+ const message = error instanceof Error ? error.message : String(error);
3564
+ 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]]);
3565
+ }
3566
+ }
2558
3567
  const apiUrlSource = urlHit.key;
2559
3568
  let baseUrl;
2560
3569
  try {
2561
- baseUrl = toV1BaseUrl(urlHit.value);
3570
+ baseUrl = toV1BaseUrl2(urlHit.value);
2562
3571
  } catch (error) {
2563
3572
  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",
3573
+ throw new ClientTransportConfigurationError2(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
3574
+ }
3575
+ return {
3576
+ resolution: {
3577
+ transport: "http",
2567
3578
  transportSource: urlHit.key,
2568
- baseUrl: null,
2569
- apiUrlSource: urlHit.key,
3579
+ baseUrl,
3580
+ apiUrlSource,
2570
3581
  apiKeyPresent: true,
2571
3582
  apiKeySource: credential.source,
2572
3583
  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
3584
+ misconfigured: false,
3585
+ warning: warnings.length > 0 ? warnings.join(" ") : null
3586
+ },
3587
+ credential
2587
3588
  };
2588
3589
  }
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>";
3590
+ function credentialDiskSourcesForMessage2(name, env) {
3591
+ const paths = credentialDiskSources2(name, env);
3592
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
2592
3593
  }
2593
3594
 
2594
3595
  class HasnaHttpError extends Error {
2595
3596
  status;
2596
3597
  method;
2597
3598
  path;
2598
- body;
2599
3599
  credentialSource;
2600
3600
  credentialTier;
2601
3601
  constructor(method, path, status, body, credential) {
@@ -2605,7 +3605,12 @@ class HasnaHttpError extends Error {
2605
3605
  this.status = status;
2606
3606
  this.method = method;
2607
3607
  this.path = path;
2608
- this.body = body;
3608
+ Object.defineProperty(this, "body", {
3609
+ value: body,
3610
+ enumerable: status !== 401 && status !== 403,
3611
+ writable: false,
3612
+ configurable: false
3613
+ });
2609
3614
  this.credentialSource = credential?.source ?? null;
2610
3615
  this.credentialTier = credential?.tier ?? null;
2611
3616
  }
@@ -2616,22 +3621,32 @@ function currentCredential(name, apiKey) {
2616
3621
  }
2617
3622
  return explicitCredential(name, apiKey);
2618
3623
  }
3624
+ async function resolveRequestCredential(name, apiKey, env = process.env) {
3625
+ const resolved = currentCredential(name, apiKey);
3626
+ if (resolved.tier === "pointer") {
3627
+ return completePointerCredential(name, resolved, env);
3628
+ }
3629
+ return resolved;
3630
+ }
2619
3631
  function authFailureGuidance(credential) {
2620
3632
  const origin = `The API key for this request came from ${credential.source}`;
2621
3633
  if (credential.deliberate) {
2622
3634
  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
3635
  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
3636
  }
2625
- if (credential.deprecated) {
3637
+ if (credential.tier === "env") {
2626
3638
  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}`;
3639
+ 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.`;
3640
+ 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}`;
3641
+ }
3642
+ if (credential.tier === "keychain") {
3643
+ 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
3644
  }
2630
3645
  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
3646
  }
2632
3647
  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([
3648
+ var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
3649
+ var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
2635
3650
  "host",
2636
3651
  ":authority",
2637
3652
  "forwarded",
@@ -2641,7 +3656,7 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
2641
3656
  function assertNoAuthorityOverrideHeaders(headers, source) {
2642
3657
  if (!headers)
2643
3658
  return;
2644
- const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS.has(name.trim().toLowerCase()));
3659
+ const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS2.has(name.trim().toLowerCase()));
2645
3660
  if (forbidden) {
2646
3661
  throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
2647
3662
  }
@@ -2668,9 +3683,9 @@ function appendQuery(path, query) {
2668
3683
  return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
2669
3684
  }
2670
3685
  var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
2671
- function createHasnaHttpTransport(options) {
3686
+ function createHasnaHttpTransportInternal(options, requestBindingProvider) {
2672
3687
  const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
2673
- const base = toV1BaseUrl(options.baseUrl);
3688
+ const base = toV1BaseUrl2(options.baseUrl);
2674
3689
  const timeoutMs = options.timeoutMs ?? 30000;
2675
3690
  const sleep = options.sleepImpl ?? defaultSleep;
2676
3691
  const defaultRetry = options.retry;
@@ -2730,13 +3745,20 @@ function createHasnaHttpTransport(options) {
2730
3745
  if (opts.signal)
2731
3746
  opts.signal.removeEventListener("abort", onAbort);
2732
3747
  }
2733
- const text = await response.text();
3748
+ const authenticationFailure = response.status === 401 || response.status === 403;
2734
3749
  let parsed = undefined;
2735
- if (text.length > 0) {
3750
+ if (authenticationFailure) {
2736
3751
  try {
2737
- parsed = JSON.parse(text);
2738
- } catch {
2739
- parsed = text;
3752
+ await response.body?.cancel();
3753
+ } catch {}
3754
+ } else {
3755
+ const text = await response.text();
3756
+ if (text.length > 0) {
3757
+ try {
3758
+ parsed = JSON.parse(text);
3759
+ } catch {
3760
+ parsed = text;
3761
+ }
2740
3762
  }
2741
3763
  }
2742
3764
  if (!response.ok) {
@@ -2747,11 +3769,11 @@ function createHasnaHttpTransport(options) {
2747
3769
  error: new HasnaHttpError(method, rel, response.status, parsed)
2748
3770
  };
2749
3771
  }
2750
- if (response.status === 401 || response.status === 403) {
3772
+ if (authenticationFailure) {
2751
3773
  return {
2752
3774
  ok: false,
2753
3775
  retryable: false,
2754
- error: new HasnaHttpError(method, rel, response.status, parsed, {
3776
+ error: new HasnaHttpError(method, rel, response.status, undefined, {
2755
3777
  source: credential.source,
2756
3778
  tier: credential.tier,
2757
3779
  guidance: authFailureGuidance(credential)
@@ -2767,11 +3789,15 @@ function createHasnaHttpTransport(options) {
2767
3789
  async function request(method, path, body, opts = {}) {
2768
3790
  const upper = method.toUpperCase();
2769
3791
  const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
2770
- const url = `${base}${rel}`;
2771
3792
  const retry = resolveRetry(opts.retry);
2772
- const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
3793
+ const methodRetryable = IDEMPOTENT_METHODS2.has(upper) || Boolean(opts.idempotencyKey);
2773
3794
  const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
2774
- const credential = currentCredential(options.name, options.apiKey);
3795
+ const binding = requestBindingProvider ? await requestBindingProvider() : {
3796
+ baseUrl: base,
3797
+ credential: await resolveRequestCredential(options.name, options.apiKey)
3798
+ };
3799
+ const url = `${binding.baseUrl}${rel}`;
3800
+ const credential = binding.credential;
2775
3801
  let last = null;
2776
3802
  for (let attempt = 1;attempt <= maxAttempts; attempt++) {
2777
3803
  const result = await once(upper, rel, url, body, opts, credential);
@@ -2799,49 +3825,44 @@ function createHasnaHttpTransport(options) {
2799
3825
  }
2800
3826
  function createClientTransport(name, env = process.env, overrides) {
2801
3827
  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;
3828
+ const snapshotOptions = { ...credentialOptions ? { credentials: credentialOptions } : {} };
3829
+ const resolution = resolveClientTransportSnapshot2(name, env, snapshotOptions).resolution;
3830
+ 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;
3831
+ const unstableConfiguration = () => new ClientTransportConfigurationError2(name, "The configured service authority or credential changed while a request was being prepared; no authenticated request was sent.");
3832
+ const requestBindingProvider = async () => {
3833
+ const first = resolveClientTransportSnapshot2(name, env, snapshotOptions);
3834
+ const reviewed = resolveClientTransportSnapshot2(name, env, snapshotOptions);
3835
+ if (!sameBinding(first, reviewed))
3836
+ throw unstableConfiguration();
3837
+ if (reviewed.resolution.baseUrl !== resolution.baseUrl) {
3838
+ throw new ClientTransportConfigurationError2(name, "The configured service authority changed; rebuild the client before sending credentials.");
3839
+ }
3840
+ const credential = await resolveRequestCredential(name, () => reviewed.credential, env);
3841
+ const immediatelyBeforeDispatch = resolveClientTransportSnapshot2(name, env, snapshotOptions);
3842
+ if (!sameBinding(reviewed, immediatelyBeforeDispatch))
3843
+ throw unstableConfiguration();
3844
+ if (immediatelyBeforeDispatch.resolution.baseUrl !== resolution.baseUrl) {
3845
+ throw new ClientTransportConfigurationError2(name, "The configured service authority changed; rebuild the client before sending credentials.");
3846
+ }
3847
+ return { baseUrl: immediatelyBeforeDispatch.resolution.baseUrl, credential };
2815
3848
  };
2816
3849
  return {
2817
3850
  transport: "http",
2818
- client: createHasnaHttpTransport({
3851
+ client: createHasnaHttpTransportInternal({
2819
3852
  name,
2820
3853
  baseUrl: resolution.baseUrl,
2821
- apiKey: credentialProvider,
3854
+ apiKey: () => {
3855
+ throw new Error("The authenticated request binding provider was not invoked.");
3856
+ },
2822
3857
  ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
2823
3858
  ...overrides?.headers ? { headers: overrides.headers } : {},
2824
3859
  ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
2825
3860
  ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
2826
3861
  ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
2827
- }),
3862
+ }, requestBindingProvider),
2828
3863
  resolution
2829
3864
  };
2830
3865
  }
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
3866
  function resourcePath(resource) {
2846
3867
  const trimmed = resource.replace(/^\/+|\/+$/g, "");
2847
3868
  if (!trimmed)
@@ -2941,145 +3962,336 @@ function createHasnaStorageClient(name, transport) {
2941
3962
  }
2942
3963
  };
2943
3964
  }
3965
+ function resolveStorageClient(name, env = process.env, overrides) {
3966
+ const wired = createClientTransport(name, env, overrides);
3967
+ return { transport: "http", client: createHasnaStorageClient(name, wired.client) };
3968
+ }
3969
+
3970
+ // src/lib/local-opt-in.ts
3971
+ var RECORDINGS_LOCAL_OPT_IN_ENV_KEYS = [
3972
+ "HASNA_RECORDINGS_LOCAL",
3973
+ "RECORDINGS_LOCAL"
3974
+ ];
3975
+ function isRecordingsLocalOptIn(env = process.env) {
3976
+ return RECORDINGS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
3977
+ }
3978
+ var RECORDINGS_CARVED_LEGACY_ENV_KEYS = [
3979
+ "RECORDINGS_API_URL",
3980
+ "RECORDINGS_API_KEY"
3981
+ ];
3982
+ function recordingsAuthorityEnvKeys() {
3983
+ const keys = clientTransportEnvKeys("recordings");
3984
+ return [
3985
+ ...keys.apiUrlKeys,
3986
+ ...keys.apiKeyKeys,
3987
+ credentialOverrideEnvKey("recordings"),
3988
+ credentialPointerEnvKey("recordings"),
3989
+ CREDENTIAL_PROFILE_ENV_KEY
3990
+ ].filter((key) => !RECORDINGS_CARVED_LEGACY_ENV_KEYS.includes(key));
3991
+ }
3992
+ function hasRecordingsEnvAuthorityIntent(env = process.env) {
3993
+ return recordingsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
3994
+ }
3995
+ function selectsRecordingsLocalStore(env = process.env) {
3996
+ return !hasRecordingsEnvAuthorityIntent(env) && isRecordingsLocalOptIn(env);
3997
+ }
3998
+ function recordingsResolverEnv(env) {
3999
+ const carved = RECORDINGS_CARVED_LEGACY_ENV_KEYS.filter((key) => (key in env));
4000
+ const blanks = recordingsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
4001
+ const removed = new Set([...carved, ...blanks]);
4002
+ if (removed.size === 0)
4003
+ return env;
4004
+ const next = { ...env };
4005
+ for (const key of removed)
4006
+ delete next[key];
4007
+ return next;
4008
+ }
4009
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
4010
+ function isAmbientRecordingsEnv(env) {
4011
+ if (typeof process !== "undefined" && env === process.env) {
4012
+ return true;
4013
+ }
4014
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
4015
+ }
4016
+ function recordingsResolverInputs(env, credentials = {}) {
4017
+ const normalised = recordingsResolverEnv(env);
4018
+ if (normalised === env)
4019
+ return { env: normalised, credentials };
4020
+ const keychain = { ...credentials.keychain };
4021
+ if (keychain.enabled === undefined && keychain.run === undefined) {
4022
+ keychain.enabled = isAmbientRecordingsEnv(env);
4023
+ }
4024
+ return { env: normalised, credentials: { ...credentials, keychain } };
4025
+ }
2944
4026
 
2945
4027
  // src/http/client.ts
2946
- function envToken2(name) {
2947
- return name.toUpperCase().replace(/-/g, "_");
4028
+ var RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
4029
+ var IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4030
+ function throwRecordingsAuthorityFailure(error) {
4031
+ const message = error instanceof Error ? error.message : String(error);
4032
+ const name = error instanceof Error ? error.name : "";
4033
+ if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
4034
+ 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 });
4035
+ }
4036
+ if (/no API key could be resolved/.test(message)) {
4037
+ if (/is not set and no API key could be resolved/.test(message)) {
4038
+ 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 });
4039
+ }
4040
+ 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 });
4041
+ }
4042
+ throw new Error(`REMOTE_API_URL_INVALID: ${message} local SQLite fallback is disabled`, { cause: error });
2948
4043
  }
2949
- function envKeys(name) {
2950
- const token = envToken2(name);
4044
+ function toRecordsAuthority(resolution) {
2951
4045
  return {
2952
- storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
2953
- apiUrlKeys: [`HASNA_${token}_API_URL`],
2954
- apiKeyKeys: [`HASNA_${token}_API_KEY`]
4046
+ transport: resolution.transport,
4047
+ transportSource: resolution.transportSource,
4048
+ baseUrl: resolution.baseUrl,
4049
+ apiUrlSource: resolution.apiUrlSource,
4050
+ apiKeyPresent: resolution.apiKeyPresent,
4051
+ apiKeySource: resolution.apiKeySource,
4052
+ apiKeyTier: resolution.apiKeyTier,
4053
+ misconfigured: resolution.misconfigured,
4054
+ warning: resolution.warning
2955
4055
  };
2956
4056
  }
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 };
4057
+ function resolveRecordingsTransport(env = process.env, options = {}) {
4058
+ if (selectsRecordingsLocalStore(env)) {
4059
+ return { transport: "sqlite", selected: false, source: "local-opt-in", authority: null };
2970
4060
  }
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.");
4061
+ const resolverInputs = recordingsResolverInputs(env, options.credentials);
4062
+ let resolution;
4063
+ try {
4064
+ resolution = resolveClientTransport("recordings", resolverInputs.env, {
4065
+ credentials: resolverInputs.credentials
4066
+ });
4067
+ } catch (error) {
4068
+ throwRecordingsAuthorityFailure(error);
2977
4069
  }
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(/\/+$/, "");
4070
+ return {
4071
+ transport: "http",
4072
+ selected: true,
4073
+ source: `${resolution.apiKeySource ?? resolution.apiKeyTier}+${resolution.apiUrlSource ?? "default"}`,
4074
+ authority: toRecordsAuthority(resolution)
4075
+ };
2985
4076
  }
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];
4077
+ function getRecordingsTransportStatus(env = process.env, options = {}) {
4078
+ let resolution;
4079
+ try {
4080
+ resolution = resolveRecordingsTransport(env, options);
4081
+ } catch (error) {
4082
+ const issue = error instanceof Error ? error.message : String(error);
4083
+ const envKeys = clientTransportEnvKeys("recordings");
4084
+ const declared = (keys) => keys.some((key) => (env[key] ?? "").trim() !== "");
3002
4085
  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.`
4086
+ selected: true,
4087
+ ok: false,
4088
+ transport: "invalid",
4089
+ api_url_configured: declared(envKeys.apiUrlKeys),
4090
+ api_key_configured: declared(envKeys.apiKeyKeys),
4091
+ api_url_source: null,
4092
+ api_key_source: null,
4093
+ api_key_tier: null,
4094
+ v1_base_url: null,
4095
+ issues: [issue],
4096
+ local_fallback: false
3010
4097
  };
3011
4098
  }
3012
- if (requested === "sqlite") {
3013
- return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
3014
- }
3015
- if (!urlHit) {
4099
+ if (!resolution.selected) {
3016
4100
  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.`
4101
+ selected: false,
4102
+ ok: true,
4103
+ transport: resolution.transport,
4104
+ api_url_configured: false,
4105
+ api_key_configured: false,
4106
+ api_url_source: null,
4107
+ api_key_source: null,
4108
+ api_key_tier: null,
4109
+ v1_base_url: null,
4110
+ issues: [],
4111
+ local_fallback: false
3024
4112
  };
3025
4113
  }
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
- };
4114
+ const authority = resolution.authority;
4115
+ return {
4116
+ selected: true,
4117
+ ok: true,
4118
+ transport: resolution.transport,
4119
+ api_url_configured: authority.apiUrlSource !== null && authority.apiUrlSource !== "default",
4120
+ api_key_configured: authority.apiKeyPresent,
4121
+ api_url_source: authority.apiUrlSource,
4122
+ api_key_source: authority.apiKeySource,
4123
+ api_key_tier: authority.apiKeyTier,
4124
+ v1_base_url: authority.baseUrl,
4125
+ issues: [],
4126
+ local_fallback: false
4127
+ };
4128
+ }
4129
+ function resolveRecordingsCloudClient(env = process.env, options = {}) {
4130
+ if (selectsRecordingsLocalStore(env))
4131
+ return null;
4132
+ const resolverInputs = recordingsResolverInputs(env, options.credentials);
4133
+ let resolved;
4134
+ try {
4135
+ resolved = resolveStorageClient("recordings", resolverInputs.env, {
4136
+ fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
4137
+ credentials: resolverInputs.credentials
4138
+ });
4139
+ } catch (error) {
4140
+ throwRecordingsAuthorityFailure(error);
3036
4141
  }
3037
- const rawUrl = urlHit.value;
3038
- let baseUrl;
4142
+ const client = resolved.client;
4143
+ return client;
4144
+ }
4145
+
4146
+ // src/store.ts
4147
+ import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
4148
+
4149
+ // src/lib/audio-artifact-storage.ts
4150
+ import { GetObjectCommand, PutObjectCommand, S3Client as AwsS3Client } from "@aws-sdk/client-s3";
4151
+ import { createHash } from "crypto";
4152
+ import { readFile as readFile2 } from "fs/promises";
4153
+ import { extname } from "path";
4154
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
4155
+ var DEFAULT_S3_PREFIX = "recordings";
4156
+ var AUDIO_CONTENT_TYPES = {
4157
+ wav: "audio/wav",
4158
+ mp3: "audio/mpeg",
4159
+ m4a: "audio/mp4",
4160
+ aac: "audio/aac",
4161
+ webm: "audio/webm",
4162
+ ogg: "audio/ogg",
4163
+ flac: "audio/flac"
4164
+ };
4165
+ function contentTypeForExtension(extension) {
4166
+ return AUDIO_CONTENT_TYPES[extension.toLowerCase()] ?? "application/octet-stream";
4167
+ }
4168
+
4169
+ class AudioArtifactStorage {
4170
+ bucket;
4171
+ prefix;
4172
+ s3;
4173
+ constructor(options = {}) {
4174
+ this.bucket = options.bucket;
4175
+ this.prefix = (options.prefix || DEFAULT_S3_PREFIX).replace(/^\/+|\/+$/g, "");
4176
+ this.s3 = this.bucket ? options.client ?? new AwsS3Client({ region: options.region || process.env.AWS_REGION || "us-east-1" }) : undefined;
4177
+ }
4178
+ get usesS3() {
4179
+ return Boolean(this.bucket);
4180
+ }
4181
+ objectKeyFor(recordingId, sha256, extension) {
4182
+ assertSha256(sha256);
4183
+ const safeExtension = extension.toLowerCase().match(/^[a-z0-9]+/)?.[0] || "bin";
4184
+ return `${this.prefix}/${recordingId}/${sha256}.${safeExtension}`;
4185
+ }
4186
+ async uploadAudio(recordingId, audioPath) {
4187
+ if (!this.bucket || !this.s3)
4188
+ return null;
4189
+ const bytes = await readFileBytes(audioPath);
4190
+ if (!bytes)
4191
+ return null;
4192
+ const sha256 = sha256Hex(bytes);
4193
+ const extension = extensionOf(audioPath);
4194
+ const key = this.objectKeyFor(recordingId, sha256, extension);
4195
+ await this.s3.send(new PutObjectCommand({
4196
+ Bucket: this.bucket,
4197
+ Key: key,
4198
+ Body: bytes,
4199
+ ContentType: contentTypeForExtension(extension)
4200
+ }));
4201
+ return { objectKey: key, sha256, bytes: bytes.byteLength };
4202
+ }
4203
+ async readAudio(objectKey) {
4204
+ if (!this.bucket || !this.s3)
4205
+ return null;
4206
+ try {
4207
+ const response = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }));
4208
+ return await bodyToBytes(response.Body);
4209
+ } catch {
4210
+ return null;
4211
+ }
4212
+ }
4213
+ }
4214
+ function resolveAudioArtifactStorage(env = process.env, client) {
4215
+ const bucket = env.HASNA_RECORDINGS_S3_BUCKET || env.RECORDINGS_S3_BUCKET || undefined;
4216
+ const prefix = env.HASNA_RECORDINGS_S3_PREFIX || env.RECORDINGS_S3_PREFIX || undefined;
4217
+ const region = env.RECORDINGS_S3_REGION || env.AWS_REGION || undefined;
4218
+ return new AudioArtifactStorage({ bucket, prefix, region, client });
4219
+ }
4220
+ async function uploadAudioAtCreation(recordingId, audioPath, storage = resolveAudioArtifactStorage()) {
4221
+ if (!audioPath)
4222
+ return null;
4223
+ if (!storage.usesS3)
4224
+ return null;
3039
4225
  try {
3040
- baseUrl = toV1BaseUrl2(rawUrl);
4226
+ const uploaded = await storage.uploadAudio(recordingId, audioPath);
4227
+ if (!uploaded)
4228
+ return null;
4229
+ console.warn(`recordings: uploaded audio for ${recordingId} to ${uploaded.objectKey}`);
4230
+ return uploaded;
3041
4231
  } 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}.` };
4232
+ console.warn(`recordings: audio upload failed for ${recordingId} (${String(error instanceof Error ? error.message : error)}); keeping local audio_path only`);
4233
+ return null;
3044
4234
  }
3045
- return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
3046
4235
  }
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
- };
4236
+ function assertSha256(value) {
4237
+ if (!SHA256_HEX.test(value)) {
4238
+ throw new Error("audio digest must be a lowercase hex sha-256; refusing to build a storage key from it");
4239
+ }
4240
+ }
4241
+ function sha256Hex(bytes) {
4242
+ const hash = createHash("sha256");
4243
+ hash.update(bytes);
4244
+ return hash.digest("hex");
4245
+ }
4246
+ async function readFileBytes(path) {
4247
+ try {
4248
+ return new Uint8Array(await readFile2(path));
4249
+ } catch {
4250
+ return null;
4251
+ }
4252
+ }
4253
+ function extensionOf(path) {
4254
+ const extension = extname(path).replace(/^\./, "").toLowerCase();
4255
+ return extension || "bin";
4256
+ }
4257
+ async function bodyToBytes(body) {
4258
+ if (!body)
4259
+ return new Uint8Array(0);
4260
+ if (body instanceof Uint8Array)
4261
+ return body;
4262
+ if (typeof body.transformToByteArray === "function") {
4263
+ return new Uint8Array(await body.transformToByteArray());
4264
+ }
4265
+ if (typeof body.getReader === "function") {
4266
+ const reader = body.getReader();
4267
+ const chunks2 = [];
4268
+ for (;; ) {
4269
+ const { value, done } = await reader.read();
4270
+ if (done)
4271
+ break;
4272
+ if (value)
4273
+ chunks2.push(value);
3067
4274
  }
3068
- throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
4275
+ return concat(chunks2);
3069
4276
  }
3070
- if (resolution.transport === "sqlite" || !resolution.baseUrl) {
3071
- return { transport: "sqlite", client: null, resolution };
4277
+ const chunks = [];
4278
+ for await (const chunk of body) {
4279
+ chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
3072
4280
  }
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.`);
4281
+ return concat(chunks);
4282
+ }
4283
+ function concat(chunks) {
4284
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
4285
+ const merged = new Uint8Array(new ArrayBuffer(total));
4286
+ let offset = 0;
4287
+ for (const chunk of chunks) {
4288
+ merged.set(chunk, offset);
4289
+ offset += chunk.byteLength;
3076
4290
  }
3077
- return { transport: "http", client: createHasnaStorageClient(name, wired.client), resolution };
4291
+ return merged;
3078
4292
  }
3079
4293
 
3080
4294
  // src/store.ts
3081
- import { createHash, randomUUID as randomUUID5 } from "crypto";
3082
- var APP = "recordings";
3083
4295
  function listQuery(filter) {
3084
4296
  if (!filter)
3085
4297
  return {};
@@ -3109,7 +4321,15 @@ var localStore = {
3109
4321
  mode: "sqlite",
3110
4322
  baseUrl: null,
3111
4323
  async createRecording(input, idempotencyKey) {
3112
- return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
4324
+ return withLocalStoreReaderLease(async () => {
4325
+ const recording = createRecording(input, undefined, idempotencyKey);
4326
+ if (!recording.audio_path)
4327
+ return recording;
4328
+ const uploaded = await uploadAudioAtCreation(recording.id, recording.audio_path, localArtifactStorageFor());
4329
+ if (!uploaded)
4330
+ return recording;
4331
+ return updateRecordingAudio(recording.id, uploaded.objectKey, uploaded.sha256, uploaded.bytes) ?? recording;
4332
+ });
3113
4333
  },
3114
4334
  async getRecording(id) {
3115
4335
  return withLocalStoreReaderLease(() => getRecording(id));
@@ -3303,7 +4523,7 @@ function apiStore(client) {
3303
4523
  };
3304
4524
  }
3305
4525
  function recordingPageFingerprint(items) {
3306
- const hash = createHash("sha256");
4526
+ const hash = createHash2("sha256");
3307
4527
  const ids = items.map((item) => String(item.id ?? "")).sort();
3308
4528
  for (const id of ids) {
3309
4529
  hash.update(String(id.length));
@@ -3339,19 +4559,23 @@ async function countStoreRecordings(store, filter) {
3339
4559
  throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
3340
4560
  }
3341
4561
  var cached = null;
3342
- function getStore(env = process.env) {
3343
- if (env === process.env && cached)
4562
+ var localArtifactStorageOverride;
4563
+ function localArtifactStorageFor() {
4564
+ return localArtifactStorageOverride ?? resolveAudioArtifactStorage();
4565
+ }
4566
+ function getStore(env = process.env, options = {}) {
4567
+ if (env === process.env && cached && options.credentials === undefined)
3344
4568
  return cached;
3345
- const resolved = resolveStoreClient(APP, env);
3346
- const store = resolved.transport === "http" ? apiStore(resolved.client) : localStore;
3347
- if (env === process.env)
4569
+ const client = resolveRecordingsCloudClient(env, options);
4570
+ const store = client ? apiStore(client) : localStore;
4571
+ if (env === process.env && options.credentials === undefined)
3348
4572
  cached = store;
3349
4573
  return store;
3350
4574
  }
3351
4575
 
3352
4576
  // src/lib/recorder.ts
3353
4577
  import { spawn as spawn2 } from "child_process";
3354
- import { join as join5 } from "path";
4578
+ import { join as join6 } from "path";
3355
4579
  import { existsSync as existsSync3 } from "fs";
3356
4580
 
3357
4581
  // src/types/index.ts
@@ -3402,7 +4626,7 @@ function startRecording(config) {
3402
4626
  }
3403
4627
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
3404
4628
  const filename = `recording-${timestamp}.${config.audio_format}`;
3405
- const filepath = join5(config.audio_dir, filename);
4629
+ const filepath = join6(config.audio_dir, filename);
3406
4630
  const args = buildRecordArgs(filepath, config);
3407
4631
  const [command, ...commandArgs] = args;
3408
4632
  if (command === undefined) {
@@ -3474,7 +4698,7 @@ function buildRecordArgs(filepath, config) {
3474
4698
  async function recordDuration(seconds, config) {
3475
4699
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
3476
4700
  const filename = `recording-${timestamp}.${config.audio_format}`;
3477
- const filepath = join5(config.audio_dir, filename);
4701
+ const filepath = join6(config.audio_dir, filename);
3478
4702
  const args = [
3479
4703
  "rec",
3480
4704
  "-r",
@@ -3512,7 +4736,7 @@ var _client = null;
3512
4736
  var _clientApiKey = null;
3513
4737
  function getClient(config) {
3514
4738
  if (!config.openai_api_key) {
3515
- throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
4739
+ 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
4740
  }
3517
4741
  if (_client && _clientApiKey === config.openai_api_key)
3518
4742
  return _client;
@@ -3537,7 +4761,7 @@ async function verifyTranscriptionCredential(config, model, options = {}) {
3537
4761
  status: null,
3538
4762
  model,
3539
4763
  role,
3540
- message: `no API key configured for the ${role} role. ` + "Set OPENAI_API_KEY env var or add to ~/.secrets"
4764
+ 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
4765
  };
3542
4766
  }
3543
4767
  try {
@@ -3646,9 +4870,9 @@ ${trimmed}`;
3646
4870
  }
3647
4871
 
3648
4872
  // 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";
4873
+ import { spawnSync as spawnSync4 } from "child_process";
4874
+ import { existsSync as existsSync4, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
4875
+ import { join as join7 } from "path";
3652
4876
  import { tmpdir } from "os";
3653
4877
 
3654
4878
  // src/lib/macos-bundle.ts
@@ -3664,7 +4888,7 @@ var WAVE_FORMAT_PCM = 1;
3664
4888
  var WAVE_FORMAT_EXTENSIBLE = 65534;
3665
4889
  var SUBFORMAT_OFFSET_IN_EXTENSION = 8;
3666
4890
  function readWavPeak(filepath) {
3667
- const buf = readFileSync3(filepath);
4891
+ const buf = readFileSync4(filepath);
3668
4892
  if (buf.length < RIFF_HEADER_BYTES) {
3669
4893
  throw new Error(`not a RIFF file (${buf.length} bytes): ${filepath}`);
3670
4894
  }
@@ -3739,9 +4963,9 @@ function probeMicrophoneCapture(config, options = {}) {
3739
4963
  peak: 0,
3740
4964
  silent: null
3741
4965
  };
3742
- const filepath = join6(tmpdir(), `recordings-capture-probe-${process.pid}-${Date.now()}.wav`);
4966
+ const filepath = join7(tmpdir(), `recordings-capture-probe-${process.pid}-${Date.now()}.wav`);
3743
4967
  try {
3744
- const result = spawnSync2(executable, [
4968
+ const result = spawnSync4(executable, [
3745
4969
  "-q",
3746
4970
  "-r",
3747
4971
  String(config.sample_rate),
@@ -3839,37 +5063,37 @@ function captureProbeSubject(env = process.env, options = {}) {
3839
5063
  const termProgram = env.TERM_PROGRAM?.trim();
3840
5064
  const hasTty = options.hasTty ?? Boolean(process.stdin.isTTY || process.stdout.isTTY);
3841
5065
  if (overSsh) {
3842
- const subject2 = "the SSH session (sshd), not HasnaRecordings.app";
5066
+ const subject2 = "the SSH session (sshd), not Hasna Recordings.app";
3843
5067
  return {
3844
5068
  headless: true,
3845
5069
  subject_known: true,
3846
5070
  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."
5071
+ 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
5072
  };
3849
5073
  }
3850
5074
  if (inTmux) {
3851
- const subject2 = "the tmux server, not HasnaRecordings.app and not this pane's shell";
5075
+ const subject2 = "the tmux server, not Hasna Recordings.app and not this pane's shell";
3852
5076
  return {
3853
5077
  headless: false,
3854
5078
  subject_known: true,
3855
5079
  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."
5080
+ 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
5081
  };
3858
5082
  }
3859
5083
  if (!termProgram && !hasTty) {
3860
5084
  return {
3861
5085
  headless: true,
3862
5086
  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."
5087
+ subject: "an unidentified responsible process (not Hasna Recordings.app)",
5088
+ 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
5089
  };
3866
5090
  }
3867
- const subject = `${termProgram || "the terminal application running this command"}, not HasnaRecordings.app`;
5091
+ const subject = `${termProgram || "the terminal application running this command"}, not Hasna Recordings.app`;
3868
5092
  return {
3869
5093
  headless: false,
3870
5094
  subject_known: Boolean(termProgram),
3871
5095
  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."
5096
+ 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
5097
  };
3874
5098
  }
3875
5099
  var TCC_UNREADABLE_STATE = TCC_DATABASE_UNREADABLE_STATE;
@@ -3899,7 +5123,7 @@ function microphoneGrantInstruction(options) {
3899
5123
  const bundlePath = candidates[0] ?? null;
3900
5124
  const steps = [];
3901
5125
  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').");
5126
+ 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
5127
  return {
3904
5128
  bundle_path: null,
3905
5129
  bundle_identifier: RECORDINGS_BUNDLE_IDENTIFIER,
@@ -3908,10 +5132,10 @@ function microphoneGrantInstruction(options) {
3908
5132
  };
3909
5133
  }
3910
5134
  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.");
5135
+ 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
5136
  }
3913
5137
  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")}.`);
5138
+ 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
5139
  if (options.requestState === "never_requested") {
3916
5140
  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
5141
  } else if (options.requestState === "unknown") {
@@ -3930,7 +5154,7 @@ function microphoneGrantInstruction(options) {
3930
5154
  // src/lib/persistence-probe.ts
3931
5155
  import { Database as Database2 } from "bun:sqlite";
3932
5156
  import { existsSync as existsSync5 } from "fs";
3933
- var AUTO_FLIP_MODE_SOURCE = "auto:api-url+api-key";
5157
+ var ENV_CREDENTIAL_MODE_SOURCE = "env";
3934
5158
  function safeBaseUrl(baseUrl) {
3935
5159
  if (!baseUrl)
3936
5160
  return baseUrl;
@@ -3974,42 +5198,51 @@ function localStoreIsBehindSchema(dbPath) {
3974
5198
  return null;
3975
5199
  }
3976
5200
  }
3977
- function describeActiveStore(config, env = process.env) {
5201
+ function describeActiveStore(config, env = process.env, options = {}) {
3978
5202
  const localDbPath = config.db_path;
3979
5203
  const localDbPresent = existsSync5(localDbPath);
3980
- const localDbRecordings = localDbPresent ? readLocalRecordingCount(localDbPath) : null;
3981
- let resolution;
3982
- try {
3983
- resolution = resolveTransport(APP, env);
3984
- } catch (error) {
5204
+ const status = getRecordingsTransportStatus(env, options);
5205
+ if (!status.ok) {
5206
+ const issues = status.issues.length > 0 ? status.issues.join(" ") : null;
3985
5207
  return {
3986
- transport: "sqlite",
5208
+ transport: "none",
3987
5209
  mode_source: "unresolved",
3988
5210
  base_url: null,
3989
5211
  local_db_path: localDbPath,
3990
5212
  local_db_present: localDbPresent,
3991
- local_db_recordings: localDbRecordings,
5213
+ local_db_recordings: null,
5214
+ divergent: false,
5215
+ warning: issues ? redactKeyMaterial(issues) : null
5216
+ };
5217
+ }
5218
+ if (!status.selected) {
5219
+ return {
5220
+ transport: "sqlite",
5221
+ mode_source: "local-opt-in",
5222
+ base_url: null,
5223
+ local_db_path: localDbPath,
5224
+ local_db_present: localDbPresent,
5225
+ local_db_recordings: localDbPresent ? readLocalRecordingCount(localDbPath) : null,
3992
5226
  divergent: false,
3993
- warning: redactKeyMaterial(`could not resolve the active store: ${error.message}`)
5227
+ warning: null
3994
5228
  };
3995
5229
  }
3996
- const divergent = resolution.transport === "http" && (localDbRecordings ?? 0) > 0;
5230
+ const localDbRecordings = localDbPresent ? readLocalRecordingCount(localDbPath) : null;
5231
+ const divergent = (localDbRecordings ?? 0) > 0;
3997
5232
  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'.");
5233
+ if (status.api_key_tier === ENV_CREDENTIAL_MODE_SOURCE) {
5234
+ 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
5235
  }
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.");
5236
+ if (localDbPresent && localDbRecordings === null) {
5237
+ 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
5238
  }
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.");
5239
+ if (divergent) {
5240
+ 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
5241
  }
4009
5242
  return {
4010
- transport: resolution.transport,
4011
- mode_source: resolution.modeSource,
4012
- base_url: safeBaseUrl(resolution.baseUrl),
5243
+ transport: "http",
5244
+ mode_source: `${status.api_key_source ?? status.api_key_tier}+${status.api_url_source ?? "default"}`,
5245
+ base_url: safeBaseUrl(status.v1_base_url),
4013
5246
  local_db_path: localDbPath,
4014
5247
  local_db_present: localDbPresent,
4015
5248
  local_db_recordings: localDbRecordings,
@@ -4017,6 +5250,20 @@ function describeActiveStore(config, env = process.env) {
4017
5250
  warning: warnings.length > 0 ? warnings.join(" ") : null
4018
5251
  };
4019
5252
  }
5253
+ function activeStoreFailClosed(description) {
5254
+ return description.transport === "none";
5255
+ }
5256
+ function describeActiveStoreLine(description, paint) {
5257
+ if (activeStoreFailClosed(description)) {
5258
+ const refusal = description.warning ? ` (${description.warning})` : "";
5259
+ const file = description.local_db_present ? `${description.local_db_path} is present but NOT opened` : `${description.local_db_path} absent`;
5260
+ return paint.fail(`\u2717 Active store: none \u2014 fail-closed${refusal}; ${file}`);
5261
+ }
5262
+ if (description.transport === "sqlite") {
5263
+ return paint.pass(`\u2713 Active store: sqlite \u2192 ${description.local_db_path} (selected by ${description.mode_source})`);
5264
+ }
5265
+ return paint.pass(`\u2713 Active store: http \u2192 ${description.base_url ?? "?"} (selected by ${description.mode_source})`);
5266
+ }
4020
5267
  var PROBE_SEVERITY_GLYPHS = {
4021
5268
  pass: "\u2713",
4022
5269
  warn: "?",
@@ -4443,13 +5690,13 @@ ${block}` : block;
4443
5690
  }
4444
5691
 
4445
5692
  // 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";
5693
+ import { spawnSync as spawnSync5 } from "child_process";
5694
+ import { existsSync as existsSync6, mkdtempSync, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
4448
5695
  import { tmpdir as tmpdir2 } from "os";
4449
- import { join as join7 } from "path";
4450
- var defaultPermissionHelperRunner = (executable, arguments_, options) => spawnSync3(executable, arguments_, options);
5696
+ import { join as join8 } from "path";
5697
+ var defaultPermissionHelperRunner = (executable, arguments_, options) => spawnSync5(executable, arguments_, options);
4451
5698
  function runMacOSPermissionRequest(appPath, runner = defaultPermissionHelperRunner) {
4452
- const executable = join7(appPath, "Contents", "MacOS", "Recordings");
5699
+ const executable = join8(appPath, "Contents", "MacOS", "Recordings");
4453
5700
  const result = runner(executable, ["--request-permissions", "--open-permission-settings"], { stdio: "inherit" });
4454
5701
  return {
4455
5702
  exitCode: result.error ? 1 : result.status ?? 1,
@@ -4460,8 +5707,8 @@ var CODESIGN_REQUIREMENT_SATISFIED_STATUS = 0;
4460
5707
  var CODESIGN_REQUIREMENT_UNSATISFIED_STATUS = 3;
4461
5708
  function tccDatabasePaths(home) {
4462
5709
  return [
4463
- join7(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
4464
- join7("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
5710
+ join8(home, "Library", "Application Support", "com.apple.TCC", "TCC.db"),
5711
+ join8("/", "Library", "Application Support", "com.apple.TCC", "TCC.db")
4465
5712
  ];
4466
5713
  }
4467
5714
  function tccAuthValueLabel(value) {
@@ -4478,7 +5725,7 @@ function tccAuthValueLabel(value) {
4478
5725
  return `unknown(${value})`;
4479
5726
  }
4480
5727
  }
4481
- var defaultCodesignRequirementRunner = (requirementPath, appPath) => spawnSync3("/usr/bin/codesign", ["--verify", "--verbose=2", "-R", requirementPath, appPath], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
5728
+ var defaultCodesignRequirementRunner = (requirementPath, appPath) => spawnSync5("/usr/bin/codesign", ["--verify", "--verbose=2", "-R", requirementPath, appPath], { encoding: "utf8", stdio: ["ignore", "ignore", "ignore"] });
4482
5729
  function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = defaultCodesignRequirementRunner, bundleExists = existsSync6) {
4483
5730
  const normalized = csreqHex.trim();
4484
5731
  if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(normalized)) {
@@ -4488,8 +5735,8 @@ function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = default
4488
5735
  return "unverifiable";
4489
5736
  let scratchDirectory = null;
4490
5737
  try {
4491
- scratchDirectory = mkdtempSync(join7(tmpdir2(), "recordings-tcc-csreq-"));
4492
- const requirementPath = join7(scratchDirectory, "tcc-requirement.bin");
5738
+ scratchDirectory = mkdtempSync(join8(tmpdir2(), "recordings-tcc-csreq-"));
5739
+ const requirementPath = join8(scratchDirectory, "tcc-requirement.bin");
4493
5740
  writeFileSync2(requirementPath, Buffer.from(normalized, "hex"));
4494
5741
  const result = runner(requirementPath, appPath);
4495
5742
  if (result.error)
@@ -4509,7 +5756,7 @@ function verifyStoredRequirementWithCodesign(csreqHex, appPath, runner = default
4509
5756
  var defaultTccPermissionProbe = {
4510
5757
  databasePresence: (dbPath) => {
4511
5758
  try {
4512
- statSync3(dbPath);
5759
+ statSync2(dbPath);
4513
5760
  return "present";
4514
5761
  } catch (error) {
4515
5762
  return error.code === "ENOENT" ? "absent" : "indeterminate";
@@ -4517,7 +5764,7 @@ var defaultTccPermissionProbe = {
4517
5764
  },
4518
5765
  readAccessRow: (dbPath, service) => {
4519
5766
  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], {
5767
+ const result = spawnSync5("/usr/bin/sqlite3", [dbPath, sql], {
4521
5768
  encoding: "utf8",
4522
5769
  stdio: ["ignore", "pipe", "pipe"]
4523
5770
  });
@@ -4538,7 +5785,7 @@ var defaultTccPermissionProbe = {
4538
5785
  verifyStoredRequirement: (csreqHex, appPath) => verifyStoredRequirementWithCodesign(csreqHex, appPath),
4539
5786
  describeStoredRequirement: (csreqHex) => describeStoredRequirementWithCsreq(csreqHex)
4540
5787
  };
4541
- function describeStoredRequirementWithCsreq(csreqHex, runner = (requirementPath) => spawnSync3("/usr/bin/csreq", ["-r", requirementPath, "-t"], {
5788
+ function describeStoredRequirementWithCsreq(csreqHex, runner = (requirementPath) => spawnSync5("/usr/bin/csreq", ["-r", requirementPath, "-t"], {
4542
5789
  encoding: "utf8",
4543
5790
  stdio: ["ignore", "pipe", "ignore"]
4544
5791
  })) {
@@ -4548,8 +5795,8 @@ function describeStoredRequirementWithCsreq(csreqHex, runner = (requirementPath)
4548
5795
  }
4549
5796
  let scratchDirectory = null;
4550
5797
  try {
4551
- scratchDirectory = mkdtempSync(join7(tmpdir2(), "recordings-tcc-decode-"));
4552
- const requirementPath = join7(scratchDirectory, "tcc-requirement.bin");
5798
+ scratchDirectory = mkdtempSync(join8(tmpdir2(), "recordings-tcc-decode-"));
5799
+ const requirementPath = join8(scratchDirectory, "tcc-requirement.bin");
4553
5800
  writeFileSync2(requirementPath, Buffer.from(normalized, "hex"));
4554
5801
  const result = runner(requirementPath);
4555
5802
  if (result.error || result.status !== 0)
@@ -4638,7 +5885,7 @@ function describeTccAuthorizationSubject(appPath) {
4638
5885
  }
4639
5886
 
4640
5887
  // src/cli/macos-shortcut.ts
4641
- import { spawnSync as spawnSync4 } from "child_process";
5888
+ import { spawnSync as spawnSync6 } from "child_process";
4642
5889
  import { existsSync as existsSync7 } from "fs";
4643
5890
  var TRIGGER_DEFAULTS_EXECUTABLE = process.platform === "darwin" ? "/usr/bin/defaults" : process.env.RECORDINGS_TEST_DEFAULTS_EXECUTABLE ?? null;
4644
5891
  var RECORDINGS_BUNDLE_ID = RECORDINGS_BUNDLE_IDENTIFIER;
@@ -4845,14 +6092,14 @@ var defaultBundleIdentifierReader = (bundlePath) => {
4845
6092
  if (!existsSync7(`${bundlePath}/Contents/Info.plist`) && !existsSync7(`${bundlePath}/Contents/Info`)) {
4846
6093
  return null;
4847
6094
  }
4848
- const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["read", `${bundlePath}/Contents/Info`, "CFBundleIdentifier"], { encoding: "utf8" });
6095
+ const result = spawnSync6(TRIGGER_DEFAULTS_EXECUTABLE, ["read", `${bundlePath}/Contents/Info`, "CFBundleIdentifier"], { encoding: "utf8" });
4849
6096
  if (result.status !== 0)
4850
6097
  return null;
4851
6098
  return result.stdout.trim() || null;
4852
6099
  };
4853
6100
  var PROCESS_LISTER_EXECUTABLE = process.platform === "darwin" ? "/bin/ps" : process.env.RECORDINGS_TEST_PS_EXECUTABLE ?? "/bin/ps";
4854
6101
  var defaultProcessLister = () => {
4855
- const result = spawnSync4(PROCESS_LISTER_EXECUTABLE, ["-Awwo", "comm="], { encoding: "utf8" });
6102
+ const result = spawnSync6(PROCESS_LISTER_EXECUTABLE, ["-Awwo", "comm="], { encoding: "utf8" });
4856
6103
  if (result.status !== 0)
4857
6104
  return null;
4858
6105
  return result.stdout;
@@ -4897,7 +6144,7 @@ function isThisApp(candidate, readBundleIdentifier) {
4897
6144
  function readDefault(key) {
4898
6145
  if (TRIGGER_DEFAULTS_EXECUTABLE === null)
4899
6146
  return null;
4900
- const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["read", RECORDINGS_BUNDLE_ID, key], {
6147
+ const result = spawnSync6(TRIGGER_DEFAULTS_EXECUTABLE, ["read", RECORDINGS_BUNDLE_ID, key], {
4901
6148
  encoding: "utf8"
4902
6149
  });
4903
6150
  if (result.status !== 0)
@@ -4908,7 +6155,7 @@ function writeDefault(key, value) {
4908
6155
  if (TRIGGER_DEFAULTS_EXECUTABLE === null) {
4909
6156
  throw new Error(`cannot write ${key}: this machine has no readable app UserDefaults domain`);
4910
6157
  }
4911
- const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, key, "-string", value], {
6158
+ const result = spawnSync6(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, key, "-string", value], {
4912
6159
  encoding: "utf8"
4913
6160
  });
4914
6161
  if (result.status !== 0) {
@@ -4944,14 +6191,14 @@ function writeUseFnKey(enabled) {
4944
6191
  if (TRIGGER_DEFAULTS_EXECUTABLE === null) {
4945
6192
  throw new Error(`cannot write ${USE_FN_KEY_DEFAULTS_KEY}: this machine has no readable app UserDefaults domain`);
4946
6193
  }
4947
- const result = spawnSync4(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, USE_FN_KEY_DEFAULTS_KEY, "-bool", enabled ? "true" : "false"], { encoding: "utf8" });
6194
+ const result = spawnSync6(TRIGGER_DEFAULTS_EXECUTABLE, ["write", RECORDINGS_BUNDLE_ID, USE_FN_KEY_DEFAULTS_KEY, "-bool", enabled ? "true" : "false"], { encoding: "utf8" });
4948
6195
  if (result.status !== 0) {
4949
6196
  throw new Error(`defaults write ${USE_FN_KEY_DEFAULTS_KEY} failed: ${result.stderr?.trim() || `exit ${result.status}`}`);
4950
6197
  }
4951
6198
  }
4952
6199
 
4953
6200
  // src/cli/trigger-probe.ts
4954
- import { closeSync, openSync, readSync, statSync as statSync4 } from "fs";
6201
+ import { closeSync as closeSync3, openSync as openSync3, readSync, statSync as statSync3 } from "fs";
4955
6202
  var FN_BLOCKING_ACCESSIBILITY_STATES = [
4956
6203
  "denied",
4957
6204
  "stale_allowed_for_previous_app_build"
@@ -5070,12 +6317,12 @@ function readAppLogTail(logPath, maxBytes = APP_LOG_TAIL_BYTES) {
5070
6317
  return null;
5071
6318
  let handle = null;
5072
6319
  try {
5073
- const size = statSync4(logPath).size;
6320
+ const size = statSync3(logPath).size;
5074
6321
  if (size === 0)
5075
6322
  return null;
5076
6323
  const length = Math.min(size, maxBytes);
5077
6324
  const buffer = Buffer.allocUnsafe(length);
5078
- handle = openSync(logPath, "r");
6325
+ handle = openSync3(logPath, "r");
5079
6326
  const read = readSync(handle, buffer, 0, length, size - length);
5080
6327
  return buffer.subarray(0, read).toString("utf8");
5081
6328
  } catch {
@@ -5083,7 +6330,7 @@ function readAppLogTail(logPath, maxBytes = APP_LOG_TAIL_BYTES) {
5083
6330
  } finally {
5084
6331
  if (handle !== null) {
5085
6332
  try {
5086
- closeSync(handle);
6333
+ closeSync3(handle);
5087
6334
  } catch {}
5088
6335
  }
5089
6336
  }
@@ -5190,10 +6437,10 @@ function currentMachineId(env = process.env, hostName = hostname()) {
5190
6437
  }
5191
6438
 
5192
6439
  // 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";
6440
+ import { accessSync, constants as fsConstants, realpathSync as realpathSync3, statSync as statSync4 } from "fs";
6441
+ import { isAbsolute as isAbsolute4 } from "path";
5195
6442
  import { randomBytes } from "crypto";
5196
- import { spawnSync as spawnSync5 } from "child_process";
6443
+ import { spawnSync as spawnSync7 } from "child_process";
5197
6444
  var BUN_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
5198
6445
  var INSTALLER_ENVIRONMENT_KEYS = [
5199
6446
  "HOME",
@@ -5207,19 +6454,19 @@ var INSTALLER_ENVIRONMENT_KEYS = [
5207
6454
  ];
5208
6455
  var INSTALLER_PATH = "/usr/bin:/bin:/usr/sbin:/sbin";
5209
6456
  function validateBunExecutable(candidate) {
5210
- if (!isAbsolute2(candidate))
6457
+ if (!isAbsolute4(candidate))
5211
6458
  return { reason: "path is not absolute" };
5212
6459
  let executable;
5213
6460
  try {
5214
6461
  executable = realpathSync3(candidate);
5215
- if (!statSync5(executable).isFile())
6462
+ if (!statSync4(executable).isFile())
5216
6463
  return { reason: "resolved path is not a regular file" };
5217
6464
  accessSync(executable, fsConstants.X_OK);
5218
6465
  } catch {
5219
6466
  return { reason: "path is missing, inaccessible, or not executable" };
5220
6467
  }
5221
6468
  const nonce = randomBytes(24).toString("hex");
5222
- const probe = spawnSync5(executable, [
6469
+ const probe = spawnSync7(executable, [
5223
6470
  "-e",
5224
6471
  `
5225
6472
  import { realpathSync, statSync } from "node:fs";
@@ -5283,20 +6530,20 @@ function createInstallerEnvironment(environment, bunExecutable) {
5283
6530
  }
5284
6531
 
5285
6532
  // src/lib/release-install-policy.ts
5286
- import { createHash as createHash2 } from "crypto";
6533
+ import { createHash as createHash3 } from "crypto";
5287
6534
  import {
5288
6535
  chmodSync as chmodSync2,
5289
- closeSync as closeSync2,
6536
+ closeSync as closeSync4,
5290
6537
  constants,
5291
- fstatSync,
6538
+ fstatSync as fstatSync3,
5292
6539
  fsyncSync,
5293
6540
  mkdtempSync as mkdtempSync2,
5294
- openSync as openSync2,
5295
- readFileSync as readFileSync4,
6541
+ openSync as openSync4,
6542
+ readFileSync as readFileSync5,
5296
6543
  rmSync as rmSync4,
5297
6544
  writeFileSync as writeFileSync3
5298
6545
  } from "fs";
5299
- import { isAbsolute as isAbsolute3, join as join8 } from "path";
6546
+ import { isAbsolute as isAbsolute5, join as join9 } from "path";
5300
6547
  var LOWER_SHA256 = /^[a-f0-9]{64}$/;
5301
6548
  var LOWER_SOURCE_SHA = /^[a-f0-9]{40}$/;
5302
6549
  var RELEASE_VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/;
@@ -5356,7 +6603,7 @@ function prepareReleaseInstallInputs(input) {
5356
6603
  throw new Error("Team ID must be 10 uppercase alphanumeric characters");
5357
6604
  }
5358
6605
  const manifestBytes = readBoundedRegularFile(input.manifestPath, "manifest", MAX_MANIFEST_BYTES);
5359
- const actualManifestSha256 = createHash2("sha256").update(manifestBytes).digest("hex");
6606
+ const actualManifestSha256 = createHash3("sha256").update(manifestBytes).digest("hex");
5360
6607
  if (actualManifestSha256 !== input.manifestSha256) {
5361
6608
  throw new Error("manifest does not match the operator-approved SHA-256");
5362
6609
  }
@@ -5366,10 +6613,10 @@ function prepareReleaseInstallInputs(input) {
5366
6613
  assertReleaseManifest(manifest, input);
5367
6614
  assertReleaseEnvelope(envelope, manifest, manifestBytes.byteLength, input);
5368
6615
  const snapshotRoot = input.snapshotRoot ?? "/private/tmp";
5369
- if (!isAbsolute3(snapshotRoot)) {
6616
+ if (!isAbsolute5(snapshotRoot)) {
5370
6617
  throw new Error("release snapshot root must be absolute");
5371
6618
  }
5372
- const snapshotDirectory = mkdtempSync2(join8(snapshotRoot, "recordings-release-install."));
6619
+ const snapshotDirectory = mkdtempSync2(join9(snapshotRoot, "recordings-release-install."));
5373
6620
  let cleaned = false;
5374
6621
  const cleanup = () => {
5375
6622
  if (cleaned)
@@ -5381,9 +6628,9 @@ function prepareReleaseInstallInputs(input) {
5381
6628
  rmSync4(snapshotDirectory, { recursive: true, force: true });
5382
6629
  };
5383
6630
  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`);
6631
+ const manifestSnapshot = join9(snapshotDirectory, `${input.manifestSha256}.manifest.json`);
6632
+ const envelopeDigest = createHash3("sha256").update(envelopeBytes).digest("hex");
6633
+ const envelopeSnapshot = join9(snapshotDirectory, `${envelopeDigest}.envelope.json`);
5387
6634
  writeSnapshot(manifestSnapshot, manifestBytes);
5388
6635
  writeSnapshot(envelopeSnapshot, envelopeBytes);
5389
6636
  chmodSync2(snapshotDirectory, 320);
@@ -5394,19 +6641,19 @@ function prepareReleaseInstallInputs(input) {
5394
6641
  }
5395
6642
  }
5396
6643
  function assertAbsolutePath(value, label) {
5397
- if (!isAbsolute3(value))
6644
+ if (!isAbsolute5(value))
5398
6645
  throw new Error(`${label} path must be absolute`);
5399
6646
  }
5400
6647
  function readBoundedRegularFile(path, label, maximum) {
5401
- const descriptor = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
6648
+ const descriptor = openSync4(path, constants.O_RDONLY | constants.O_NOFOLLOW);
5402
6649
  try {
5403
- const metadata = fstatSync(descriptor);
6650
+ const metadata = fstatSync3(descriptor);
5404
6651
  if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximum) {
5405
6652
  throw new Error(`${label} must be a non-empty bounded regular file`);
5406
6653
  }
5407
- return readFileSync4(descriptor);
6654
+ return readFileSync5(descriptor);
5408
6655
  } finally {
5409
- closeSync2(descriptor);
6656
+ closeSync4(descriptor);
5410
6657
  }
5411
6658
  }
5412
6659
  function parseObject(bytes, label) {
@@ -5486,22 +6733,22 @@ function isObject(value) {
5486
6733
  }
5487
6734
  function writeSnapshot(path, bytes) {
5488
6735
  writeFileSync3(path, bytes, { flag: "wx", mode: 256 });
5489
- const descriptor = openSync2(path, constants.O_RDONLY | constants.O_NOFOLLOW);
6736
+ const descriptor = openSync4(path, constants.O_RDONLY | constants.O_NOFOLLOW);
5490
6737
  try {
5491
6738
  fsyncSync(descriptor);
5492
6739
  } finally {
5493
- closeSync2(descriptor);
6740
+ closeSync4(descriptor);
5494
6741
  }
5495
6742
  }
5496
6743
 
5497
6744
  // 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";
6745
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync3, renameSync as renameSync2, rmSync as rmSync5, statSync as statSync5 } from "fs";
6746
+ import { dirname as dirname4, join as join10, resolve as resolve3 } from "path";
6747
+ import { spawnSync as spawnSync8 } from "child_process";
5501
6748
  var SCREEN_CAPTURE_EXECUTABLE = "/usr/sbin/screencapture";
5502
6749
  var DEFAULT_DESKTOP_SNAPSHOT = "desktop-snapshot.png";
5503
6750
  function runScreenCapture(executable, arguments_) {
5504
- const result = spawnSync6(executable, arguments_, {
6751
+ const result = spawnSync8(executable, arguments_, {
5505
6752
  encoding: "utf8",
5506
6753
  stdio: ["ignore", "ignore", "pipe"]
5507
6754
  });
@@ -5518,9 +6765,9 @@ function exportDesktopSnapshot(output = DEFAULT_DESKTOP_SNAPSHOT, dependencies =
5518
6765
  const destination = resolve3(dependencies.cwd ?? process.cwd(), output);
5519
6766
  const destinationDirectory = dirname4(destination);
5520
6767
  mkdirSync4(destinationDirectory, { recursive: true });
5521
- const stagingDirectory = mkdtempSync3(join9(destinationDirectory, ".recordings-desktop-"));
6768
+ const stagingDirectory = mkdtempSync3(join10(destinationDirectory, ".recordings-desktop-"));
5522
6769
  chmodSync3(stagingDirectory, 448);
5523
- const stagingPath = join9(stagingDirectory, "snapshot.png");
6770
+ const stagingPath = join10(stagingDirectory, "snapshot.png");
5524
6771
  try {
5525
6772
  const capture = dependencies.capture ?? runScreenCapture;
5526
6773
  const result = capture(SCREEN_CAPTURE_EXECUTABLE, [
@@ -5540,7 +6787,7 @@ function exportDesktopSnapshot(output = DEFAULT_DESKTOP_SNAPSHOT, dependencies =
5540
6787
  }
5541
6788
  let size = 0;
5542
6789
  try {
5543
- const snapshot = statSync6(stagingPath);
6790
+ const snapshot = statSync5(stagingPath);
5544
6791
  if (snapshot.isFile())
5545
6792
  size = snapshot.size;
5546
6793
  } catch {}
@@ -5643,7 +6890,7 @@ Output:`));
5643
6890
  })
5644
6891
  });
5645
6892
  if (parentOpts.json) {
5646
- console.log(JSON.stringify(recording, null, 2));
6893
+ await printJSON(recording);
5647
6894
  } else {
5648
6895
  console.log(chalk.dim(`
5649
6896
  Saved as ${recording.id.slice(0, 8)}`));
@@ -5695,7 +6942,7 @@ program.command("transcribe <file>").description("Transcribe an existing audio f
5695
6942
  })
5696
6943
  }, recordingId);
5697
6944
  if (parentOpts.json) {
5698
- console.log(JSON.stringify(recording, null, 2));
6945
+ await printJSON(recording);
5699
6946
  } else if (processed.mode === "enhanced") {
5700
6947
  console.log(chalk.green("Enhanced:"));
5701
6948
  console.log(processed.text);
@@ -5750,7 +6997,7 @@ program.command("save-text [text]").description("Save already-transcribed text a
5750
6997
  metadata
5751
6998
  }, recordingId);
5752
6999
  if (parentOpts.json) {
5753
- console.log(JSON.stringify(recording, null, 2));
7000
+ await printJSON(recording);
5754
7001
  } else if (processed.mode === "enhanced") {
5755
7002
  console.log(processed.text);
5756
7003
  } else {
@@ -5772,11 +7019,11 @@ ${text}`;
5772
7019
  try {
5773
7020
  const result = await enhanceText(text, instruction, config);
5774
7021
  if (parentOpts.json) {
5775
- console.log(JSON.stringify({
7022
+ await printJSON({
5776
7023
  raw_text: text,
5777
7024
  processed_text: result.enhanced,
5778
7025
  model_used: result.model
5779
- }, null, 2));
7026
+ });
5780
7027
  } else {
5781
7028
  console.log(result.enhanced);
5782
7029
  }
@@ -5815,7 +7062,7 @@ program.command("save <text>").description("Save raw text as a recording (no aud
5815
7062
  machine_id: currentMachineId()
5816
7063
  });
5817
7064
  if (parentOpts.json) {
5818
- console.log(JSON.stringify(recording, null, 2));
7065
+ await printJSON(recording);
5819
7066
  } else {
5820
7067
  console.log(chalk.green(`\u2713 Saved recording ${recording.id}`));
5821
7068
  }
@@ -5842,7 +7089,7 @@ program.command("list").description("List recordings in compact form").option("-
5842
7089
  const store = getStore();
5843
7090
  const recordings = await store.listRecordings(filter);
5844
7091
  if (parentOpts.json) {
5845
- console.log(JSON.stringify(recordings, null, 2));
7092
+ await printJSON(recordings);
5846
7093
  return;
5847
7094
  }
5848
7095
  const total = await countStoreRecordings(store, withoutPagination(filter));
@@ -5874,7 +7121,7 @@ program.command("search <query>").description("Search recordings by text content
5874
7121
  const store = getStore();
5875
7122
  const results = await store.searchRecordings(query, filter);
5876
7123
  if (parentOpts.json) {
5877
- console.log(JSON.stringify(results, null, 2));
7124
+ await printJSON(results);
5878
7125
  return;
5879
7126
  }
5880
7127
  const total = await countStoreRecordings(store, withoutPagination({ ...filter, search: query }));
@@ -5900,7 +7147,7 @@ program.command("stats").description("Show recording statistics").action(async (
5900
7147
  const parentOpts = program.opts();
5901
7148
  const stats = await getStore().getRecordingStats();
5902
7149
  if (parentOpts.json) {
5903
- console.log(JSON.stringify(stats, null, 2));
7150
+ await printJSON(stats);
5904
7151
  return;
5905
7152
  }
5906
7153
  console.log(chalk.bold(`Recording Statistics
@@ -5926,7 +7173,7 @@ program.command("agents").description("List registered agents").option("-n, --li
5926
7173
  const agents = await getStore().listAgents();
5927
7174
  const page = parentOpts.json ? maybePageJson(agents, pagination, opts) : pageItems(agents, pagination);
5928
7175
  if (parentOpts.json) {
5929
- console.log(JSON.stringify(page, null, 2));
7176
+ await printJSON(page);
5930
7177
  return;
5931
7178
  }
5932
7179
  if (page.length === 0) {
@@ -5953,7 +7200,7 @@ projectCommand.command("register").description("Register a project in the active
5953
7200
  const parentOpts = program.opts();
5954
7201
  const project = await getStore().registerProject(opts.name, opts.path, opts.description);
5955
7202
  if (parentOpts.json) {
5956
- console.log(JSON.stringify(project, null, 2));
7203
+ await printJSON(project);
5957
7204
  return;
5958
7205
  }
5959
7206
  console.log(`${chalk.cyan(truncateText(project.id, 80))} ${chalk.bold(truncateText(project.name, 80))} \u2014 ${truncatePath(project.path, 120)}`);
@@ -5964,7 +7211,7 @@ program.command("projects").description("List registered projects").option("-n,
5964
7211
  const projects = await getStore().listProjects();
5965
7212
  const page = parentOpts.json ? maybePageJson(projects, pagination, opts) : pageItems(projects, pagination);
5966
7213
  if (parentOpts.json) {
5967
- console.log(JSON.stringify(page, null, 2));
7214
+ await printJSON(page);
5968
7215
  return;
5969
7216
  }
5970
7217
  if (page.length === 0) {
@@ -5989,10 +7236,10 @@ program.command("projects").description("List registered projects").option("-n,
5989
7236
  });
5990
7237
  program.command("init").description("Initialize .recordings/ in current directory").action(() => {
5991
7238
  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");
7239
+ const { join: join11 } = __require("path");
7240
+ const dir = join11(process.cwd(), ".recordings");
7241
+ const audioDir = join11(dir, "audio");
7242
+ const configFile = join11(dir, "config.json");
5996
7243
  mkdirSync5(audioDir, { recursive: true });
5997
7244
  if (!existsSync9(configFile)) {
5998
7245
  const defaultConf = {
@@ -6015,9 +7262,9 @@ program.command("init").description("Initialize .recordings/ in current director
6015
7262
  console.log(chalk.dim(" db: .recordings/recordings.db"));
6016
7263
  });
6017
7264
  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) => {
7265
+ 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
7266
  if (process.platform !== "darwin") {
6020
- console.error(chalk.red("HasnaRecordings.app installation is only supported on macOS"));
7267
+ console.error(chalk.red("Hasna Recordings.app installation is only supported on macOS"));
6021
7268
  process.exit(1);
6022
7269
  }
6023
7270
  if (opts.variant !== "full" && opts.variant !== "bar") {
@@ -6037,7 +7284,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6037
7284
  try {
6038
7285
  assertReleaseOnlyOptions(opts);
6039
7286
  if (opts.expectedHostname) {
6040
- const hostnameResult = spawnSync7("/bin/hostname", ["-s"], {
7287
+ const hostnameResult = spawnSync9("/bin/hostname", ["-s"], {
6041
7288
  encoding: "utf8",
6042
7289
  stdio: ["ignore", "pipe", "ignore"],
6043
7290
  env: {
@@ -6066,7 +7313,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6066
7313
  process.exit(1);
6067
7314
  }
6068
7315
  const home = process.env.HOME || process.env.USERPROFILE || "";
6069
- const canonicalAppPath = pathJoin(home, "Applications", "HasnaRecordings.app");
7316
+ const canonicalAppPath = pathJoin(home, "Applications", "Hasna Recordings.app");
6070
7317
  const legacyInstallPaths = findLegacyMacOSAppPaths(home, canonicalAppPath);
6071
7318
  const installedAppPath = resolveInstalledAppPath(home, canonicalAppPath, legacyInstallPaths);
6072
7319
  const updateClientPath = pathJoin(installedAppPath, "Contents", "Helpers", "recordings-update-client");
@@ -6077,7 +7324,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6077
7324
  }
6078
7325
  const result2 = (() => {
6079
7326
  try {
6080
- return spawnSync7(updateClientPath, [
7327
+ return spawnSync9(updateClientPath, [
6081
7328
  "install",
6082
7329
  "--artifact",
6083
7330
  opts.artifact,
@@ -6176,7 +7423,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6176
7423
  if (opts.launch)
6177
7424
  installerArgs.push("--launch");
6178
7425
  const installerEnvironment = createInstallerEnvironment(process.env, bunExecutable);
6179
- const result = spawnSync7("/bin/bash", installerArgs, {
7426
+ const result = spawnSync9("/bin/bash", installerArgs, {
6180
7427
  stdio: "inherit",
6181
7428
  env: installerEnvironment
6182
7429
  });
@@ -6186,7 +7433,7 @@ appCommand.command("install").description("Install a release or explicitly appro
6186
7433
  }
6187
7434
  process.exit(result.status ?? 1);
6188
7435
  });
6189
- appCommand.command("status").description("Show installed HasnaRecordings.app status").option("--verbose", "Show package paths, code hash, and log path").action((opts) => {
7436
+ appCommand.command("status").description("Show installed Hasna Recordings.app status").option("--verbose", "Show package paths, code hash, and log path").action((opts) => {
6190
7437
  const status = getMacOSAppStatus();
6191
7438
  const trigger = probeTriggerDiagnostics({
6192
7439
  accessibilityPermission: process.platform === "darwin" ? status.accessibility_permission : null,
@@ -6226,7 +7473,7 @@ appCommand.command("status").description("Show installed HasnaRecordings.app sta
6226
7473
  console.log(chalk.dim("Use --verbose for paths/code hash/log, or --json for the full status object."));
6227
7474
  }
6228
7475
  });
6229
- appCommand.command("permissions").description("Show macOS permission state for HasnaRecordings.app").action(() => {
7476
+ appCommand.command("permissions").description("Show macOS permission state for Hasna Recordings.app").action(() => {
6230
7477
  const status = getMacOSAppStatus();
6231
7478
  const permissions = {
6232
7479
  platform: status.platform,
@@ -6262,21 +7509,21 @@ appCommand.command("permissions").description("Show macOS permission state for H
6262
7509
  }
6263
7510
  console.log(`Log: ${permissions.log_path}`);
6264
7511
  });
6265
- appCommand.command("reset-permissions").description("Reset macOS Microphone and Accessibility permissions for HasnaRecordings.app").action(() => {
7512
+ appCommand.command("reset-permissions").description("Reset macOS Microphone and Accessibility permissions for Hasna Recordings.app").action(() => {
6266
7513
  if (process.platform !== "darwin") {
6267
7514
  console.error(chalk.red("Permission reset is only available on macOS"));
6268
7515
  process.exit(1);
6269
7516
  }
6270
7517
  resetMacOSPermissions();
6271
7518
  });
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) => {
7519
+ 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
7520
  if (process.platform !== "darwin") {
6274
7521
  console.error(chalk.red("Permission prompts are only available on macOS"));
6275
7522
  process.exit(1);
6276
7523
  }
6277
7524
  const status = getMacOSAppStatus();
6278
7525
  if (!status.installed) {
6279
- console.error(chalk.red("HasnaRecordings.app is not installed. Run: recordings app install"));
7526
+ console.error(chalk.red("Hasna Recordings.app is not installed. Run: recordings app install"));
6280
7527
  process.exit(1);
6281
7528
  }
6282
7529
  if (opts.reset) {
@@ -6288,14 +7535,14 @@ appCommand.command("request-permissions").description("Open HasnaRecordings.app
6288
7535
  }
6289
7536
  process.exit(result.exitCode);
6290
7537
  });
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) => {
7538
+ 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
7539
  const status = getMacOSAppStatus();
6293
7540
  if (!existsSync8(status.log_path)) {
6294
7541
  console.log("");
6295
7542
  return;
6296
7543
  }
6297
7544
  const lines = Math.max(1, parseInt(opts.lines, 10) || DEFAULT_LOG_LINES);
6298
- const result = spawnSync7("tail", ["-n", String(lines), status.log_path], {
7545
+ const result = spawnSync9("tail", ["-n", String(lines), status.log_path], {
6299
7546
  encoding: "utf8"
6300
7547
  });
6301
7548
  if (result.error) {
@@ -6314,17 +7561,17 @@ appCommand.command("snapshot [output]").description("Write the current main desk
6314
7561
  console.log(chalk.green(`Desktop snapshot written: ${path}`));
6315
7562
  }
6316
7563
  });
6317
- appCommand.command("open").description("Open the installed HasnaRecordings.app").action(() => {
7564
+ appCommand.command("open").description("Open the installed Hasna Recordings.app").action(() => {
6318
7565
  const status = getMacOSAppStatus();
6319
7566
  if (process.platform !== "darwin") {
6320
- console.error(chalk.red("HasnaRecordings.app can only be opened on macOS"));
7567
+ console.error(chalk.red("Hasna Recordings.app can only be opened on macOS"));
6321
7568
  process.exit(1);
6322
7569
  }
6323
7570
  if (!status.installed) {
6324
- console.error(chalk.red("HasnaRecordings.app is not installed. Run: recordings app install"));
7571
+ console.error(chalk.red("Hasna Recordings.app is not installed. Run: recordings app install"));
6325
7572
  process.exit(1);
6326
7573
  }
6327
- const result = spawnSync7("open", [status.installed_app_path], { stdio: "inherit" });
7574
+ const result = spawnSync9("open", [status.installed_app_path], { stdio: "inherit" });
6328
7575
  if (result.error) {
6329
7576
  console.error(chalk.red(result.error.message));
6330
7577
  process.exit(1);
@@ -6410,8 +7657,9 @@ program.command("check").description("Check system dependencies (sox, API keys)
6410
7657
  persistence_probe: persistence,
6411
7658
  trigger
6412
7659
  }, null, 2));
6413
- if (probeFailed || triggerFailed)
7660
+ if (probeFailed || triggerFailed || activeStoreFailClosed(activeStore)) {
6414
7661
  process.exitCode = 1;
7662
+ }
6415
7663
  return;
6416
7664
  }
6417
7665
  if (deps.available) {
@@ -6422,14 +7670,18 @@ program.command("check").description("Check system dependencies (sox, API keys)
6422
7670
  if (config.openai_api_key) {
6423
7671
  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
7672
  } else {
6425
- console.log(chalk.red(`\u2717 OpenAI API key not found. Set OPENAI_API_KEY env var or add to ~/.secrets`));
7673
+ 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
7674
  }
6427
7675
  if (enhKey) {
6428
7676
  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
7677
  } else {
6430
7678
  console.log(chalk.yellow(`\u26A0 Enhancement API key not configured \u2014 enhancement disabled`));
6431
7679
  }
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})`));
7680
+ console.log(describeActiveStoreLine(activeStore, {
7681
+ pass: (text) => chalk.green(text),
7682
+ warn: (text) => chalk.yellow(text),
7683
+ fail: (text) => chalk.red(text)
7684
+ }));
6433
7685
  if (activeStore.divergent) {
6434
7686
  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
7687
  }
@@ -6453,7 +7705,7 @@ program.command("check").description("Check system dependencies (sox, API keys)
6453
7705
  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
7706
  }
6455
7707
  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:"));
7708
+ 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
7709
  const instruction = microphoneGrantInstruction({
6458
7710
  installedAppPath: macStatus.installed_app_path,
6459
7711
  otherAppPaths: macStatus.legacy_install_paths,
@@ -6486,7 +7738,7 @@ program.command("check").description("Check system dependencies (sox, API keys)
6486
7738
  });
6487
7739
  console.log(persistenceMarker + ` Persistence round-trip: ${persistence.message}`);
6488
7740
  }
6489
- if (probeFailed || triggerFailed)
7741
+ if (probeFailed || triggerFailed || activeStoreFailClosed(activeStore))
6490
7742
  process.exitCode = 1;
6491
7743
  });
6492
7744
  function reportTriggerDiagnosis(trigger) {
@@ -6612,7 +7864,7 @@ Bye.`));
6612
7864
  }
6613
7865
  });
6614
7866
  });
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) => {
7867
+ 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
7868
  const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync5, chmodSync: chmodSync4, existsSync: existsSync9 } = __require("fs");
6617
7869
  const { join: pathJoin2 } = __require("path");
6618
7870
  const { homedir: getHome } = __require("os");
@@ -6726,10 +7978,10 @@ program.command("shortcut").description("Show or change the global recording tri
6726
7978
  function reportPickup() {
6727
7979
  const pickup = describeTriggerPickup(runningBundles());
6728
7980
  if (pickup.armed) {
6729
- console.log(chalk.dim(" HasnaRecordings.app is not running; it will register this on next launch."));
7981
+ console.log(chalk.dim(" Hasna Recordings.app is not running; it will register this on next launch."));
6730
7982
  return;
6731
7983
  }
6732
- console.log(chalk.yellow(" HasnaRecordings.app is running and still holds the previous trigger.") + `
7984
+ console.log(chalk.yellow(" Hasna Recordings.app is running and still holds the previous trigger.") + `
6733
7985
  Quit and reopen it to arm this one:`);
6734
7986
  for (const path of pickup.runningBundlePaths)
6735
7987
  console.log(chalk.dim(` ${path}`));
@@ -6788,7 +8040,7 @@ program.command("shortcut").description("Show or change the global recording tri
6788
8040
  ` + ` ${fnGrant.settingsPath} > enable Recordings`);
6789
8041
  const target = grantTargetPaths();
6790
8042
  if (target.paths.length === 0) {
6791
- console.log(chalk.dim(" (no installed HasnaRecordings.app found to grant it to)"));
8043
+ console.log(chalk.dim(" (no installed Hasna Recordings.app found to grant it to)"));
6792
8044
  }
6793
8045
  for (const path of target.paths) {
6794
8046
  console.log(chalk.dim(` grant it to: ${path}${target.running ? "" : " (not running \u2014 installed copy)"}`));
@@ -6815,7 +8067,7 @@ program.command("shortcut").description("Show or change the global recording tri
6815
8067
  # Toggle recording on/off. Run this from a global hotkey.
6816
8068
  # Each press toggles: start recording -> stop + transcribe + copy to clipboard
6817
8069
  #
6818
- # This is the app-less path. If HasnaRecordings.app is running, prefer its own hotkey
8070
+ # This is the app-less path. If Hasna Recordings.app is running, prefer its own hotkey
6819
8071
  # ("recordings shortcut --set ..."): the app streams transcription and pastes into
6820
8072
  # the focused field, which this script cannot do.
6821
8073
  set -e
@@ -7012,7 +8264,7 @@ async function readSaveTextInput(text, opts) {
7012
8264
  }
7013
8265
  let rawText;
7014
8266
  if (opts.textFile !== undefined) {
7015
- rawText = readFileSync5(opts.textFile, "utf8");
8267
+ rawText = readFileSync6(opts.textFile, "utf8");
7016
8268
  } else if (opts.stdin) {
7017
8269
  rawText = await Bun.stdin.text();
7018
8270
  } else {
@@ -7106,7 +8358,7 @@ async function printRecordingDetail(id) {
7106
8358
  return;
7107
8359
  }
7108
8360
  if (parentOpts.json) {
7109
- console.log(JSON.stringify(recording, null, 2));
8361
+ await printJSON(recording);
7110
8362
  return;
7111
8363
  }
7112
8364
  console.log(formatRecordingDetail(recording));
@@ -7226,7 +8478,7 @@ function relativeHint(value) {
7226
8478
  return `${Math.floor(hours / 24)}d ago`;
7227
8479
  }
7228
8480
  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");
8481
+ const { readFileSync: readFileSync7, writeFileSync: writeFileSync4, existsSync: fileExists } = __require("fs");
7230
8482
  const { join: pathJoin2 } = __require("path");
7231
8483
  const { homedir: getHome } = __require("os");
7232
8484
  const { execSync } = __require("child_process");
@@ -7259,7 +8511,7 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
7259
8511
  if (target === "codex") {
7260
8512
  const configPath = pathJoin2(home, ".codex", "config.toml");
7261
8513
  if (fileExists(configPath)) {
7262
- const content = readFileSync6(configPath, "utf-8");
8514
+ const content = readFileSync7(configPath, "utf-8");
7263
8515
  if (opts.uninstall) {
7264
8516
  const { content: next, removed } = removeCodexServerBlock(content, "recordings");
7265
8517
  writeFileSync4(configPath, next, "utf-8");
@@ -7277,7 +8529,7 @@ program.command("mcp").description("Install recordings MCP server into Claude Co
7277
8529
  const configPath = pathJoin2(home, ".gemini", "settings.json");
7278
8530
  let config = {};
7279
8531
  if (fileExists(configPath)) {
7280
- config = JSON.parse(readFileSync6(configPath, "utf-8"));
8532
+ config = JSON.parse(readFileSync7(configPath, "utf-8"));
7281
8533
  }
7282
8534
  const servers = config["mcpServers"] || {};
7283
8535
  if (opts.uninstall) {
@@ -7316,7 +8568,7 @@ program.command("feedback <message>").description("Send feedback").option("--ema
7316
8568
  function getMacOSAppStatus() {
7317
8569
  const packageRoot = findPackageRoot();
7318
8570
  const home = process.env.HOME || process.env.USERPROFILE || "";
7319
- const canonicalAppPath = pathJoin(home, "Applications", "HasnaRecordings.app");
8571
+ const canonicalAppPath = pathJoin(home, "Applications", "Hasna Recordings.app");
7320
8572
  const legacyInstallPaths = findLegacyMacOSAppPaths(home, canonicalAppPath);
7321
8573
  const installedAppPath = resolveInstalledAppPath(home, canonicalAppPath, legacyInstallPaths);
7322
8574
  const installedAppPathForGrants = existsSync8(installedAppPath) ? installedAppPath : null;
@@ -7364,7 +8616,7 @@ function buildPermissionWarnings(status) {
7364
8616
  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
8617
  }
7366
8618
  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(", ")}`);
8619
+ 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
8620
  }
7369
8621
  for (const [service, durability] of [
7370
8622
  ["Microphone", status.microphone_grant_durability],
@@ -7387,8 +8639,11 @@ function buildPermissionWarnings(status) {
7387
8639
  function resolveInstalledAppPath(home, canonicalPath, legacyPaths) {
7388
8640
  const preference = [
7389
8641
  canonicalPath,
8642
+ pathJoin("/", "Applications", "Hasna Recordings.app"),
8643
+ pathJoin(home, "Applications", "HasnaRecordings.app"),
7390
8644
  pathJoin("/", "Applications", "HasnaRecordings.app"),
7391
8645
  pathJoin("/", "Applications", "Recordings.app"),
8646
+ pathJoin(home, ".hasna", "recordings", "Hasna Recordings.app"),
7392
8647
  pathJoin(home, ".hasna", "recordings", "HasnaRecordings.app"),
7393
8648
  pathJoin(home, ".hasna", "recordings", "Recordings.app")
7394
8649
  ];
@@ -7400,15 +8655,18 @@ function resolveInstalledAppPath(home, canonicalPath, legacyPaths) {
7400
8655
  }
7401
8656
  function findLegacyMacOSAppPaths(home, canonicalPath) {
7402
8657
  const candidates = [
7403
- pathJoin(home, ".hasna", "recordings", "Recordings.app"),
8658
+ pathJoin(home, "Applications", "HasnaRecordings.app"),
7404
8659
  pathJoin(home, ".hasna", "recordings", "HasnaRecordings.app"),
8660
+ pathJoin("/", "Applications", "HasnaRecordings.app"),
8661
+ pathJoin(home, ".hasna", "recordings", "Recordings.app"),
8662
+ pathJoin(home, ".hasna", "recordings", "Hasna Recordings.app"),
7405
8663
  pathJoin("/", "Applications", "Recordings.app"),
7406
- pathJoin("/", "Applications", "HasnaRecordings.app")
8664
+ pathJoin("/", "Applications", "Hasna Recordings.app")
7407
8665
  ];
7408
8666
  const userApplications = pathJoin(home, "Applications");
7409
8667
  if (existsSync8(userApplications)) {
7410
8668
  for (const entry of readdirSync2(userApplications, { withFileTypes: true })) {
7411
- if (entry.isDirectory() && (entry.name.startsWith("Recordings.app.") || entry.name.startsWith("HasnaRecordings.app."))) {
8669
+ if (entry.isDirectory() && (entry.name.startsWith("Recordings.app.") || entry.name.startsWith("Hasna Recordings.app.") || entry.name.startsWith("HasnaRecordings.app."))) {
7412
8670
  candidates.push(pathJoin(userApplications, entry.name));
7413
8671
  }
7414
8672
  }
@@ -7418,7 +8676,7 @@ function findLegacyMacOSAppPaths(home, canonicalPath) {
7418
8676
  function resetMacOSPermissions() {
7419
8677
  const services = ["Microphone", "Accessibility"];
7420
8678
  for (const service of services) {
7421
- const result = spawnSync7("/usr/bin/tccutil", ["reset", service, RECORDINGS_BUNDLE_IDENTIFIER], { stdio: "inherit" });
8679
+ const result = spawnSync9("/usr/bin/tccutil", ["reset", service, RECORDINGS_BUNDLE_IDENTIFIER], { stdio: "inherit" });
7422
8680
  if (result.error) {
7423
8681
  console.error(chalk.red(result.error.message));
7424
8682
  process.exit(1);
@@ -7436,7 +8694,7 @@ function getCodeSigningInfo(appPath) {
7436
8694
  authorities: []
7437
8695
  };
7438
8696
  }
7439
- const result = spawnSync7("/usr/bin/codesign", ["-d", "-r-", "--verbose=4", appPath], {
8697
+ const result = spawnSync9("/usr/bin/codesign", ["-d", "-r-", "--verbose=4", appPath], {
7440
8698
  encoding: "utf8",
7441
8699
  stdio: ["ignore", "pipe", "pipe"]
7442
8700
  });
@@ -7465,7 +8723,7 @@ function findPackageRoot() {
7465
8723
  const packagePath = pathJoin(current, "package.json");
7466
8724
  if (existsSync8(packagePath)) {
7467
8725
  try {
7468
- const pkg = JSON.parse(readFileSync5(packagePath, "utf8"));
8726
+ const pkg = JSON.parse(readFileSync6(packagePath, "utf8"));
7469
8727
  if (pkg.name === "@hasna/recordings") {
7470
8728
  return current;
7471
8729
  }
@@ -7481,6 +8739,17 @@ function findPackageRoot() {
7481
8739
  function getMacOSInstallerPath(packageRoot = findPackageRoot()) {
7482
8740
  return pathJoin(packageRoot, "scripts", "install_macos_app.sh");
7483
8741
  }
8742
+ async function printJSON(value) {
8743
+ await new Promise((resolve4, reject) => {
8744
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
8745
+ `, (error) => {
8746
+ if (error)
8747
+ reject(error);
8748
+ else
8749
+ resolve4();
8750
+ });
8751
+ });
8752
+ }
7484
8753
  program.parseAsync().catch((error) => {
7485
8754
  const msg = error instanceof Error ? error.message : String(error);
7486
8755
  console.error(`ERROR: ${msg}`);