@hasna/domains 0.0.47 → 0.1.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 (55) hide show
  1. package/README.md +58 -34
  2. package/dist/cli/commands/dns.d.ts.map +1 -1
  3. package/dist/cli/commands/doctor.d.ts.map +1 -1
  4. package/dist/cli/commands/serve.d.ts +1 -0
  5. package/dist/cli/commands/serve.d.ts.map +1 -1
  6. package/dist/cli/index.js +4754 -2105
  7. package/dist/db/database.d.ts +6 -23
  8. package/dist/db/database.d.ts.map +1 -1
  9. package/dist/db/domains.d.ts +3 -14
  10. package/dist/db/domains.d.ts.map +1 -1
  11. package/dist/db/store.d.ts +36 -32
  12. package/dist/db/store.d.ts.map +1 -1
  13. package/dist/generated/storage-kit/backend.d.ts +4 -4
  14. package/dist/generated/storage-kit/backend.d.ts.map +1 -1
  15. package/dist/generated/storage-kit/index.d.ts +1 -1
  16. package/dist/generated/storage-kit/index.d.ts.map +1 -1
  17. package/dist/generated/storage-kit/migrations.d.ts.map +1 -1
  18. package/dist/generated/storage-kit/pool.d.ts +2 -5
  19. package/dist/generated/storage-kit/pool.d.ts.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1127 -1734
  23. package/dist/lib/app-home.d.ts +32 -44
  24. package/dist/lib/app-home.d.ts.map +1 -1
  25. package/dist/lib/client-storage-policy.d.ts +10 -0
  26. package/dist/lib/client-storage-policy.d.ts.map +1 -0
  27. package/dist/lib/client-types.d.ts +184 -0
  28. package/dist/lib/client-types.d.ts.map +1 -0
  29. package/dist/lib/cloudflare.d.ts.map +1 -1
  30. package/dist/lib/cloudflare.js +23 -0
  31. package/dist/lib/config.d.ts +6 -16
  32. package/dist/lib/config.d.ts.map +1 -1
  33. package/dist/lib/dns-plan.d.ts +1 -1
  34. package/dist/lib/dns-plan.d.ts.map +1 -1
  35. package/dist/lib/domains-resolver.d.ts +96 -0
  36. package/dist/lib/domains-resolver.d.ts.map +1 -0
  37. package/dist/lib/registrar.d.ts +8 -0
  38. package/dist/lib/registrar.d.ts.map +1 -1
  39. package/dist/lib/registrar.js +23 -0
  40. package/dist/mcp/harness.d.ts +46 -0
  41. package/dist/mcp/harness.d.ts.map +1 -0
  42. package/dist/mcp/http.d.ts +3 -2
  43. package/dist/mcp/http.d.ts.map +1 -1
  44. package/dist/mcp/index.d.ts.map +1 -1
  45. package/dist/mcp/index.js +24162 -25378
  46. package/dist/mcp/tool-filter.d.ts +6 -0
  47. package/dist/mcp/tool-filter.d.ts.map +1 -1
  48. package/dist/sdk/index.d.ts +21 -10
  49. package/dist/sdk/index.d.ts.map +1 -1
  50. package/dist/sdk/index.js +813 -275
  51. package/dist/server/index.d.ts +6 -0
  52. package/dist/server/index.d.ts.map +1 -1
  53. package/dist/server/index.js +84 -29
  54. package/package.json +5 -7
  55. package/postinstall.js +13 -14
package/dist/sdk/index.js CHANGED
@@ -1,169 +1,12 @@
1
1
  // @bun
