@hasna/recordings 0.3.13 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/README.md +279 -43
  2. package/bun.lock +37 -38
  3. package/contracts/v1/fixtures.json +1206 -0
  4. package/dist/cli/index.js +1909 -619
  5. package/dist/cli/macos-shortcut.d.ts +2 -2
  6. package/dist/contracts/hosted-v1.d.ts +105 -0
  7. package/dist/contracts/hosted-v1.d.ts.map +1 -0
  8. package/dist/contracts/hosted-v1.js +41 -0
  9. package/dist/contracts/stream-v1.d.ts +117 -0
  10. package/dist/contracts/stream-v1.d.ts.map +1 -0
  11. package/dist/contracts/stream-v1.js +35 -0
  12. package/dist/db/database.d.ts.map +1 -1
  13. package/dist/db/pg-migrations.d.ts.map +1 -1
  14. package/dist/db/recordings.d.ts +6 -0
  15. package/dist/db/recordings.d.ts.map +1 -1
  16. package/dist/hosted/index.d.ts +58 -0
  17. package/dist/hosted/index.d.ts.map +1 -0
  18. package/dist/hosted/index.js +266 -0
  19. package/dist/hosted/transport.d.ts +36 -0
  20. package/dist/hosted/transport.d.ts.map +1 -0
  21. package/dist/hosted-v1-aavn7ktb.js +4114 -0
  22. package/dist/hosted-v1-gdr9extc.js +84 -0
  23. package/dist/http/client.d.ts +183 -22
  24. package/dist/http/client.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -3
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2879 -2288
  28. package/dist/lib/audio-artifact-storage.d.ts +98 -0
  29. package/dist/lib/audio-artifact-storage.d.ts.map +1 -0
  30. package/dist/lib/capture-probe.d.ts +3 -3
  31. package/dist/lib/capture-probe.d.ts.map +1 -1
  32. package/dist/lib/config.d.ts.map +1 -1
  33. package/dist/lib/local-opt-in.d.ts +132 -0
  34. package/dist/lib/local-opt-in.d.ts.map +1 -0
  35. package/dist/lib/macos-bundle.d.ts +1 -1
  36. package/dist/lib/persistence-probe.d.ts +56 -17
  37. package/dist/lib/persistence-probe.d.ts.map +1 -1
  38. package/dist/lib/transcriber.d.ts.map +1 -1
  39. package/dist/mcp/index.d.ts.map +1 -1
  40. package/dist/mcp/index.js +1713 -459
  41. package/dist/sdk/index.d.ts +13 -5
  42. package/dist/sdk/index.d.ts.map +1 -1
  43. package/dist/sdk/index.js +904 -1
  44. package/dist/sdk/resolve.d.ts +53 -0
  45. package/dist/sdk/resolve.d.ts.map +1 -0
  46. package/dist/sdk/v1.generated.d.ts +6 -0
  47. package/dist/sdk/v1.generated.d.ts.map +1 -1
  48. package/dist/server/client-ip.d.ts +39 -0
  49. package/dist/server/client-ip.d.ts.map +1 -0
  50. package/dist/server/cloud-config.d.ts +12 -0
  51. package/dist/server/cloud-config.d.ts.map +1 -1
  52. package/dist/server/cloud-readiness.d.ts.map +1 -1
  53. package/dist/server/cloud.d.ts +20 -2
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.js +590 -287
  56. package/dist/server/migrate-command.d.ts +1 -1
  57. package/dist/server/migrate-command.d.ts.map +1 -1
  58. package/dist/server/openapi.d.ts +21 -0
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/repo.d.ts.map +1 -1
  61. package/dist/server/serve.d.ts.map +1 -1
  62. package/dist/storage.d.ts +2 -2
  63. package/dist/storage.d.ts.map +1 -1
  64. package/dist/storage.js +1358 -874
  65. package/dist/store.d.ts +12 -3
  66. package/dist/store.d.ts.map +1 -1
  67. package/dist/types/index.d.ts +15 -0
  68. package/dist/types/index.d.ts.map +1 -1
  69. package/docs/hosted-sdk.md +71 -0
  70. package/docs/wire-contracts.md +24 -0
  71. package/package.json +33 -11
  72. package/packaging/macos/build_release_pkg.sh +3 -3
  73. package/packaging/macos/managed_bootstrap.sh +4 -4
  74. package/packaging/macos/scripts/postinstall +2 -2
  75. package/packaging/macos/scripts/preinstall +2 -2
  76. package/scripts/build_companion_cli.sh +7 -1
  77. package/scripts/ci-linux-suite.ts +23 -13
  78. package/scripts/install_macos_app.sh +27 -24
  79. package/scripts/macos_artifact.ts +60 -49
  80. package/scripts/migrate.ts +4 -20
  81. package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
  82. package/scripts/native/recordings_fs_guard.c +36 -4
  83. package/scripts/native-core-receipt.py +171 -0
  84. package/scripts/native_fs_guard.ts +2 -0
  85. package/scripts/release-suite-gate.ts +227 -150
  86. package/scripts/resolve_tailscale_cli.sh +24 -3
  87. package/scripts/smoke_macos_app.sh +21 -21
  88. package/src/native/Recordings/App/ContentView.swift +53 -68
  89. package/src/native/Recordings/App/MenuBarStatusView.swift +171 -61
  90. package/src/native/Recordings/App/RecordWorkspaceView.swift +47 -449
  91. package/src/native/Recordings/App/RecorderSettingsView.swift +75 -0
  92. package/src/native/Recordings/App/RecordingDetailView.swift +3 -4
  93. package/src/native/Recordings/App/RecordingsApp.swift +185 -9
  94. package/src/native/Recordings/App/RecordingsListView.swift +60 -112
  95. package/src/native/Recordings/App/RecordingsStore.swift +124 -109
  96. package/src/native/Recordings/App/Theme.swift +73 -61
  97. package/src/native/Recordings/Package.swift +1 -0
  98. package/src/native/Recordings/RecordingsLib/BlockingOperation.swift +29 -0
  99. package/src/native/Recordings/RecordingsLib/CaptureMonitor.swift +65 -0
  100. package/src/native/Recordings/RecordingsLib/Info.plist +3 -3
  101. package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +75 -21
  102. package/src/native/Recordings/RecordingsLib/ProjectStore.swift +4 -4
  103. package/src/native/Recordings/RecordingsLib/Recording.swift +14 -2
  104. package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +539 -89
  105. package/src/native/Recordings/RecordingsLib/RecordingGlassBackground.swift +34 -0
  106. package/src/native/Recordings/RecordingsLib/RecordingPasteTarget.swift +114 -0
  107. package/src/native/Recordings/RecordingsLib/RecordingProvider.swift +112 -0
  108. package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +17 -10
  109. package/src/native/Recordings/RecordingsLib/ServiceAPIConfiguration.swift +107 -0
  110. package/src/native/Recordings/RecordingsLib/ServiceConnectionView.swift +76 -0
  111. package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -149
  112. package/src/native/Recordings/RecordingsLib/SettingsWindowController.swift +48 -0
  113. package/src/native/Recordings/RecordingsTests/BlockingOperationTests.swift +85 -0
  114. package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +454 -76
  115. package/src/native/Recordings/RecordingsTests/OpenAIAPIKeyStoreTests.swift +43 -38
  116. package/src/native/Recordings/RecordingsTests/PipeClosureFixture.swift +215 -0
  117. package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +10 -10
  118. package/src/native/Recordings/RecordingsTests/RecorderSurfaceTests.swift +60 -0
  119. package/src/native/Recordings/RecordingsTests/RecordingDateTests.swift +26 -0
  120. package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +1 -1
  121. package/src/native/Recordings/RecordingsTests/RecordingFrozenPasteTargetTests.swift +50 -0
  122. package/src/native/Recordings/RecordingsTests/RecordingPasteTargetTrackerTests.swift +48 -0
  123. package/src/native/Recordings/RecordingsTests/RecordingProviderTests.swift +308 -0
  124. package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +1 -1
  125. package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +123 -21
  126. package/src/native/Recordings/RecordingsTests/ServiceAPIConfigurationTests.swift +75 -0
  127. package/src/native/Recordings/RecordingsTests/SettingsWindowControllerTests.swift +28 -0
  128. package/src/native/Recordings/RecordingsTests/TestHomeDirectory.swift +5 -1
  129. package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +1 -1
  130. package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +2 -2
  131. package/src/native/Recordings/Updater/Broker/BrokerMain.swift +1 -1
  132. package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +1 -1
  133. package/src/native/Recordings/Updater/Broker/InstallJournal.swift +1 -1
  134. package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +2 -2
  135. package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +1 -1
  136. package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +2 -2
  137. package/src/native/Recordings/build.sh +8 -7
  138. package/dist/__tests__/helpers/installer-guard-execution.d.ts +0 -22
  139. package/dist/__tests__/helpers/installer-guard-execution.d.ts.map +0 -1
  140. package/dist/__tests__/helpers/installer-preflight.d.ts +0 -22
  141. package/dist/__tests__/helpers/installer-preflight.d.ts.map +0 -1
  142. package/dist/__tests__/helpers/native-fs-guard.d.ts +0 -2
  143. package/dist/__tests__/helpers/native-fs-guard.d.ts.map +0 -1
  144. package/dist/__tests__/helpers/source-assertions.d.ts +0 -171
  145. package/dist/__tests__/helpers/source-assertions.d.ts.map +0 -1
  146. package/dist/__tests__/preload.d.ts +0 -2
  147. package/dist/__tests__/preload.d.ts.map +0 -1
  148. package/src/native/Recordings/App/SidebarView.swift +0 -212
package/dist/mcp/index.js CHANGED
@@ -4246,6 +4246,9 @@ var DEFAULT_CONFIG = {
4246
4246
  db_path: "",
4247
4247
  audio_dir: "",
4248
4248
  max_recording_seconds: 1800,
4249
+ s3_bucket: "",
4250
+ s3_prefix: "",
4251
+ s3_region: "",
4249
4252
  config_warnings: []
4250
4253
  };
