@hasna/instructions 0.5.6 → 0.6.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 (61) hide show
  1. package/README.md +64 -20
  2. package/dist/cli/fail-closed-no-env.test.d.ts +2 -0
  3. package/dist/cli/fail-closed-no-env.test.d.ts.map +1 -0
  4. package/dist/cli/index.js +1711 -422
  5. package/dist/data/config-store.d.ts +62 -33
  6. package/dist/data/config-store.d.ts.map +1 -1
  7. package/dist/db/database.d.ts.map +1 -1
  8. package/dist/generated/storage-kit/backend.d.ts +4 -4
  9. package/dist/generated/storage-kit/backend.d.ts.map +1 -1
  10. package/dist/generated/storage-kit/index.d.ts +1 -1
  11. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  12. package/dist/generated/storage-kit/migrations.d.ts +21 -0
  13. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  14. package/dist/generated/storage-kit/pool.d.ts +2 -5
  15. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  16. package/dist/index.d.ts +5 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2486 -386
  19. package/dist/lib/app-home.d.ts +9 -0
  20. package/dist/lib/app-home.d.ts.map +1 -1
  21. package/dist/lib/client-types.d.ts +130 -0
  22. package/dist/lib/client-types.d.ts.map +1 -0
  23. package/dist/lib/client-types.test.d.ts +2 -0
  24. package/dist/lib/client-types.test.d.ts.map +1 -0
  25. package/dist/lib/local-opt-in.d.ts +82 -0
  26. package/dist/lib/local-opt-in.d.ts.map +1 -0
  27. package/dist/lib/project-context.d.ts +14 -14
  28. package/dist/lib/project-context.d.ts.map +1 -1
  29. package/dist/lib/project-dashboard-standard.d.ts +1 -1
  30. package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
  31. package/dist/lib/session-render-state-hermeticity.test.d.ts +2 -0
  32. package/dist/lib/session-render-state-hermeticity.test.d.ts.map +1 -0
  33. package/dist/lib/session-render-state.d.ts +9 -0
  34. package/dist/lib/session-render-state.d.ts.map +1 -1
  35. package/dist/lib/transport-resolver.d.ts +80 -0
  36. package/dist/lib/transport-resolver.d.ts.map +1 -0
  37. package/dist/lib/transport-resolver.test.d.ts +2 -0
  38. package/dist/lib/transport-resolver.test.d.ts.map +1 -0
  39. package/dist/mcp/index.d.ts.map +1 -1
  40. package/dist/mcp/index.js +1439 -195
  41. package/dist/mcp/server.d.ts.map +1 -1
  42. package/dist/sdk/index.d.ts +22 -0
  43. package/dist/sdk/index.d.ts.map +1 -0
  44. package/dist/sdk/index.js +1042 -0
  45. package/dist/sdk/resolve.d.ts +82 -0
  46. package/dist/sdk/resolve.d.ts.map +1 -0
  47. package/dist/sdk/resolve.test.d.ts +2 -0
  48. package/dist/sdk/resolve.test.d.ts.map +1 -0
  49. package/dist/sdk/sdk-bundle-self-contained.test.d.ts +2 -0
  50. package/dist/sdk/sdk-bundle-self-contained.test.d.ts.map +1 -0
  51. package/dist/sdk/v1.generated.d.ts +288 -0
  52. package/dist/sdk/v1.generated.d.ts.map +1 -0
  53. package/dist/server/cloud.d.ts.map +1 -1
  54. package/dist/server/index.d.ts.map +1 -1
  55. package/dist/server/index.js +55 -64
  56. package/dist/test-support/preload-state-home.d.ts +2 -0
  57. package/dist/test-support/preload-state-home.d.ts.map +1 -0
  58. package/package.json +12 -12
  59. package/dashboard/README.md +0 -37
  60. package/dist/lib/retired-storage-mode.d.ts +0 -8
  61. package/dist/lib/retired-storage-mode.d.ts.map +0 -1
package/dist/mcp/index.js CHANGED
@@ -73,116 +73,167 @@ var init_types = __esm(() => {
73
73
  };
74
74
  });
75
75
 