2
- // src/sdk/client.ts
3
- class ApiError extends Error {
4
- status;
5
- body;
6
- constructor(status, message, body) {
7
- super(message);
8
- this.status = status;
9
- this.body = body;
10
- this.name = "ApiError";
11
- }
12
- }
13
-
14
- class DomainsClient {
15
- baseUrl;
16
- apiKey;
17
- fetchImpl;
18
- baseHeaders;
19
- constructor(options) {
20
- if (!options.baseUrl)
21
- throw new Error("DomainsClient requires a baseUrl.");
22
- this.baseUrl = options.baseUrl.replace(/\/$/, "");
23
- this.apiKey = options.apiKey;
24
- this.fetchImpl = options.fetch ?? globalThis.fetch;
25
- this.baseHeaders = options.headers ?? {};
26
- }
27
- async request(method, path, opts) {
28
- const url = new URL(this.baseUrl + path);
29
- if (opts.query) {
30
- for (const [key, value] of Object.entries(opts.query)) {
31
- if (value !== undefined && value !== null)
32
- url.searchParams.set(key, String(value));
33
- }
34
- }
35
- const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
36
- if (this.apiKey)
37
- headers["x-api-key"] = this.apiKey;
38
- let payload;
39
- if (opts.body !== undefined) {
40
- headers["Content-Type"] = "application/json";
41
- payload = JSON.stringify(opts.body);
42
- }
43
- const response = await this.fetchImpl(url.toString(), { ...opts.init, method, headers, body: payload });
44
- const text = await response.text();
45
- const data = text ? (() => {
46
- try {
47
- return JSON.parse(text);
48
- } catch {
49
- return text;
50
- }
51
- })() : undefined;
52
- if (!response.ok) {
53
- throw new ApiError(response.status, `${method} ${path} failed: ${response.status}`, data);
54
- }
55
- return data;
56
- }
57
- async getHealth(init) {
58
- return this.request("GET", `/health`, {
59
- body: undefined,
60
- query: undefined,
61
- init
62
- });
63
- }
64
- async getReady(init) {
65
- return this.request("GET", `/ready`, {
66
- body: undefined,
67
- query: undefined,
68
- init
69
- });
70
- }
71
- async getDnsRecord(id, init) {
72
- return this.request("GET", `/v1/dns/${encodeURIComponent(String(id))}`, {
73
- body: undefined,
74
- query: undefined,
75
- init
76
- });
77
- }
78
- async deleteDnsRecord(id, init) {
79
- return this.request("DELETE", `/v1/dns/${encodeURIComponent(String(id))}`, {
80
- body: undefined,
81
- query: undefined,
82
- init
83
- });
84
- }
85
- async listDomains(query, init) {
86
- return this.request("GET", `/v1/domains`, {
87
- body: undefined,
88
- query,
89
- init
90
- });
91
- }
92
- async createDomain(body, init) {
93
- return this.request("POST", `/v1/domains`, {
94
- body,
95
- query: undefined,
96
- init
97
- });
98
- }
99
- async getDomain(id, init) {
100
- return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}`, {
101
- body: undefined,
102
- query: undefined,
103
- init
104
- });
105
- }
106
- async deleteDomain(id, init) {
107
- return this.request("DELETE", `/v1/domains/${encodeURIComponent(String(id))}`, {
108
- body: undefined,
109
- query: undefined,
110
- init
111
- });
112
- }
113
- async updateDomain(id, body, init) {
114
- return this.request("PATCH", `/v1/domains/${encodeURIComponent(String(id))}`, {
115
- body,
116
- query: undefined,
117
- init
118
- });
119
- }
120
- async listDnsRecords(id, init) {
121
- return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}/dns`, {
122
- body: undefined,
123
- query: undefined,
124
- init
125
- });
126
- }
127
- async createDnsRecord(id, body, init) {
128
- return this.request("POST", `/v1/domains/${encodeURIComponent(String(id))}/dns`, {
129
- body,
130
- query: undefined,
131
- init
132
- });
133
- }
134
- async listOffers(id, init) {
135
- return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}/offers`, {
136
- body: undefined,
137
- query: undefined,
138
- init
139
- });
140
- }
141
- async createOffer(id, body, init) {
142
- return this.request("POST", `/v1/domains/${encodeURIComponent(String(id))}/offers`, {
143
- body,
144
- query: undefined,
145
- init
146
- });
147
- }
148
- async getDomainStats(init) {
149
- return this.request("GET", `/v1/stats`, {
150
- body: undefined,
151
- query: undefined,
152
- init
153
- });
154
- }
155
- async getVersion(init) {
156
- return this.request("GET", `/version`, {
157
- body: undefined,
158
- query: undefined,
159
- init
160
- });
161
- }
162
- }
163
2
  // ../contracts/dist/client/transport.js
164
- import { readFileSync, statSync } from "fs";
3
+ import { isIP } from "net";
4
+ import { spawnSync } from "child_process";
5
+ import { closeSync, fstatSync, openSync, readFileSync } from "fs";
6
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
165
7
  import { createRequire } from "module";
166
- import { join } from "path";
8
+ import { hostname as osHostname } from "os";
9
+ import { isAbsolute, join } from "path";
167
10
  function envToken(name) {
168
11
  return name.toUpperCase().replace(/-/g, "_");
169
12
  }
@@ -192,12 +35,25 @@ class CredentialResolutionError extends Error {
192
35
  this.attempted = attempted;
193
36
  }
194
37
  }
195
- var HASNA_STATE_DIR = ".hasna";
196
- var FLEET_CREDENTIAL_DIR = "fleet-env";
197
- var LEGACY_CLOUD_DIR = "cloud";
198
- var CONFIG_DIR = ".config";
199
- var CONFIG_NAMESPACE = "hasna";
200
- var LEGACY_CLOUD_REMOVAL_DEADLINE = "2026-10-01";
38
+
39
+ class CredentialFileUnsafeError extends Error {
40
+ path;
41
+ constructor(path, reason) {
42
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
43
+ this.name = "CredentialFileUnsafeError";
44
+ this.path = path;
45
+ }
46
+ }
47
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
48
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
49
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
50
+ var HASNA_HOME_DIR = ".hasna";
51
+ var CONFIG_SUBDIR = "config";
52
+ var CREDENTIALS_FILE = "credentials";
53
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
54
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
55
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
56
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
201
57
  var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
202
58
  var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
203
59
  var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
@@ -207,34 +63,32 @@ function homeDir(env) {
207
63
  const home = env.HOME?.trim();
208
64
  return home ? home : null;
209
65
  }
210
- function credentialDiskSourceList(name, env, profile = null) {
66
+ function absoluteOverride(env, key) {
67
+ const value = env[key]?.trim();
68
+ return value && isAbsolute(value) ? value : null;
69
+ }
70
+ function hasnaHomeDir(env) {
71
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
72
+ if (override)
73
+ return override;
211
74
  const home = homeDir(env);
212
- if (!home || !SAFE_APP_SLUG.test(name))
75
+ return home ? join(home, HASNA_HOME_DIR) : null;
76
+ }
77
+ function appConfigDir(name, env) {
78
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
79
+ if (configRoot)
80
+ return join(configRoot, name);
81
+ const root = hasnaHomeDir(env);
82
+ return root ? join(root, name, CONFIG_SUBDIR) : null;
83
+ }
84
+ function credentialDiskSourceList(name, env, profile = null) {
85
+ if (!SAFE_APP_SLUG.test(name))
213
86
  return [];
214
- const stem = profile ? `${name}.${profile}` : name;
215
- const configStem = profile ? `${name}-${profile}` : name;
216
- return [
217
- {
218
- path: join(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
219
- tier: "fleet-env",
220
- deprecated: false
221
- },
222
- {
223
- path: join(home, HASNA_STATE_DIR, LEGACY_CLOUD_DIR, `${stem}.env`),
224
- tier: "legacy-cloud",
225
- deprecated: true
226
- },
227
- {
228
- path: join(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}.env`),
229
- tier: "config",
230
- deprecated: false
231
- },
232
- {
233
- path: join(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`),
234
- tier: "config-legacy",
235
- deprecated: true
236
- }
237
- ];
87
+ const directory = appConfigDir(name, env);
88
+ if (!directory)
89
+ return [];
90
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
91
+ return [{ path: join(directory, file), tier: "disk" }];
238
92
  }
239
93
  function credentialDiskSources(name, env) {
240
94
  return credentialDiskSourceList(name, env, null).map((s) => s.path);
@@ -244,6 +98,7 @@ function profileDiskSources(name, env, profile) {
244
98
  }
245
99
  function parseEnvFile(text) {
246
100
  const values = new Map;
101
+ const unusable = new Set;
247
102
  for (const rawLine of text.split(/\r?\n/)) {
248
103
  const line = rawLine.trim();
249
104
  if (line.length === 0 || line.startsWith("#"))
@@ -258,36 +113,104 @@ function parseEnvFile(text) {
258
113
  let value = withoutExport.slice(equals + 1).trim();
259
114
  const quote = value[0];
260
115
  if (quote === '"' || quote === "'") {
261
- if (value.length < 2 || !value.endsWith(quote))
116
+ if (value.length < 2 || !value.endsWith(quote)) {
117
+ unusable.add(key);
262
118
  continue;
119
+ }
263
120
  value = value.slice(1, -1);
264
121
  }
265
- if (value.length === 0)
122
+ if (value.trim().length === 0) {
123
+ unusable.add(key);
266
124
  continue;
125
+ }
126
+ if (values.has(key) && values.get(key) !== value)
127
+ unusable.add(key);
267
128
  values.set(key, value);
268
129
  }
269
- return values;
130
+ return { values, unusable };
131
+ }
132
+ function configFileModeAllowed(mode) {
133
+ const permissions = mode & 4095;
134
+ return permissions === 256 || permissions === 384;
135
+ }
136
+ function configFileReadsCoherent(before, after) {
137
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
270
138
  }
271
139
  function readAppConfigFile(path) {
272
- let text;
140
+ const unsafe = (reason) => {
141
+ throw new CredentialFileUnsafeError(path, reason);
142
+ };
143
+ let fd = -1;
273
144
  try {
274
- const stats = statSync(path);
275
- if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
145
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
146
+ } catch (error) {
147
+ const code = error.code;
148
+ if (code === "ENOENT" || code === "ENOTDIR")
276
149
  return null;
277
- text = readFileSync(path, "utf8");
278
- } catch {
279
- return null;
150
+ if (code === "ELOOP")
151
+ unsafe("the path is a symlink");
152
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
153
+ }
154
+ try {
155
+ const before = fstatSync(fd);
156
+ if (!before.isFile())
157
+ unsafe("the path is not a regular file");
158
+ if (!configFileModeAllowed(before.mode)) {
159
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
160
+ }
161
+ const uid = process.getuid?.() ?? process.geteuid?.();
162
+ if (uid !== undefined && before.uid !== uid)
163
+ unsafe("the file is not owned by the current user");
164
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
165
+ unsafe("the file exceeds the size limit");
166
+ const bytes = readFileSync(fd);
167
+ const after = fstatSync(fd);
168
+ if (!configFileReadsCoherent(before, after)) {
169
+ unsafe("the file changed while being read");
170
+ }
171
+ return parseEnvFile(bytes.toString("utf8"));
172
+ } finally {
173
+ if (fd !== -1)
174
+ closeSync(fd);
280
175
  }
281
- return parseEnvFile(text);
282
176
  }
283
177
  function readCredentialFile(path, apiKeyKeys) {
284
- const values = readAppConfigFile(path);
285
- if (!values)
178
+ const parsed = readAppConfigFile(path);
179
+ if (!parsed)
286
180
  return null;
287
181
  for (const key of apiKeyKeys) {
288
- const value = values.get(key)?.trim();
289
- if (value)
290
- return value;
182
+ if (parsed.unusable.has(key)) {
183
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
184
+ }
185
+ }
186
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
187
+ if (new Set(values).size > 1) {
188
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
189
+ }
190
+ return values[0] ?? null;
191
+ }
192
+ var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
193
+ function appConfigDiskValue(name, env, keys) {
194
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
195
+ if (wanted.length === 0)
196
+ return null;
197
+ for (const path of credentialDiskSources(name, env)) {
198
+ const parsed = readAppConfigFile(path);
199
+ if (!parsed)
200
+ continue;
201
+ if (wanted.some((key) => parsed.unusable.has(key))) {
202
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
203
+ }
204
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
205
+ if (new Set(values).size > 1)
206
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
207
+ for (const key of wanted) {
208
+ if (parsed.unusable.has(key))
209
+ return { key, value: "", path, unusable: true };
210
+ const value = parsed.values.get(key)?.trim();
211
+ if (value)
212
+ return { key, value, path };
213
+ }
291
214
  }
292
215
  return null;
293
216
  }
@@ -307,7 +230,6 @@ function sealCredential(fields) {
307
230
  tier: fields.tier,
308
231
  source: fields.source,
309
232
  deliberate: fields.deliberate,
310
- deprecated: fields.deprecated,
311
233
  diskCandidates: Object.freeze([...fields.diskCandidates]),
312
234
  warning: fields.warning
313
235
  };
@@ -342,46 +264,140 @@ function sealCredential(fields) {
342
264
  }
343
265
  function firstEnvValue(env, keys) {
344
266
  for (const key of keys) {
267
+ if (!Object.prototype.hasOwnProperty.call(env, key))
268
+ continue;
345
269
  const value = env[key]?.trim();
346
270
  if (value)
347
271
  return { key, value };
348
272
  }
349
273
  return null;
350
274
  }
351
- var DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
352
- function deprecationNotified() {
353
- const host = globalThis;
354
- const existing = host[DEPRECATION_REGISTRY];
355
- if (existing instanceof Set)
356
- return existing;
357
- const created = new Set;
358
- host[DEPRECATION_REGISTRY] = created;
359
- return created;
275
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
276
+ function isAmbientEnvironment(env) {
277
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
360
278
  }
361
- function defaultDeprecationSink(message) {
362
- if (typeof process !== "undefined" && process.stderr) {
363
- process.stderr.write(`${message}
364
- `);
279
+ function defaultKeychainRunner(argv) {
280
+ const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
281
+ encoding: "utf8",
282
+ stdio: ["ignore", "pipe", "pipe"],
283
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
284
+ });
285
+ return {
286
+ status: result.status,
287
+ stdout: result.stdout ?? "",
288
+ stderr: result.error ? result.error.message : result.stderr ?? ""
289
+ };
290
+ }
291
+ function keychainTierEnabled(env, options) {
292
+ if ((options.platform ?? process.platform) !== "darwin")
293
+ return false;
294
+ if (options.enabled !== undefined)
295
+ return options.enabled;
296
+ return options.run !== undefined || isAmbientEnvironment(env);
297
+ }
298
+ function keychainAccount(env, options) {
299
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
300
+ if (station)
301
+ return station;
302
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
303
+ if (host)
304
+ return host;
305
+ const user = env.USER?.trim();
306
+ return user || null;
307
+ }
308
+ function keychainFailureHint(text) {
309
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
310
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
311
+ return clean ? `: ${clean}` : "";
312
+ }
313
+ function readKeychainItem(name, env, kind, options) {
314
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
315
+ return null;
316
+ const account = keychainAccount(env, options);
317
+ if (!account)
318
+ return null;
319
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
320
+ const source = `keychain:${service}@${account}`;
321
+ const run = options.run ?? defaultKeychainRunner;
322
+ let result;
323
+ try {
324
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
325
+ } catch (error) {
326
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
327
+ 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]);
365
328
  }
329
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
330
+ return null;
331
+ if (result.status !== 0) {
332
+ 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]);
333
+ }
334
+ const value = result.stdout.trim();
335
+ if (!value) {
336
+ 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]);
337
+ }
338
+ return { value, source };
339
+ }
340
+ function keychainConfigValue(name, env, options = {}) {
341
+ return readKeychainItem(name, env, "api-url", options);
342
+ }
343
+ function snapshotClientEnvironment(name, env) {
344
+ const keys = clientTransportEnvKeys(name);
345
+ const ambient = isAmbientEnvironment(env);
346
+ const snapshot = Object.create(null);
347
+ for (const key of [
348
+ ...keys.apiUrlKeys,
349
+ ...keys.apiKeyKeys,
350
+ credentialOverrideEnvKey(name),
351
+ credentialPointerEnvKey(name),
352
+ CREDENTIAL_PROFILE_ENV_KEY,
353
+ "HOME",
354
+ HASNA_HOME_ENV_KEY,
355
+ HASNA_CONFIG_HOME_ENV_KEY,
356
+ KEYCHAIN_STATION_ENV_KEY,
357
+ "USER"
358
+ ]) {
359
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
360
+ if (!descriptor)
361
+ continue;
362
+ if (!("value" in descriptor)) {
363
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
364
+ }
365
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
366
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
367
+ }
368
+ snapshot[key] = descriptor.value;
369
+ }
370
+ if (ambient) {
371
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
372
+ value: true,
373
+ enumerable: false,
374
+ writable: false,
375
+ configurable: false
376
+ });
377
+ }
378
+ return Object.freeze(snapshot);
366
379
  }
367
380
  function resolveCredential(name, env, options = {}) {
381
+ env = snapshotClientEnvironment(name, env);
368
382
  const { apiKeyKeys } = clientTransportEnvKeys(name);
369
383
  const diskPaths = credentialDiskSources(name, env);
370
- const explicitKey = options.apiKey?.trim();
371
- if (explicitKey) {
384
+ if (options.apiKey !== undefined) {
385
+ const explicitKey = options.apiKey.trim();
386
+ if (!explicitKey) {
387
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
388
+ }
372
389
  assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
373
390
  return sealCredential({
374
391
  apiKey: explicitKey,
375
392
  tier: "argument",
376
393
  source: "explicit apiKey argument",
377
394
  deliberate: true,
378
- deprecated: false,
379
395
  diskCandidates: diskPaths,
380
396
  warning: null
381
397
  });
382
398
  }
383
399
  const overrideKeyName = credentialOverrideEnvKey(name);
384
- const overrideRaw = env[overrideKeyName];
400
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
385
401
  if (overrideRaw !== undefined) {
386
402
  const override = overrideRaw.trim();
387
403
  if (!override) {
@@ -393,13 +409,12 @@ function resolveCredential(name, env, options = {}) {
393
409
  tier: "override",
394
410
  source: overrideKeyName,
395
411
  deliberate: true,
396
- deprecated: false,
397
412
  diskCandidates: diskPaths,
398
413
  warning: null
399
414
  });
400
415
  }
401
416
  const pointerKeyName = credentialPointerEnvKey(name);
402
- const pointerRaw = env[pointerKeyName];
417
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
403
418
  if (pointerRaw !== undefined) {
404
419
  const pointer = pointerRaw.trim();
405
420
  if (!pointer) {
@@ -414,12 +429,18 @@ function resolveCredential(name, env, options = {}) {
414
429
  tier: "pointer",
415
430
  source: pointerKeyName,
416
431
  deliberate: true,
417
- deprecated: false,
418
432
  diskCandidates: diskPaths,
419
433
  warning: null
420
434
  });
421
435
  }
422
- const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
436
+ if (options.profile !== undefined && !options.profile.trim()) {
437
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
438
+ }
439
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
440
+ if (profileRaw !== undefined && !profileRaw.trim()) {
441
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
442
+ }
443
+ const profile = options.profile?.trim() || profileRaw?.trim();
423
444
  if (profile) {
424
445
  const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
425
446
  if (!SAFE_PROFILE.test(profile)) {
@@ -435,7 +456,6 @@ function resolveCredential(name, env, options = {}) {
435
456
  tier: "profile",
436
457
  source: path,
437
458
  deliberate: true,
438
- deprecated: false,
439
459
  diskCandidates: paths,
440
460
  warning: null
441
461
  });
@@ -443,6 +463,28 @@ function resolveCredential(name, env, options = {}) {
443
463
  }
444
464
  throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
445
465
  }
466
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
467
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
468
+ if (blankEnv) {
469
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
470
+ }
471
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
472
+ 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));
473
+ }
474
+ const envHit = firstEnvValue(env, apiKeyKeys);
475
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
476
+ if (keychainHit) {
477
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
478
+ 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;
479
+ return sealCredential({
480
+ apiKey: keychainHit.value,
481
+ tier: "keychain",
482
+ source: keychainHit.source,
483
+ deliberate: false,
484
+ diskCandidates: diskPaths,
485
+ warning
486
+ });
487
+ }
446
488
  const diskSourceList = credentialDiskSourceList(name, env, null);
447
489
  const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
448
490
  if (diskHits.length > 0) {
@@ -450,62 +492,287 @@ function resolveCredential(name, env, options = {}) {
450
492
  assertUsableCredential(name, winner.src.path, winner.value);
451
493
  const divergentSources = [
452
494
  ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
453
- ...(() => {
454
- const legacyHit = firstEnvValue(env, apiKeyKeys);
455
- return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
456
- })()
495
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
457
496
  ];
458
497
  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;
459
- let deprecated = winner.src.deprecated;
460
- let finalWarning = warning;
461
- if (winner.src.deprecated) {
462
- deprecated = true;
463
- const sink = options.onDeprecation ?? defaultDeprecationSink;
464
- const notified = deprecationNotified();
465
- const noticeKey = `${name}:${winner.src.path}`;
466
- if (!notified.has(noticeKey)) {
467
- notified.add(noticeKey);
468
- const target = diskSourceList[0]?.path ?? "<none>";
469
- const message = `[${name}] DEPRECATED: the API key came from ${winner.src.path} \u2014 a legacy credential location. ` + `The primary location is ${target} (~/.hasna/fleet-env/<app>.env). The legacy 'cloud' tiers are ` + `removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}. Migrate the key to the primary location.`;
470
- sink(message);
471
- }
472
- finalWarning = [warning, `Legacy credential source: ${winner.src.path}. Removed after ${LEGACY_CLOUD_REMOVAL_DEADLINE}.`].filter(Boolean).join(" ") || null;
473
- }
474
498
  return sealCredential({
475
499
  apiKey: winner.value,
476
500
  tier: winner.src.tier,
477
501
  source: winner.src.path,
478
502
  deliberate: false,
479
- deprecated,
480
503
  diskCandidates: diskPaths,
481
- warning: finalWarning
504
+ warning
482
505
  });
483
506
  }
484
- const legacy = firstEnvValue(env, apiKeyKeys);
485
- if (legacy) {
486
- assertUsableCredential(name, legacy.key, legacy.value);
487
- const where = diskPaths.length > 0 ? `Put the current key in ${diskPaths[0]} \u2014 it is re-read on every call, so rotations take effect immediately.` : `This environment has no HOME, so no credential file could be consulted at all; the disk tier is ` + `unavailable here and this process will keep using the environment snapshot.`;
488
- const message = `[${name}] DEPRECATED: the API key came from ${legacy.key} in this process's environment. ` + `Environment variables are a snapshot taken when this process started, so a shell that started ` + `before a key rotation keeps using the old key until it exits. ${where}`;
489
- const sink = options.onDeprecation ?? defaultDeprecationSink;
490
- const notified = deprecationNotified();
491
- if (!notified.has(name)) {
492
- notified.add(name);
493
- sink(message);
494
- }
507
+ if (envHit) {
508
+ assertUsableCredential(name, envHit.key, envHit.value);
495
509
  return sealCredential({
496
- apiKey: legacy.value,
497
- tier: "legacy-env",
498
- source: legacy.key,
510
+ apiKey: envHit.value,
511
+ tier: "env",
512
+ source: envHit.key,
499
513
  deliberate: false,
500
- deprecated: true,
501
514
  diskCandidates: diskPaths,
502
- warning: message
515
+ warning: null
503
516
  });
504
517
  }
505
518
  return null;
506
519
  }