4251
4254
  function loadConfig(configPath) {
@@ -4272,6 +4275,9 @@ function loadConfig(configPath) {
4272
4275
  if (process.env.RECORDINGS_API_KEY) {
4273
4276
  config.openai_api_key = process.env.RECORDINGS_API_KEY;
4274
4277
  }
4278
+ if (process.env.RECORDINGS_OPENAI_API_KEY) {
4279
+ config.openai_api_key = process.env.RECORDINGS_OPENAI_API_KEY;
4280
+ }
4275
4281
  if (process.env.RECORDINGS_ENHANCEMENT_KEY) {
4276
4282
  config.enhancement_api_key = process.env.RECORDINGS_ENHANCEMENT_KEY;
4277
4283
  }
@@ -4315,14 +4321,26 @@ function loadConfig(configPath) {
4315
4321
  if (process.env.RECORDINGS_AUDIO_DIR) {
4316
4322
  config.audio_dir = process.env.RECORDINGS_AUDIO_DIR;
4317
4323
  }
4324
+ if (process.env.HASNA_RECORDINGS_S3_BUCKET) {
4325
+ config.s3_bucket = process.env.HASNA_RECORDINGS_S3_BUCKET;
4326
+ } else if (process.env.RECORDINGS_S3_BUCKET) {
4327
+ config.s3_bucket = process.env.RECORDINGS_S3_BUCKET;
4328
+ }
4329
+ if (process.env.HASNA_RECORDINGS_S3_PREFIX) {
4330
+ config.s3_prefix = process.env.HASNA_RECORDINGS_S3_PREFIX;
4331
+ } else if (process.env.RECORDINGS_S3_PREFIX) {
4332
+ config.s3_prefix = process.env.RECORDINGS_S3_PREFIX;
4333
+ }
4334
+ if (process.env.RECORDINGS_S3_REGION) {
4335
+ config.s3_region = process.env.RECORDINGS_S3_REGION;
4336
+ } else if (process.env.AWS_REGION) {
4337
+ config.s3_region = process.env.AWS_REGION;
4338
+ }
4318
4339
  if (process.env.RECORDINGS_MAX_SECONDS) {
4319
4340
  config.max_recording_seconds = parseInt(process.env.RECORDINGS_MAX_SECONDS, 10);
4320
4341
  }
4321
- if (!config.openai_api_key) {
4322
- config.openai_api_key = loadSecretKey("OPENAI_API_KEY");
4323
- }
4324
4342
  if (!config.enhancement_api_key) {
4325
- config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
4343
+ config.enhancement_api_key = config.openai_api_key;
4326
4344
  }
4327
4345
  if (!explicitTranscriberModel) {
4328
4346
  config.transcriber_model = config.enhancement_model;
@@ -4502,51 +4520,6 @@ function mergeDirectoryContents(sourceDir, targetDir) {
4502
4520
  }
4503
4521
  }
4504
4522
  }
4505
- function loadSecretKey(keyName) {
4506
- const secretsPath = join2(getHomeDir(), ".secrets");
4507
- if (!existsSync(secretsPath))
4508
- return "";
4509
- for (const candidate of listSecretFiles(secretsPath)) {
4510
- try {
4511
- const content = readFileSync(candidate, "utf-8");
4512
- const match = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*"([^"]+)"`));
4513
- const doubleQuotedValue = match?.[1];
4514
- if (doubleQuotedValue !== undefined)
4515
- return doubleQuotedValue;
4516
- const match2 = content.match(new RegExp(`export\\s+${keyName}\\s*=\\s*'([^']+)'`));
4517
- const singleQuotedValue = match2?.[1];
4518
- if (singleQuotedValue !== undefined)
4519
- return singleQuotedValue;
4520
- const match3 = content.match(new RegExp(`${keyName}\\s*=\\s*(.+)`));
4521
- const unquotedValue = match3?.[1];
4522
- if (unquotedValue !== undefined)
4523
- return unquotedValue.trim().replace(/^["']|["']$/g, "");
4524
- } catch {}
4525
- }
4526
- return "";
4527
- }
4528
- function listSecretFiles(path) {
4529
- try {
4530
- const stats = statSync(path);
4531
- if (stats.isFile())
4532
- return [path];
4533
- if (!stats.isDirectory())
4534
- return [];
4535
- return readdirSync(path).sort().flatMap((entry) => {
4536
- const child = join2(path, entry);
4537
- try {
4538
- const childStats = statSync(child);
4539
- if (childStats.isDirectory())
4540
- return listSecretFiles(child);
4541
- if (childStats.isFile() && child.endsWith(".env"))
4542
- return [child];
4543
- } catch {}
4544
- return [];
4545
- });
4546
- } catch {
4547
- return [];
4548
- }
4549
- }
4550
4523
  function getHomeDir() {
4551
4524
  return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
4552
4525
  }
@@ -4676,6 +4649,11 @@ var MIGRATIONS = [
4676
4649
  `,
4677
4650
  `
4678
4651
  ALTER TABLE agents ADD COLUMN active_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL;
4652
+ `,
4653
+ `
4654
+ ALTER TABLE recordings ADD COLUMN audio_object_key TEXT;
4655
+ ALTER TABLE recordings ADD COLUMN audio_sha256 TEXT;
4656
+ ALTER TABLE recordings ADD COLUMN audio_bytes INTEGER;
4679
4657
  `
4680
4658
  ];
4681
4659
  var CURRENT_MIGRATION_LEVEL = MIGRATIONS.length - 1;
@@ -4731,6 +4709,9 @@ function repairSchemaDrift(db) {
4731
4709
  ensureColumn(db, "recordings", "task_list_id", "TEXT");
4732
4710
  ensureColumn(db, "recordings", "machine_id", "TEXT");
4733
4711
  ensureColumn(db, "recordings", "metadata", "TEXT DEFAULT '{}'");
4712
+ ensureColumn(db, "recordings", "audio_object_key", "TEXT");
4713
+ ensureColumn(db, "recordings", "audio_sha256", "TEXT");
4714
+ ensureColumn(db, "recordings", "audio_bytes", "INTEGER");
4734
4715
  ensureColumn(db, "agents", "active_project_id", "TEXT REFERENCES projects(id) ON DELETE SET NULL");
4735
4716
  }
4736
4717
  function ensureColumn(db, table, column, definition) {
@@ -4806,6 +4787,9 @@ function parseRow(row) {
4806
4787
  return {
4807
4788
  id: row["id"],
4808
4789
  audio_path: row["audio_path"] || null,
4790
+ audio_object_key: row["audio_object_key"] || null,
4791
+ audio_sha256: row["audio_sha256"] || null,
4792
+ audio_bytes: Number(row["audio_bytes"] ?? 0) || null,
4809
4793
  raw_text: row["raw_text"],
4810
4794
  processed_text: row["processed_text"] || null,
4811
4795
  processing_mode: row["processing_mode"] || "raw",
@@ -4837,9 +4821,9 @@ function createRecording(input, db, idempotencyKey) {
4837
4821
  }
4838
4822
  const tagsJson = JSON.stringify(input.tags || []);
4839
4823
  const metadataJson = JSON.stringify(input.metadata || {});
4840
- 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)
4841
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4842
- 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);
4824
+ 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)
4825
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4826
+ 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);
4843
4827
  if (insertResult.changes === 0) {
4844
4828
  const existing = getRecording(id, d);
4845
4829
  if (existing?.id === id)
@@ -4856,6 +4840,13 @@ function createRecording(input, db, idempotencyKey) {
4856
4840
  });
4857
4841
  return create();
4858
4842
  }
4843
+ function updateRecordingAudio(id, audioObjectKey, audioSha256, audioBytes, db) {
4844
+ const d = db || getDatabase();
4845
+ const result = d.query(`UPDATE recordings SET audio_object_key = ?, audio_sha256 = ?, audio_bytes = ? WHERE id = ?`).run(audioObjectKey, audioSha256, audioBytes, id);
4846
+ if (result.changes === 0)
4847
+ return null;
4848
+ return getRecording(id, d);
4849
+ }
4859
4850
  function getRecording(id, db) {
4860
4851
  const d = db || getDatabase();
4861
4852
  let row = d.query("SELECT * FROM recordings WHERE id = ?").get(id);
@@ -5057,7 +5048,7 @@ function setAgentFocus(idOrName, projectId, db) {
5057
5048
  // package.json
5058
5049
  var package_default = {
5059
5050
  name: "@hasna/recordings",
5060
- version: "0.3.13",
5051
+ version: "0.5.0",
5061
5052
  type: "module",
5062
5053
  description: "Speech-to-text recording tool with MCP and CLI \u2014 records, transcribes, and optionally enhances text using AI",
5063
5054
  repository: {
@@ -5083,6 +5074,19 @@ var package_default = {
5083
5074
  "./sdk": {
5084
5075
  import: "./dist/sdk/index.js",
5085
5076
  types: "./dist/sdk/index.d.ts"
5077
+ },
5078
+ "./contracts/hosted-v1": {
5079
+ import: "./dist/contracts/hosted-v1.js",
5080
+ types: "./dist/contracts/hosted-v1.d.ts"
5081
+ },
5082
+ "./contracts/stream-v1": {
5083
+ import: "./dist/contracts/stream-v1.js",
5084
+ types: "./dist/contracts/stream-v1.d.ts"
5085
+ },
5086
+ "./contracts/fixtures/v1": "./contracts/v1/fixtures.json",
5087
+ "./hosted": {
5088
+ import: "./dist/hosted/index.js",
5089
+ types: "./dist/hosted/index.d.ts"
5086
5090
  }
5087
5091
  },
5088
5092
  engines: {
@@ -5091,20 +5095,20 @@ var package_default = {
5091
5095
  scripts: {
5092
5096
  clean: "rm -rf dist",
5093
5097
  build: "bun run clean && bun run build:cli && bun run build:mcp && bun run build:serve && bun run build:lib && tsc --emitDeclarationOnly --outDir dist",
5094
- "build:cli": "bun build src/cli/index.ts --target=bun --outfile=dist/cli/index.js --external=commander --external=chalk --external=openai",
5095
- "build:mcp": "bun build src/mcp/index.ts --target=bun --outfile=dist/mcp/index.js --external=@modelcontextprotocol/sdk --external=openai",
5096
- "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",
5097
- "build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai",
5098
+ "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",
5099
+ "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",
5100
+ "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",
5101
+ "build:lib": "bun build src/index.ts src/storage.ts src/sdk/index.ts --target=bun --outdir=dist --external=openai --external=@aws-sdk/client-s3 && bun run build:contracts",
5098
5102
  "build:native-fs-guard": "/bin/bash scripts/build_native_fs_guard.sh",
5099
5103
  "generate:sdk": "bun run scripts/generate-sdk.ts",
5100
5104
  migrate: "bun run scripts/migrate.ts",
5101
5105
  typecheck: "tsc --noEmit",
5102
5106
  "typecheck:tcc-contract": "tsc --noEmit -p tsconfig.tcc-contract.json",
5103
- test: "bun test",
5107
+ test: "bun scripts/release-suite-gate.ts --all",
5104
5108
  "verify:ci-suite": "bun scripts/ci-linux-suite.ts --check",
5105
5109
  "verify:ci-suite:run": "bun scripts/ci-linux-suite.ts --verify-run",
5106
5110
  "verify:ci-native": "bun scripts/ci-native-build.ts",
5107
- "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)',
5111
+ "test:gated": "bun scripts/release-suite-gate.ts",
5108
5112
  "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"',
5109
5113
  "test:vacuity-battery:source": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/source-side.tsv",
5110
5114
  "test:vacuity-battery:chain": "bun scripts/vacuity-mutation-battery.ts scripts/vacuity-manifests/install-chain.tsv",
@@ -5122,11 +5126,16 @@ var package_default = {
5122
5126
  "prepack:platform-gate": `bash -c 'if [ "$(uname -s)" = Darwin ]; then bun run build:native-fs-guard; else echo "WARN: native fs-guard build skipped on $(uname -s) \u2014 dry-run/CI pack; publish recordings from macOS"; fi'`,
5123
5127
  prepublishOnly: "bun run typecheck && bun run release-suite-gate",
5124
5128
  "release-suite-gate": "bun run scripts/release-suite-gate.ts",
5125
- "typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json"
5129
+ "typecheck:shortcut-contract": "tsc --noEmit -p tsconfig.test.json",
5130
+ "build:contracts": "bun build src/contracts/hosted-v1.ts src/contracts/stream-v1.ts src/hosted/index.ts --target=bun --root src --outdir=dist --splitting --external=@hasna/contracts"
5126
5131
  },
5127
5132
  files: [
5128
5133
  "dist/",
5134
+ "!dist/__tests__/",
5129
5135
  "scripts/",
5136
+ "!scripts/test-recorder-fixtures.ts",
5137
+ "!scripts/**/__pycache__/",
5138
+ "!scripts/**/*.pyc",
5130
5139
  "Dockerfile.package",
5131
5140
  "bun.lock",
5132
5141
  "src/native/Recordings/App/",
@@ -5138,10 +5147,14 @@ var package_default = {
5138
5147
  "src/native/Recordings/build.sh",
5139
5148
  "packaging/macos/",
5140
5149
  "README.md",
5141
- "LICENSE"
5150
+ "LICENSE",
5151
+ "contracts/v1/fixtures.json",
5152
+ "docs/wire-contracts.md",
5153
+ "docs/hosted-sdk.md"
5142
5154
  ],
5143
5155
  dependencies: {
5144
- "@hasna/contracts": "0.14.1",
5156
+ "@aws-sdk/client-s3": "^3.1007.0",
5157
+ "@hasna/contracts": "1.0.2",
5145
5158
  "@hasna/events": "0.1.11",
5146
5159
  "@modelcontextprotocol/sdk": "^1.12.1",
5147
5160
  chalk: "^5.4.1",
@@ -5151,7 +5164,7 @@ var package_default = {
5151
5164
  zod: "^3.24.2"
5152
5165
  },
5153
5166
  devDependencies: {
5154
- "@types/bun": "^1.2.5",
5167
+ "@types/bun": "1.3.14",
5155
5168
  "@types/pg": "^8.11.11",
5156
5169
  "node-api-headers": "1.9.0",
5157
5170
  typescript: "^5.8.2"
@@ -5175,8 +5188,12 @@ function saveFeedback(input) {
5175
5188
 
5176
5189
  // ../contracts/dist/client/transport.js
5177
5190
  import { isIP } from "net";
5178
- import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
5179
- import { join as join3 } from "path";
5191
+ import { spawnSync as spawnSync2 } from "child_process";
5192
+ import { closeSync, fstatSync, openSync, readFileSync as readFileSync2 } from "fs";
5193
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
5194
+ import { createRequire } from "module";
5195
+ import { hostname as osHostname } from "os";
5196
+ import { isAbsolute as isAbsolute2, join as join3 } from "path";
5180
5197
  function envToken(name) {
5181
5198
  return name.toUpperCase().replace(/-/g, "_");
5182
5199
  }
@@ -5191,6 +5208,9 @@ function credentialOverrideEnvKey(name) {
5191
5208
  return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
5192
5209
  }
5193
5210
  var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
5211
+ function credentialPointerEnvKey(name) {
5212
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
5213
+ }
5194
5214
 
5195
5215
  class CredentialResolutionError extends Error {
5196
5216
  appName;
@@ -5202,34 +5222,70 @@ class CredentialResolutionError extends Error {
5202
5222
  this.attempted = attempted;
5203
5223
  }
5204
5224
  }
5205
- var HASNA_STATE_DIR = ".hasna";
5206
- var FLEET_CREDENTIAL_DIR = "cloud";
5207
- var CONFIG_DIR = ".config";
5208
- var CONFIG_NAMESPACE = "hasna";
5225
+
5226
+ class CredentialFileUnsafeError extends Error {
5227
+ path;
5228
+ constructor(path, reason) {
5229
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
5230
+ this.name = "CredentialFileUnsafeError";
5231
+ this.path = path;
5232
+ }
5233
+ }
5234
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
5235
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
5236
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
5237
+ var HASNA_HOME_DIR = ".hasna";
5238
+ var CONFIG_SUBDIR = "config";
5239
+ var CREDENTIALS_FILE = "credentials";
5240
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
5241
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
5242
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
5243
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
5209
5244
  var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
5210
5245
  var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
5211
5246
  var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
5212
5247
  var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
5248
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
5213
5249
  function homeDir(env) {
5214
5250
  const home = env.HOME?.trim();
5215
5251
  return home ? home : null;
5216
5252
  }
5217
- function credentialDiskSources(name, env) {
5218
- return profileDiskSources(name, env, null);
5253
+ function absoluteOverride(env, key) {
5254
+ const value = env[key]?.trim();
5255
+ return value && isAbsolute2(value) ? value : null;
5219
5256
  }
5220
- function profileDiskSources(name, env, profile) {
5257
+ function hasnaHomeDir(env) {
5258
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
5259
+ if (override)
5260
+ return override;
5221
5261
  const home = homeDir(env);
5222
- if (!home || !SAFE_APP_SLUG.test(name))
5262
+ return home ? join3(home, HASNA_HOME_DIR) : null;
5263
+ }
5264
+ function appConfigDir(name, env) {
5265
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
5266
+ if (configRoot)
5267
+ return join3(configRoot, name);
5268
+ const root = hasnaHomeDir(env);
5269
+ return root ? join3(root, name, CONFIG_SUBDIR) : null;
5270
+ }
5271
+ function credentialDiskSourceList(name, env, profile = null) {
5272
+ if (!SAFE_APP_SLUG.test(name))
5223
5273
  return [];
5224
- const stem = profile ? `${name}.${profile}` : name;
5225
- const configStem = profile ? `${name}-${profile}` : name;
5226
- return [
5227
- join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
5228
- join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
5229
- ];
5274
+ const directory = appConfigDir(name, env);
5275
+ if (!directory)
5276
+ return [];
5277
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
5278
+ return [{ path: join3(directory, file), tier: "disk" }];
5279
+ }
5280
+ function credentialDiskSources(name, env) {
5281
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
5282
+ }
5283
+ function profileDiskSources(name, env, profile) {
5284
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
5230
5285
  }
5231
5286
  function parseEnvFile(text) {
5232
5287
  const values = new Map;
5288
+ const unusable = new Set;
5233
5289
  for (const rawLine of text.split(/\r?\n/)) {
5234
5290
  const line = rawLine.trim();
5235
5291
  if (line.length === 0 || line.startsWith("#"))
@@ -5244,38 +5300,81 @@ function parseEnvFile(text) {
5244
5300
  let value = withoutExport.slice(equals + 1).trim();
5245
5301
  const quote = value[0];
5246
5302
  if (quote === '"' || quote === "'") {
5247
- if (value.length < 2 || !value.endsWith(quote))
5303
+ if (value.length < 2 || !value.endsWith(quote)) {
5304
+ unusable.add(key);
5248
5305
  continue;
5306
+ }
5249
5307
  value = value.slice(1, -1);
5250
5308
  }
5251
- if (value.length === 0)
5309
+ if (value.trim().length === 0) {
5310
+ unusable.add(key);
5252
5311
  continue;
5312
+ }
5313
+ if (values.has(key) && values.get(key) !== value)
5314
+ unusable.add(key);
5253
5315
  values.set(key, value);
5254
5316
  }
5255
- return values;
5317
+ return { values, unusable };
5318
+ }
5319
+ function configFileModeAllowed(mode) {
5320
+ const permissions = mode & 4095;
5321
+ return permissions === 256 || permissions === 384;
5322
+ }
5323
+ function configFileReadsCoherent(before, after) {
5324
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
5256
5325
  }
5257
5326
  function readAppConfigFile(path) {
5258
- let text;
5327
+ const unsafe = (reason) => {
5328
+ throw new CredentialFileUnsafeError(path, reason);
5329
+ };
5330
+ let fd = -1;
5259
5331
  try {
5260
- const stats = statSync2(path);
5261
- if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
5332
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5333
+ } catch (error) {
5334
+ const code = error.code;
5335
+ if (code === "ENOENT" || code === "ENOTDIR")
5262
5336
  return null;
5263
- text = readFileSync2(path, "utf8");
5264
- } catch {
5265
- return null;
5337
+ if (code === "ELOOP")
5338
+ unsafe("the path is a symlink");
5339
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
5340
+ }
5341
+ try {
5342
+ const before = fstatSync(fd);
5343
+ if (!before.isFile())
5344
+ unsafe("the path is not a regular file");
5345
+ if (!configFileModeAllowed(before.mode)) {
5346
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
5347
+ }
5348
+ const uid = process.getuid?.() ?? process.geteuid?.();
5349
+ if (uid !== undefined && before.uid !== uid)
5350
+ unsafe("the file is not owned by the current user");
5351
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
5352
+ unsafe("the file exceeds the size limit");
5353
+ const bytes = readFileSync2(fd);
5354
+ const after = fstatSync(fd);
5355
+ if (!configFileReadsCoherent(before, after)) {
5356
+ unsafe("the file changed while being read");
5357
+ }
5358
+ return parseEnvFile(bytes.toString("utf8"));
5359
+ } finally {
5360
+ if (fd !== -1)
5361
+ closeSync(fd);
5266
5362
  }
5267
- return parseEnvFile(text);
5268
5363
  }
5269
5364
  function readCredentialFile(path, apiKeyKeys) {
5270
- const values = readAppConfigFile(path);
5271
- if (!values)
5365
+ const parsed = readAppConfigFile(path);
5366
+ if (!parsed)
5272
5367
  return null;
5273
5368
  for (const key of apiKeyKeys) {
5274
- const value = values.get(key)?.trim();
5275
- if (value)
5276
- return value;
5369
+ if (parsed.unusable.has(key)) {
5370
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
5371
+ }
5277
5372
  }
5278
- return null;
5373
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
5374
+ if (new Set(values).size > 1) {
5375
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
5376
+ }
5377
+ return values[0] ?? null;
5279
5378
  }
5280
5379
  var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
5281
5380
  function appConfigDiskValue(name, env, keys) {
@@ -5283,11 +5382,19 @@ function appConfigDiskValue(name, env, keys) {
5283
5382
  if (wanted.length === 0)
5284
5383
  return null;
5285
5384
  for (const path of credentialDiskSources(name, env)) {
5286
- const values = readAppConfigFile(path);
5287
- if (!values)
5385
+ const parsed = readAppConfigFile(path);
5386
+ if (!parsed)
5288
5387
  continue;
5388
+ if (wanted.some((key) => parsed.unusable.has(key))) {
5389
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
5390
+ }
5391
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
5392
+ if (new Set(values).size > 1)
5393
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
5289
5394
  for (const key of wanted) {
5290
- const value = values.get(key)?.trim();
5395
+ if (parsed.unusable.has(key))
5396
+ return { key, value: "", path, unusable: true };
5397
+ const value = parsed.values.get(key)?.trim();
5291
5398
  if (value)
5292
5399
  return { key, value, path };
5293
5400
  }
@@ -5295,20 +5402,21 @@ function appConfigDiskValue(name, env, keys) {
5295
5402
  return null;
5296
5403
  }
5297
5404
  function assertUsableCredential(appName, source, value) {
5405
+ if (VAULT_POINTER_SHAPE.test(value)) {
5406
+ 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]);
5407
+ }
5298
5408
  if (!ILLEGAL_IN_HEADER_VALUE.test(value))
5299
5409
  return;
5300
5410
  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]);
5301
5411
  }
5302
5412
  var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
5303
5413
  var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
5304
- var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
5305
5414
  function sealCredential(fields) {
5306
5415
  const { apiKey } = fields;
5307
5416
  const visible = {
5308
5417
  tier: fields.tier,
5309
5418
  source: fields.source,
5310
5419
  deliberate: fields.deliberate,
5311
- deprecated: fields.deprecated,
5312
5420
  diskCandidates: Object.freeze([...fields.diskCandidates]),
5313
5421
  warning: fields.warning
5314
5422
  };
@@ -5319,6 +5427,14 @@ function sealCredential(fields) {
5319
5427
  writable: false,
5320
5428
  configurable: false
5321
5429
  });
5430
+ if (fields.pointerVaultKey !== undefined) {
5431
+ Object.defineProperty(sealed, "pointerVaultKey", {
5432
+ value: fields.pointerVaultKey,
5433
+ enumerable: false,
5434
+ writable: false,
5435
+ configurable: false
5436
+ });
5437
+ }
5322
5438
  Object.defineProperty(sealed, INSPECT_CUSTOM, {
5323
5439
  value: () => ({ ...visible, apiKey: "[redacted]" }),
5324
5440
  enumerable: false,
@@ -5333,198 +5449,1105 @@ function sealCredential(fields) {
5333
5449
  });
5334
5450
  return Object.freeze(sealed);
5335
5451
  }
5452
+ function firstEnvValue(env, keys) {
5453
+ for (const key of keys) {
5454
+ if (!Object.prototype.hasOwnProperty.call(env, key))
5455
+ continue;
5456
+ const value = env[key]?.trim();
5457
+ if (value)
5458
+ return { key, value };
5459
+ }
5460
+ return null;
5461
+ }
5462
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
5463
+ function isAmbientEnvironment(env) {
5464
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
5465
+ }
5466
+ function defaultKeychainRunner(argv) {
5467
+ const result = spawnSync2(KEYCHAIN_SECURITY_BIN, [...argv], {
5468
+ encoding: "utf8",
5469
+ stdio: ["ignore", "pipe", "pipe"],
5470
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
5471
+ });
5472
+ return {
5473
+ status: result.status,
5474
+ stdout: result.stdout ?? "",
5475
+ stderr: result.error ? result.error.message : result.stderr ?? ""
5476
+ };
5477
+ }
5478
+ function keychainTierEnabled(env, options) {
5479
+ if ((options.platform ?? process.platform) !== "darwin")
5480
+ return false;
5481
+ if (options.enabled !== undefined)
5482
+ return options.enabled;
5483
+ return options.run !== undefined || isAmbientEnvironment(env);
5484
+ }
5485
+ function keychainAccount(env, options) {
5486
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
5487
+ if (station)
5488
+ return station;
5489
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
5490
+ if (host)
5491
+ return host;
5492
+ const user = env.USER?.trim();
5493
+ return user || null;
5494
+ }
5495
+ function keychainFailureHint(text) {
5496
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
5497
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
5498
+ return clean ? `: ${clean}` : "";
5499
+ }
5500
+ function readKeychainItem(name, env, kind, options) {
5501
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
5502
+ return null;
5503
+ const account = keychainAccount(env, options);
5504
+ if (!account)
5505
+ return null;
5506
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
5507
+ const source = `keychain:${service}@${account}`;
5508
+ const run = options.run ?? defaultKeychainRunner;
5509
+ let result;
5510
+ try {
5511
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
5512
+ } catch (error) {
5513
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
5514
+ 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]);
5515
+ }
5516
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
5517
+ return null;
5518
+ if (result.status !== 0) {
5519
+ 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]);
5520
+ }
5521
+ const value = result.stdout.trim();
5522
+ if (!value) {
5523
+ 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]);
5524
+ }
5525
+ return { value, source };
5526
+ }
5527
+ function keychainConfigValue(name, env, options = {}) {
5528
+ return readKeychainItem(name, env, "api-url", options);
5529
+ }
5530
+ function snapshotClientEnvironment(name, env) {
5531
+ const keys = clientTransportEnvKeys(name);
5532
+ const ambient = isAmbientEnvironment(env);
5533
+ const snapshot = Object.create(null);
5534
+ for (const key of [
5535
+ ...keys.apiUrlKeys,
5536
+ ...keys.apiKeyKeys,
5537
+ credentialOverrideEnvKey(name),
5538
+ credentialPointerEnvKey(name),
5539
+ CREDENTIAL_PROFILE_ENV_KEY,
5540
+ "HOME",
5541
+ HASNA_HOME_ENV_KEY,
5542
+ HASNA_CONFIG_HOME_ENV_KEY,
5543
+ KEYCHAIN_STATION_ENV_KEY,
5544
+ "USER"
5545
+ ]) {
5546
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
5547
+ if (!descriptor)
5548
+ continue;
5549
+ if (!("value" in descriptor)) {
5550
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
5551
+ }
5552
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
5553
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
5554
+ }
5555
+ snapshot[key] = descriptor.value;
5556
+ }
5557
+ if (ambient) {
5558
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
5559
+ value: true,
5560
+ enumerable: false,
5561
+ writable: false,
5562
+ configurable: false
5563
+ });
5564
+ }
5565
+ return Object.freeze(snapshot);
5566
+ }
5567
+ function resolveCredential(name, env, options = {}) {
5568
+ env = snapshotClientEnvironment(name, env);
5569
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
5570
+ const diskPaths = credentialDiskSources(name, env);
5571
+ if (options.apiKey !== undefined) {
5572
+ const explicitKey = options.apiKey.trim();
5573
+ if (!explicitKey) {
5574
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
5575
+ }
5576
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
5577
+ return sealCredential({
5578
+ apiKey: explicitKey,
5579
+ tier: "argument",
5580
+ source: "explicit apiKey argument",
5581
+ deliberate: true,
5582
+ diskCandidates: diskPaths,
5583
+ warning: null
5584
+ });
5585
+ }
5586
+ const overrideKeyName = credentialOverrideEnvKey(name);
5587
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
5588
+ if (overrideRaw !== undefined) {
5589
+ const override = overrideRaw.trim();
5590
+ if (!override) {
5591
+ throw new CredentialResolutionError(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]);
5592
+ }
5593
+ assertUsableCredential(name, overrideKeyName, override);
5594
+ return sealCredential({
5595
+ apiKey: override,
5596
+ tier: "override",
5597
+ source: overrideKeyName,
5598
+ deliberate: true,
5599
+ diskCandidates: diskPaths,
5600
+ warning: null
5601
+ });
5602
+ }
5603
+ const pointerKeyName = credentialPointerEnvKey(name);
5604
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
5605
+ if (pointerRaw !== undefined) {
5606
+ const pointer = pointerRaw.trim();
5607
+ if (!pointer) {
5608
+ 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]);
5609
+ }
5610
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
5611
+ 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]);
5612
+ }
5613
+ return sealCredential({
5614
+ apiKey: "",
5615
+ pointerVaultKey: pointer,
5616
+ tier: "pointer",
5617
+ source: pointerKeyName,
5618
+ deliberate: true,
5619
+ diskCandidates: diskPaths,
5620
+ warning: null
5621
+ });
5622
+ }
5623
+ if (options.profile !== undefined && !options.profile.trim()) {
5624
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
5625
+ }
5626
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
5627
+ if (profileRaw !== undefined && !profileRaw.trim()) {
5628
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
5629
+ }
5630
+ const profile = options.profile?.trim() || profileRaw?.trim();
5631
+ if (profile) {
5632
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
5633
+ if (!SAFE_PROFILE.test(profile)) {
5634
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
5635
+ }
5636
+ const paths = profileDiskSources(name, env, profile);
5637
+ for (const path of paths) {
5638
+ const value = readCredentialFile(path, apiKeyKeys);
5639
+ if (value) {
5640
+ assertUsableCredential(name, path, value);
5641
+ return sealCredential({
5642
+ apiKey: value,
5643
+ tier: "profile",
5644
+ source: path,
5645
+ deliberate: true,
5646
+ diskCandidates: paths,
5647
+ warning: null
5648
+ });
5649
+ }
5650
+ }
5651
+ 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);
5652
+ }
5653
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
5654
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
5655
+ if (blankEnv) {
5656
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
5657
+ }
5658
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
5659
+ 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));
5660
+ }
5661
+ const envHit = firstEnvValue(env, apiKeyKeys);
5662
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
5663
+ if (keychainHit) {
5664
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
5665
+ 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;
5666
+ return sealCredential({
5667
+ apiKey: keychainHit.value,
5668
+ tier: "keychain",
5669
+ source: keychainHit.source,
5670
+ deliberate: false,
5671
+ diskCandidates: diskPaths,
5672
+ warning
5673
+ });
5674
+ }
5675
+ const diskSourceList = credentialDiskSourceList(name, env, null);
5676
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
5677
+ if (diskHits.length > 0) {
5678
+ const winner = diskHits[0];
5679
+ assertUsableCredential(name, winner.src.path, winner.value);
5680
+ const divergentSources = [
5681
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
5682
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
5683
+ ];
5684
+ 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;
5685
+ return sealCredential({
5686
+ apiKey: winner.value,
5687
+ tier: winner.src.tier,
5688
+ source: winner.src.path,
5689
+ deliberate: false,
5690
+ diskCandidates: diskPaths,
5691
+ warning
5692
+ });
5693
+ }
5694
+ if (envHit) {
5695
+ assertUsableCredential(name, envHit.key, envHit.value);
5696
+ return sealCredential({
5697
+ apiKey: envHit.value,
5698
+ tier: "env",
5699
+ source: envHit.key,
5700
+ deliberate: false,
5701
+ diskCandidates: diskPaths,
5702
+ warning: null
5703
+ });
5704
+ }
5705
+ return null;
5706
+ }
5707
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
5708
+ var requireSecretsSdk = createRequire(import.meta.url);
5709
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
5710
+ var DEFAULT_AUTHORITY_SOURCE = "default";
5711
+ function defaultFleetGatewayBaseUrl(name) {
5712
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
5713
+ }
5714
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
5715
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
5716
+ function isValidDnsDomain(value) {
5717
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
5718
+ return false;
5719
+ }
5720
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
5721
+ }
5722
+ function validateAppSlug(name) {
5723
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
5724
+ throw new Error("App name must be one lowercase DNS label.");
5725
+ }
5726
+ return name;
5727
+ }
5728
+ function rawAuthority(value) {
5729
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
5730
+ if (!match)
5731
+ throw new Error("API URL must be absolute.");
5732
+ const afterScheme = value.slice(match[0].length);
5733
+ const boundary = afterScheme.search(/[/?#]/);
5734
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
5735
+ if (!authority)
5736
+ throw new Error("API URL must include a hostname.");
5737
+ return authority;
5738
+ }
5739
+ function assertCanonicalPort(port) {
5740
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
5741
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
5742
+ }
5743
+ const numericPort = Number(port);
5744
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
5745
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
5746
+ }
5747
+ }
5748
+ function canonicalAuthorityHostname(authority) {
5749
+ let rawHostname;
5750
+ if (authority.startsWith("[")) {
5751
+ const closingBracket = authority.indexOf("]");
5752
+ if (closingBracket === -1) {
5753
+ throw new Error("API URL authority must contain a canonical hostname.");
5754
+ }
5755
+ rawHostname = authority.slice(0, closingBracket + 1);
5756
+ const portSuffix = authority.slice(closingBracket + 1);
5757
+ if (portSuffix) {
5758
+ if (!portSuffix.startsWith(":")) {
5759
+ throw new Error("API URL authority must contain a canonical hostname and port.");
5760
+ }
5761
+ assertCanonicalPort(portSuffix.slice(1));
5762
+ }
5763
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
5764
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
5765
+ }
5766
+ } else {
5767
+ const firstColon = authority.indexOf(":");
5768
+ const lastColon = authority.lastIndexOf(":");
5769
+ if (firstColon !== lastColon) {
5770
+ throw new Error("IPv6 API URL authorities must use brackets.");
5771
+ }
5772
+ if (lastColon !== -1) {
5773
+ const port = authority.slice(lastColon + 1);
5774
+ assertCanonicalPort(port);
5775
+ rawHostname = authority.slice(0, lastColon);
5776
+ } else {
5777
+ rawHostname = authority;
5778
+ }
5779
+ const ipVersion = isIP(rawHostname);
5780
+ const numericAddressParts = rawHostname.split(".");
5781
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
5782
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
5783
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
5784
+ }
5785
+ }
5786
+ return rawHostname.toLowerCase();
5787
+ }
5788
+ function isDeliberateLoopbackHttpAuthority(authority) {
5789
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
5790
+ }
5791
+ function toV1BaseUrl(apiUrl) {
5792
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
5793
+ throw new Error("API URL must not contain ASCII control characters.");
5794
+ }
5795
+ const input = apiUrl.trim();
5796
+ const authority = rawAuthority(input);
5797
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
5798
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
5799
+ }
5800
+ const canonicalHostname = canonicalAuthorityHostname(authority);
5801
+ const url = new URL(input);
5802
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
5803
+ throw new Error("API URL must use http or https.");
5804
+ }
5805
+ if (url.username || url.password) {
5806
+ throw new Error("API URL must not include credentials.");
5807
+ }
5808
+ if (!url.hostname || url.hostname.endsWith(".")) {
5809
+ throw new Error("API URL must include a canonical hostname.");
5810
+ }
5811
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
5812
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
5813
+ }
5814
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
5815
+ throw new Error("API URL must not use IDN or punycode hostnames.");
5816
+ }
5817
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
5818
+ throw new Error("API URL may use http only for an exact loopback authority.");
5819
+ }
5820
+ if (url.search || url.hash) {
5821
+ throw new Error("API URL must not include a query string or fragment.");
5822
+ }
5823
+ let path = url.pathname.replace(/\/+$/, "");
5824
+ if (path.endsWith("/v1"))
5825
+ path = path.slice(0, -"/v1".length);
5826
+ url.pathname = `${path}/v1`;
5827
+ return url.toString().replace(/\/+$/, "");
5828
+ }
5829
+ class ClientTransportConfigurationError extends Error {
5830
+ appName;
5831
+ sources;
5832
+ constructor(appName, message, sources = []) {
5833
+ super(message);
5834
+ this.name = "ClientTransportConfigurationError";
5835
+ this.appName = appName;
5836
+ this.sources = Object.freeze([...sources]);
5837
+ }
5838
+ }
5839
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
5840
+ env = snapshotClientEnvironment(name, env);
5841
+ const keys = clientTransportEnvKeys(name);
5842
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
5843
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
5844
+ if (blankUrl) {
5845
+ 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]);
5846
+ }
5847
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
5848
+ if (controlledUrl) {
5849
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
5850
+ }
5851
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
5852
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
5853
+ 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));
5854
+ }
5855
+ const envUrlHit = usableUrlEntries[0] ?? null;
5856
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
5857
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
5858
+ if (diskConfigUrlHit?.unusable) {
5859
+ 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]);
5860
+ }
5861
+ const urlCandidates = [
5862
+ ...envUrlHit ? [envUrlHit] : [],
5863
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
5864
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
5865
+ ];
5866
+ const configuredUrl = urlCandidates[0] ?? null;
5867
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
5868
+ if (configuredUrl && divergentUrls.length > 0) {
5869
+ 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));
5870
+ }
5871
+ const warnings = [];
5872
+ if (configuredUrl && !envUrlHit) {
5873
+ 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.`);
5874
+ }
5875
+ const credential = resolveCredential(name, env, options.credentials);
5876
+ if (!credential) {
5877
+ const diskHint = credentialDiskSourcesForMessage(name, env);
5878
+ 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`;
5879
+ 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.`);
5880
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
5881
+ }
5882
+ if (credential.warning)
5883
+ warnings.push(credential.warning);
5884
+ let urlHit;
5885
+ if (configuredUrl) {
5886
+ urlHit = configuredUrl;
5887
+ } else {
5888
+ try {
5889
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
5890
+ } catch (error) {
5891
+ const message = error instanceof Error ? error.message : String(error);
5892
+ 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]]);
5893
+ }
5894
+ }
5895
+ const apiUrlSource = urlHit.key;
5896
+ let baseUrl;
5897
+ try {
5898
+ baseUrl = toV1BaseUrl(urlHit.value);
5899
+ } catch (error) {
5900
+ const message = error instanceof Error ? error.message : String(error);
5901
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
5902
+ }
5903
+ return {
5904
+ resolution: {
5905
+ transport: "http",
5906
+ transportSource: urlHit.key,
5907
+ baseUrl,
5908
+ apiUrlSource,
5909
+ apiKeyPresent: true,
5910
+ apiKeySource: credential.source,
5911
+ apiKeyTier: credential.tier,
5912
+ misconfigured: false,
5913
+ warning: warnings.length > 0 ? warnings.join(" ") : null
5914
+ },
5915
+ credential
5916
+ };
5917
+ }
5918
+ function resolveClientTransport(name, env = process.env, options = {}) {
5919
+ return resolveClientTransportSnapshot(name, env, options).resolution;
5920
+ }
5921
+ function credentialDiskSourcesForMessage(name, env) {
5922
+ const paths = credentialDiskSources(name, env);
5923
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
5924
+ }
5925
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
5926
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
5927
+ "host",
5928
+ ":authority",
5929
+ "forwarded",
5930
+ "x-forwarded-host",
5931
+ "x-original-host"
5932
+ ]);
5933
+
5934
+ // ../contracts/dist/client/storage.js
5935
+ import { isIP as isIP2 } from "net";
5936
+ import { spawnSync as spawnSync3 } from "child_process";
5937
+ import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync3 } from "fs";
5938
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
5939
+ import { createRequire as createRequire2 } from "module";
5940
+ import { hostname as osHostname2 } from "os";
5941
+ import { isAbsolute as isAbsolute3, join as join4 } from "path";
5942
+ function envToken2(name) {
5943
+ return name.toUpperCase().replace(/-/g, "_");
5944
+ }
5945
+ function clientTransportEnvKeys2(name) {
5946
+ const envSegment = envToken2(name);
5947
+ return {
5948
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
5949
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
5950
+ };
5951
+ }
5952
+ function credentialOverrideEnvKey2(name) {
5953
+ return `HASNA_${envToken2(name)}_API_KEY_OVERRIDE`;
5954
+ }
5955
+ var CREDENTIAL_PROFILE_ENV_KEY2 = "HASNA_PROFILE";
5956
+ function credentialPointerEnvKey2(name) {
5957
+ return `HASNA_${envToken2(name)}_API_KEY_REF`;
5958
+ }
5959
+
5960
+ class CredentialResolutionError2 extends Error {
5961
+ appName;
5962
+ attempted;
5963
+ constructor(appName, message, attempted) {
5964
+ super(message);
5965
+ this.name = "CredentialResolutionError";
5966
+ this.appName = appName;
5967
+ this.attempted = attempted;
5968
+ }
5969
+ }
5970
+
5971
+ class CredentialFileUnsafeError2 extends Error {
5972
+ path;
5973
+ constructor(path, reason) {
5974
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
5975
+ this.name = "CredentialFileUnsafeError";
5976
+ this.path = path;
5977
+ }
5978
+ }
5979
+ var HASNA_HOME_ENV_KEY2 = "HASNA_HOME";
5980
+ var HASNA_CONFIG_HOME_ENV_KEY2 = "HASNA_CONFIG_HOME";
5981
+ var KEYCHAIN_STATION_ENV_KEY2 = "HASNA_STATION";
5982
+ var HASNA_HOME_DIR2 = ".hasna";
5983
+ var CONFIG_SUBDIR2 = "config";
5984
+ var CREDENTIALS_FILE2 = "credentials";
5985
+ var KEYCHAIN_SECURITY_BIN2 = "/usr/bin/security";
5986
+ var KEYCHAIN_SERVICE_PREFIX2 = "hasna.credentials";
5987
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS2 = 44;
5988
+ var KEYCHAIN_SPAWN_TIMEOUT_MS2 = 1e4;
5989
+ var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
5990
+ var SAFE_APP_SLUG2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
5991
+ var SAFE_PROFILE2 = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
5992
+ var ILLEGAL_IN_HEADER_VALUE2 = /[^\t\x20-\x7e]/;
5993
+ var VAULT_POINTER_SHAPE2 = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
5994
+ function homeDir2(env) {
5995
+ const home = env.HOME?.trim();
5996
+ return home ? home : null;
5997
+ }
5998
+ function absoluteOverride2(env, key) {
5999
+ const value = env[key]?.trim();
6000
+ return value && isAbsolute3(value) ? value : null;
6001
+ }
6002
+ function hasnaHomeDir2(env) {
6003
+ const override = absoluteOverride2(env, HASNA_HOME_ENV_KEY2);
6004
+ if (override)
6005
+ return override;
6006
+ const home = homeDir2(env);
6007
+ return home ? join4(home, HASNA_HOME_DIR2) : null;
6008
+ }
6009
+ function appConfigDir2(name, env) {
6010
+ const configRoot = absoluteOverride2(env, HASNA_CONFIG_HOME_ENV_KEY2);
6011
+ if (configRoot)
6012
+ return join4(configRoot, name);
6013
+ const root = hasnaHomeDir2(env);
6014
+ return root ? join4(root, name, CONFIG_SUBDIR2) : null;
6015
+ }
6016
+ function credentialDiskSourceList2(name, env, profile = null) {
6017
+ if (!SAFE_APP_SLUG2.test(name))
6018
+ return [];
6019
+ const directory = appConfigDir2(name, env);
6020
+ if (!directory)
6021
+ return [];
6022
+ const file = profile ? `${CREDENTIALS_FILE2}-${profile}` : CREDENTIALS_FILE2;
6023
+ return [{ path: join4(directory, file), tier: "disk" }];
6024
+ }
6025
+ function credentialDiskSources2(name, env) {
6026
+ return credentialDiskSourceList2(name, env, null).map((s) => s.path);
6027
+ }
6028
+ function profileDiskSources2(name, env, profile) {
6029
+ return credentialDiskSourceList2(name, env, profile).map((s) => s.path);
6030
+ }
6031
+ function parseEnvFile2(text) {
6032
+ const values = new Map;
6033
+ const unusable = new Set;
6034
+ for (const rawLine of text.split(/\r?\n/)) {
6035
+ const line = rawLine.trim();
6036
+ if (line.length === 0 || line.startsWith("#"))
6037
+ continue;
6038
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
6039
+ const equals = withoutExport.indexOf("=");
6040
+ if (equals <= 0)
6041
+ continue;
6042
+ const key = withoutExport.slice(0, equals).trim();
6043
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
6044
+ continue;
6045
+ let value = withoutExport.slice(equals + 1).trim();
6046
+ const quote = value[0];
6047
+ if (quote === '"' || quote === "'") {
6048
+ if (value.length < 2 || !value.endsWith(quote)) {
6049
+ unusable.add(key);
6050
+ continue;
6051
+ }
6052
+ value = value.slice(1, -1);
6053
+ }
6054
+ if (value.trim().length === 0) {
6055
+ unusable.add(key);
6056
+ continue;
6057
+ }
6058
+ if (values.has(key) && values.get(key) !== value)
6059
+ unusable.add(key);
6060
+ values.set(key, value);
6061
+ }
6062
+ return { values, unusable };
6063
+ }
6064
+ function configFileModeAllowed2(mode) {
6065
+ const permissions = mode & 4095;
6066
+ return permissions === 256 || permissions === 384;
6067
+ }
6068
+ function configFileReadsCoherent2(before, after) {
6069
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
6070
+ }
6071
+ function readAppConfigFile2(path) {
6072
+ const unsafe = (reason) => {
6073
+ throw new CredentialFileUnsafeError2(path, reason);
6074
+ };
6075
+ let fd = -1;
6076
+ try {
6077
+ fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
6078
+ } catch (error) {
6079
+ const code = error.code;
6080
+ if (code === "ENOENT" || code === "ENOTDIR")
6081
+ return null;
6082
+ if (code === "ELOOP")
6083
+ unsafe("the path is a symlink");
6084
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
6085
+ }
6086
+ try {
6087
+ const before = fstatSync2(fd);
6088
+ if (!before.isFile())
6089
+ unsafe("the path is not a regular file");
6090
+ if (!configFileModeAllowed2(before.mode)) {
6091
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
6092
+ }
6093
+ const uid = process.getuid?.() ?? process.geteuid?.();
6094
+ if (uid !== undefined && before.uid !== uid)
6095
+ unsafe("the file is not owned by the current user");
6096
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
6097
+ unsafe("the file exceeds the size limit");
6098
+ const bytes = readFileSync3(fd);
6099
+ const after = fstatSync2(fd);
6100
+ if (!configFileReadsCoherent2(before, after)) {
6101
+ unsafe("the file changed while being read");
6102
+ }
6103
+ return parseEnvFile2(bytes.toString("utf8"));
6104
+ } finally {
6105
+ if (fd !== -1)
6106
+ closeSync2(fd);
6107
+ }
6108
+ }
6109
+ function readCredentialFile2(path, apiKeyKeys) {
6110
+ const parsed = readAppConfigFile2(path);
6111
+ if (!parsed)
6112
+ return null;
6113
+ for (const key of apiKeyKeys) {
6114
+ if (parsed.unusable.has(key)) {
6115
+ throw new CredentialFileUnsafeError2(path, `${key} is declared but blank or malformed`);
6116
+ }
6117
+ }
6118
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
6119
+ if (new Set(values).size > 1) {
6120
+ throw new CredentialFileUnsafeError2(path, "credential aliases disagree");
6121
+ }
6122
+ return values[0] ?? null;
6123
+ }
6124
+ var CREDENTIAL_SHAPED_KEY2 = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
6125
+ function appConfigDiskValue2(name, env, keys) {
6126
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY2.test(key));
6127
+ if (wanted.length === 0)
6128
+ return null;
6129
+ for (const path of credentialDiskSources2(name, env)) {
6130
+ const parsed = readAppConfigFile2(path);
6131
+ if (!parsed)
6132
+ continue;
6133
+ if (wanted.some((key) => parsed.unusable.has(key))) {
6134
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
6135
+ }
6136
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
6137
+ if (new Set(values).size > 1)
6138
+ throw new CredentialFileUnsafeError2(path, "configuration aliases disagree");
6139
+ for (const key of wanted) {
6140
+ if (parsed.unusable.has(key))
6141
+ return { key, value: "", path, unusable: true };
6142
+ const value = parsed.values.get(key)?.trim();
6143
+ if (value)
6144
+ return { key, value, path };
6145
+ }
6146
+ }
6147
+ return null;
6148
+ }
6149
+ function assertUsableCredential2(appName, source, value) {
6150
+ if (VAULT_POINTER_SHAPE2.test(value)) {
6151
+ 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]);
6152
+ }
6153
+ if (!ILLEGAL_IN_HEADER_VALUE2.test(value))
6154
+ return;
6155
+ 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]);
6156
+ }
6157
+ var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
6158
+ var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
6159
+ var CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider";
6160
+ function sealCredential2(fields) {
6161
+ const { apiKey } = fields;
6162
+ const visible = {
6163
+ tier: fields.tier,
6164
+ source: fields.source,
6165
+ deliberate: fields.deliberate,
6166
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
6167
+ warning: fields.warning
6168
+ };
6169
+ const sealed = { ...visible };
6170
+ Object.defineProperty(sealed, "apiKey", {
6171
+ value: apiKey,
6172
+ enumerable: false,
6173
+ writable: false,
6174
+ configurable: false
6175
+ });
6176
+ if (fields.pointerVaultKey !== undefined) {
6177
+ Object.defineProperty(sealed, "pointerVaultKey", {
6178
+ value: fields.pointerVaultKey,
6179
+ enumerable: false,
6180
+ writable: false,
6181
+ configurable: false
6182
+ });
6183
+ }
6184
+ Object.defineProperty(sealed, INSPECT_CUSTOM2, {
6185
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
6186
+ enumerable: false,
6187
+ writable: false,
6188
+ configurable: false
6189
+ });
6190
+ Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
6191
+ value: true,
6192
+ enumerable: false,
6193
+ writable: false,
6194
+ configurable: false
6195
+ });
6196
+ return Object.freeze(sealed);
6197
+ }
5336
6198
  function isSealedCredential(credential) {
5337
- return credential[CREDENTIAL_SEAL] === true;
6199
+ return credential[CREDENTIAL_SEAL2] === true;
5338
6200
  }