76
- // src/lib/retired-storage-mode.ts
77
- function firstDefinedEnvKey(env, keys) {
78
- for (const key of keys) {
79
- if (Object.hasOwn(env, key) && env[key] !== undefined)
80
- return key;
81
- }
82
- return null;
76
+ // ../contracts/dist/client/transport.js
77
+ import { createRequire } from "module";
78
+ function envToken(name) {
79
+ return name.toUpperCase().replace(/-/g, "_");
83
80
  }
84
- function assertNoLegacyStorageMode(env = process.env) {
85
- const legacyKey = firstDefinedEnvKey(env, LEGACY_STORAGE_MODE_KEYS);
86
- if (!legacyKey)
87
- return;
88
- throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client uses the local SQLite store, or the HTTP API selected by ` + `HASNA_INSTRUCTIONS_API_URL + HASNA_INSTRUCTIONS_API_KEY. ` + `On the server, set HASNA_INSTRUCTIONS_DATABASE_URL to select the postgresql backend, ` + `or leave it unset for sqlite.`);
89
- }
90
- var LEGACY_STORAGE_MODE_KEYS;
91
- var init_retired_storage_mode = __esm(() => {
92
- LEGACY_STORAGE_MODE_KEYS = [
93
- "HASNA_INSTRUCTIONS_STORAGE_MODE",
94
- "HASNA_INSTRUCTIONS_MODE",
95
- "INSTRUCTIONS_STORAGE_MODE",
96
- "INSTRUCTIONS_MODE"
81
+ function clientTransportEnvKeys(name) {
82
+ const envSegment = envToken(name);
83
+ return {
84
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
85
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
86
+ };
87
+ }
88
+ function credentialOverrideEnvKey(name) {
89
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
90
+ }
91
+ function credentialPointerEnvKey(name) {
92
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
93
+ }
94
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", MAX_CREDENTIAL_FILE_BYTES, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
95
+ var init_transport = __esm(() => {
96
+ MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
97
+ INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
98
+ CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
99
+ AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
100
+ SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
101
+ requireSecretsSdk = createRequire(import.meta.url);
102
+ IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
103
+ AUTHORITY_OVERRIDE_HEADERS = new Set([
104
+ "host",
105
+ ":authority",
106
+ "forwarded",
107
+ "x-forwarded-host",
108
+ "x-original-host"
109
+ ]);
110
+ });
111
+
112
+ // src/lib/local-opt-in.ts
113
+ function instructionsLocalModeNotice() {
114
+ return `instructions: local mode \u2014 ${INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS[0]}=1 selects the on-box SQLite store, ` + `and no hosted authority was configured. Set HASNA_INSTRUCTIONS_API_KEY (or add the Keychain item ` + `hasna.credentials.instructions.api-key, or write ~/.hasna/instructions/config/credentials) to go hosted.`;
115
+ }
116
+ function isInstructionsLocalOptIn(env = process.env) {
117
+ return INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() === "1");
118
+ }
119
+ function instructionsAuthorityEnvKeys() {
120
+ const keys = clientTransportEnvKeys("instructions");
121
+ return [
122
+ ...keys.apiUrlKeys,
123
+ ...keys.apiKeyKeys,
124
+ credentialOverrideEnvKey("instructions"),
125
+ credentialPointerEnvKey("instructions"),
126
+ CREDENTIAL_PROFILE_ENV_KEY
97
127
  ];
128
+ }
129
+ function hasInstructionsEnvAuthorityIntent(env = process.env) {
130
+ return instructionsAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
131
+ }
132
+ function selectsInstructionsLocalStore(env = process.env) {
133
+ return !hasInstructionsEnvAuthorityIntent(env) && isInstructionsLocalOptIn(env);
134
+ }
135
+ function instructionsResolverEnv(env) {
136
+ const blanks = instructionsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
137
+ if (blanks.length === 0)
138
+ return env;
139
+ const next = { ...env };
140
+ for (const key of blanks)
141
+ delete next[key];
142
+ return next;
143
+ }
144
+ function isAmbientInstructionsEnv(env) {
145
+ if (typeof process !== "undefined" && env === process.env)
146
+ return true;
147
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
148
+ }
149
+ function instructionsResolverInputs(env, credentials = {}) {
150
+ const normalised = instructionsResolverEnv(env);
151
+ if (normalised === env)
152
+ return { env: normalised, credentials };
153
+ const keychain = { ...credentials.keychain };
154
+ if (keychain.enabled === undefined && keychain.run === undefined) {
155
+ keychain.enabled = isAmbientInstructionsEnv(env);
156
+ }
157
+ return { env: normalised, credentials: { ...credentials, keychain } };
158
+ }
159
+ var INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
160
+ var init_local_opt_in = __esm(() => {
161
+ init_transport();
162
+ INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_INSTRUCTIONS_LOCAL"];
163
+ CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
98
164
  });
99
165
 
100
- // ../../node_modules/.bun/@hasna+paths@0.1.0/node_modules/@hasna/paths/dist/index.js
166
+ // src/lib/app-home.ts
167
+ import { existsSync as existsSync2 } from "fs";
101
168
  import { homedir } from "os";
102
- import { join as join2 } from "path";
103
- function assertApp(app) {
169
+ import { join as join2, resolve } from "path";
170
+ import { homedir as pathsResolverHomedir } from "os";
171
+ import { join as pathsResolverJoin } from "path";
172
+ function pathsResolverAssertApp(app) {
104
173
  if (typeof app !== "string" || app.length === 0) {
105
174
  throw new TypeError("paths: app must be a non-empty string");
106
175
  }
107
- if (!APP_SLUG_RE.test(app)) {
176
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
108
177
  throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
109
178
  }
110
179
  }
111
- function envOf(options) {
112
- return options.env ?? process.env;
113
- }
114
- function envValue(options, kind) {
115
- const value = envOf(options)[KIND_ENV[kind]];
116
- return typeof value === "string" && value.length > 0 ? value : undefined;
117
- }
118
- function isMacOS(platform) {
119
- return platform === "darwin";
180
+ function pathsResolverAssertKind(kind) {
181
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
182
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
183
+ }
120
184
  }
121
- function baseDir(kind, options) {
122
- const override = envValue(options, kind);
123
- if (override)
185
+ function pathsResolverBaseDir(kind, options) {
186
+ pathsResolverAssertKind(kind);
187
+ const env = options.env ?? process.env;
188
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
189
+ if (typeof override === "string" && override.length > 0)
124
190
  return override;
125
- const home = options.home ?? homedir();
191
+ const home = options.home ?? pathsResolverHomedir();
126
192
  const platform = options.platform ?? process.platform;
127
- if (isMacOS(platform)) {
193
+ if (platform === "darwin") {
128
194
  switch (kind) {
129
195
  case "config":
130
196
  case "data":
131
- return join2(home, "Library", "Application Support", "Hasna");
197
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
132
198
  case "cache":
133
- return join2(home, "Library", "Caches", "Hasna");
199
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
134
200
  case "state":
135
- return join2(home, "Library", "Logs", "Hasna");
201
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
136
202
  }
137
203
  }
138
204
  switch (kind) {
139
205
  case "config":
140
- return join2(home, ".config", "hasna");
206
+ return pathsResolverJoin(home, ".config", "hasna");
141
207
  case "data":
142
- return join2(home, ".local", "share", "hasna");
208
+ return pathsResolverJoin(home, ".local", "share", "hasna");
143
209
  case "state":
144
- return join2(home, ".local", "state", "hasna");
210
+ return pathsResolverJoin(home, ".local", "state", "hasna");
145
211
  case "cache":
146
- return join2(home, ".cache", "hasna");
212
+ return pathsResolverJoin(home, ".cache", "hasna");
147
213
  }
148
214
  }
149
- function resolvePath(kind, options) {
150
- assertApp(options.app);
151
- const appSegment = options.internal === true ? join2("internal", options.app) : options.app;
152
- return join2(baseDir(kind, options), appSegment);
215
+ function pathsResolverResolve(kind, options) {
216
+ pathsResolverAssertApp(options.app);
217
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
218
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
153
219
  }
154
220
  function configDir(options) {
155
- return resolvePath("config", options);
221
+ return pathsResolverResolve("config", options);
156
222
  }
157
- var KIND_ENV, APP_SLUG_RE;
158
- var init_dist = __esm(() => {
159
- KIND_ENV = {
160
- config: "HASNA_CONFIG_HOME",
161
- data: "HASNA_DATA_HOME",
162
- state: "HASNA_STATE_HOME",
163
- cache: "HASNA_CACHE_HOME"
164
- };
165
- APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
166
- });
167
-
168
- // src/lib/app-home.ts
169
- import { existsSync as existsSync2 } from "fs";
170
- import { homedir as homedir2 } from "os";
171
- import { join as join3, resolve } from "path";
172
223
  function homeDir(env = process.env) {
173
- return env["HOME"] || env["USERPROFILE"] || homedir2();
224
+ return env["HOME"] || env["USERPROFILE"] || homedir();
174
225
  }
175
226
  function legacyStoreHome(env = process.env) {
176
- return resolve(join3(homeDir(env), ".hasna", "instructions"));
227
+ return resolve(join2(homeDir(env), ".hasna", "instructions"));
177
228
  }
178
229
  function resolverStoreHome(env = process.env) {
179
- return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE || homedir2() });
230
+ return configDir({ app: "configs", env, home: env.HOME || env.USERPROFILE || homedir() });
180
231
  }
181
232
  function adoptResolverStoreHome(resolved, env = process.env) {
182
233
  const override = env.HASNA_CONFIG_HOME;
183
234
  if (typeof override === "string" && override.trim().length > 0)
184
235
  return true;
185
- return existsSync2(join3(resolved, "instructions.db"));
236
+ return existsSync2(join2(resolved, "instructions.db"));
186
237
  }
187
238
  function exactStoreHome(env = process.env) {
188
239
  const v = env[HASNA_CONFIGS_HOME_ENV];
@@ -196,7 +247,7 @@ function getConfigsStoreHome(env = process.env) {
196
247
  return adoptResolverStoreHome(resolved, env) ? resolve(resolved) : legacyStoreHome(env);
197
248
  }
198
249
  function getConfigsStoreDbPath(env = process.env) {
199
- return join3(getConfigsStoreHome(env), "instructions.db");
250
+ return join2(getConfigsStoreHome(env), "instructions.db");
200
251
  }
201
252
  function getReportedDbPath(env = process.env) {
202
253
  const override = env["HASNA_INSTRUCTIONS_DB_PATH"];
@@ -204,9 +255,15 @@ function getReportedDbPath(env = process.env) {
204
255
  return override;
205
256
  return getConfigsStoreDbPath(env);
206
257
  }
207
- var HASNA_CONFIGS_HOME_ENV = "HASNA_CONFIGS_HOME";
258
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, HASNA_CONFIGS_HOME_ENV = "HASNA_CONFIGS_HOME";
208
259
  var init_app_home = __esm(() => {
209
- init_dist();
260
+ PATHS_RESOLVER_KIND_ENV = {
261
+ config: "HASNA_CONFIG_HOME",
262
+ data: "HASNA_DATA_HOME",
263
+ state: "HASNA_STATE_HOME",
264
+ cache: "HASNA_CACHE_HOME"
265
+ };
266
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
210
267
  });
211
268
 
212
269
  // src/lib/raw-store-root.ts
@@ -221,7 +278,7 @@ var init_raw_store_root = __esm(() => {
221
278
  // src/db/database.ts
222
279
  import { Database } from "bun:sqlite";
223
280
  import { existsSync as existsSync3, mkdirSync, rmSync } from "fs";
224
- import { join as join4 } from "path";
281
+ import { join as join3 } from "path";
225
282
  import { randomUUID } from "crypto";
226
283
  function getDbPath() {
227
284
  if (process.env["HASNA_INSTRUCTIONS_DB_PATH"]) {
@@ -229,7 +286,7 @@ function getDbPath() {
229
286
  }
230
287
  const dir = getRawStoreRoot();
231
288
  mkdirSync(dir, { recursive: true });
232
- return join4(dir, "instructions.db");
289
+ return join3(dir, "instructions.db");
233
290
  }
234
291
  function uuid() {
235
292
  return randomUUID();
@@ -243,9 +300,8 @@ function slugify(name) {
243
300
  function getDatabase(path) {
244
301
  if (_db)
245
302
  return _db;
246
- assertNoLegacyStorageMode();
247
- if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
248
- throw new Error("instructions is using the HTTP API transport (HASNA_INSTRUCTIONS_API_URL set): this command is not wired to the API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
303
+ if (!path && hasInstructionsEnvAuthorityIntent(process.env)) {
304
+ throw new Error("instructions is using the hosted API transport (a HASNA_INSTRUCTIONS_* credential is configured): this command is not wired to the API yet. " + "Point this run at the local store (HASNA_INSTRUCTIONS_LOCAL=1 with no hosted credential) to use it against the local SQLite store.");
249
305
  }
250
306
  const dbPath = path || getDbPath();
251
307
  const db = new Database(dbPath);
@@ -326,7 +382,7 @@ function insertFeedback(input, db) {
326
382
  }
327
383
  var MIGRATIONS, _db = null;
328
384
  var init_database = __esm(() => {
329
- init_retired_storage_mode();
385
+ init_local_opt_in();
330
386
  init_raw_store_root();
331
387
  MIGRATIONS = [
332
388
  `
@@ -707,9 +763,9 @@ var init_template = __esm(() => {
707
763
  });
708
764
 
709
765
  // src/lib/machine.ts
710
- import { arch as currentArch, homedir as homedir3, hostname as currentHostname, type as currentOsType } from "os";
766
+ import { arch as currentArch, homedir as homedir2, hostname as currentHostname, type as currentOsType } from "os";
711
767
  import { existsSync as existsSync4 } from "fs";
712
- import { join as join5 } from "path";
768
+ import { join as join4 } from "path";
713
769
  function normalizeOsFamily(os) {
714
770
  const value = (os ?? "").trim().toLowerCase();
715
771
  if (value === "darwin" || value === "macos" || value === "mac" || value === "osx")
@@ -721,11 +777,11 @@ function normalizeOsFamily(os) {
721
777
  return value || "unknown";
722
778
  }
723
779
  function detectMachineContext(overrides = {}) {
724
- const homeDir2 = overrides.home_dir ?? process.env["CONFIGS_HOME"] ?? process.env["HOME"] ?? homedir3();
780
+ const homeDir2 = overrides.home_dir ?? process.env["CONFIGS_HOME"] ?? process.env["HOME"] ?? homedir2();
725
781
  const os = overrides.os ?? currentOsType();
726
782
  const osFamily = normalizeOsFamily(os);
727
- const bunBinDir = overrides.bun_bin_dir ?? join5(homeDir2, ".bun", "bin");
728
- const defaultBunPath = osFamily === "macos" && existsSync4(BREW_BUN_PATH) ? BREW_BUN_PATH : join5(bunBinDir, "bun");
783
+ const bunBinDir = overrides.bun_bin_dir ?? join4(homeDir2, ".bun", "bin");
784
+ const defaultBunPath = osFamily === "macos" && existsSync4(BREW_BUN_PATH) ? BREW_BUN_PATH : join4(bunBinDir, "bun");
729
785
  return {
730
786
  id: "current-machine",
731
787
  hostname: overrides.hostname ?? currentHostname(),
@@ -735,10 +791,10 @@ function detectMachineContext(overrides = {}) {
735
791
  created_at: "",
736
792
  os_family: osFamily,
737
793
  home_dir: homeDir2,
738
- workspace_root: overrides.workspace_root ?? join5(homeDir2, osFamily === "macos" ? "Workspace" : "workspace"),
794
+ workspace_root: overrides.workspace_root ?? join4(homeDir2, osFamily === "macos" ? "Workspace" : "workspace"),
739
795
  bun_bin_dir: bunBinDir,
740
796
  bun_path: overrides.bun_path ?? defaultBunPath,
741
- path_prefix: overrides.path_prefix ?? (osFamily === "macos" ? `${join5("/opt", "homebrew", "bin")}:${bunBinDir}` : bunBinDir)
797
+ path_prefix: overrides.path_prefix ?? (osFamily === "macos" ? `${join4("/opt", "homebrew", "bin")}:${bunBinDir}` : bunBinDir)
742
798
  };
743
799
  }
744
800
  function machineContextToVariables(machine) {
@@ -5251,12 +5307,10 @@ var init_session_render_contract = __esm(() => {
5251
5307
  });
5252
5308
 
5253
5309
  // src/lib/session-render-state.ts
5254
- var init_session_render_state = __esm(() => {
5255
- init_dist();
5256
- });
5310
+ var init_session_render_state = () => {};
5257
5311
 
5258
5312
  // src/lib/project-context.ts
5259
- import { basename, dirname as dirname2, isAbsolute, join as join6, parse, relative, resolve as resolve3 } from "path";
5313
+ import { basename, dirname as dirname2, isAbsolute, join as join5, parse, relative, resolve as resolve3 } from "path";
5260
5314
  function revisionKey(value) {
5261
5315
  const sequence = value.match(/^(?:rev-)?([0-9]+)$/);
5262
5316
  if (sequence)
@@ -5819,13 +5873,13 @@ var init_asset_plan = __esm(() => {
5819
5873
 
5820
5874
  // src/lib/cursor-authority.ts
5821
5875
  import { createHash } from "crypto";
5822
- import { homedir as homedir4 } from "os";
5823
- import { join as join7, resolve as resolve4 } from "path";
5876
+ import { homedir as homedir3 } from "os";
5877
+ import { join as join6, resolve as resolve4 } from "path";
5824
5878
  function sha256(content) {
5825
5879
  return createHash("sha256").update(content).digest("hex");
5826
5880
  }
5827
5881
  function homeDir2() {
5828
- return process.env["HOME"] || homedir4();
5882
+ return process.env["HOME"] || homedir3();
5829
5883
  }
5830
5884
  function markerPayload(content, markerLine, markerIndex) {
5831
5885
  const index = markerIndex ?? content.indexOf(markerLine);
@@ -5834,7 +5888,7 @@ function markerPayload(content, markerLine, markerIndex) {
5834
5888
  return content.slice(0, index) + content.slice(index + markerLine.length).replace(/^\n/, "");
5835
5889
  }
5836
5890
  function isCursorGlobalAuthorityPath(path) {
5837
- return resolve4(path) === resolve4(join7(homeDir2(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
5891
+ return resolve4(path) === resolve4(join6(homeDir2(), CURSOR_GLOBAL_AUTHORITY_RELATIVE_PATH));
5838
5892
  }
5839
5893
  function stampCursorGlobalAuthorityMarker(content) {
5840
5894
  const existing = content.match(CURSOR_GLOBAL_AUTHORITY_MARKER_PATTERN);
@@ -6506,6 +6560,1222 @@ var init_machines = __esm(() => {
6506
6560
  init_database();
6507
6561
  });
6508
6562
 
6563
+ // ../contracts/dist/client/storage.js
6564
+ import { isIP } from "net";
6565
+ import { spawnSync } from "child_process";
6566
+ import { closeSync, fstatSync, openSync, readFileSync as readFileSync2 } from "fs";
6567
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
6568
+ import { createRequire as createRequire2 } from "module";
6569
+ import { hostname as osHostname } from "os";
6570
+ import { isAbsolute as isAbsolute2, join as join7 } from "path";
6571
+ function envToken2(name) {
6572
+ return name.toUpperCase().replace(/-/g, "_");
6573
+ }
6574
+ function clientTransportEnvKeys2(name) {
6575
+ const envSegment = envToken2(name);
6576
+ return {
6577
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
6578
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
6579
+ };
6580
+ }
6581
+ function credentialOverrideEnvKey2(name) {
6582
+ return `HASNA_${envToken2(name)}_API_KEY_OVERRIDE`;
6583
+ }
6584
+ function credentialPointerEnvKey2(name) {
6585
+ return `HASNA_${envToken2(name)}_API_KEY_REF`;
6586
+ }
6587
+ function homeDir3(env) {
6588
+ const home = env.HOME?.trim();
6589
+ return home ? home : null;
6590
+ }
6591
+ function absoluteOverride(env, key) {
6592
+ const value = env[key]?.trim();
6593
+ return value && isAbsolute2(value) ? value : null;
6594
+ }
6595
+ function hasnaHomeDir(env) {
6596
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
6597
+ if (override)
6598
+ return override;
6599
+ const home = homeDir3(env);
6600
+ return home ? join7(home, HASNA_HOME_DIR) : null;
6601
+ }
6602
+ function appConfigDir(name, env) {
6603
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
6604
+ if (configRoot)
6605
+ return join7(configRoot, name);
6606
+ const root = hasnaHomeDir(env);
6607
+ return root ? join7(root, name, CONFIG_SUBDIR) : null;
6608
+ }
6609
+ function credentialDiskSourceList(name, env, profile = null) {
6610
+ if (!SAFE_APP_SLUG.test(name))
6611
+ return [];
6612
+ const directory = appConfigDir(name, env);
6613
+ if (!directory)
6614
+ return [];
6615
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
6616
+ return [{ path: join7(directory, file), tier: "disk" }];
6617
+ }
6618
+ function credentialDiskSources(name, env) {
6619
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
6620
+ }
6621
+ function profileDiskSources(name, env, profile) {
6622
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
6623
+ }
6624
+ function parseEnvFile(text) {
6625
+ const values = new Map;
6626
+ const unusable = new Set;
6627
+ for (const rawLine of text.split(/\r?\n/)) {
6628
+ const line = rawLine.trim();
6629
+ if (line.length === 0 || line.startsWith("#"))
6630
+ continue;
6631
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
6632
+ const equals = withoutExport.indexOf("=");
6633
+ if (equals <= 0)
6634
+ continue;
6635
+ const key = withoutExport.slice(0, equals).trim();
6636
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
6637
+ continue;
6638
+ let value = withoutExport.slice(equals + 1).trim();
6639
+ const quote = value[0];
6640
+ if (quote === '"' || quote === "'") {
6641
+ if (value.length < 2 || !value.endsWith(quote)) {
6642
+ unusable.add(key);
6643
+ continue;
6644
+ }
6645
+ value = value.slice(1, -1);
6646
+ }
6647
+ if (value.trim().length === 0) {
6648
+ unusable.add(key);
6649
+ continue;
6650
+ }
6651
+ if (values.has(key) && values.get(key) !== value)
6652
+ unusable.add(key);
6653
+ values.set(key, value);
6654
+ }
6655
+ return { values, unusable };
6656
+ }
6657
+ function configFileModeAllowed(mode) {
6658
+ const permissions = mode & 4095;
6659
+ return permissions === 256 || permissions === 384;
6660
+ }
6661
+ function configFileReadsCoherent(before, after) {
6662
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
6663
+ }
6664
+ function readAppConfigFile(path) {
6665
+ const unsafe = (reason) => {
6666
+ throw new CredentialFileUnsafeError(path, reason);
6667
+ };
6668
+ let fd = -1;
6669
+ try {
6670
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
6671
+ } catch (error) {
6672
+ const code = error.code;
6673
+ if (code === "ENOENT" || code === "ENOTDIR")
6674
+ return null;
6675
+ if (code === "ELOOP")
6676
+ unsafe("the path is a symlink");
6677
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
6678
+ }
6679
+ try {
6680
+ const before = fstatSync(fd);
6681
+ if (!before.isFile())
6682
+ unsafe("the path is not a regular file");
6683
+ if (!configFileModeAllowed(before.mode)) {
6684
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
6685
+ }
6686
+ const uid = process.getuid?.() ?? process.geteuid?.();
6687
+ if (uid !== undefined && before.uid !== uid)
6688
+ unsafe("the file is not owned by the current user");
6689
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
6690
+ unsafe("the file exceeds the size limit");
6691
+ const bytes = readFileSync2(fd);
6692
+ const after = fstatSync(fd);
6693
+ if (!configFileReadsCoherent(before, after)) {
6694
+ unsafe("the file changed while being read");
6695
+ }
6696
+ return parseEnvFile(bytes.toString("utf8"));
6697
+ } finally {
6698
+ if (fd !== -1)
6699
+ closeSync(fd);
6700
+ }
6701
+ }
6702
+ function readCredentialFile(path, apiKeyKeys) {
6703
+ const parsed = readAppConfigFile(path);
6704
+ if (!parsed)
6705
+ return null;
6706
+ for (const key of apiKeyKeys) {
6707
+ if (parsed.unusable.has(key)) {
6708
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
6709
+ }
6710
+ }
6711
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
6712
+ if (new Set(values).size > 1) {
6713
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
6714
+ }
6715
+ return values[0] ?? null;
6716
+ }
6717
+ function appConfigDiskValue(name, env, keys) {
6718
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
6719
+ if (wanted.length === 0)
6720
+ return null;
6721
+ for (const path of credentialDiskSources(name, env)) {
6722
+ const parsed = readAppConfigFile(path);
6723
+ if (!parsed)
6724
+ continue;
6725
+ if (wanted.some((key) => parsed.unusable.has(key))) {
6726
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
6727
+ }
6728
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
6729
+ if (new Set(values).size > 1)
6730
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
6731
+ for (const key of wanted) {
6732
+ if (parsed.unusable.has(key))
6733
+ return { key, value: "", path, unusable: true };
6734
+ const value = parsed.values.get(key)?.trim();
6735
+ if (value)
6736
+ return { key, value, path };
6737
+ }
6738
+ }
6739
+ return null;
6740
+ }
6741
+ function assertUsableCredential(appName, source, value) {
6742
+ if (VAULT_POINTER_SHAPE.test(value)) {
6743
+ 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 ${credentialPointerEnvKey2(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
6744
+ }
6745
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
6746
+ return;
6747
+ 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]);
6748
+ }
6749
+ function sealCredential(fields) {
6750
+ const { apiKey } = fields;
6751
+ const visible = {
6752
+ tier: fields.tier,
6753
+ source: fields.source,
6754
+ deliberate: fields.deliberate,
6755
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
6756
+ warning: fields.warning
6757
+ };
6758
+ const sealed = { ...visible };
6759
+ Object.defineProperty(sealed, "apiKey", {
6760
+ value: apiKey,
6761
+ enumerable: false,
6762
+ writable: false,
6763
+ configurable: false
6764
+ });
6765
+ if (fields.pointerVaultKey !== undefined) {
6766
+ Object.defineProperty(sealed, "pointerVaultKey", {
6767
+ value: fields.pointerVaultKey,
6768
+ enumerable: false,
6769
+ writable: false,
6770
+ configurable: false
6771
+ });
6772
+ }
6773
+ Object.defineProperty(sealed, INSPECT_CUSTOM2, {
6774
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
6775
+ enumerable: false,
6776
+ writable: false,
6777
+ configurable: false
6778
+ });
6779
+ Object.defineProperty(sealed, CREDENTIAL_SEAL2, {
6780
+ value: true,
6781
+ enumerable: false,
6782
+ writable: false,
6783
+ configurable: false
6784
+ });
6785
+ return Object.freeze(sealed);
6786
+ }
6787
+ function isSealedCredential(credential) {
6788
+ return credential[CREDENTIAL_SEAL2] === true;
6789
+ }
6790
+ function explicitCredential(appName, apiKey) {
6791
+ const source = "explicit apiKey option";
6792
+ assertUsableCredential(appName, source, apiKey);
6793
+ return sealCredential({
6794
+ apiKey,
6795
+ tier: "argument",
6796
+ source,
6797
+ deliberate: true,
6798
+ diskCandidates: [],
6799
+ warning: null
6800
+ });
6801
+ }
6802
+ function validateAndSealResolvedCredential(appName, credential) {
6803
+ const apiKey = credential.apiKey;
6804
+ assertUsableCredential(appName, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE, apiKey);
6805
+ if (!isSealedCredential(credential)) {
6806
+ return sealCredential({
6807
+ apiKey,
6808
+ tier: "argument",
6809
+ source: CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE,
6810
+ deliberate: true,
6811
+ diskCandidates: [],
6812
+ warning: null
6813
+ });
6814
+ }
6815
+ return sealCredential({
6816
+ apiKey,
6817
+ tier: credential.tier,
6818
+ source: credential.source,
6819
+ deliberate: credential.deliberate,
6820
+ diskCandidates: credential.diskCandidates,
6821
+ warning: credential.warning,
6822
+ ...credential.pointerVaultKey !== undefined ? { pointerVaultKey: credential.pointerVaultKey } : {}
6823
+ });
6824
+ }
6825
+ function firstEnvValue(env, keys) {
6826
+ for (const key of keys) {
6827
+ if (!Object.prototype.hasOwnProperty.call(env, key))
6828
+ continue;
6829
+ const value = env[key]?.trim();
6830
+ if (value)
6831
+ return { key, value };
6832
+ }
6833
+ return null;
6834
+ }
6835
+ function isAmbientEnvironment(env) {
6836
+ return env === process.env || env[AMBIENT_ENVIRONMENT2] === true;
6837
+ }
6838
+ function defaultKeychainRunner(argv) {
6839
+ const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
6840
+ encoding: "utf8",
6841
+ stdio: ["ignore", "pipe", "pipe"],
6842
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
6843
+ });
6844
+ return {
6845
+ status: result.status,
6846
+ stdout: result.stdout ?? "",
6847
+ stderr: result.error ? result.error.message : result.stderr ?? ""
6848
+ };
6849
+ }
6850
+ function keychainTierEnabled(env, options) {
6851
+ if ((options.platform ?? process.platform) !== "darwin")
6852
+ return false;
6853
+ if (options.enabled !== undefined)
6854
+ return options.enabled;
6855
+ return options.run !== undefined || isAmbientEnvironment(env);
6856
+ }
6857
+ function keychainAccount(env, options) {
6858
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
6859
+ if (station)
6860
+ return station;
6861
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
6862
+ if (host)
6863
+ return host;
6864
+ const user = env.USER?.trim();
6865
+ return user || null;
6866
+ }
6867
+ function keychainFailureHint(text) {
6868
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
6869
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
6870
+ return clean ? `: ${clean}` : "";
6871
+ }
6872
+ function readKeychainItem(name, env, kind, options) {
6873
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
6874
+ return null;
6875
+ const account = keychainAccount(env, options);
6876
+ if (!account)
6877
+ return null;
6878
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
6879
+ const source = `keychain:${service}@${account}`;
6880
+ const run = options.run ?? defaultKeychainRunner;
6881
+ let result;
6882
+ try {
6883
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
6884
+ } catch (error) {
6885
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
6886
+ 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]);
6887
+ }
6888
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
6889
+ return null;
6890
+ if (result.status !== 0) {
6891
+ 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]);
6892
+ }
6893
+ const value = result.stdout.trim();
6894
+ if (!value) {
6895
+ 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]);
6896
+ }
6897
+ return { value, source };
6898
+ }
6899
+ function keychainConfigValue(name, env, options = {}) {
6900
+ return readKeychainItem(name, env, "api-url", options);
6901
+ }
6902
+ function snapshotClientEnvironment(name, env) {
6903
+ const keys = clientTransportEnvKeys2(name);
6904
+ const ambient = isAmbientEnvironment(env);
6905
+ const snapshot = Object.create(null);
6906
+ for (const key of [
6907
+ ...keys.apiUrlKeys,
6908
+ ...keys.apiKeyKeys,
6909
+ credentialOverrideEnvKey2(name),
6910
+ credentialPointerEnvKey2(name),
6911
+ CREDENTIAL_PROFILE_ENV_KEY2,
6912
+ "HOME",
6913
+ HASNA_HOME_ENV_KEY,
6914
+ HASNA_CONFIG_HOME_ENV_KEY,
6915
+ KEYCHAIN_STATION_ENV_KEY,
6916
+ "USER"
6917
+ ]) {
6918
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
6919
+ if (!descriptor)
6920
+ continue;
6921
+ if (!("value" in descriptor)) {
6922
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
6923
+ }
6924
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
6925
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
6926
+ }
6927
+ snapshot[key] = descriptor.value;
6928
+ }
6929
+ if (ambient) {
6930
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT2, {
6931
+ value: true,
6932
+ enumerable: false,
6933
+ writable: false,
6934
+ configurable: false
6935
+ });
6936
+ }
6937
+ return Object.freeze(snapshot);
6938
+ }
6939
+ function resolveCredential(name, env, options = {}) {
6940
+ env = snapshotClientEnvironment(name, env);
6941
+ const { apiKeyKeys } = clientTransportEnvKeys2(name);
6942
+ const diskPaths = credentialDiskSources(name, env);
6943
+ if (options.apiKey !== undefined) {
6944
+ const explicitKey = options.apiKey.trim();
6945
+ if (!explicitKey) {
6946
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
6947
+ }
6948
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
6949
+ return sealCredential({
6950
+ apiKey: explicitKey,
6951
+ tier: "argument",
6952
+ source: "explicit apiKey argument",
6953
+ deliberate: true,
6954
+ diskCandidates: diskPaths,
6955
+ warning: null
6956
+ });
6957
+ }
6958
+ const overrideKeyName = credentialOverrideEnvKey2(name);
6959
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
6960
+ if (overrideRaw !== undefined) {
6961
+ const override = overrideRaw.trim();
6962
+ if (!override) {
6963
+ 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]);
6964
+ }
6965
+ assertUsableCredential(name, overrideKeyName, override);
6966
+ return sealCredential({
6967
+ apiKey: override,
6968
+ tier: "override",
6969
+ source: overrideKeyName,
6970
+ deliberate: true,
6971
+ diskCandidates: diskPaths,
6972
+ warning: null
6973
+ });
6974
+ }
6975
+ const pointerKeyName = credentialPointerEnvKey2(name);
6976
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
6977
+ if (pointerRaw !== undefined) {
6978
+ const pointer = pointerRaw.trim();
6979
+ if (!pointer) {
6980
+ 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]);
6981
+ }
6982
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
6983
+ 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]);
6984
+ }
6985
+ return sealCredential({
6986
+ apiKey: "",
6987
+ pointerVaultKey: pointer,
6988
+ tier: "pointer",
6989
+ source: pointerKeyName,
6990
+ deliberate: true,
6991
+ diskCandidates: diskPaths,
6992
+ warning: null
6993
+ });
6994
+ }
6995
+ if (options.profile !== undefined && !options.profile.trim()) {
6996
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
6997
+ }
6998
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY2) ? env[CREDENTIAL_PROFILE_ENV_KEY2] : undefined;
6999
+ if (profileRaw !== undefined && !profileRaw.trim()) {
7000
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY2} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY2]);
7001
+ }
7002
+ const profile = options.profile?.trim() || profileRaw?.trim();
7003
+ if (profile) {
7004
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY2;
7005
+ if (!SAFE_PROFILE.test(profile)) {
7006
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
7007
+ }
7008
+ const paths = profileDiskSources(name, env, profile);
7009
+ for (const path of paths) {
7010
+ const value = readCredentialFile(path, apiKeyKeys);
7011
+ if (value) {
7012
+ assertUsableCredential(name, path, value);
7013
+ return sealCredential({
7014
+ apiKey: value,
7015
+ tier: "profile",
7016
+ source: path,
7017
+ deliberate: true,
7018
+ diskCandidates: paths,
7019
+ warning: null
7020
+ });
7021
+ }
7022
+ }
7023
+ 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_KEY2}.`, paths);
7024
+ }
7025
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
7026
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
7027
+ if (blankEnv) {
7028
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
7029
+ }
7030
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
7031
+ 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));
7032
+ }
7033
+ const envHit = firstEnvValue(env, apiKeyKeys);
7034
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
7035
+ if (keychainHit) {
7036
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
7037
+ 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;
7038
+ return sealCredential({
7039
+ apiKey: keychainHit.value,
7040
+ tier: "keychain",
7041
+ source: keychainHit.source,
7042
+ deliberate: false,
7043
+ diskCandidates: diskPaths,
7044
+ warning
7045
+ });
7046
+ }
7047
+ const diskSourceList = credentialDiskSourceList(name, env, null);
7048
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
7049
+ if (diskHits.length > 0) {
7050
+ const winner = diskHits[0];
7051
+ assertUsableCredential(name, winner.src.path, winner.value);
7052
+ const divergentSources = [
7053
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
7054
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
7055
+ ];
7056
+ 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;
7057
+ return sealCredential({
7058
+ apiKey: winner.value,
7059
+ tier: winner.src.tier,
7060
+ source: winner.src.path,
7061
+ deliberate: false,
7062
+ diskCandidates: diskPaths,
7063
+ warning
7064
+ });
7065
+ }
7066
+ if (envHit) {
7067
+ assertUsableCredential(name, envHit.key, envHit.value);
7068
+ return sealCredential({
7069
+ apiKey: envHit.value,
7070
+ tier: "env",
7071
+ source: envHit.key,
7072
+ deliberate: false,
7073
+ diskCandidates: diskPaths,
7074
+ warning: null
7075
+ });
7076
+ }
7077
+ return null;
7078
+ }
7079
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
7080
+ const vaultKey = pointerResolution.pointerVaultKey;
7081
+ const pointerEnvKey = pointerResolution.source;
7082
+ if (!vaultKey) {
7083
+ throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
7084
+ }
7085
+ let secretsSdk;
7086
+ try {
7087
+ secretsSdk = requireSecretsSdk2(SECRETS_PACKAGE_SPECIFIER2);
7088
+ } catch {
7089
+ throw new CredentialResolutionError(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]);
7090
+ }
7091
+ let client;
7092
+ try {
7093
+ client = secretsSdk.createSecretsClientFromEnv(env);
7094
+ } catch {
7095
+ throw new CredentialResolutionError(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]);
7096
+ }
7097
+ let secret;
7098
+ try {
7099
+ secret = await client.getSecret({ key: vaultKey });
7100
+ } catch {
7101
+ throw new CredentialResolutionError(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]);
7102
+ }
7103
+ const value = secret.value;
7104
+ if (!value) {
7105
+ throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
7106
+ }
7107
+ assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
7108
+ return sealCredential({
7109
+ apiKey: value,
7110
+ tier: "pointer",
7111
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
7112
+ deliberate: true,
7113
+ diskCandidates: pointerResolution.diskCandidates,
7114
+ warning: null
7115
+ });
7116
+ }
7117
+ function defaultFleetGatewayBaseUrl(name) {
7118
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
7119
+ }
7120
+ function isValidDnsDomain(value) {
7121
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
7122
+ return false;
7123
+ }
7124
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
7125
+ }
7126
+ function validateAppSlug(name) {
7127
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
7128
+ throw new Error("App name must be one lowercase DNS label.");
7129
+ }
7130
+ return name;
7131
+ }
7132
+ function rawAuthority(value) {
7133
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
7134
+ if (!match)
7135
+ throw new Error("API URL must be absolute.");
7136
+ const afterScheme = value.slice(match[0].length);
7137
+ const boundary = afterScheme.search(/[/?#]/);
7138
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
7139
+ if (!authority)
7140
+ throw new Error("API URL must include a hostname.");
7141
+ return authority;
7142
+ }
7143
+ function assertCanonicalPort(port) {
7144
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
7145
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
7146
+ }
7147
+ const numericPort = Number(port);
7148
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
7149
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
7150
+ }
7151
+ }
7152
+ function canonicalAuthorityHostname(authority) {
7153
+ let rawHostname;
7154
+ if (authority.startsWith("[")) {
7155
+ const closingBracket = authority.indexOf("]");
7156
+ if (closingBracket === -1) {
7157
+ throw new Error("API URL authority must contain a canonical hostname.");
7158
+ }
7159
+ rawHostname = authority.slice(0, closingBracket + 1);
7160
+ const portSuffix = authority.slice(closingBracket + 1);
7161
+ if (portSuffix) {
7162
+ if (!portSuffix.startsWith(":")) {
7163
+ throw new Error("API URL authority must contain a canonical hostname and port.");
7164
+ }
7165
+ assertCanonicalPort(portSuffix.slice(1));
7166
+ }
7167
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
7168
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
7169
+ }
7170
+ } else {
7171
+ const firstColon = authority.indexOf(":");
7172
+ const lastColon = authority.lastIndexOf(":");
7173
+ if (firstColon !== lastColon) {
7174
+ throw new Error("IPv6 API URL authorities must use brackets.");
7175
+ }
7176
+ if (lastColon !== -1) {
7177
+ const port = authority.slice(lastColon + 1);
7178
+ assertCanonicalPort(port);
7179
+ rawHostname = authority.slice(0, lastColon);
7180
+ } else {
7181
+ rawHostname = authority;
7182
+ }
7183
+ const ipVersion = isIP(rawHostname);
7184
+ const numericAddressParts = rawHostname.split(".");
7185
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
7186
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
7187
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
7188
+ }
7189
+ }
7190
+ return rawHostname.toLowerCase();
7191
+ }
7192
+ function isDeliberateLoopbackHttpAuthority(authority) {
7193
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
7194
+ }
7195
+ function toV1BaseUrl(apiUrl) {
7196
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
7197
+ throw new Error("API URL must not contain ASCII control characters.");
7198
+ }
7199
+ const input = apiUrl.trim();
7200
+ const authority = rawAuthority(input);
7201
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
7202
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
7203
+ }
7204
+ const canonicalHostname = canonicalAuthorityHostname(authority);
7205
+ const url = new URL(input);
7206
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7207
+ throw new Error("API URL must use http or https.");
7208
+ }
7209
+ if (url.username || url.password) {
7210
+ throw new Error("API URL must not include credentials.");
7211
+ }
7212
+ if (!url.hostname || url.hostname.endsWith(".")) {
7213
+ throw new Error("API URL must include a canonical hostname.");
7214
+ }
7215
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
7216
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
7217
+ }
7218
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
7219
+ throw new Error("API URL must not use IDN or punycode hostnames.");
7220
+ }
7221
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
7222
+ throw new Error("API URL may use http only for an exact loopback authority.");
7223
+ }
7224
+ if (url.search || url.hash) {
7225
+ throw new Error("API URL must not include a query string or fragment.");
7226
+ }
7227
+ let path = url.pathname.replace(/\/+$/, "");
7228
+ if (path.endsWith("/v1"))
7229
+ path = path.slice(0, -"/v1".length);
7230
+ url.pathname = `${path}/v1`;
7231
+ return url.toString().replace(/\/+$/, "");
7232
+ }
7233
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
7234
+ env = snapshotClientEnvironment(name, env);
7235
+ const keys = clientTransportEnvKeys2(name);
7236
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
7237
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
7238
+ if (blankUrl) {
7239
+ 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]);
7240
+ }
7241
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
7242
+ if (controlledUrl) {
7243
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
7244
+ }
7245
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
7246
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
7247
+ 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));
7248
+ }
7249
+ const envUrlHit = usableUrlEntries[0] ?? null;
7250
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
7251
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
7252
+ if (diskConfigUrlHit?.unusable) {
7253
+ 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]);
7254
+ }
7255
+ const urlCandidates = [
7256
+ ...envUrlHit ? [envUrlHit] : [],
7257
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
7258
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
7259
+ ];
7260
+ const configuredUrl = urlCandidates[0] ?? null;
7261
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
7262
+ if (configuredUrl && divergentUrls.length > 0) {
7263
+ 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));
7264
+ }
7265
+ const warnings = [];
7266
+ if (configuredUrl && !envUrlHit) {
7267
+ 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.`);
7268
+ }
7269
+ const credential = resolveCredential(name, env, options.credentials);
7270
+ if (!credential) {
7271
+ const diskHint = credentialDiskSourcesForMessage(name, env);
7272
+ 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`;
7273
+ 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.`);
7274
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
7275
+ }
7276
+ if (credential.warning)
7277
+ warnings.push(credential.warning);
7278
+ let urlHit;
7279
+ if (configuredUrl) {
7280
+ urlHit = configuredUrl;
7281
+ } else {
7282
+ try {
7283
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
7284
+ } catch (error) {
7285
+ const message = error instanceof Error ? error.message : String(error);
7286
+ 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]]);
7287
+ }
7288
+ }
7289
+ const apiUrlSource = urlHit.key;
7290
+ let baseUrl;
7291
+ try {
7292
+ baseUrl = toV1BaseUrl(urlHit.value);
7293
+ } catch (error) {
7294
+ const message = error instanceof Error ? error.message : String(error);
7295
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
7296
+ }
7297
+ return {
7298
+ resolution: {
7299
+ transport: "http",
7300
+ transportSource: urlHit.key,
7301
+ baseUrl,
7302
+ apiUrlSource,
7303
+ apiKeyPresent: true,
7304
+ apiKeySource: credential.source,
7305
+ apiKeyTier: credential.tier,
7306
+ misconfigured: false,
7307
+ warning: warnings.length > 0 ? warnings.join(" ") : null
7308
+ },
7309
+ credential
7310
+ };
7311
+ }
7312
+ function credentialDiskSourcesForMessage(name, env) {
7313
+ const paths = credentialDiskSources(name, env);
7314
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
7315
+ }
7316
+ function currentCredential(name, apiKey) {
7317
+ if (typeof apiKey === "function") {
7318
+ return validateAndSealResolvedCredential(name, apiKey());
7319
+ }
7320
+ return explicitCredential(name, apiKey);
7321
+ }
7322
+ async function resolveRequestCredential(name, apiKey, env = process.env) {
7323
+ const resolved = currentCredential(name, apiKey);
7324
+ if (resolved.tier === "pointer") {
7325
+ return completePointerCredential(name, resolved, env);
7326
+ }
7327
+ return resolved;
7328
+ }
7329
+ function authFailureGuidance(credential) {
7330
+ const origin = `The API key for this request came from ${credential.source}`;
7331
+ if (credential.deliberate) {
7332
+ 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.`;
7333
+ 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}`;
7334
+ }
7335
+ if (credential.tier === "env") {
7336
+ const target = credential.diskCandidates[0];
7337
+ 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.`;
7338
+ 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}`;
7339
+ }
7340
+ if (credential.tier === "keychain") {
7341
+ 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.`;
7342
+ }
7343
+ 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.`;
7344
+ }
7345
+ function assertNoAuthorityOverrideHeaders(headers, source) {
7346
+ if (!headers)
7347
+ return;
7348
+ const forbidden = Object.keys(headers).find((name) => AUTHORITY_OVERRIDE_HEADERS2.has(name.trim().toLowerCase()));
7349
+ if (forbidden) {
7350
+ throw new Error(`Authenticated ${source} headers must not set authority header '${forbidden}'.`);
7351
+ }
7352
+ }
7353
+ function appendQuery(path, query) {
7354
+ if (!query)
7355
+ return path;
7356
+ const params = query instanceof URLSearchParams ? query : new URLSearchParams;
7357
+ if (!(query instanceof URLSearchParams)) {
7358
+ for (const [key, value] of Object.entries(query)) {
7359
+ if (value === null || value === undefined)
7360
+ continue;
7361
+ if (Array.isArray(value)) {
7362
+ for (const v of value)
7363
+ params.append(key, String(v));
7364
+ } else {
7365
+ params.append(key, String(value));
7366
+ }
7367
+ }
7368
+ }
7369
+ const qs = params.toString();
7370
+ if (!qs)
7371
+ return path;
7372
+ return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
7373
+ }
7374
+ function createHasnaHttpTransportInternal(options, requestBindingProvider) {
7375
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
7376
+ const base = toV1BaseUrl(options.baseUrl);
7377
+ const timeoutMs = options.timeoutMs ?? 30000;
7378
+ const sleep = options.sleepImpl ?? defaultSleep;
7379
+ const defaultRetry = options.retry;
7380
+ function resolveRetry(callRetry) {
7381
+ const chosen = callRetry !== undefined ? callRetry : defaultRetry;
7382
+ if (chosen === false)
7383
+ return null;
7384
+ const r = chosen ?? {};
7385
+ return {
7386
+ retries: r.retries ?? 2,
7387
+ baseDelayMs: r.baseDelayMs ?? 200,
7388
+ maxDelayMs: r.maxDelayMs ?? 2000,
7389
+ retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
7390
+ };
7391
+ }
7392
+ async function once(method, rel, url, body, opts, credential) {
7393
+ assertNoAuthorityOverrideHeaders(options.headers, "transport");
7394
+ assertNoAuthorityOverrideHeaders(opts.headers, "request");
7395
+ const headers = {
7396
+ "x-api-key": credential.apiKey,
7397
+ Authorization: `Bearer ${credential.apiKey}`,
7398
+ Accept: "application/json",
7399
+ ...options.headers ?? {},
7400
+ ...opts.headers ?? {}
7401
+ };
7402
+ if (opts.idempotencyKey)
7403
+ headers["Idempotency-Key"] = opts.idempotencyKey;
7404
+ const init = {
7405
+ method,
7406
+ headers,
7407
+ redirect: "manual"
7408
+ };
7409
+ if (body !== undefined) {
7410
+ headers["Content-Type"] = "application/json";
7411
+ init.body = JSON.stringify(body);
7412
+ }
7413
+ const controller = new AbortController;
7414
+ const onAbort = () => controller.abort();
7415
+ if (opts.signal) {
7416
+ if (opts.signal.aborted)
7417
+ controller.abort();
7418
+ else
7419
+ opts.signal.addEventListener("abort", onAbort, { once: true });
7420
+ }
7421
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
7422
+ init.signal = controller.signal;
7423
+ let response;
7424
+ try {
7425
+ response = await fetchImpl(url, init);
7426
+ } catch (error) {
7427
+ const err = error instanceof Error ? error : new Error(String(error));
7428
+ if (opts.signal?.aborted)
7429
+ return { ok: false, retryable: false, error: err };
7430
+ return { ok: false, retryable: true, error: err };
7431
+ } finally {
7432
+ clearTimeout(timer);
7433
+ if (opts.signal)
7434
+ opts.signal.removeEventListener("abort", onAbort);
7435
+ }
7436
+ const authenticationFailure = response.status === 401 || response.status === 403;
7437
+ let parsed = undefined;
7438
+ if (authenticationFailure) {
7439
+ try {
7440
+ await response.body?.cancel();
7441
+ } catch {}
7442
+ } else {
7443
+ const text = await response.text();
7444
+ if (text.length > 0) {
7445
+ try {
7446
+ parsed = JSON.parse(text);
7447
+ } catch {
7448
+ parsed = text;
7449
+ }
7450
+ }
7451
+ }
7452
+ if (!response.ok) {
7453
+ if (response.status >= 300 && response.status < 400) {
7454
+ return {
7455
+ ok: false,
7456
+ retryable: false,
7457
+ error: new HasnaHttpError(method, rel, response.status, parsed)
7458
+ };
7459
+ }
7460
+ if (authenticationFailure) {
7461
+ return {
7462
+ ok: false,
7463
+ retryable: false,
7464
+ error: new HasnaHttpError(method, rel, response.status, undefined, {
7465
+ source: credential.source,
7466
+ tier: credential.tier,
7467
+ guidance: authFailureGuidance(credential)
7468
+ })
7469
+ };
7470
+ }
7471
+ const retry = resolveRetry(opts.retry);
7472
+ const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
7473
+ return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
7474
+ }
7475
+ return { ok: true, value: parsed };
7476
+ }
7477
+ async function request(method, path, body, opts = {}) {
7478
+ const upper = method.toUpperCase();
7479
+ const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
7480
+ const retry = resolveRetry(opts.retry);
7481
+ const methodRetryable = IDEMPOTENT_METHODS2.has(upper) || Boolean(opts.idempotencyKey);
7482
+ const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
7483
+ const binding = requestBindingProvider ? await requestBindingProvider() : {
7484
+ baseUrl: base,
7485
+ credential: await resolveRequestCredential(options.name, options.apiKey)
7486
+ };
7487
+ const url = `${binding.baseUrl}${rel}`;
7488
+ const credential = binding.credential;
7489
+ let last = null;
7490
+ for (let attempt = 1;attempt <= maxAttempts; attempt++) {
7491
+ const result = await once(upper, rel, url, body, opts, credential);
7492
+ if (result.ok)
7493
+ return result.value;
7494
+ last = result;
7495
+ const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
7496
+ if (!canRetry)
7497
+ break;
7498
+ const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
7499
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
7500
+ await sleep(backoff + jitter);
7501
+ }
7502
+ throw last.error;
7503
+ }
7504
+ return {
7505
+ baseUrl: base,
7506
+ request,
7507
+ get: (path, opts) => request("GET", path, undefined, opts),
7508
+ post: (path, body, opts) => request("POST", path, body, opts),
7509
+ put: (path, body, opts) => request("PUT", path, body, opts),
7510
+ patch: (path, body, opts) => request("PATCH", path, body, opts),
7511
+ del: (path, body, opts) => request("DELETE", path, body, opts)
7512
+ };
7513
+ }
7514
+ function createClientTransport(name, env = process.env, overrides) {
7515
+ const credentialOptions = overrides?.credentials;
7516
+ const snapshotOptions = { ...credentialOptions ? { credentials: credentialOptions } : {} };
7517
+ const resolution = resolveClientTransportSnapshot(name, env, snapshotOptions).resolution;
7518
+ 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;
7519
+ const unstableConfiguration = () => new ClientTransportConfigurationError(name, "The configured service authority or credential changed while a request was being prepared; no authenticated request was sent.");
7520
+ const requestBindingProvider = async () => {
7521
+ const first = resolveClientTransportSnapshot(name, env, snapshotOptions);
7522
+ const reviewed = resolveClientTransportSnapshot(name, env, snapshotOptions);
7523
+ if (!sameBinding(first, reviewed))
7524
+ throw unstableConfiguration();
7525
+ if (reviewed.resolution.baseUrl !== resolution.baseUrl) {
7526
+ throw new ClientTransportConfigurationError(name, "The configured service authority changed; rebuild the client before sending credentials.");
7527
+ }
7528
+ const credential = await resolveRequestCredential(name, () => reviewed.credential, env);
7529
+ const immediatelyBeforeDispatch = resolveClientTransportSnapshot(name, env, snapshotOptions);
7530
+ if (!sameBinding(reviewed, immediatelyBeforeDispatch))
7531
+ throw unstableConfiguration();
7532
+ if (immediatelyBeforeDispatch.resolution.baseUrl !== resolution.baseUrl) {
7533
+ throw new ClientTransportConfigurationError(name, "The configured service authority changed; rebuild the client before sending credentials.");
7534
+ }
7535
+ return { baseUrl: immediatelyBeforeDispatch.resolution.baseUrl, credential };
7536
+ };
7537
+ return {
7538
+ transport: "http",
7539
+ client: createHasnaHttpTransportInternal({
7540
+ name,
7541
+ baseUrl: resolution.baseUrl,
7542
+ apiKey: () => {
7543
+ throw new Error("The authenticated request binding provider was not invoked.");
7544
+ },
7545
+ ...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
7546
+ ...overrides?.headers ? { headers: overrides.headers } : {},
7547
+ ...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
7548
+ ...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
7549
+ ...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
7550
+ }, requestBindingProvider),
7551
+ resolution
7552
+ };
7553
+ }
7554
+ function resourcePath(resource) {
7555
+ const trimmed = resource.replace(/^\/+|\/+$/g, "");
7556
+ if (!trimmed)
7557
+ throw new Error("resource must be a non-empty path segment");
7558
+ return `/${trimmed}`;
7559
+ }
7560
+ function entityPath(resource, id) {
7561
+ if (id === undefined || id === null || `${id}`.length === 0) {
7562
+ throw new Error("id must be a non-empty string");
7563
+ }
7564
+ return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
7565
+ }
7566
+ function newIdempotencyKey() {
7567
+ const g = globalThis;
7568
+ if (g.crypto?.randomUUID)
7569
+ return g.crypto.randomUUID();
7570
+ return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
7571
+ }
7572
+ function extractItems(raw) {
7573
+ if (Array.isArray(raw))
7574
+ return raw;
7575
+ if (raw && typeof raw === "object") {
7576
+ const obj = raw;
7577
+ for (const key of ["items", "data", "results", "rows", "records"]) {
7578
+ if (Array.isArray(obj[key]))
7579
+ return obj[key];
7580
+ }
7581
+ }
7582
+ return [];
7583
+ }
7584
+ function extractTotal(raw) {
7585
+ if (raw && typeof raw === "object") {
7586
+ const obj = raw;
7587
+ for (const key of ["total", "count", "totalCount", "total_count"]) {
7588
+ if (typeof obj[key] === "number")
7589
+ return obj[key];
7590
+ }
7591
+ }
7592
+ return null;
7593
+ }
7594
+ function extractCursor(raw) {
7595
+ if (raw && typeof raw === "object") {
7596
+ const obj = raw;
7597
+ for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
7598
+ if (typeof obj[key] === "string")
7599
+ return obj[key];
7600
+ }
7601
+ }
7602
+ return null;
7603
+ }
7604
+ function isNotFoundHttpError(error) {
7605
+ return typeof error === "object" && error !== null && error.name === "HasnaHttpError" && error.status === 404;
7606
+ }
7607
+ function createHasnaStorageClient(name, transport) {
7608
+ return {
7609
+ name,
7610
+ baseUrl: transport.baseUrl,
7611
+ transport,
7612
+ async list(resource, options = {}) {
7613
+ const raw = await transport.get(resourcePath(resource), options);
7614
+ return {
7615
+ items: extractItems(raw),
7616
+ total: extractTotal(raw),
7617
+ cursor: extractCursor(raw),
7618
+ raw
7619
+ };
7620
+ },
7621
+ async get(resource, id, options = {}) {
7622
+ try {
7623
+ return await transport.get(entityPath(resource, id), options);
7624
+ } catch (error) {
7625
+ if (isNotFoundHttpError(error))
7626
+ return null;
7627
+ throw error;
7628
+ }
7629
+ },
7630
+ async create(resource, body, options = {}) {
7631
+ const { idempotencyKey, ...rest } = options;
7632
+ return transport.post(resourcePath(resource), body, {
7633
+ ...rest,
7634
+ idempotencyKey: idempotencyKey ?? newIdempotencyKey()
7635
+ });
7636
+ },
7637
+ async update(resource, id, patch, options = {}) {
7638
+ const { method = "PATCH", idempotencyKey, ...rest } = options;
7639
+ const call = method === "PUT" ? transport.put : transport.patch;
7640
+ return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
7641
+ },
7642
+ async delete(resource, id, options = {}) {
7643
+ try {
7644
+ await transport.del(entityPath(resource, id), undefined, options);
7645
+ } catch (error) {
7646
+ if (isNotFoundHttpError(error))
7647
+ return;
7648
+ throw error;
7649
+ }
7650
+ }
7651
+ };
7652
+ }
7653
+ function resolveStorageClient(name, env = process.env, overrides) {
7654
+ const wired = createClientTransport(name, env, overrides);
7655
+ return { transport: "http", client: createHasnaStorageClient(name, wired.client) };
7656
+ }
7657
+ var CREDENTIAL_PROFILE_ENV_KEY2 = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, CALLER_SUPPLIED_CREDENTIAL_PROVIDER_SOURCE = "caller-supplied CredentialProvider", AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, HasnaHttpError, DEFAULT_RETRY_STATUSES, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2, defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
7658
+ var init_storage = __esm(() => {
7659
+ CredentialResolutionError = class CredentialResolutionError extends Error {
7660
+ appName;
7661
+ attempted;
7662
+ constructor(appName, message, attempted) {
7663
+ super(message);
7664
+ this.name = "CredentialResolutionError";
7665
+ this.appName = appName;
7666
+ this.attempted = attempted;
7667
+ }
7668
+ };
7669
+ CredentialFileUnsafeError = class CredentialFileUnsafeError extends Error {
7670
+ path;
7671
+ constructor(path, reason) {
7672
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
7673
+ this.name = "CredentialFileUnsafeError";
7674
+ this.path = path;
7675
+ }
7676
+ };
7677
+ MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
7678
+ SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7679
+ SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
7680
+ ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
7681
+ VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
7682
+ CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
7683
+ INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
7684
+ CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
7685
+ AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
7686
+ SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
7687
+ requireSecretsSdk2 = createRequire2(import.meta.url);
7688
+ ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
7689
+ DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7690
+ ClientTransportConfigurationError = class ClientTransportConfigurationError extends Error {
7691
+ appName;
7692
+ sources;
7693
+ constructor(appName, message, sources = []) {
7694
+ super(message);
7695
+ this.name = "ClientTransportConfigurationError";
7696
+ this.appName = appName;
7697
+ this.sources = Object.freeze([...sources]);
7698
+ }
7699
+ };
7700
+ HasnaHttpError = class HasnaHttpError extends Error {
7701
+ status;
7702
+ method;
7703
+ path;
7704
+ credentialSource;
7705
+ credentialTier;
7706
+ constructor(method, path, status, body, credential) {
7707
+ const guidance = credential ? `. ${credential.guidance}` : "";
7708
+ super(`Hasna cloud request failed: ${method} ${path} -> ${status}${guidance}`);
7709
+ this.name = "HasnaHttpError";
7710
+ this.status = status;
7711
+ this.method = method;
7712
+ this.path = path;
7713
+ Object.defineProperty(this, "body", {
7714
+ value: body,
7715
+ enumerable: status !== 401 && status !== 403,
7716
+ writable: false,
7717
+ configurable: false
7718
+ });
7719
+ this.credentialSource = credential?.source ?? null;
7720
+ this.credentialTier = credential?.tier ?? null;
7721
+ }
7722
+ };
7723
+ DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
7724
+ IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
7725
+ AUTHORITY_OVERRIDE_HEADERS2 = new Set([
7726
+ "host",
7727
+ ":authority",
7728
+ "forwarded",
7729
+ "x-forwarded-host",
7730
+ "x-original-host"
7731
+ ]);
7732
+ });
7733
+
7734
+ // src/lib/transport-resolver.ts
7735
+ function rethrowInstructionsAuthorityFailure(error) {
7736
+ const message = error instanceof Error ? error.message : String(error);
7737
+ const name = error instanceof Error ? error.name : "";
7738
+ const failure = (code, lead) => {
7739
+ throw new Error(`${code}: ${lead} ${message} There is no local fallback: local SQLite is opt-in only ` + `(${INSTRUCTIONS_LOCAL_OPT_IN_ENV}=1) and is disabled by default \u2014 failing closed`, { cause: error });
7740
+ };
7741
+ if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
7742
+ return failure("REMOTE_API_CREDENTIAL_INVALID", "The configured Instructions credential could not be used.");
7743
+ }
7744
+ if (/no API key could be resolved/.test(message)) {
7745
+ if (/is not set and no API key could be resolved/.test(message)) {
7746
+ return failure("REMOTE_API_CONFIG_MISSING", "no Instructions credential resolved from the Keychain item " + "hasna.credentials.instructions.api-key, ~/.hasna/instructions/config/credentials, " + `or ${clientTransportEnvKeys(INSTRUCTIONS_APP).apiKeyKeys[0]}.`);
7747
+ }
7748
+ return failure("REMOTE_API_KEY_MISSING", "an Instructions authority is configured but no API key resolved \u2014 looked in " + "hasna.credentials.instructions.api-key, ~/.hasna/instructions/config/credentials, " + `and ${clientTransportEnvKeys(INSTRUCTIONS_APP).apiKeyKeys[0]}.`);
7749
+ }
7750
+ return failure("REMOTE_API_URL_INVALID", "the configured Instructions authority is invalid.");
7751
+ }
7752
+ function resolveInstructionsStorageClient(env = process.env, options = {}) {
7753
+ const inputs = instructionsResolverInputs(env, options.credentials);
7754
+ try {
7755
+ return resolveStorageClient(INSTRUCTIONS_APP, inputs.env, {
7756
+ fetchImpl: (input, init) => fetch(input, { ...init, redirect: "manual" }),
7757
+ credentials: inputs.credentials
7758
+ });
7759
+ } catch (error) {
7760
+ rethrowInstructionsAuthorityFailure(error);
7761
+ }
7762
+ }
7763
+ function announceLocalInstructionsMode(write = (line) => process.stderr.write(`${line}
7764
+ `)) {
7765
+ if (localNoticePrinted)
7766
+ return false;
7767
+ localNoticePrinted = true;
7768
+ write(instructionsLocalModeNotice());
7769
+ return true;
7770
+ }
7771
+ var INSTRUCTIONS_APP = "instructions", INSTRUCTIONS_LOCAL_OPT_IN_ENV, localNoticePrinted = false;
7772
+ var init_transport_resolver = __esm(() => {
7773
+ init_transport();
7774
+ init_storage();
7775
+ init_local_opt_in();
7776
+ INSTRUCTIONS_LOCAL_OPT_IN_ENV = INSTRUCTIONS_LOCAL_OPT_IN_ENV_KEYS[0];
7777
+ });
7778
+
6509
7779
  // src/data/config-store.ts