507
520
  var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
508
521
  var requireSecretsSdk = createRequire(import.meta.url);
522
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
523
+ const vaultKey = pointerResolution.pointerVaultKey;
524
+ const pointerEnvKey = pointerResolution.source;
525
+ if (!vaultKey) {
526
+ throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
527
+ }
528
+ let secretsSdk;
529
+ try {
530
+ secretsSdk = requireSecretsSdk(SECRETS_PACKAGE_SPECIFIER);
531
+ } catch {
532
+ 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]);
533
+ }
534
+ let client;
535
+ try {
536
+ client = secretsSdk.createSecretsClientFromEnv(env);
537
+ } catch {
538
+ 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]);
539
+ }
540
+ let secret;
541
+ try {
542
+ secret = await client.getSecret({ key: vaultKey });
543
+ } catch {
544
+ 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]);
545
+ }
546
+ const value = secret.value;
547
+ if (!value) {
548
+ throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
549
+ }
550
+ assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
551
+ return sealCredential({
552
+ apiKey: value,
553
+ tier: "pointer",
554
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
555
+ deliberate: true,
556
+ diskCandidates: pointerResolution.diskCandidates,
557
+ warning: null
558
+ });
559
+ }
560
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
561
+ var DEFAULT_AUTHORITY_SOURCE = "default";
562
+ function defaultFleetGatewayBaseUrl(name) {
563
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
564
+ }
565
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
566
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
567
+ function isValidDnsDomain(value) {
568
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
569
+ return false;
570
+ }
571
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
572
+ }
573
+ function validateAppSlug(name) {
574
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
575
+ throw new Error("App name must be one lowercase DNS label.");
576
+ }
577
+ return name;
578
+ }
579
+ function rawAuthority(value) {
580
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
581
+ if (!match)
582
+ throw new Error("API URL must be absolute.");
583
+ const afterScheme = value.slice(match[0].length);
584
+ const boundary = afterScheme.search(/[/?#]/);
585
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
586
+ if (!authority)
587
+ throw new Error("API URL must include a hostname.");
588
+ return authority;
589
+ }
590
+ function assertCanonicalPort(port) {
591
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
592
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
593
+ }
594
+ const numericPort = Number(port);
595
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
596
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
597
+ }
598
+ }
599
+ function canonicalAuthorityHostname(authority) {
600
+ let rawHostname;
601
+ if (authority.startsWith("[")) {
602
+ const closingBracket = authority.indexOf("]");
603
+ if (closingBracket === -1) {
604
+ throw new Error("API URL authority must contain a canonical hostname.");
605
+ }
606
+ rawHostname = authority.slice(0, closingBracket + 1);
607
+ const portSuffix = authority.slice(closingBracket + 1);
608
+ if (portSuffix) {
609
+ if (!portSuffix.startsWith(":")) {
610
+ throw new Error("API URL authority must contain a canonical hostname and port.");
611
+ }
612
+ assertCanonicalPort(portSuffix.slice(1));
613
+ }
614
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
615
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
616
+ }
617
+ } else {
618
+ const firstColon = authority.indexOf(":");
619
+ const lastColon = authority.lastIndexOf(":");
620
+ if (firstColon !== lastColon) {
621
+ throw new Error("IPv6 API URL authorities must use brackets.");
622
+ }
623
+ if (lastColon !== -1) {
624
+ const port = authority.slice(lastColon + 1);
625
+ assertCanonicalPort(port);
626
+ rawHostname = authority.slice(0, lastColon);
627
+ } else {
628
+ rawHostname = authority;
629
+ }
630
+ const ipVersion = isIP(rawHostname);
631
+ const numericAddressParts = rawHostname.split(".");
632
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
633
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
634
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
635
+ }
636
+ }
637
+ return rawHostname.toLowerCase();
638
+ }
639
+ function isDeliberateLoopbackHttpAuthority(authority) {
640
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
641
+ }
642
+ function toV1BaseUrl(apiUrl) {
643
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
644
+ throw new Error("API URL must not contain ASCII control characters.");
645
+ }
646
+ const input = apiUrl.trim();
647
+ const authority = rawAuthority(input);
648
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
649
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
650
+ }
651
+ const canonicalHostname = canonicalAuthorityHostname(authority);
652
+ const url = new URL(input);
653
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
654
+ throw new Error("API URL must use http or https.");
655
+ }
656
+ if (url.username || url.password) {
657
+ throw new Error("API URL must not include credentials.");
658
+ }
659
+ if (!url.hostname || url.hostname.endsWith(".")) {
660
+ throw new Error("API URL must include a canonical hostname.");
661
+ }
662
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
663
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
664
+ }
665
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
666
+ throw new Error("API URL must not use IDN or punycode hostnames.");
667
+ }
668
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
669
+ throw new Error("API URL may use http only for an exact loopback authority.");
670
+ }
671
+ if (url.search || url.hash) {
672
+ throw new Error("API URL must not include a query string or fragment.");
673
+ }
674
+ let path = url.pathname.replace(/\/+$/, "");
675
+ if (path.endsWith("/v1"))
676
+ path = path.slice(0, -"/v1".length);
677
+ url.pathname = `${path}/v1`;
678
+ return url.toString().replace(/\/+$/, "");
679
+ }
680
+ class ClientTransportConfigurationError extends Error {
681
+ appName;
682
+ sources;
683
+ constructor(appName, message, sources = []) {
684
+ super(message);
685
+ this.name = "ClientTransportConfigurationError";
686
+ this.appName = appName;
687
+ this.sources = Object.freeze([...sources]);
688
+ }
689
+ }
690
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
691
+ env = snapshotClientEnvironment(name, env);
692
+ const keys = clientTransportEnvKeys(name);
693
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
694
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
695
+ if (blankUrl) {
696
+ 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]);
697
+ }
698
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
699
+ if (controlledUrl) {
700
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
701
+ }
702
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
703
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
704
+ 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));
705
+ }
706
+ const envUrlHit = usableUrlEntries[0] ?? null;
707
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
708
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
709
+ if (diskConfigUrlHit?.unusable) {
710
+ 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]);
711
+ }
712
+ const urlCandidates = [
713
+ ...envUrlHit ? [envUrlHit] : [],
714
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
715
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
716
+ ];
717
+ const configuredUrl = urlCandidates[0] ?? null;
718
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
719
+ if (configuredUrl && divergentUrls.length > 0) {
720
+ 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));
721
+ }
722
+ const warnings = [];
723
+ if (configuredUrl && !envUrlHit) {
724
+ 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.`);
725
+ }
726
+ const credential = resolveCredential(name, env, options.credentials);
727
+ if (!credential) {
728
+ const diskHint = credentialDiskSourcesForMessage(name, env);
729
+ 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`;
730
+ 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.`);
731
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
732
+ }
733
+ if (credential.warning)
734
+ warnings.push(credential.warning);
735
+ let urlHit;
736
+ if (configuredUrl) {
737
+ urlHit = configuredUrl;
738
+ } else {
739
+ try {
740
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
741
+ } catch (error) {
742
+ const message = error instanceof Error ? error.message : String(error);
743
+ 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]]);
744
+ }
745
+ }
746
+ const apiUrlSource = urlHit.key;
747
+ let baseUrl;
748
+ try {
749
+ baseUrl = toV1BaseUrl(urlHit.value);
750
+ } catch (error) {
751
+ const message = error instanceof Error ? error.message : String(error);
752
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
753
+ }
754
+ return {
755
+ resolution: {
756
+ transport: "http",
757
+ transportSource: urlHit.key,
758
+ baseUrl,
759
+ apiUrlSource,
760
+ apiKeyPresent: true,
761
+ apiKeySource: credential.source,
762
+ apiKeyTier: credential.tier,
763
+ misconfigured: false,
764
+ warning: warnings.length > 0 ? warnings.join(" ") : null
765
+ },
766
+ credential
767
+ };
768
+ }
769
+ function resolveClientTransport(name, env = process.env, options = {}) {
770
+ return resolveClientTransportSnapshot(name, env, options).resolution;
771
+ }
772
+ function credentialDiskSourcesForMessage(name, env) {
773
+ const paths = credentialDiskSources(name, env);
774
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
775
+ }
509
776
  var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
510
777
  var AUTHORITY_OVERRIDE_HEADERS = new Set([
511
778
  "host",
@@ -515,14 +782,285 @@ var AUTHORITY_OVERRIDE_HEADERS = new Set([
515
782
  "x-original-host"
516
783
  ]);
517
784
 
785
+ // src/lib/client-storage-policy.ts
786
+ var LOCAL_PATH_VARS = [
787
+ "HASNA_DOMAINS_DB_PATH",
788
+ "DOMAINS_DB_PATH",
789
+ "HASNA_DOMAINS_DIR",
790
+ "DOMAINS_DIR"
791
+ ];
792
+ function explicitLocalPathVar(env = process.env) {
793
+ return LOCAL_PATH_VARS.find((key) => (env[key] ?? "").trim() !== "");
794
+ }
795
+ function domainsAuthorityEnvKeys() {
796
+ const keys = clientTransportEnvKeys("domains");
797
+ return [
798
+ ...keys.apiUrlKeys,
799
+ ...keys.apiKeyKeys,
800
+ credentialOverrideEnvKey("domains"),
801
+ credentialPointerEnvKey("domains"),
802
+ CREDENTIAL_PROFILE_ENV_KEY
803
+ ];
804
+ }
805
+ function assertDomainsClientStorage(env = process.env) {
806
+ const pathVar = explicitLocalPathVar(env);
807
+ if (pathVar)
808
+ throw new Error(`domains: ${pathVar} is no longer supported by clients. Unset local database path variables and configure the shared API with HASNA_DOMAINS_API_KEY or saved credentials. Preserve any existing database until its records have been migrated and verified.`);
809
+ }
810
+
811
+ // src/sdk/client.ts
812
+ class ApiError extends Error {
813
+ status;
814
+ body;
815
+ constructor(status, message, body) {
816
+ super(message);
817
+ this.status = status;
818
+ this.body = body;
819
+ this.name = "ApiError";
820
+ }
821
+ }
822
+
823
+ class DomainsClient {
824
+ baseUrl;
825
+ apiKey;
826
+ fetchImpl;
827
+ baseHeaders;
828
+ constructor(options) {
829
+ if (!options.baseUrl)
830
+ throw new Error("DomainsClient requires a baseUrl.");
831
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
832
+ this.apiKey = options.apiKey;
833
+ this.fetchImpl = options.fetch ?? globalThis.fetch;
834
+ this.baseHeaders = options.headers ?? {};
835
+ }
836
+ async request(method, path, opts) {
837
+ const url = new URL(this.baseUrl + path);
838
+ if (opts.query) {
839
+ for (const [key, value] of Object.entries(opts.query)) {
840
+ if (value !== undefined && value !== null)
841
+ url.searchParams.set(key, String(value));
842
+ }
843
+ }
844
+ const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
845
+ if (this.apiKey)
846
+ headers["x-api-key"] = this.apiKey;
847
+ let payload;
848
+ if (opts.body !== undefined) {
849
+ headers["Content-Type"] = "application/json";
850
+ payload = JSON.stringify(opts.body);
851
+ }
852
+ const response = await this.fetchImpl(url.toString(), { ...opts.init, method, headers, body: payload });
853
+ const text = await response.text();
854
+ const data = text ? (() => {
855
+ try {
856
+ return JSON.parse(text);
857
+ } catch {
858
+ return text;
859
+ }
860
+ })() : undefined;
861
+ if (!response.ok) {
862
+ throw new ApiError(response.status, `${method} ${path} failed: ${response.status}`, data);
863
+ }
864
+ return data;
865
+ }
866
+ async getHealth(init) {
867
+ return this.request("GET", `/health`, {
868
+ body: undefined,
869
+ query: undefined,
870
+ init
871
+ });
872
+ }
873
+ async getReady(init) {
874
+ return this.request("GET", `/ready`, {
875
+ body: undefined,
876
+ query: undefined,
877
+ init
878
+ });
879
+ }
880
+ async getDnsRecord(id, init) {
881
+ return this.request("GET", `/v1/dns/${encodeURIComponent(String(id))}`, {
882
+ body: undefined,
883
+ query: undefined,
884
+ init
885
+ });
886
+ }
887
+ async deleteDnsRecord(id, init) {
888
+ return this.request("DELETE", `/v1/dns/${encodeURIComponent(String(id))}`, {
889
+ body: undefined,
890
+ query: undefined,
891
+ init
892
+ });
893
+ }
894
+ async listDomains(query, init) {
895
+ return this.request("GET", `/v1/domains`, {
896
+ body: undefined,
897
+ query,
898
+ init
899
+ });
900
+ }
901
+ async createDomain(body, init) {
902
+ return this.request("POST", `/v1/domains`, {
903
+ body,
904
+ query: undefined,
905
+ init
906
+ });
907
+ }
908
+ async getDomain(id, init) {
909
+ return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}`, {
910
+ body: undefined,
911
+ query: undefined,
912
+ init
913
+ });
914
+ }
915
+ async deleteDomain(id, init) {
916
+ return this.request("DELETE", `/v1/domains/${encodeURIComponent(String(id))}`, {
917
+ body: undefined,
918
+ query: undefined,
919
+ init
920
+ });
921
+ }
922
+ async updateDomain(id, body, init) {
923
+ return this.request("PATCH", `/v1/domains/${encodeURIComponent(String(id))}`, {
924
+ body,
925
+ query: undefined,
926
+ init
927
+ });
928
+ }
929
+ async listDnsRecords(id, init) {
930
+ return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}/dns`, {
931
+ body: undefined,
932
+ query: undefined,
933
+ init
934
+ });
935
+ }
936
+ async createDnsRecord(id, body, init) {
937
+ return this.request("POST", `/v1/domains/${encodeURIComponent(String(id))}/dns`, {
938
+ body,
939
+ query: undefined,
940
+ init
941
+ });
942
+ }
943
+ async listOffers(id, init) {
944
+ return this.request("GET", `/v1/domains/${encodeURIComponent(String(id))}/offers`, {
945
+ body: undefined,
946
+ query: undefined,
947
+ init
948
+ });
949
+ }
950
+ async createOffer(id, body, init) {
951
+ return this.request("POST", `/v1/domains/${encodeURIComponent(String(id))}/offers`, {
952
+ body,
953
+ query: undefined,
954
+ init
955
+ });
956
+ }
957
+ async getDomainStats(init) {
958
+ return this.request("GET", `/v1/stats`, {
959
+ body: undefined,
960
+ query: undefined,
961
+ init
962
+ });
963
+ }
964
+ async getVersion(init) {
965
+ return this.request("GET", `/version`, {
966
+ body: undefined,
967
+ query: undefined,
968
+ init
969
+ });
970
+ }
971
+ }
972
+ // ../contracts/dist/client/storage.js
973
+ import { createRequire as createRequire2 } from "module";
974
+ var MAX_CREDENTIAL_FILE_BYTES2 = 64 * 1024;
975
+ var INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
976
+ var CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
977
+ var AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
978
+ var SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
979
+ var requireSecretsSdk2 = createRequire2(import.meta.url);
980
+ var IDEMPOTENT_METHODS2 = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
981
+ var AUTHORITY_OVERRIDE_HEADERS2 = new Set([
982
+ "host",
983
+ ":authority",
984
+ "forwarded",
985
+ "x-forwarded-host",
986
+ "x-original-host"
987
+ ]);
988
+
989
+ // src/lib/domains-resolver.ts
990
+ var DOMAINS_APP_NAME = "domains";
991
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
992
+ function isAmbientEnv(env) {
993
+ if (typeof process !== "undefined" && env === process.env)
994
+ return true;
995
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
996
+ }
997
+ function domainsResolverEnv(env) {
998
+ const blanks = domainsAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
999
+ if (blanks.length === 0)
1000
+ return env;
1001
+ const next = { ...env };
1002
+ for (const key of blanks)
1003
+ delete next[key];
1004
+ return next;
1005
+ }
1006
+ function domainsResolverInputs(env, credentials = {}) {
1007
+ assertDomainsClientStorage(env);
1008
+ const normalised = domainsResolverEnv(env);
1009
+ if (normalised === env)
1010
+ return { env: normalised, credentials };
1011
+ const keychain = { ...credentials.keychain };
1012
+ if (keychain.enabled === undefined && keychain.run === undefined) {
1013
+ keychain.enabled = isAmbientEnv(env);
1014
+ }
1015
+ return { env: normalised, credentials: { ...credentials, keychain } };
1016
+ }
1017
+
518
1018
  // src/sdk/index.ts
519
1019
  function createDomainsClientFromEnv(env = process.env, overrides = {}) {
520
- const baseUrl = overrides.baseUrl ?? env["DOMAINS_API_URL"] ?? env["HASNA_DOMAINS_API_URL"];
521
- if (!baseUrl) {
522
- throw new Error("createDomainsClientFromEnv requires DOMAINS_API_URL (the domains-serve base URL).");
1020
+ assertDomainsClientStorage(env);
1021
+ const { baseUrl: baseUrlOverride, apiKey: apiKeyOverride, profile, keychain, ...rest } = overrides;
1022
+ if (baseUrlOverride !== undefined) {
1023
+ return new DomainsClient({
1024
+ baseUrl: baseUrlOverride,
1025
+ ...apiKeyOverride !== undefined ? { apiKey: apiKeyOverride } : {},
1026
+ ...rest
1027
+ });
523
1028
  }
524
- const resolved = resolveCredential("domains", env, { apiKey: overrides.apiKey });
525
- return new DomainsClient({ baseUrl, ...resolved?.apiKey ? { apiKey: resolved.apiKey } : {}, ...overrides });
1029
+ const credentials = {
1030
+ ...apiKeyOverride !== undefined ? { apiKey: apiKeyOverride } : {},
1031
+ ...profile ? { profile } : {},
1032
+ ...keychain ? { keychain } : {}
1033
+ };
1034
+ const { env: resolverEnv, credentials: resolverCredentials } = domainsResolverInputs(env, credentials);
1035
+ const resolution = resolveClientTransport(DOMAINS_APP_NAME, resolverEnv, {
1036
+ credentials: resolverCredentials
1037
+ });
1038
+ const credential = resolveCredential(DOMAINS_APP_NAME, resolverEnv, resolverCredentials);
1039
+ if (!credential) {
1040
+ throw new Error("domains SDK: no API key resolved from any credential tier; refusing to build an unauthenticated client. " + "Looked at HASNA_DOMAINS_API_KEY_OVERRIDE / HASNA_PROFILE / HASNA_DOMAINS_API_KEY_REF, the Keychain item " + "hasna.credentials.domains.api-key, ~/.hasna/domains/config/credentials, then HASNA_DOMAINS_API_KEY.");
1041
+ }
1042
+ const baseUrl = resolution.baseUrl.replace(/\/v1$/, "");
1043
+ const baseFetch = rest.fetch ?? ((input, init) => fetch(input, init));
1044
+ const fetchWithFreshCredential = async (input, init) => {
1045
+ const headers = {};
1046
+ new Headers(init?.headers ?? {}).forEach((value, key) => {
1047
+ headers[key] = value;
1048
+ });
1049
+ try {
1050
+ const fresh = resolveCredential(DOMAINS_APP_NAME, resolverEnv, resolverCredentials);
1051
+ if (fresh) {
1052
+ const key = fresh.tier === "pointer" ? (await completePointerCredential(DOMAINS_APP_NAME, fresh, resolverEnv)).apiKey : fresh.apiKey;
1053
+ headers["x-api-key"] = key;
1054
+ }
1055
+ } catch {}
1056
+ return baseFetch(input, { ...init, headers });
1057
+ };
1058
+ return new DomainsClient({
1059
+ baseUrl,
1060
+ apiKey: credential.apiKey,
1061
+ ...rest,
1062
+ fetch: fetchWithFreshCredential
1063
+ });
526
1064
  }
527
1065
  export {
528
1066
  createDomainsClientFromEnv,