5339
6201
  function explicitCredential(appName, apiKey) {
5340
6202
  const source = "explicit apiKey option";
5341
- assertUsableCredential(appName, source, apiKey);
5342
- return sealCredential({
6203
+ assertUsableCredential2(appName, source, apiKey);
6204
+ return sealCredential2({
5343
6205
  apiKey,
5344
6206
  tier: "argument",
5345
6207
  source,
5346
6208
  deliberate: true,
5347
- deprecated: false,
5348
6209
  diskCandidates: [],
5349
6210
  warning: null
5350
6211
  });
5351
6212
  }
5352
6213
  function validateAndSealResolvedCredential(appName, credential) {
5353
6214
  const apiKey = credential.apiKey;
5354
- assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
6215
+ assertUsableCredential2(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
5355
6216
  if (!isSealedCredential(credential)) {
5356
- return sealCredential({
6217
+ return sealCredential2({
5357
6218
  apiKey,
5358
6219
  tier: "argument",
5359
6220
  source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
5360
6221
  deliberate: true,
5361
- deprecated: false,
5362
6222
  diskCandidates: [],
5363
6223
  warning: null
5364
6224
  });
5365
6225
  }
5366
- return sealCredential({
6226
+ return sealCredential2({
5367
6227
  apiKey,
5368
6228
  tier: credential.tier,
5369
6229
  source: credential.source,
5370
6230
  deliberate: credential.deliberate,
5371
- deprecated: credential.deprecated,
5372
6231
  diskCandidates: credential.diskCandidates,
5373
- warning: credential.warning
6232
+ warning: credential.warning,
6233
+ ...credential.pointerVaultKey !== undefined ? { pointerVaultKey: credential.pointerVaultKey } : {}
5374
6234
  });
5375
6235
  }
5376
- function firstEnvValue(env, keys) {
6236
+ function firstEnvValue2(env, keys) {
5377
6237
  for (const key of keys) {
6238
+ if (!Object.prototype.hasOwnProperty.call(env, key))
6239
+ continue;
5378
6240
  const value = env[key]?.trim();
5379
6241
  if (value)
5380
6242
  return { key, value };
5381
6243
  }
5382
- return null;
5383
- }
5384
- var DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
5385
- function deprecationNotified() {
5386
- const host = globalThis;
5387
- const existing = host[DEPRECATION_REGISTRY];
5388
- if (existing instanceof Set)
5389
- return existing;
5390
- const created = new Set;
5391
- host[DEPRECATION_REGISTRY] = created;
5392
- return created;
5393
- }
5394
- function defaultDeprecationSink(message) {
5395
- if (typeof process !== "undefined" && process.stderr) {
5396
- process.stderr.write(`${message}
5397
- `);
6244
+ return null;
6245
+ }
6246
+ var AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
6247
+ function isAmbientEnvironment2(env) {
6248
+ return env === process.env || env[AMBIENT_ENVIRONMENT2] === true;
6249
+ }
6250
+ function defaultKeychainRunner2(argv) {
6251
+ const result = spawnSync3(KEYCHAIN_SECURITY_BIN2, [...argv], {
6252
+ encoding: "utf8",
6253
+ stdio: ["ignore", "pipe", "pipe"],
6254
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS2
6255
+ });
6256
+ return {
6257
+ status: result.status,
6258
+ stdout: result.stdout ?? "",
6259
+ stderr: result.error ? result.error.message : result.stderr ?? ""
6260
+ };
6261
+ }
6262
+ function keychainTierEnabled2(env, options) {
6263
+ if ((options.platform ?? process.platform) !== "darwin")
6264
+ return false;
6265
+ if (options.enabled !== undefined)
6266
+ return options.enabled;
6267
+ return options.run !== undefined || isAmbientEnvironment2(env);
6268
+ }
6269
+ function keychainAccount2(env, options) {
6270
+ const station = env[KEYCHAIN_STATION_ENV_KEY2]?.trim();
6271
+ if (station)
6272
+ return station;
6273
+ const host = (options.hostname ?? osHostname2)().split(".")[0]?.trim() ?? "";
6274
+ if (host)
6275
+ return host;
6276
+ const user = env.USER?.trim();
6277
+ return user || null;
6278
+ }
6279
+ function keychainFailureHint2(text) {
6280
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
6281
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
6282
+ return clean ? `: ${clean}` : "";
6283
+ }
6284
+ function readKeychainItem2(name, env, kind, options) {
6285
+ if (!SAFE_APP_SLUG2.test(name) || !keychainTierEnabled2(env, options))
6286
+ return null;
6287
+ const account = keychainAccount2(env, options);
6288
+ if (!account)
6289
+ return null;
6290
+ const service = `${KEYCHAIN_SERVICE_PREFIX2}.${name}.${kind}`;
6291
+ const source = `keychain:${service}@${account}`;
6292
+ const run = options.run ?? defaultKeychainRunner2;
6293
+ let result;
6294
+ try {
6295
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
6296
+ } catch (error) {
6297
+ const reason = keychainFailureHint2(error instanceof Error ? error.message : String(error));
6298
+ 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]);
6299
+ }
6300
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS2)
6301
+ return null;
6302
+ if (result.status !== 0) {
6303
+ 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]);
6304
+ }
6305
+ const value = result.stdout.trim();
6306
+ if (!value) {
6307
+ 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]);
6308
+ }
6309
+ return { value, source };
6310
+ }
6311
+ function keychainConfigValue2(name, env, options = {}) {
6312
+ return readKeychainItem2(name, env, "api-url", options);
6313
+ }
6314
+ function snapshotClientEnvironment2(name, env) {
6315
+ const keys = clientTransportEnvKeys2(name);
6316
+ const ambient = isAmbientEnvironment2(env);
6317
+ const snapshot = Object.create(null);
6318
+ for (const key of [
6319
+ ...keys.apiUrlKeys,
6320
+ ...keys.apiKeyKeys,
6321
+ credentialOverrideEnvKey2(name),
6322
+ credentialPointerEnvKey2(name),
6323
+ CREDENTIAL_PROFILE_ENV_KEY2,
6324
+ "HOME",
6325
+ HASNA_HOME_ENV_KEY2,
6326
+ HASNA_CONFIG_HOME_ENV_KEY2,
6327
+ KEYCHAIN_STATION_ENV_KEY2,
6328
+ "USER"
6329
+ ]) {
6330
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
6331
+ if (!descriptor)
6332
+ continue;
6333
+ if (!("value" in descriptor)) {
6334
+ throw new CredentialResolutionError2(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
6335
+ }
6336
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
6337
+ throw new CredentialResolutionError2(name, `${key} must be a string data property.`, [key]);
6338
+ }
6339
+ snapshot[key] = descriptor.value;
6340
+ }
6341
+ if (ambient) {
6342
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT2, {
6343
+ value: true,
6344
+ enumerable: false,
6345
+ writable: false,
6346
+ configurable: false
6347
+ });
5398
6348
  }
6349
+ return Object.freeze(snapshot);
5399
6350
  }
5400
- function resolveCredential(name, env, options = {}) {
5401
- const { apiKeyKeys } = clientTransportEnvKeys(name);
5402
- const diskPaths = credentialDiskSources(name, env);
5403
- const explicitKey = options.apiKey?.trim();
5404
- if (explicitKey) {
5405
- assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
5406
- return sealCredential({
6351
+ function resolveCredential2(name, env, options = {}) {
6352
+ env = snapshotClientEnvironment2(name, env);
6353
+ const { apiKeyKeys } = clientTransportEnvKeys2(name);
6354
+ const diskPaths = credentialDiskSources2(name, env);
6355
+ if (options.apiKey !== undefined) {
6356
+ const explicitKey = options.apiKey.trim();
6357
+ if (!explicitKey) {
6358
+ throw new CredentialResolutionError2(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
6359
+ }
6360
+ assertUsableCredential2(name, "the explicit apiKey argument", explicitKey);
6361
+ return sealCredential2({
5407
6362
  apiKey: explicitKey,
5408
6363
  tier: "argument",
5409
6364
  source: "explicit apiKey argument",
5410
6365
  deliberate: true,
5411
- deprecated: false,
5412
6366
  diskCandidates: diskPaths,
5413
6367
  warning: null
5414
6368
  });
5415
6369
  }
5416
- const overrideKeyName = credentialOverrideEnvKey(name);
5417
- const overrideRaw = env[overrideKeyName];
6370
+ const overrideKeyName = credentialOverrideEnvKey2(name);
6371
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
5418
6372
  if (overrideRaw !== undefined) {
5419
6373
  const override = overrideRaw.trim();
5420
6374
  if (!override) {
5421
- throw new CredentialResolutionError(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]);
6375
+ 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]);
5422
6376
  }
5423
- assertUsableCredential(name, overrideKeyName, override);
5424
- return sealCredential({
6377
+ assertUsableCredential2(name, overrideKeyName, override);
6378
+ return sealCredential2({
5425
6379
  apiKey: override,
5426
6380
  tier: "override",
5427
6381
  source: overrideKeyName,
5428
6382
  deliberate: true,
5429
- deprecated: false,
5430
6383
  diskCandidates: diskPaths,
5431
6384
  warning: null
5432
6385
  });
5433
6386
  }
5434
- const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
6387
+ const pointerKeyName = credentialPointerEnvKey2(name);
6388
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
6389
+ if (pointerRaw !== undefined) {
6390
+ const pointer = pointerRaw.trim();
6391
+ if (!pointer) {
6392
+ 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]);
6393
+ }
6394
+ if (!VAULT_POINTER_SHAPE2.test(pointer)) {
6395
+ 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]);
6396
+ }
6397
+ return sealCredential2({
6398
+ apiKey: "",
6399
+ pointerVaultKey: pointer,
6400
+ tier: "pointer",
6401
+ source: pointerKeyName,
6402
+ deliberate: true,
6403
+ diskCandidates: diskPaths,
6404
+ warning: null
6405
+ });
6406
+ }
6407
+ if (options.profile !== undefined && !options.profile.trim()) {
6408
+ throw new CredentialResolutionError2(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
6409
+ }
6410
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY2) ? env[CREDENTIAL_PROFILE_ENV_KEY2] : undefined;
6411
+ if (profileRaw !== undefined && !profileRaw.trim()) {
6412
+ throw new CredentialResolutionError2(name, `${CREDENTIAL_PROFILE_ENV_KEY2} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY2]);
6413
+ }
6414
+ const profile = options.profile?.trim() || profileRaw?.trim();
5435
6415
  if (profile) {
5436
- const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
5437
- if (!SAFE_PROFILE.test(profile)) {
5438
- throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
6416
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY2;
6417
+ if (!SAFE_PROFILE2.test(profile)) {
6418
+ throw new CredentialResolutionError2(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
5439
6419
  }
5440
- const paths = profileDiskSources(name, env, profile);
6420
+ const paths = profileDiskSources2(name, env, profile);
5441
6421
  for (const path of paths) {
5442
- const value = readCredentialFile(path, apiKeyKeys);
6422
+ const value = readCredentialFile2(path, apiKeyKeys);
5443
6423
  if (value) {
5444
- assertUsableCredential(name, path, value);
5445
- return sealCredential({
6424
+ assertUsableCredential2(name, path, value);
6425
+ return sealCredential2({
5446
6426
  apiKey: value,
5447
6427
  tier: "profile",
5448
6428
  source: path,
5449
6429
  deliberate: true,
5450
- deprecated: false,
5451
6430
  diskCandidates: paths,
5452
6431
  warning: null
5453
6432
  });
5454
6433
  }
5455
6434
  }
5456
- 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);
6435
+ 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);
6436
+ }
6437
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
6438
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
6439
+ if (blankEnv) {
6440
+ throw new CredentialResolutionError2(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
6441
+ }
6442
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
6443
+ 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));
6444
+ }
6445
+ const envHit = firstEnvValue2(env, apiKeyKeys);
6446
+ const keychainHit = readKeychainItem2(name, env, "api-key", options.keychain ?? {});
6447
+ if (keychainHit) {
6448
+ assertUsableCredential2(name, keychainHit.source, keychainHit.value);
6449
+ 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;
6450
+ return sealCredential2({
6451
+ apiKey: keychainHit.value,
6452
+ tier: "keychain",
6453
+ source: keychainHit.source,
6454
+ deliberate: false,
6455
+ diskCandidates: diskPaths,
6456
+ warning
6457
+ });
5457
6458
  }
5458
- const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
6459
+ const diskSourceList = credentialDiskSourceList2(name, env, null);
6460
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile2(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
5459
6461
  if (diskHits.length > 0) {
5460
6462
  const winner = diskHits[0];
5461
- assertUsableCredential(name, winner.path, winner.value);
6463
+ assertUsableCredential2(name, winner.src.path, winner.value);
5462
6464
  const divergentSources = [
5463
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
5464
- ...(() => {
5465
- const legacyHit = firstEnvValue(env, apiKeyKeys);
5466
- return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
5467
- })()
6465
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
6466
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
5468
6467
  ];
5469
- 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;
5470
- return sealCredential({
6468
+ 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;
6469
+ return sealCredential2({
5471
6470
  apiKey: winner.value,
5472
- tier: "disk",
5473
- source: winner.path,
6471
+ tier: winner.src.tier,
6472
+ source: winner.src.path,
5474
6473
  deliberate: false,
5475
- deprecated: false,
5476
6474
  diskCandidates: diskPaths,
5477
6475
  warning
5478
6476
  });
5479
6477
  }
5480
- const legacy = firstEnvValue(env, apiKeyKeys);
5481
- if (legacy) {
5482
- assertUsableCredential(name, legacy.key, legacy.value);
5483
- 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.`;
5484
- 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}`;
5485
- const sink = options.onDeprecation ?? defaultDeprecationSink;
5486
- const notified = deprecationNotified();
5487
- if (!notified.has(name)) {
5488
- notified.add(name);
5489
- sink(message);
5490
- }
5491
- return sealCredential({
5492
- apiKey: legacy.value,
5493
- tier: "legacy-env",
5494
- source: legacy.key,
6478
+ if (envHit) {
6479
+ assertUsableCredential2(name, envHit.key, envHit.value);
6480
+ return sealCredential2({
6481
+ apiKey: envHit.value,
6482
+ tier: "env",
6483
+ source: envHit.key,
5495
6484
  deliberate: false,
5496
- deprecated: true,
5497
6485
  diskCandidates: diskPaths,
5498
- warning: message
6486
+ warning: null
5499
6487
  });
5500
6488
  }
5501
6489
  return null;
5502
6490
  }
5503
- var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
5504
- var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
5505
- function isValidDnsDomain(value) {
5506
- if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
5507
- return false;
6491
+ var SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
6492
+ var requireSecretsSdk2 = createRequire2(import.meta.url);
6493
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
6494
+ const vaultKey = pointerResolution.pointerVaultKey;
6495
+ const pointerEnvKey = pointerResolution.source;
6496
+ if (!vaultKey) {
6497
+ throw new CredentialResolutionError2(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
5508
6498
  }
5509
- return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
6499
+ let secretsSdk;
6500
+ try {
6501
+ secretsSdk = requireSecretsSdk2(SECRETS_PACKAGE_SPECIFIER2);
6502
+ } catch {
6503
+ 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]);
6504
+ }
6505
+ let client;
6506
+ try {
6507
+ client = secretsSdk.createSecretsClientFromEnv(env);
6508
+ } catch {
6509
+ 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]);
6510
+ }
6511
+ let secret;
6512
+ try {
6513
+ secret = await client.getSecret({ key: vaultKey });
6514
+ } catch {
6515
+ 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]);
6516
+ }
6517
+ const value = secret.value;
6518
+ if (!value) {
6519
+ throw new CredentialResolutionError2(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
6520
+ }
6521
+ assertUsableCredential2(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
6522
+ return sealCredential2({
6523
+ apiKey: value,
6524
+ tier: "pointer",
6525
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
6526
+ deliberate: true,
6527
+ diskCandidates: pointerResolution.diskCandidates,
6528
+ warning: null
6529
+ });
5510
6530
  }
5511
- function firstEnv(env, keys, options = {}) {
5512
- for (const key of keys) {
5513
- const raw = env[key];
5514
- const value = raw?.trim();
5515
- if (value)
5516
- return { key, value: options.preserveRaw ? raw : value };
6531
+ var DEFAULT_FLEET_GATEWAY_ORIGIN2 = "https://api.hasna.com";
6532
+ var DEFAULT_AUTHORITY_SOURCE2 = "default";
6533
+ function defaultFleetGatewayBaseUrl2(name) {
6534
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN2}/${validateAppSlug2(name)}`;
6535
+ }
6536
+ var ASCII_CONTROL_PATTERN2 = /[\u0000-\u001f\u007f]/;
6537
+ var DNS_LABEL_PATTERN2 = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
6538
+ function isValidDnsDomain2(value) {
6539
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN2.test(value) || /[^\x00-\x7f]/.test(value)) {
6540
+ return false;
5517
6541
  }
5518
- return null;
6542
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN2.test(label));
5519
6543
  }