6510
7780
  import { randomUUID as randomUUID2 } from "crypto";
6511
7781
  function parseBoundedPagePayload(value, label) {
@@ -6513,7 +7783,7 @@ function parseBoundedPagePayload(value, label) {
6513
7783
  const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
6514
7784
  const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
6515
7785
  if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed)) {
6516
- throw new CloudHttpError(502, `${label} returned an invalid or truncated bounded-read envelope`, value);
7786
+ throw new Error(`${label} returned an invalid or truncated bounded-read envelope`);
6517
7787
  }
6518
7788
  return {
6519
7789
  ...page,
@@ -6528,27 +7798,20 @@ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
6528
7798
  }
6529
7799
  }
6530
7800
  if (!Array.isArray(legacyItems)) {
6531
- throw new CloudHttpError(502, `${label} returned neither a bounded envelope nor a complete legacy array`, value);
7801
+ throw new Error(`${label} returned neither a bounded envelope nor a complete legacy array`);
6532
7802
  }
6533
7803
  const normalized = normalizeBoundedReadOptions(options);
6534
7804
  const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
6535
7805
  return { ...page, source_bounded: false };
6536
7806
  }
6537
- function resolveCloudConfig(env = process.env) {
6538
- assertNoLegacyStorageMode(env);
6539
- const apiUrl = env[API_URL_ENV]?.trim();
6540
- const apiKey = env[API_KEY_ENV]?.trim();
6541
- if (!apiUrl && !apiKey)
6542
- return null;
6543
- if (!apiUrl || !apiKey) {
6544
- throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the HTTP API, ` + `or unset both to use the local store.`);
6545
- }
6546
- return { apiUrl, apiKey };
7807
+ function isNotFoundHttpError2(err) {
7808
+ return typeof err === "object" && err !== null && err.name === "HasnaHttpError" && err.status === 404;
6547
7809
  }
6548
7810
 
6549
7811
  class LocalConfigStore {
6550
7812
  db;
6551
7813
  mode = "local";
7814
+ v1BaseUrl = null;
6552
7815
  constructor(db) {
6553
7816
  this.db = db;
6554
7817
  }
@@ -6677,50 +7940,22 @@ class LocalConfigStore {
6677
7940
 
6678
7941
  class CloudConfigStore {
6679
7942
  mode = "api";
6680
- base;
6681
- apiKey;
6682
- timeoutMs;
6683
- constructor(config) {
6684
- this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
6685
- this.apiKey = config.apiKey;
6686
- this.timeoutMs = config.timeoutMs ?? 30000;
7943
+ v1BaseUrl;
7944
+ client;
7945
+ constructor(client) {
7946
+ this.client = client;
7947
+ this.v1BaseUrl = client.baseUrl;
6687
7948
  }
6688
7949
  async request(method, path, body, opts = {}) {
6689
- const controller = new AbortController;
6690
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
6691
- const headers = {
6692
- Authorization: `Bearer ${this.apiKey}`,
6693
- Accept: "application/json"
6694
- };
6695
- if (body !== undefined)
6696
- headers["Content-Type"] = "application/json";
6697
- if (opts.idempotent)
6698
- headers["Idempotency-Key"] = randomUUID2();
6699
7950
  try {
6700
- const res = await fetch(`${this.base}${path}`, {
6701
- method,
6702
- headers,
6703
- body: body === undefined ? undefined : JSON.stringify(body),
6704
- signal: controller.signal
7951
+ const data = await this.client.transport.request(method, path, body, {
7952
+ ...opts.idempotent ? { idempotencyKey: randomUUID2() } : {}
6705
7953
  });
6706
- if (res.status === 404 && opts.allow404)
7954
+ return { status: 200, data };
7955
+ } catch (err) {
7956
+ if (opts.allow404 && isNotFoundHttpError2(err))
6707
7957
  return { status: 404, data: null };
6708
- const text = await res.text();
6709
- let parsed = null;
6710
- if (text) {
6711
- try {
6712
- parsed = JSON.parse(text);
6713
- } catch {
6714
- parsed = text;
6715
- }
6716
- }
6717
- if (!res.ok) {
6718
- const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
6719
- throw new CloudHttpError(res.status, message, parsed);
6720
- }
6721
- return { status: res.status, data: parsed };
6722
- } finally {
6723
- clearTimeout(timer);
7958
+ throw err;
6724
7959
  }
6725
7960
  }
6726
7961
  async listConfigs(filter = {}) {
@@ -6876,7 +8111,7 @@ class CloudConfigStore {
6876
8111
  if (first.status !== 404) {
6877
8112
  if (isUsable(first.data))
6878
8113
  return first;
6879
- throw new CloudHttpError(502, "profile follow-up returned an invalid response", first.data);
8114
+ throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(idOrSlug)}`);
6880
8115
  }
6881
8116
  const profiles = await this.listProfiles();
6882
8117
  const profile = profiles.find((candidate) => candidate.id === idOrSlug || candidate.slug === idOrSlug);
@@ -6889,7 +8124,7 @@ class CloudConfigStore {
6889
8124
  if (response.status !== 404) {
6890
8125
  if (isUsable(response.data))
6891
8126
  return response;
6892
- throw new CloudHttpError(502, "profile follow-up returned an invalid response", response.data);
8127
+ throw new Error(`Cloud /v1 returned an invalid profile response for ${pathForId(candidate)}`);
6893
8128
  }
6894
8129
  }
6895
8130
  return { status: 404, data: null };
@@ -6972,7 +8207,7 @@ class CloudConfigStore {
6972
8207
  }
6973
8208
  if (data && "complete" in data) {
6974
8209
  if (data.complete !== true || data.truncated !== false) {
6975
- throw new CloudHttpError(502, "profile resolve returned an incomplete or truncated read", data);
8210
+ throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
6976
8211
  }
6977
8212
  return { ...data, source_bounded: data.source_bounded ?? true };
6978
8213
  }
@@ -6988,7 +8223,7 @@ class CloudConfigStore {
6988
8223
  };
6989
8224
  }
6990
8225
  {
6991
- throw new CloudHttpError(502, "profile resolve returned an incomplete or truncated read", data);
8226
+ throw new Error("Cloud /v1 profile resolve returned an incomplete or truncated read");
6992
8227
  }