5520
- function firstEnvDefinedKey(env, keys) {
5521
- for (const key of keys) {
5522
- if (env[key] !== undefined)
5523
- return key;
6544
+ function validateAppSlug2(name) {
6545
+ if (name.length > 63 || !DNS_LABEL_PATTERN2.test(name)) {
6546
+ throw new Error("App name must be one lowercase DNS label.");
5524
6547
  }
5525
- return null;
6548
+ return name;
5526
6549
  }
5527
- function rawAuthority(value) {
6550
+ function rawAuthority2(value) {
5528
6551
  const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
5529
6552
  if (!match)
5530
6553
  throw new Error("API URL must be absolute.");
@@ -5535,7 +6558,7 @@ function rawAuthority(value) {
5535
6558
  throw new Error("API URL must include a hostname.");
5536
6559
  return authority;
5537
6560
  }
5538
- function assertCanonicalPort(port) {
6561
+ function assertCanonicalPort2(port) {
5539
6562
  if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
5540
6563
  throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
5541
6564
  }
@@ -5544,7 +6567,7 @@ function assertCanonicalPort(port) {
5544
6567
  throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
5545
6568
  }
5546
6569
  }
5547
- function canonicalAuthorityHostname(authority) {
6570
+ function canonicalAuthorityHostname2(authority) {
5548
6571
  let rawHostname;
5549
6572
  if (authority.startsWith("[")) {
5550
6573
  const closingBracket = authority.indexOf("]");
@@ -5557,9 +6580,9 @@ function canonicalAuthorityHostname(authority) {
5557
6580
  if (!portSuffix.startsWith(":")) {
5558
6581
  throw new Error("API URL authority must contain a canonical hostname and port.");
5559
6582
  }
5560
- assertCanonicalPort(portSuffix.slice(1));
6583
+ assertCanonicalPort2(portSuffix.slice(1));
5561
6584
  }
5562
- if (isIP(rawHostname.slice(1, -1)) !== 6) {
6585
+ if (isIP2(rawHostname.slice(1, -1)) !== 6) {
5563
6586
  throw new Error("API URL authority must contain a canonical IPv6 literal.");
5564
6587
  }
5565
6588
  } else {
@@ -5570,33 +6593,33 @@ function canonicalAuthorityHostname(authority) {
5570
6593
  }
5571
6594
  if (lastColon !== -1) {
5572
6595
  const port = authority.slice(lastColon + 1);
5573
- assertCanonicalPort(port);
6596
+ assertCanonicalPort2(port);
5574
6597
  rawHostname = authority.slice(0, lastColon);
5575
6598
  } else {
5576
6599
  rawHostname = authority;
5577
6600
  }
5578
- const ipVersion = isIP(rawHostname);
6601
+ const ipVersion = isIP2(rawHostname);
5579
6602
  const numericAddressParts = rawHostname.split(".");
5580
6603
  const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
5581
- if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
6604
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain2(rawHostname.toLowerCase())) {
5582
6605
  throw new Error("API URL authority must contain a canonical ASCII hostname.");
5583
6606
  }
5584
6607
  }
5585
6608
  return rawHostname.toLowerCase();
5586
6609
  }
5587
- function isDeliberateLoopbackHttpAuthority(authority) {
6610
+ function isDeliberateLoopbackHttpAuthority2(authority) {
5588
6611
  return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
5589
6612
  }
5590
- function toV1BaseUrl(apiUrl) {
5591
- if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
6613
+ function toV1BaseUrl2(apiUrl) {
6614
+ if (ASCII_CONTROL_PATTERN2.test(apiUrl)) {
5592
6615
  throw new Error("API URL must not contain ASCII control characters.");
5593
6616
  }
5594
6617
  const input = apiUrl.trim();
5595
- const authority = rawAuthority(input);
6618
+ const authority = rawAuthority2(input);
5596
6619
  if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
5597
6620
  throw new Error("API URL authority must be canonical ASCII without credentials.");
5598
6621
  }
5599
- const canonicalHostname = canonicalAuthorityHostname(authority);
6622
+ const canonicalHostname = canonicalAuthorityHostname2(authority);
5600
6623
  const url = new URL(input);
5601
6624
  if (url.protocol !== "http:" && url.protocol !== "https:") {
5602
6625
  throw new Error("API URL must use http or https.");
@@ -5613,7 +6636,7 @@ function toV1BaseUrl(apiUrl) {
5613
6636
  if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
5614
6637
  throw new Error("API URL must not use IDN or punycode hostnames.");
5615
6638
  }
5616
- if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
6639
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority2(authority)) {
5617
6640
  throw new Error("API URL may use http only for an exact loopback authority.");
5618
6641
  }
5619
6642
  if (url.search || url.hash) {
@@ -5625,106 +6648,104 @@ function toV1BaseUrl(apiUrl) {
5625
6648
  url.pathname = `${path}/v1`;
5626
6649
  return url.toString().replace(/\/+$/, "");
5627
6650
  }
5628
- function resolveClientTransport(name, env = process.env, options = {}) {
5629
- const keys = clientTransportEnvKeys(name);
5630
- const envUrlHit = firstEnv(env, keys.apiUrlKeys, { preserveRaw: true });
5631
- const explicitLocalKey = envUrlHit ? null : firstEnvDefinedKey(env, keys.apiUrlKeys);
5632
- const diskUrlHit = envUrlHit || explicitLocalKey ? null : appConfigDiskValue(name, env, keys.apiUrlKeys);
5633
- const urlHit = envUrlHit ?? (diskUrlHit ? { key: diskUrlHit.path, value: diskUrlHit.value } : null);
5634
- const keyHit = firstEnv(env, keys.apiKeyKeys);
5635
- const warnings = [];
5636
- if (!urlHit) {
5637
- if (explicitLocalKey) {
5638
- const overriddenPointer = appConfigDiskValue(name, env, keys.apiUrlKeys);
5639
- if (overriddenPointer) {
5640
- 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.`);
5641
- }
5642
- return {
5643
- transport: "sqlite",
5644
- transportSource: explicitLocalKey,
5645
- baseUrl: null,
5646
- apiUrlSource: null,
5647
- apiKeyPresent: Boolean(keyHit),
5648
- apiKeySource: keyHit ? keyHit.key : null,
5649
- apiKeyTier: null,
5650
- misconfigured: false,
5651
- warning: warnings.length > 0 ? warnings.join(" ") : null
5652
- };
5653
- }
5654
- return {
5655
- transport: "sqlite",
5656
- transportSource: "default",
5657
- baseUrl: null,
5658
- apiUrlSource: null,
5659
- apiKeyPresent: Boolean(keyHit),
5660
- apiKeySource: keyHit ? keyHit.key : null,
5661
- apiKeyTier: null,
5662
- misconfigured: false,
5663
- warning: null
5664
- };
6651
+ class ClientTransportConfigurationError2 extends Error {
6652
+ appName;
6653
+ sources;
6654
+ constructor(appName, message, sources = []) {
6655
+ super(message);
6656
+ this.name = "ClientTransportConfigurationError";
6657
+ this.appName = appName;
6658
+ this.sources = Object.freeze([...sources]);
6659
+ }
6660
+ }
6661
+ function resolveClientTransportSnapshot2(name, env = process.env, options = {}) {
6662
+ env = snapshotClientEnvironment2(name, env);
6663
+ const keys = clientTransportEnvKeys2(name);
6664
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
6665
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
6666
+ if (blankUrl) {
6667
+ 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]);
6668
+ }
6669
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN2.test(entry.raw));
6670
+ if (controlledUrl) {
6671
+ throw new ClientTransportConfigurationError2(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
6672
+ }
6673
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
6674
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
6675
+ 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));
6676
+ }
6677
+ const envUrlHit = usableUrlEntries[0] ?? null;
6678
+ const keychainUrlHit = keychainConfigValue2(name, env, options.credentials?.keychain);
6679
+ const diskConfigUrlHit = appConfigDiskValue2(name, env, keys.apiUrlKeys);
6680
+ if (diskConfigUrlHit?.unusable) {
6681
+ 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]);
6682
+ }
6683
+ const urlCandidates = [
6684
+ ...envUrlHit ? [envUrlHit] : [],
6685
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
6686
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
6687
+ ];
6688
+ const configuredUrl = urlCandidates[0] ?? null;
6689
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
6690
+ if (configuredUrl && divergentUrls.length > 0) {
6691
+ 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));
5665
6692
  }
5666
- if (diskUrlHit) {
5667
- 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.`);
6693
+ const warnings = [];
6694
+ if (configuredUrl && !envUrlHit) {
6695
+ 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.`);
5668
6696
  }
5669
- const credential = resolveCredential(name, env, options.credentials);
6697
+ const credential = resolveCredential2(name, env, options.credentials);
5670
6698
  if (!credential) {
5671
- const diskHint = credentialDiskSourcesForMessage(name, env);
5672
- 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.`);
5673
- return {
5674
- transport: "sqlite",
5675
- transportSource: urlHit.key,
5676
- baseUrl: null,
5677
- apiUrlSource: urlHit.key,
5678
- apiKeyPresent: false,
5679
- apiKeySource: null,
5680
- apiKeyTier: null,
5681
- misconfigured: true,
5682
- warning: warnings.join(" ")
5683
- };
6699
+ const diskHint = credentialDiskSourcesForMessage2(name, env);
6700
+ 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`;
6701
+ 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.`);
6702
+ throw new ClientTransportConfigurationError2(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
5684
6703
  }
5685
6704
  if (credential.warning)
5686
6705
  warnings.push(credential.warning);
6706
+ let urlHit;
6707
+ if (configuredUrl) {
6708
+ urlHit = configuredUrl;
6709
+ } else {
6710
+ try {
6711
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE2, value: defaultFleetGatewayBaseUrl2(name) };
6712
+ } catch (error) {
6713
+ const message = error instanceof Error ? error.message : String(error);
6714
+ 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]]);
6715
+ }
6716
+ }
5687
6717
  const apiUrlSource = urlHit.key;
5688
6718
  let baseUrl;
5689
6719
  try {
5690
- baseUrl = toV1BaseUrl(urlHit.value);
6720
+ baseUrl = toV1BaseUrl2(urlHit.value);
5691
6721
  } catch (error) {
5692
6722
  const message = error instanceof Error ? error.message : String(error);
5693
- warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
5694
- return {
5695
- transport: "sqlite",
6723
+ throw new ClientTransportConfigurationError2(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
6724
+ }
6725
+ return {
6726
+ resolution: {
6727
+ transport: "http",
5696
6728
  transportSource: urlHit.key,
5697
- baseUrl: null,
5698
- apiUrlSource: urlHit.key,
6729
+ baseUrl,
6730
+ apiUrlSource,
5699
6731
  apiKeyPresent: true,
5700
6732
  apiKeySource: credential.source,
5701
6733
  apiKeyTier: credential.tier,
5702
- misconfigured: true,
5703
- warning: warnings.join(" ")
5704
- };
5705
- }
5706
- return {
5707
- transport: "http",
5708
- transportSource: urlHit.key,
5709
- baseUrl,
5710
- apiUrlSource,
5711
- apiKeyPresent: true,
5712
- apiKeySource: credential.source,
5713
- apiKeyTier: credential.tier,
5714
- misconfigured: false,
5715
- warning: warnings.length > 0 ? warnings.join(" ") : null
6734
+ misconfigured: false,
6735
+ warning: warnings.length > 0 ? warnings.join(" ") : null
6736
+ },
6737
+ credential
5716
6738
  };
5717
6739
  }
5718
- function credentialDiskSourcesForMessage(name, env) {
5719
- const paths = credentialDiskSources(name, env);
5720
- return paths.length > 0 ? paths.join(" or ") : "<no HOME set in this environment, so no credential file was consulted>";
6740
+ function credentialDiskSourcesForMessage2(name, env) {
6741
+ const paths = credentialDiskSources2(name, env);
6742
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
5721
6743
  }
5722
6744
 
5723
6745
  class HasnaHttpError extends Error {
5724
6746
  status;
5725
6747
  method;
5726
6748
  path;
5727
- body;
5728
6749
  credentialSource;
5729
6750
  credentialTier;
5730
6751
  constructor(method, path, status, body, credential) {
@@ -5734,7 +6755,12 @@ class HasnaHttpError extends Error {
5734
6755
  this.status = status;
5735
6756
  this.method = method;
5736
6757
  this.path = path;
5737
- this.body = body;
6758
+ Object.defineProperty(this, "body", {
6759
+ value: body,
6760
+ enumerable: status !== 401 && status !== 403,
6761
+ writable: false,
6762
+ configurable: false
6763
+ });
5738
6764
  this.credentialSource = credential?.source ?? null;
5739
6765
  this.credentialTier = credential?.tier ?? null;
5740
6766
  }
@@ -5745,22 +6771,32 @@ function currentCredential(name, apiKey) {
5745
6771
  }
5746
6772
  return explicitCredential(name, apiKey);
5747
6773
  }
6774
+ async function resolveRequestCredential(name, apiKey, env = process.env) {
6775
+ const resolved = currentCredential(name, apiKey);
6776
+ if (resolved.tier === "pointer") {
6777
+ return completePointerCredential(name, resolved, env);
6778
+ }
6779
+ return resolved;
6780
+ }
5748
6781
  function authFailureGuidance(credential) {
5749
6782
  const origin = `The API key for this request came from ${credential.source}`;
5750
6783
  if (credential.deliberate) {
5751
6784
  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.`;
5752
6785
  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}`;
5753
6786
  }
5754
- if (credential.deprecated) {
6787
+ if (credential.tier === "env") {
5755
6788
  const target = credential.diskCandidates[0];
5756
- 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.`;
5757
- 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}`;
6789
+ 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.`;
6790
+ 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}`;
6791
+ }
6792
+ if (credential.tier === "keychain") {
6793
+ 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.`;
5758
6794
  }
5759
6795
  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.`;
5760
6796
  }
5761
6797
  var DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
5762
- var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
5763
- var AUTHORITY_OVERRIDE_HEADERS = new Set([
6798
+ var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
6799
+ var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
5764
6800
  "host",
5765
6801
  ":authority",
5766
6802
  "forwarded",
@@ -5770,7 +6806,7 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
5770
6806
  function assertNoAuthorityOverrideHeaders(headers, source) {
5771
6807
  if (!headers)
5772
6808
  return;
5773
- const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS.has(name.trim().toLowerCase()));
6809
+ const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS2.has(name.trim().toLowerCase()));
5774
6810
  if (forbidden) {
5775
6811
  throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
5776
6812
  }
@@ -5797,9 +6833,9 @@ function appendQuery(path, query) {
5797
6833
  return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
5798
6834
  }
5799
6835
  var defaultSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
5800
- function createHasnaHttpTransport(options) {
6836
+ function createHasnaHttpTransportInternal(options, requestBindingProvider) {
5801
6837
  const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
5802
- const base = toV1BaseUrl(options.baseUrl);
6838
+ const base = toV1BaseUrl2(options.baseUrl);
5803
6839
  const timeoutMs = options.timeoutMs ?? 30000;
5804
6840
  const sleep = options.sleepImpl ?? defaultSleep;
5805
6841
  const defaultRetry = options.retry;
@@ -5859,13 +6895,20 @@ function createHasnaHttpTransport(options) {
5859
6895
  if (opts.signal)
5860
6896
  opts.signal.removeEventListener("abort", onAbort);
5861
6897
  }
5862
- const text = await response.text();
6898
+ const authenticationFailure = response.status === 401 || response.status === 403;
5863
6899
  let parsed = undefined;
5864
- if (text.length > 0) {
6900
+ if (authenticationFailure) {
5865
6901
  try {
5866
- parsed = JSON.parse(text);
5867
- } catch {
5868
- parsed = text;
6902
+ await response.body?.cancel();
6903
+ } catch {}
6904
+ } else {
6905
+ const text = await response.text();
6906
+ if (text.length > 0) {
6907
+ try {
6908
+ parsed = JSON.parse(text);
6909
+ } catch {
6910
+ parsed = text;
6911
+ }
5869
6912
  }
5870
6913
  }
5871
6914
  if (!response.ok) {
@@ -5876,11 +6919,11 @@ function createHasnaHttpTransport(options) {
5876
6919
  error: new HasnaHttpError(method, rel, response.status, parsed)
5877
6920
  };
5878
6921
  }
5879
- if (response.status === 401 || response.status === 403) {
6922
+ if (authenticationFailure) {
5880
6923
  return {
5881
6924
  ok: false,
5882
6925
  retryable: false,
5883
- error: new HasnaHttpError(method, rel, response.status, parsed, {
6926
+ error: new HasnaHttpError(method, rel, response.status, undefined, {
5884
6927
  source: credential.source,
5885
6928
  tier: credential.tier,
5886
6929
  guidance: authFailureGuidance(credential)
@@ -5896,11 +6939,15 @@ function createHasnaHttpTransport(options) {
5896
6939
  async function request(method, path, body, opts = {}) {
5897
6940
  const upper = method.toUpperCase();
5898
6941
  const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
5899
- const url = `${base}${rel}`;
5900
6942
  const retry = resolveRetry(opts.retry);
5901
- const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
6943
+ const methodRetryable = IDEMPOTENT_METHODS2.has(upper) || Boolean(opts.idempotencyKey);
5902
6944
  const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
5903
- const credential = currentCredential(options.name, options.apiKey);
6945
+ const binding = requestBindingProvider ? await requestBindingProvider() : {
6946
+ baseUrl: base,
6947
+ credential: await resolveRequestCredential(options.name, options.apiKey)
6948
+ };
6949
+ const url = `${binding.baseUrl}${rel}`;
6950
+ const credential = binding.credential;
5904
6951
  let last = null;
5905
6952
  for (let attempt = 1;attempt <= maxAttempts; attempt++) {
5906
6953
  const result = await once(upper, rel, url, body, opts, credential);
@@ -5928,49 +6975,44 @@ function createHasnaHttpTransport(options) {
5928
6975
  }
5929
6976
  function createClientTransport(name, env = process.env, overrides) {
5930
6977
  const credentialOptions = overrides?.credentials;
5931
- const resolution = resolveClientTransport(name, env, { ...credentialOptions ? { credentials: credentialOptions } : {} });
5932
- if (resolution.misconfigured) {
5933
- throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the API client.`);
5934
- }
5935
- if (resolution.transport === "sqlite" || !resolution.baseUrl) {
5936
- return { transport: "sqlite", client: null, resolution };
5937
- }
5938
- const credentialProvider = () => {
5939
- const resolved = resolveCredential(name, env, credentialOptions);
5940
- if (!resolved) {
5941
- 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.`);
5942
- }
5943
- return resolved;
6978
+ const snapshotOptions = { ...credentialOptions ? { credentials: credentialOptions } : {} };
6979
+ const resolution = resolveClientTransportSnapshot2(name, env, snapshotOptions).resolution;
6980
+ 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;
6981
+ const unstableConfiguration = () => new ClientTransportConfigurationError2(name, "The configured service authority or credential changed while a request was being prepared; no authenticated request was sent.");
6982
+ const requestBindingProvider = async () => {
6983
+ const first = resolveClientTransportSnapshot2(name, env, snapshotOptions);
6984
+ const reviewed = resolveClientTransportSnapshot2(name, env, snapshotOptions);
6985
+ if (!sameBinding(first, reviewed))
6986
+ throw unstableConfiguration();
6987
+ if (reviewed.resolution.baseUrl !== resolution.baseUrl) {
6988
+ throw new ClientTransportConfigurationError2(name, "The configured service authority changed; rebuild the client before sending credentials.");
6989
+ }
6990
+ const credential = await resolveRequestCredential(name, () => reviewed.credential, env);
6991
+ const immediatelyBeforeDispatch = resolveClientTransportSnapshot2(name, env, snapshotOptions);
6992
+ if (!sameBinding(reviewed, immediatelyBeforeDispatch))
6993
+ throw unstableConfiguration();
6994
+ if (immediatelyBeforeDispatch.resolution.baseUrl !== resolution.baseUrl) {
6995
+ throw new ClientTransportConfigurationError2(name, "The configured service authority changed; rebuild the client before sending credentials.");
6996
+ }
6997
+ return { baseUrl: immediatelyBeforeDispatch.resolution.baseUrl, credential };
5944
6998
  };
5945
6999
  return {
5946
7000
  transport: "http",
5947
- client: createHasnaHttpTransport({
7001
+ client: createHasnaHttpTransportInternal({
5948
7002
  name,
5949
7003
  baseUrl: resolution.baseUrl,
5950
- apiKey: credentialProvider,
7004
+ apiKey: () => {
7005
+ throw new Error("The authenticated request binding provider was not invoked.");
7006
+ },
5951
7007
  ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
5952
7008
  ...overrides?.headers ? { headers: overrides.headers } : {},
5953
7009
  ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
5954
7010
  ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
5955
7011
  ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
5956
- }),
7012
+ }, requestBindingProvider),
5957
7013
  resolution
5958
7014
  };
5959
7015
  }
5960
-
5961
- // ../contracts/dist/client/storage.js
5962
- var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
5963
- var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
5964
- var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
5965
- var DEPRECATION_REGISTRY2 = Symbol.for("hasna:contracts:credentialDeprecationNotices");
5966
- var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
5967
- var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
5968
- "host",
5969
- ":authority",
5970
- "forwarded",
5971
- "x-forwarded-host",
5972
- "x-original-host"
5973
- ]);
5974
7016
  function resourcePath(resource) {
5975
7017
  const trimmed = resource.replace(/^\/+|\/+$/g, "");
5976
7018
  if (!trimmed)
@@ -6070,145 +7112,336 @@ function createHasnaStorageClient(name, transport) {
6070
7112
  }
6071
7113
  };
6072
7114
  }
7115
+ function resolveStorageClient(name, env = process.env, overrides) {
7116
+ const wired = createClientTransport(name, env, overrides);
7117
+ return { transport: "http", client: createHasnaStorageClient(name, wired.client) };
7118
+ }
7119
+
7120
+ // src/lib/local-opt-in.ts
7121
+ var RECORDINGS_LOCAL_OPT_IN_ENV_KEYS = [
7122
+ "HASNA_RECORDINGS_LOCAL",
7123
+ "RECORDINGS_LOCAL"
7124
+ ];
7125
+ function isRecordingsLocalOptIn(env = process.env) {
7126
+ return RECORDINGS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
7127
+ }
7128
+ var RECORDINGS_CARVED_LEGACY_ENV_KEYS = [
7129
+ "RECORDINGS_API_URL",
7130
+ "RECORDINGS_API_KEY"
7131
+ ];
7132
+ function recordingsAuthorityEnvKeys() {
7133
+ const keys = clientTransportEnvKeys("recordings");
7134
+ return [
7135
+ ...keys.apiUrlKeys,
7136
+ ...keys.apiKeyKeys,
7137
+ credentialOverrideEnvKey("recordings"),
7138
+ credentialPointerEnvKey("recordings"),
7139
+ CREDENTIAL_PROFILE_ENV_KEY
7140
+ ].filter((key) => !RECORDINGS_CARVED_LEGACY_ENV_KEYS.includes(key));
7141
+ }
7142
+ function hasRecordingsEnvAuthorityIntent(env = process.env) {
7143
+ return recordingsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
7144
+ }
7145
+ function selectsRecordingsLocalStore(env = process.env) {
7146
+ return !hasRecordingsEnvAuthorityIntent(env) && isRecordingsLocalOptIn(env);
7147
+ }
7148
+ function recordingsResolverEnv(env) {
7149
+ const carved = RECORDINGS_CARVED_LEGACY_ENV_KEYS.filter((key) => (key in env));
7150
+ const blanks = recordingsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
7151
+ const removed = new Set([...carved, ...blanks]);
7152
+ if (removed.size === 0)
7153
+ return env;
7154
+ const next = { ...env };
7155
+ for (const key of removed)
7156
+ delete next[key];
7157
+ return next;
7158
+ }
7159
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
7160
+ function isAmbientRecordingsEnv(env) {
7161
+ if (typeof process !== "undefined" && env === process.env) {
7162
+ return true;
7163
+ }
7164
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
7165
+ }
7166
+ function recordingsResolverInputs(env, credentials = {}) {
7167
+ const normalised = recordingsResolverEnv(env);
7168
+ if (normalised === env)
7169
+ return { env: normalised, credentials };
7170
+ const keychain = { ...credentials.keychain };
7171
+ if (keychain.enabled === undefined && keychain.run === undefined) {
7172
+ keychain.enabled = isAmbientRecordingsEnv(env);
7173
+ }
7174
+ return { env: normalised, credentials: { ...credentials, keychain } };
7175
+ }
6073
7176
 
6074
7177
  // src/http/client.ts
6075
- function envToken2(name) {
6076
- return name.toUpperCase().replace(/-/g, "_");
7178
+ var RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
7179
+ var IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
7180
+ function throwRecordingsAuthorityFailure(error) {
7181
+ const message = error instanceof Error ? error.message : String(error);
7182
+ const name = error instanceof Error ? error.name : "";
7183
+ if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
7184
+ 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 });
7185
+ }
7186
+ if (/no API key could be resolved/.test(message)) {
7187
+ if (/is not set and no API key could be resolved/.test(message)) {
7188
+ 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 });
7189
+ }
7190
+ 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 });
7191
+ }
7192
+ throw new Error(`REMOTE_API_URL_INVALID: ${message} local SQLite fallback is disabled`, { cause: error });
6077
7193
  }
6078
- function envKeys(name) {
6079
- const token = envToken2(name);
7194
+ function toRecordsAuthority(resolution) {
6080
7195
  return {
6081
- storeKeys: [`HASNA_${token}_CLIENT_STORE`, `${token}_CLIENT_STORE`],
6082
- apiUrlKeys: [`HASNA_${token}_API_URL`],
6083
- apiKeyKeys: [`HASNA_${token}_API_KEY`]
7196
+ transport: resolution.transport,
7197
+ transportSource: resolution.transportSource,
7198
+ baseUrl: resolution.baseUrl,
7199
+ apiUrlSource: resolution.apiUrlSource,
7200
+ apiKeyPresent: resolution.apiKeyPresent,
7201
+ apiKeySource: resolution.apiKeySource,
7202
+ apiKeyTier: resolution.apiKeyTier,
7203
+ misconfigured: resolution.misconfigured,
7204
+ warning: resolution.warning
6084
7205
  };
6085
7206
  }
6086
- function normalizeClientStore(value) {
6087
- const normalized = value.trim().toLowerCase();
6088
- if (normalized === "sqlite")
6089
- return "sqlite";
6090
- if (normalized === "http" || normalized === "https")
6091
- return "http";
6092
- throw new Error(`Unknown client store: ${value}. Use sqlite or http.`);
6093
- }
6094
- function firstEnv2(env, keys) {
6095
- for (const key of keys) {
6096
- const value = env[key]?.trim();
6097
- if (value)
6098
- return { key, value };
7207
+ function resolveRecordingsTransport(env = process.env, options = {}) {
7208
+ if (selectsRecordingsLocalStore(env)) {
7209
+ return { transport: "sqlite", selected: false, source: "local-opt-in", authority: null };
6099
7210
  }
6100
- return null;
6101
- }
6102
- function toV1BaseUrl2(apiUrl) {
6103
- const url = new URL(apiUrl);
6104
- if (url.protocol !== "http:" && url.protocol !== "https:") {
6105
- throw new Error("API URL must use http or https.");
7211
+ const resolverInputs = recordingsResolverInputs(env, options.credentials);
7212
+ let resolution;
7213
+ try {
7214
+ resolution = resolveClientTransport("recordings", resolverInputs.env, {
7215
+ credentials: resolverInputs.credentials
7216
+ });
7217
+ } catch (error) {
7218
+ throwRecordingsAuthorityFailure(error);
6106
7219
  }
6107
- let path = url.pathname.replace(/\/+$/, "");
6108
- if (path.endsWith("/v1"))
6109
- path = path.slice(0, -"/v1".length);
6110
- url.pathname = `${path}/v1`;
6111
- url.search = "";
6112
- url.hash = "";
6113
- return url.toString().replace(/\/+$/, "");
7220
+ return {
7221
+ transport: "http",
7222
+ selected: true,
7223
+ source: `${resolution.apiKeySource ?? resolution.apiKeyTier}+${resolution.apiUrlSource ?? "default"}`,
7224
+ authority: toRecordsAuthority(resolution)
7225
+ };
6114
7226
  }
6115
- function resolveTransport(name, env = process.env) {
6116
- const keys = envKeys(name);
6117
- const storeHit = firstEnv2(env, keys.storeKeys);
6118
- const urlHit = firstEnv2(env, keys.apiUrlKeys);
6119
- const keyHit = firstEnv2(env, keys.apiKeyKeys);
6120
- let requested = "sqlite";
6121
- let modeSource = "default";
6122
- if (storeHit) {
6123
- requested = normalizeClientStore(storeHit.value);
6124
- modeSource = storeHit.key;
6125
- } else if (urlHit && keyHit) {
6126
- requested = "http";
6127
- modeSource = "auto:api-url+api-key";
6128
- } else if (urlHit || keyHit) {
6129
- const missing = urlHit ? keys.apiKeyKeys[0] : keys.apiUrlKeys[0];
6130
- const present = urlHit ? keys.apiUrlKeys[0] : keys.apiKeyKeys[0];
7227
+ function getRecordingsTransportStatus(env = process.env, options = {}) {
7228
+ let resolution;
7229
+ try {
7230
+ resolution = resolveRecordingsTransport(env, options);
7231
+ } catch (error) {
7232
+ const issue = error instanceof Error ? error.message : String(error);
7233
+ const envKeys = clientTransportEnvKeys("recordings");
7234
+ const declared = (keys) => keys.some((key) => (env[key] ?? "").trim() !== "");
6131
7235
  return {
6132
- transport: "sqlite",
6133
- requested,
6134
- modeSource,
6135
- baseUrl: null,
6136
- apiKeyPresent: Boolean(keyHit),
6137
- misconfigured: true,
6138
- 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.`
7236
+ selected: true,
7237
+ ok: false,
7238
+ transport: "invalid",
7239
+ api_url_configured: declared(envKeys.apiUrlKeys),
7240
+ api_key_configured: declared(envKeys.apiKeyKeys),
7241
+ api_url_source: null,
7242
+ api_key_source: null,
7243
+ api_key_tier: null,
7244
+ v1_base_url: null,
7245
+ issues: [issue],
7246
+ local_fallback: false
6139
7247
  };
6140
7248
  }
6141
- if (requested === "sqlite") {
6142
- return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: Boolean(keyHit), misconfigured: false, warning: null };
6143
- }
6144
- if (!urlHit) {
7249
+ if (!resolution.selected) {
6145
7250
  return {
6146
- transport: "sqlite",
6147
- requested,
6148
- modeSource,
6149
- baseUrl: null,
6150
- apiKeyPresent: Boolean(keyHit),
6151
- misconfigured: true,
6152
- warning: `${modeSource}=http but no API URL is set (${keys.apiUrlKeys[0]}). Refusing to route to the API.`
7251
+ selected: false,
7252
+ ok: true,
7253
+ transport: resolution.transport,
7254
+ api_url_configured: false,
7255
+ api_key_configured: false,
7256
+ api_url_source: null,
7257
+ api_key_source: null,
7258
+ api_key_tier: null,
7259
+ v1_base_url: null,
7260
+ issues: [],
7261
+ local_fallback: false
6153
7262
  };
6154
7263
  }
6155
- if (!keyHit) {
6156
- return {
6157
- transport: "sqlite",
6158
- requested,
6159
- modeSource,
6160
- baseUrl: null,
6161
- apiKeyPresent: false,
6162
- misconfigured: true,
6163
- warning: `${modeSource}=http but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to the API.`
6164
- };
7264
+ const authority = resolution.authority;
7265
+ return {
7266
+ selected: true,
7267
+ ok: true,
7268
+ transport: resolution.transport,
7269
+ api_url_configured: authority.apiUrlSource !== null && authority.apiUrlSource !== "default",
7270
+ api_key_configured: authority.apiKeyPresent,
7271
+ api_url_source: authority.apiUrlSource,
7272
+ api_key_source: authority.apiKeySource,
7273
+ api_key_tier: authority.apiKeyTier,
7274
+ v1_base_url: authority.baseUrl,
7275
+ issues: [],
7276
+ local_fallback: false
7277
+ };
7278
+ }
7279
+ function resolveRecordingsCloudClient(env = process.env, options = {}) {
7280
+ if (selectsRecordingsLocalStore(env))
7281
+ return null;
7282
+ const resolverInputs = recordingsResolverInputs(env, options.credentials);
7283
+ let resolved;
7284
+ try {
7285
+ resolved = resolveStorageClient("recordings", resolverInputs.env, {
7286
+ fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
7287
+ credentials: resolverInputs.credentials
7288
+ });
7289
+ } catch (error) {
7290
+ throwRecordingsAuthorityFailure(error);
6165
7291
  }
6166
- const rawUrl = urlHit.value;
6167
- let baseUrl;
7292
+ const client = resolved.client;
7293
+ return client;
7294
+ }
7295
+
7296
+ // src/store.ts
7297
+ import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
7298
+
7299
+ // src/lib/audio-artifact-storage.ts
7300
+ import { GetObjectCommand, PutObjectCommand, S3Client as AwsS3Client } from "@aws-sdk/client-s3";
7301
+ import { createHash } from "crypto";
7302
+ import { readFile } from "fs/promises";
7303
+ import { extname } from "path";
7304
+ var SHA256_HEX = /^[0-9a-f]{64}$/;
7305
+ var DEFAULT_S3_PREFIX = "recordings";
7306
+ var AUDIO_CONTENT_TYPES = {
7307
+ wav: "audio/wav",
7308
+ mp3: "audio/mpeg",
7309
+ m4a: "audio/mp4",
7310
+ aac: "audio/aac",
7311
+ webm: "audio/webm",
7312
+ ogg: "audio/ogg",
7313
+ flac: "audio/flac"
7314
+ };
7315
+ function contentTypeForExtension(extension) {
7316
+ return AUDIO_CONTENT_TYPES[extension.toLowerCase()] ?? "application/octet-stream";
7317
+ }
7318
+
7319
+ class AudioArtifactStorage {
7320
+ bucket;
7321
+ prefix;
7322
+ s3;
7323
+ constructor(options = {}) {
7324
+ this.bucket = options.bucket;
7325
+ this.prefix = (options.prefix || DEFAULT_S3_PREFIX).replace(/^\/+|\/+$/g, "");
7326
+ this.s3 = this.bucket ? options.client ?? new AwsS3Client({ region: options.region || process.env.AWS_REGION || "us-east-1" }) : undefined;
7327
+ }
7328
+ get usesS3() {
7329
+ return Boolean(this.bucket);
7330
+ }
7331
+ objectKeyFor(recordingId, sha256, extension) {
7332
+ assertSha256(sha256);
7333
+ const safeExtension = extension.toLowerCase().match(/^[a-z0-9]+/)?.[0] || "bin";
7334
+ return `${this.prefix}/${recordingId}/${sha256}.${safeExtension}`;
7335
+ }
7336
+ async uploadAudio(recordingId, audioPath) {
7337
+ if (!this.bucket || !this.s3)
7338
+ return null;
7339
+ const bytes = await readFileBytes(audioPath);
7340
+ if (!bytes)
7341
+ return null;
7342
+ const sha256 = sha256Hex(bytes);
7343
+ const extension = extensionOf(audioPath);
7344
+ const key = this.objectKeyFor(recordingId, sha256, extension);
7345
+ await this.s3.send(new PutObjectCommand({
7346
+ Bucket: this.bucket,
7347
+ Key: key,
7348
+ Body: bytes,
7349
+ ContentType: contentTypeForExtension(extension)
7350
+ }));
7351
+ return { objectKey: key, sha256, bytes: bytes.byteLength };
7352
+ }
7353
+ async readAudio(objectKey) {
7354
+ if (!this.bucket || !this.s3)
7355
+ return null;
7356
+ try {
7357
+ const response = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: objectKey }));
7358
+ return await bodyToBytes(response.Body);
7359
+ } catch {
7360
+ return null;
7361
+ }
7362
+ }
7363
+ }
7364
+ function resolveAudioArtifactStorage(env = process.env, client) {
7365
+ const bucket = env.HASNA_RECORDINGS_S3_BUCKET || env.RECORDINGS_S3_BUCKET || undefined;
7366
+ const prefix = env.HASNA_RECORDINGS_S3_PREFIX || env.RECORDINGS_S3_PREFIX || undefined;
7367
+ const region = env.RECORDINGS_S3_REGION || env.AWS_REGION || undefined;
7368
+ return new AudioArtifactStorage({ bucket, prefix, region, client });
7369
+ }
7370
+ async function uploadAudioAtCreation(recordingId, audioPath, storage = resolveAudioArtifactStorage()) {
7371
+ if (!audioPath)
7372
+ return null;
7373
+ if (!storage.usesS3)
7374
+ return null;
6168
7375
  try {
6169
- baseUrl = toV1BaseUrl2(rawUrl);
7376
+ const uploaded = await storage.uploadAudio(recordingId, audioPath);
7377
+ if (!uploaded)
7378
+ return null;
7379
+ console.warn(`recordings: uploaded audio for ${recordingId} to ${uploaded.objectKey}`);
7380
+ return uploaded;
6170
7381
  } catch (error) {
6171
- const message = error instanceof Error ? error.message : String(error);
6172
- return { transport: "sqlite", requested, modeSource, baseUrl: null, apiKeyPresent: true, misconfigured: true, warning: `Invalid API URL: ${message}.` };
7382
+ console.warn(`recordings: audio upload failed for ${recordingId} (${String(error instanceof Error ? error.message : error)}); keeping local audio_path only`);
7383
+ return null;
6173
7384
  }
6174
- return { transport: "http", requested, modeSource, baseUrl, apiKeyPresent: true, misconfigured: false, warning: null };
6175
7385
  }
6176
- var RETRY_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
6177
- var IDEMPOTENT = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
6178
- function resolveStoreClient(name, env = process.env) {
6179
- const resolution = resolveTransport(name, env);
6180
- if (resolution.misconfigured) {
6181
- const wired2 = createClientTransport(name, env);
6182
- if (wired2.transport === "http") {
6183
- return {
6184
- transport: "http",
6185
- client: createHasnaStorageClient(name, wired2.client),
6186
- resolution: {
6187
- transport: "http",
6188
- requested: "http",
6189
- modeSource: resolution.modeSource === "default" ? "auto:api-url+seam-credential" : resolution.modeSource,
6190
- baseUrl: wired2.resolution.baseUrl,
6191
- apiKeyPresent: true,
6192
- misconfigured: false,
6193
- warning: null
6194
- }
6195
- };
7386
+ function assertSha256(value) {
7387
+ if (!SHA256_HEX.test(value)) {
7388
+ throw new Error("audio digest must be a lowercase hex sha-256; refusing to build a storage key from it");
7389
+ }
7390
+ }
7391
+ function sha256Hex(bytes) {
7392
+ const hash = createHash("sha256");
7393
+ hash.update(bytes);
7394
+ return hash.digest("hex");
7395
+ }
7396
+ async function readFileBytes(path) {
7397
+ try {
7398
+ return new Uint8Array(await readFile(path));
7399
+ } catch {
7400
+ return null;
7401
+ }
7402
+ }
7403
+ function extensionOf(path) {
7404
+ const extension = extname(path).replace(/^\./, "").toLowerCase();
7405
+ return extension || "bin";
7406
+ }
7407
+ async function bodyToBytes(body) {
7408
+ if (!body)
7409
+ return new Uint8Array(0);
7410
+ if (body instanceof Uint8Array)
7411
+ return body;
7412
+ if (typeof body.transformToByteArray === "function") {
7413
+ return new Uint8Array(await body.transformToByteArray());
7414
+ }
7415
+ if (typeof body.getReader === "function") {
7416
+ const reader = body.getReader();
7417
+ const chunks2 = [];
7418
+ for (;; ) {
7419
+ const { value, done } = await reader.read();
7420
+ if (done)
7421
+ break;
7422
+ if (value)
7423
+ chunks2.push(value);
6196
7424
  }
6197
- throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for the /v1 API.`);
7425
+ return concat(chunks2);
6198
7426
  }
6199
- if (resolution.transport === "sqlite" || !resolution.baseUrl) {
6200
- return { transport: "sqlite", client: null, resolution };
7427
+ const chunks = [];
7428
+ for await (const chunk of body) {
7429
+ chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
6201
7430
  }
6202
- const wired = createClientTransport(name, env);
6203
- if (wired.transport !== "http") {
6204
- throw new Error(`Client for '${name}' resolved to the /v1 API without an API key.`);
7431
+ return concat(chunks);
7432
+ }
7433
+ function concat(chunks) {
7434
+ const total = chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0);
7435
+ const merged = new Uint8Array(new ArrayBuffer(total));
7436
+ let offset = 0;
7437
+ for (const chunk of chunks) {
7438
+ merged.set(chunk, offset);
7439
+ offset += chunk.byteLength;
6205
7440
  }
6206
- return { transport: "http", client: createHasnaStorageClient(name, wired.client), resolution };
7441
+ return merged;
6207
7442
  }
6208
7443
 
6209
7444
  // src/store.ts
6210
- import { createHash, randomUUID as randomUUID2 } from "crypto";
6211
- var APP = "recordings";
6212
7445
  function listQuery(filter) {
6213
7446
  if (!filter)
6214
7447
  return {};
@@ -6238,7 +7471,15 @@ var localStore = {
6238
7471
  mode: "sqlite",
6239
7472
  baseUrl: null,
6240
7473
  async createRecording(input, idempotencyKey) {
6241
- return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
7474
+ return withLocalStoreReaderLease(async () => {
7475
+ const recording = createRecording(input, undefined, idempotencyKey);
7476
+ if (!recording.audio_path)
7477
+ return recording;
7478
+ const uploaded = await uploadAudioAtCreation(recording.id, recording.audio_path, localArtifactStorageFor());
7479
+ if (!uploaded)
7480
+ return recording;
7481
+ return updateRecordingAudio(recording.id, uploaded.objectKey, uploaded.sha256, uploaded.bytes) ?? recording;
7482
+ });
6242
7483
  },
6243
7484
  async getRecording(id) {
6244
7485
  return withLocalStoreReaderLease(() => getRecording(id));
@@ -6432,7 +7673,7 @@ function apiStore(client) {
6432
7673
  };
6433
7674
  }
6434
7675
  function recordingPageFingerprint(items) {
6435
- const hash = createHash("sha256");
7676
+ const hash = createHash2("sha256");
6436
7677
  const ids = items.map((item) => String(item.id ?? "")).sort();
6437
7678
  for (const id of ids) {
6438
7679
  hash.update(String(id.length));
@@ -6468,12 +7709,16 @@ async function countStoreRecordings(store, filter) {
6468
7709
  throw new Error(`Legacy Store exceeded ${maxPageRequests} pages while counting recordings`);
6469
7710
  }
6470
7711
  var cached = null;
6471
- function getStore(env = process.env) {
6472
- if (env === process.env && cached)
7712
+ var localArtifactStorageOverride;
7713
+ function localArtifactStorageFor() {
7714
+ return localArtifactStorageOverride ?? resolveAudioArtifactStorage();
7715
+ }
7716
+ function getStore(env = process.env, options = {}) {
7717
+ if (env === process.env && cached && options.credentials === undefined)
6473
7718
  return cached;
6474
- const resolved = resolveStoreClient(APP, env);
6475
- const store = resolved.transport === "http" ? apiStore(resolved.client) : localStore;
6476
- if (env === process.env)
7719
+ const client = resolveRecordingsCloudClient(env, options);
7720
+ const store = client ? apiStore(client) : localStore;
7721
+ if (env === process.env && options.credentials === undefined)
6477
7722
  cached = store;
6478
7723
  return store;
6479
7724
  }
@@ -6502,7 +7747,7 @@ var _client = null;
6502
7747
  var _clientApiKey = null;
6503
7748
  function getClient(config) {
6504
7749
  if (!config.openai_api_key) {
6505
- throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
7750
+ 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')");
6506
7751
  }
6507
7752
  if (_client && _clientApiKey === config.openai_api_key)
6508
7753
  return _client;
@@ -6731,7 +7976,6 @@ function currentMachineId(env = process.env, hostName = hostname()) {
6731
7976
 
6732
7977
  // src/mcp/index.ts
6733
7978
  var config = loadConfig();
6734
- ensureDataDir(config);
6735
7979
  function runtimeConfig() {
6736
7980
  return {
6737
7981
  ...loadConfig(),
@@ -7419,6 +8663,16 @@ async function main() {
7419
8663
  printHelp();
7420
8664
  return;
7421
8665
  }
8666
+ const transportStatus = getRecordingsTransportStatus();
8667
+ if (!transportStatus.ok) {
8668
+ const issue = transportStatus.issues[0] ?? "REMOTE_API_CONFIG_MISSING";
8669
+ console.error(`ERROR: ${issue}`);
8670
+ process.exit(1);
8671
+ }
8672
+ if (selectsRecordingsLocalStore(process.env)) {
8673
+ console.error("recordings: LOCAL mode \u2014 HASNA_RECORDINGS_LOCAL is set and nothing configures an authority; " + "reading and writing the on-box SQLite store, not the hosted fleet. " + "Set HASNA_RECORDINGS_API_KEY, add the Keychain item hasna.credentials.recordings.api-key, " + "or write ~/.hasna/recordings/config/credentials to go hosted.");
8674
+ ensureDataDir(config);
8675
+ }
7422
8676
  if (isStdioMode(args)) {
7423
8677
  const transport = new StdioServerTransport;
7424
8678
  await buildServer().connect(transport);