6993
8228
  }
6994
8229
  async registerMachine(hostname2, os, arch2) {
@@ -7011,14 +8246,18 @@ class CloudConfigStore {
7011
8246
  });
7012
8247
  }
7013
8248
  async reset() {
7014
- throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to reset the local store instead.");
8249
+ throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "Point this run at the local store (HASNA_INSTRUCTIONS_LOCAL=1 with no hosted credential) to reset it instead.");
7015
8250
  }
7016
8251
  }
7017
- function resolveConfigStore(env = process.env) {
7018
- const cloud = resolveCloudConfig(env);
7019
- return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
8252
+ function resolveConfigStore(env = process.env, options = {}) {
8253
+ if (selectsInstructionsLocalStore(env)) {
8254
+ if (env === process.env)
8255
+ announceLocalInstructionsMode();
8256
+ return new LocalConfigStore;
8257
+ }
8258
+ const { client } = resolveInstructionsStorageClient(env, options);
8259
+ return new CloudConfigStore(client);
7020
8260
  }
7021
- var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
7022
8261
  var init_config_store = __esm(() => {
7023
8262
  init_configs();
7024
8263
  init_profiles();
@@ -7028,21 +8267,12 @@ var init_config_store = __esm(() => {
7028
8267
  init_types();
7029
8268
  init_bounded_read();
7030
8269
  init_instruction_graph();
7031
- init_retired_storage_mode();
7032
- CloudHttpError = class CloudHttpError extends Error {
7033
- status;
7034
- body;
7035
- constructor(status, message, body) {
7036
- super(message);
7037
- this.status = status;
7038
- this.body = body;
7039
- this.name = "CloudHttpError";
7040
- }
7041
- };
8270
+ init_transport_resolver();
8271
+ init_local_opt_in();
7042
8272
  });
7043
8273
 
7044
8274
  // src/lib/session-render-ownership.ts
7045
- import { existsSync as existsSync5, readFileSync as readFileSync2, statSync } from "fs";
8275
+ import { existsSync as existsSync5, readFileSync as readFileSync3, statSync } from "fs";
7046
8276
  import { dirname as dirname3, join as join8, parse as parse2, relative as relative2, sep } from "path";
7047
8277
  function toSegments(absolutePath2) {
7048
8278
  return absolutePath2.replaceAll("\\", "/").split("/").filter(Boolean);
@@ -7074,7 +8304,7 @@ function readManifestRelativePaths(manifestPath) {
7074
8304
  }
7075
8305
  let manifest;
7076
8306
  try {
7077
- manifest = JSON.parse(readFileSync2(manifestPath, "utf-8"));
8307
+ manifest = JSON.parse(readFileSync3(manifestPath, "utf-8"));
7078
8308
  } catch {
7079
8309
  return null;
7080
8310
  }
@@ -7126,11 +8356,11 @@ __export(exports_apply, {
7126
8356
  applyConfigs: () => applyConfigs,
7127
8357
  applyConfig: () => applyConfig
7128
8358
  });
7129
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync3, realpathSync, writeFileSync } from "fs";
8359
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync4, realpathSync, writeFileSync } from "fs";
7130
8360
  import { basename as basename3, dirname as dirname4, join as join9, resolve as resolve5 } from "path";
7131
- import { homedir as homedir5 } from "os";
8361
+ import { homedir as homedir4 } from "os";
7132
8362
  function getConfigHome() {
7133
- return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir5();
8363
+ return process.env["CONFIGS_HOME"] || process.env["HOME"] || homedir4();
7134
8364
  }
7135
8365
  function expandPath(p) {
7136
8366
  if (p.startsWith("~/")) {
@@ -7177,7 +8407,7 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
7177
8407
  }
7178
8408
  const path = expandPath(renderedTargetPath);
7179
8409
  const renderedForTarget = isCursorGlobalAuthorityPath(path) ? stampCursorGlobalAuthorityMarker(renderedContent) : renderedContent;
7180
- const previousContent = existsSync6(path) ? readFileSync3(path, "utf-8") : null;
8410
+ const previousContent = existsSync6(path) ? readFileSync4(path, "utf-8") : null;
7181
8411
  const changed = previousContent !== renderedForTarget;
7182
8412
  if (!opts.dryRun) {
7183
8413
  const dir = dirname4(path);
@@ -7217,7 +8447,7 @@ function wouldDestroyACredential(targetPath, renderedContent, format) {
7217
8447
  const path = expandPath(targetPath);
7218
8448
  if (!existsSync6(path))
7219
8449
  return [];
7220
- current = readFileSync3(path, "utf-8");
8450
+ current = readFileSync4(path, "utf-8");
7221
8451
  } catch {
7222
8452
  return secretTokens;
7223
8453
  }
@@ -7574,7 +8804,7 @@ __export(exports_sync, {
7574
8804
  KNOWN_CONFIGS: () => KNOWN_CONFIGS,
7575
8805
  CLAUDE_PROMPT_OUTPUTS: () => CLAUDE_PROMPT_OUTPUTS
7576
8806
  });
7577
- import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4 } from "fs";
8807
+ import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync5 } from "fs";
7578
8808
  import { basename as basename4, extname as extname2, join as join10 } from "path";
7579
8809
  function claudeRuleOutputs(fileName) {
7580
8810
  const stem = basename4(fileName, extname2(fileName));
@@ -7643,7 +8873,7 @@ async function syncProject(opts) {
7643
8873
  if (!existsSync7(abs))
7644
8874
  continue;
7645
8875
  try {
7646
- const rawContent = readFileSync4(abs, "utf-8");
8876
+ const rawContent = readFileSync5(abs, "utf-8");
7647
8877
  if (rawContent.length > 500000) {
7648
8878
  result.skipped.push(pf.file);
7649
8879
  continue;
@@ -7685,7 +8915,7 @@ async function syncProject(opts) {
7685
8915
  const mdFiles = readdirSync(ruleDir.dir).filter((f) => f.endsWith(".md") || f.endsWith(".mdc"));
7686
8916
  for (const f of mdFiles) {
7687
8917
  const abs = join10(ruleDir.dir, f);
7688
- const raw = readFileSync4(abs, "utf-8");
8918
+ const raw = readFileSync5(abs, "utf-8");
7689
8919
  const redacted = redactContent(raw, "markdown");
7690
8920
  const machineAware = templateizeMachineContent(redacted.content, machine);
7691
8921
  const content = machineAware.content;
@@ -7737,7 +8967,7 @@ async function syncKnown(opts = {}) {
7737
8967
  result.skipped.push(`${targetPath} (generated output)`);
7738
8968
  continue;
7739
8969
  }
7740
- const raw = readFileSync4(abs2, "utf-8");
8970
+ const raw = readFileSync5(abs2, "utf-8");
7741
8971
  const redacted = redactContent(raw, "markdown");
7742
8972
  const machineAware = templateizeMachineContent(redacted.content, machine);
7743
8973
  const content = machineAware.content;
@@ -7770,7 +9000,7 @@ async function syncKnown(opts = {}) {
7770
9000
  continue;
7771
9001
  }
7772
9002
  try {
7773
- const rawContent = normalizeKnownConfigSource(known, readFileSync4(abs, "utf-8"));
9003
+ const rawContent = normalizeKnownConfigSource(known, readFileSync5(abs, "utf-8"));
7774
9004
  if (rawContent.length > 500000) {
7775
9005
  result.skipped.push(known.path + " (too large)");
7776
9006
  continue;
@@ -7873,7 +9103,7 @@ function buildDiff(expectedContent, targetPath, storedFormat, showSecrets) {
7873
9103
  const path = expandPath(targetPath);
7874
9104
  if (!existsSync7(path))
7875
9105
  return `(file not found on disk: ${path})`;
7876
- const diskContent = readFileSync4(path, "utf-8");
9106
+ const diskContent = readFileSync5(path, "utf-8");
7877
9107
  if (diskContent === expectedContent)
7878
9108
  return "(no diff \u2014 identical)";
7879
9109
  const format = redactFormatForTarget(targetPath, storedFormat);
@@ -8101,9 +9331,9 @@ var init_sync = __esm(() => {
8101
9331
  });
8102
9332
 
8103
9333
  // src/lib/sync-dir.ts
8104
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
9334
+ import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
8105
9335
  import { join as join11, relative as relative3 } from "path";
8106
- import { homedir as homedir6 } from "os";
9336
+ import { homedir as homedir5 } from "os";
8107
9337
  function shouldSkip(p) {
8108
9338
  return SKIP.some((s) => p.includes(s));
8109
9339
  }
@@ -8114,7 +9344,7 @@ async function syncFromDir(dir, opts = {}) {
8114
9344
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
8115
9345
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join11(absDir, f)).filter((f) => statSync2(f).isFile());
8116
9346
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
8117
- const home = homedir6();
9347
+ const home = homedir5();
8118
9348
  const allConfigs = await store.listConfigs();
8119
9349
  for (const file of files) {
8120
9350
  if (shouldSkip(file)) {
@@ -8122,7 +9352,7 @@ async function syncFromDir(dir, opts = {}) {
8122
9352
  continue;
8123
9353
  }
8124
9354
  try {
8125
- const content = readFileSync5(file, "utf-8");
9355
+ const content = readFileSync6(file, "utf-8");
8126
9356
  if (content.length > 500000) {
8127
9357
  result.skipped.push(file + " (too large)");
8128
9358
  continue;
@@ -8149,7 +9379,7 @@ async function syncFromDir(dir, opts = {}) {
8149
9379
  }
8150
9380
  async function syncToDir(dir, opts = {}) {
8151
9381
  const store = opts.store ?? resolveConfigStore();
8152
- const home = homedir6();
9382
+ const home = homedir5();
8153
9383
  const absDir = expandPath(dir);
8154
9384
  const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
8155
9385
  const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
@@ -8196,8 +9426,8 @@ var init_sync_dir = __esm(() => {
8196
9426
  var require_package = __commonJS((exports, module) => {
8197
9427
  module.exports = {
8198
9428
  name: "@hasna/instructions",
8199
- version: "0.5.6",
8200
- description: "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK + Dashboard.",
9429
+ version: "0.6.0",
9430
+ description: "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK.",
8201
9431
  type: "module",
8202
9432
  main: "dist/index.js",
8203
9433
  types: "dist/index.d.ts",
@@ -8212,25 +9442,27 @@ var require_package = __commonJS((exports, module) => {
8212
9442
  ".": {
8213
9443
  types: "./dist/index.d.ts",
8214
9444
  import: "./dist/index.js"
9445
+ },
9446
+ "./sdk": {
9447
+ types: "./dist/sdk/index.d.ts",
9448
+ import: "./dist/sdk/index.js"
8215
9449
  }
8216
9450
  },
8217
9451
  files: [
8218
9452
  "dist",
8219
9453
  "assets/skills/inbox/SKILL.md",
8220
- "dashboard/dist",
8221
9454
  "LICENSE",
8222
9455
  "README.md"
8223
9456
  ],
8224
9457
  scripts: {
8225
9458
  clean: "rm -rf dist",
8226
- build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg && tsc --emitDeclarationOnly --outDir dist",
8227
- "build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg",
8228
- "build:dashboard": "cd dashboard && bun run build",
9459
+ build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg && bun build src/sdk/index.ts --outdir dist/sdk --target bun && tsc --emitDeclarationOnly --outDir dist",
9460
+ "build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg && bun build src/sdk/index.ts --outdir dist/sdk --target bun",
8229
9461
  migrate: "bun run src/server/index.ts migrate",
8230
9462
  "generate:sdk": "bun run scripts/generate-sdk.ts",
8231
9463
  "kit:check": "bunx @hasna/contracts vendor-kit --check",
8232
9464
  typecheck: "tsc --noEmit",
8233
- test: "env -u HASNA_INSTRUCTIONS_API_URL -u HASNA_INSTRUCTIONS_API_KEY HASNA_STATE_HOME=$(mktemp -d) bun test",
9465
+ test: "env -u HASNA_INSTRUCTIONS_API_URL -u HASNA_INSTRUCTIONS_API_KEY -u INSTRUCTIONS_API_URL -u INSTRUCTIONS_API_KEY -u HASNA_INSTRUCTIONS_API_KEY_OVERRIDE -u HASNA_INSTRUCTIONS_API_KEY_REF -u HASNA_PROFILE HASNA_STATE_HOME=$(mktemp -d) bun test",
8234
9466
  "check:package-secrets": "bun run src/cli/index.tsx package-manager-scan --fail-on-findings .",
8235
9467
  "dev:cli": "bun run src/cli/index.tsx",
8236
9468
  "dev:mcp": "bun run src/mcp/index.ts",
@@ -8251,8 +9483,7 @@ var require_package = __commonJS((exports, module) => {
8251
9483
  "mcp",
8252
9484
  "ai",
8253
9485
  "coding-agent",
8254
- "cli",
8255
- "dashboard"
9486
+ "cli"
8256
9487
  ],
8257
9488
  publishConfig: {
8258
9489
  registry: "https://registry.npmjs.org",
@@ -8272,9 +9503,7 @@ var require_package = __commonJS((exports, module) => {
8272
9503
  author: "Andrei Hasna <andrei@hasna.com>",
8273
9504
  license: "Apache-2.0",
8274
9505
  dependencies: {
8275
- "@hasna/contracts": "0.14.1",
8276
9506
  "@hasna/events": "^0.1.16",
8277
- "@hasna/paths": "0.1.0",
8278
9507
  "@modelcontextprotocol/sdk": "^1.12.1",
8279
9508
  chalk: "^5.4.1",
8280
9509
  commander: "^13.1.0",
@@ -8285,7 +9514,8 @@ var require_package = __commonJS((exports, module) => {
8285
9514
  zod: "^3.24.2"
8286
9515
  },
8287
9516
  devDependencies: {
8288
- "@types/bun": "^1.2.4",
9517
+ "@hasna/contracts": "1.0.2",
9518
+ "@types/bun": "1.3.14",
8289
9519
  "@types/pg": "^8.11.11",
8290
9520
  "@types/react": "^18.3.18",
8291
9521
  "bun-types": "1.3.14",
@@ -8326,6 +9556,9 @@ function getPackageVersion() {
8326
9556
  return cached;
8327
9557
  }
8328
9558
 
9559
+ // src/mcp/index.ts
9560
+ init_config_store();
9561
+
8329
9562
  // src/mcp/server.ts
8330
9563
  init_config_store();
8331
9564
  init_app_home();
@@ -8416,7 +9649,12 @@ function buildServer() {
8416
9649
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
8417
9650
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
8418
9651
  const { name, arguments: args = {} } = req.params;
8419
- const store = resolveConfigStore();
9652
+ let store;
9653
+ try {
9654
+ store = resolveConfigStore();
9655
+ } catch (e) {
9656
+ return err(e instanceof Error ? e.message : String(e));
9657
+ }
8420
9658
  try {
8421
9659
  switch (name) {
8422
9660
  case "list_configs": {
@@ -8801,6 +10039,12 @@ Options:
8801
10039
  await proc.exited;
8802
10040
  process.exit(0);
8803
10041
  }
10042
+ try {
10043
+ resolveConfigStore();
10044
+ } catch (err2) {
10045
+ console.error(err2 instanceof Error ? err2.message : String(err2));
10046
+ process.exit(1);
10047
+ }
8804
10048
  if (isStdioMode(argv)) {
8805
10049
  const server = buildServer();
8806
10050
  const transport = new StdioServerTransport;