@hasna/hooks 0.7.9 → 0.8.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 (53) hide show
  1. package/README.md +32 -6
  2. package/bin/index.js +1573 -695
  3. package/bin/serve.js +713 -335
  4. package/dist/config.d.ts +25 -19
  5. package/dist/db/index.d.ts +3 -1
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.js +1162 -473
  8. package/dist/lib/app-home.d.ts +74 -0
  9. package/dist/lib/installer.d.ts +0 -9
  10. package/dist/lib/local-opt-in.d.ts +80 -0
  11. package/dist/lib/profiles.d.ts +14 -1
  12. package/dist/lib/resolver-types.d.ts +45 -0
  13. package/dist/lib/sync.d.ts +13 -8
  14. package/dist/lib/transport.d.ts +87 -0
  15. package/dist/openapi.d.ts +1 -1
  16. package/dist/serve.d.ts +7 -0
  17. package/dist/storage.js +141 -27
  18. package/hooks/hook-affected-tests/package.json +1 -1
  19. package/hooks/hook-agent-rules-version-check/package.json +1 -1
  20. package/hooks/hook-agentmessages/package.json +1 -1
  21. package/hooks/hook-announce-start/package.json +1 -1
  22. package/hooks/hook-announce-stop/package.json +1 -1
  23. package/hooks/hook-autoformat/package.json +1 -1
  24. package/hooks/hook-branchprotect/package.json +1 -1
  25. package/hooks/hook-checkbugs/package.json +1 -1
  26. package/hooks/hook-checkdocs/package.json +1 -1
  27. package/hooks/hook-checkfiles/package.json +1 -1
  28. package/hooks/hook-checklint/package.json +1 -1
  29. package/hooks/hook-checkpoint/package.json +1 -1
  30. package/hooks/hook-checksecurity/package.json +1 -1
  31. package/hooks/hook-checktasks/package.json +1 -1
  32. package/hooks/hook-checktests/package.json +1 -1
  33. package/hooks/hook-conflict-detect/package.json +1 -1
  34. package/hooks/hook-contextrefresh/package.json +1 -1
  35. package/hooks/hook-desktopnotify/package.json +1 -1
  36. package/hooks/hook-dm-inject/package.json +1 -1
  37. package/hooks/hook-envsetup/package.json +1 -1
  38. package/hooks/hook-failure-to-task/package.json +1 -1
  39. package/hooks/hook-filelock/package.json +1 -1
  40. package/hooks/hook-fleet-blockers-gate/package.json +1 -1
  41. package/hooks/hook-fleet-catchup/package.json +1 -1
  42. package/hooks/hook-gitguard/package.json +1 -1
  43. package/hooks/hook-packageage/package.json +1 -1
  44. package/hooks/hook-permissionguard/package.json +1 -1
  45. package/hooks/hook-phonenotify/package.json +1 -1
  46. package/hooks/hook-precompact/package.json +1 -1
  47. package/hooks/hook-protectfiles/package.json +1 -1
  48. package/hooks/hook-stylescheck/package.json +1 -1
  49. package/hooks/hook-typecheck-gate/package.json +1 -1
  50. package/package.json +6 -9
  51. package/scripts/artifact-scan.ts +49 -0
  52. package/scripts/ensure-profiles-dir.mjs +99 -0
  53. package/scripts/validate-package.ts +280 -0
package/dist/index.js CHANGED
@@ -15,6 +15,123 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
+ // src/lib/app-home.ts
19
+ import { existsSync } from "fs";
20
+ import { homedir } from "os";
21
+ import { join, resolve } from "path";
22
+ import { homedir as pathsResolverHomedir } from "os";
23
+ import { join as pathsResolverJoin } from "path";
24
+ function pathsResolverAssertApp(app) {
25
+ if (typeof app !== "string" || app.length === 0) {
26
+ throw new TypeError("paths: app must be a non-empty string");
27
+ }
28
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
29
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
30
+ }
31
+ }
32
+ function pathsResolverAssertKind(kind) {
33
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
34
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
35
+ }
36
+ }
37
+ function pathsResolverBaseDir(kind, options) {
38
+ pathsResolverAssertKind(kind);
39
+ const env = options.env ?? process.env;
40
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
41
+ if (typeof override === "string" && override.length > 0)
42
+ return override;
43
+ const home = options.home ?? pathsResolverHomedir();
44
+ const platform = options.platform ?? process.platform;
45
+ if (platform === "darwin") {
46
+ switch (kind) {
47
+ case "config":
48
+ case "data":
49
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
50
+ case "cache":
51
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
52
+ case "state":
53
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
54
+ }
55
+ }
56
+ switch (kind) {
57
+ case "config":
58
+ return pathsResolverJoin(home, ".config", "hasna");
59
+ case "data":
60
+ return pathsResolverJoin(home, ".local", "share", "hasna");
61
+ case "state":
62
+ return pathsResolverJoin(home, ".local", "state", "hasna");
63
+ case "cache":
64
+ return pathsResolverJoin(home, ".cache", "hasna");
65
+ }
66
+ }
67
+ function pathsResolverResolve(kind, options) {
68
+ pathsResolverAssertApp(options.app);
69
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
70
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
71
+ }
72
+ function dataDir(options) {
73
+ return pathsResolverResolve("data", options);
74
+ }
75
+ function getHomeDir(env = process.env) {
76
+ return env.HOME || env.USERPROFILE || homedir();
77
+ }
78
+ function getResolverDataRoot(env = process.env) {
79
+ return dataDir({ app: "hooks", home: getHomeDir(env), env });
80
+ }
81
+ function getLegacyDataRoot(env = process.env) {
82
+ return join(getHomeDir(env), ".hasna", "hooks");
83
+ }
84
+ function adoptResolverDataRoot(resolved, env = process.env) {
85
+ const dataOverride = env.HASNA_DATA_HOME;
86
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
87
+ return true;
88
+ return existsSync(join(resolved, STORE_MARKER));
89
+ }
90
+ function getExplicitDataDir(env = process.env) {
91
+ const dir = env.HASNA_HOOKS_DATA_DIR ?? env.HOOKS_DATA_DIR;
92
+ if (typeof dir === "string" && dir.trim().length > 0)
93
+ return dir.trim();
94
+ return;
95
+ }
96
+ function getExactDataRoot(env = process.env) {
97
+ const dir = env.HASNA_HOOKS_HOME?.trim() || env.HOOKS_HOME?.trim();
98
+ if (dir)
99
+ return resolve(dir);
100
+ return;
101
+ }
102
+ function getEffectiveDataRoot(env = process.env) {
103
+ const explicit = getExplicitDataDir(env);
104
+ if (explicit)
105
+ return explicit;
106
+ const exact = getExactDataRoot(env);
107
+ if (exact)
108
+ return exact;
109
+ const resolved = getResolverDataRoot(env);
110
+ return adoptResolverDataRoot(resolved, env) ? resolve(resolved) : getLegacyDataRoot(env);
111
+ }
112
+ function getReportedDbPath(env = process.env) {
113
+ const explicit = getExplicitDbPath(env);
114
+ if (explicit)
115
+ return explicit;
116
+ return join(getEffectiveDataRoot(env), "hooks.db");
117
+ }
118
+ function getExplicitDbPath(env = process.env) {
119
+ const db = env.HASNA_HOOKS_DB_PATH ?? env.HOOKS_DB_PATH;
120
+ if (typeof db === "string" && db.trim().length > 0)
121
+ return db.trim();
122
+ return;
123
+ }
124
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, STORE_MARKER = "hooks.db";
125
+ var init_app_home = __esm(() => {
126
+ PATHS_RESOLVER_KIND_ENV = {
127
+ config: "HASNA_CONFIG_HOME",
128
+ data: "HASNA_DATA_HOME",
129
+ state: "HASNA_STATE_HOME",
130
+ cache: "HASNA_CACHE_HOME"
131
+ };
132
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
133
+ });
134
+
18
135
  // src/db/schema.ts
19
136
  var CREATE_HOOK_EVENTS_TABLE = `
20
137
  CREATE TABLE IF NOT EXISTS hook_events (
@@ -209,9 +326,9 @@ var init_migrations = __esm(() => {
209
326
  });
210
327
 
211
328
  // src/db/legacy-import.ts
212
- import { existsSync, readdirSync, readFileSync } from "fs";
213
- import { join } from "path";
214
- import { homedir } from "os";
329
+ import { existsSync as existsSync2, readdirSync, readFileSync } from "fs";
330
+ import { join as join2 } from "path";
331
+ import { homedir as homedir2 } from "os";
215
332
  function ensureMetaTable(db) {
216
333
  db.exec(`
217
334
  CREATE TABLE IF NOT EXISTS _meta (
@@ -285,25 +402,25 @@ function importErrorsLog(db, filePath) {
285
402
  } catch {}
286
403
  return count;
287
404
  }
288
- function runLegacyImport(db, homeDir = homedir()) {
405
+ function runLegacyImport(db, homeDir = homedir2()) {
289
406
  try {
290
407
  if (isAlreadyDone(db))
291
408
  return;
292
409
  let total = 0;
293
- const claudeProjectsDir = join(homeDir, ".claude", "projects");
294
- if (existsSync(claudeProjectsDir)) {
410
+ const claudeProjectsDir = join2(homeDir, ".claude", "projects");
411
+ if (existsSync2(claudeProjectsDir)) {
295
412
  try {
296
413
  const projectDirs = readdirSync(claudeProjectsDir);
297
414
  for (const dir of projectDirs) {
298
- const projectDir = join(claudeProjectsDir, dir);
415
+ const projectDir = join2(claudeProjectsDir, dir);
299
416
  try {
300
417
  const files = readdirSync(projectDir);
301
418
  for (const file of files) {
302
419
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
303
- total += importJsonlFile(db, join(projectDir, file));
420
+ total += importJsonlFile(db, join2(projectDir, file));
304
421
  }
305
422
  if (file === "errors.log") {
306
- total += importErrorsLog(db, join(projectDir, file));
423
+ total += importErrorsLog(db, join2(projectDir, file));
307
424
  }
308
425
  }
309
426
  } catch {}
@@ -339,31 +456,27 @@ function runRetention(db, days) {
339
456
 
340
457
  // src/db/index.ts
341
458
  import { Database } from "bun:sqlite";
342
- import { existsSync as existsSync2, mkdirSync, cpSync } from "fs";
343
- import { join as join2 } from "path";
344
- import { homedir as homedir2 } from "os";
459
+ import { existsSync as existsSync3, mkdirSync, cpSync } from "fs";
460
+ import { join as join3 } from "path";
345
461
  function resolveDataDir() {
346
- const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
347
- if (explicit)
348
- return explicit;
349
- const newDir = join2(homedir2(), ".hasna", "hooks");
350
- const oldDir = join2(homedir2(), ".hooks");
351
- if (!existsSync2(newDir) && existsSync2(oldDir)) {
352
- mkdirSync(join2(homedir2(), ".hasna"), { recursive: true });
353
- cpSync(oldDir, newDir, { recursive: true });
462
+ const effective = getEffectiveDataRoot();
463
+ const oldDir = join3(getHomeDir(), ".hooks");
464
+ if (!existsSync3(effective) && existsSync3(oldDir)) {
465
+ mkdirSync(effective, { recursive: true });
466
+ cpSync(oldDir, effective, { recursive: true });
354
467
  }
355
- return newDir;
468
+ return effective;
356
469
  }
357
470
  function getDbPath() {
358
471
  const explicitDb = process.env.HASNA_HOOKS_DB_PATH ?? process.env.HOOKS_DB_PATH;
359
472
  if (explicitDb)
360
473
  return explicitDb;
361
- const dataDir = resolveDataDir();
362
- return join2(dataDir, "hooks.db");
474
+ const dataDir2 = resolveDataDir();
475
+ return join3(dataDir2, "hooks.db");
363
476
  }
364
477
  function ensureDir(dbPath) {
365
478
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
366
- if (dir && !existsSync2(dir)) {
479
+ if (dir && !existsSync3(dir)) {
367
480
  mkdirSync(dir, { recursive: true });
368
481
  }
369
482
  }
@@ -371,7 +484,7 @@ function getDb() {
371
484
  if (instance)
372
485
  return instance;
373
486
  const dbPath = getDbPath();
374
- const isNew = dbPath === ":memory:" || !existsSync2(dbPath);
487
+ const isNew = dbPath === ":memory:" || !existsSync3(dbPath);
375
488
  ensureDir(dbPath);
376
489
  instance = new Database(dbPath);
377
490
  instance.exec("PRAGMA busy_timeout=5000");
@@ -395,6 +508,7 @@ function getDb() {
395
508
  }
396
509
  var instance = null;
397
510
  var init_db = __esm(() => {
511
+ init_app_home();
398
512
  init_migrations();
399
513
  init_legacy_import();
400
514
  });
@@ -445,8 +559,8 @@ var init_redact = __esm(() => {
445
559
  /AKIA[0-9A-Z]{16}/g,
446
560
  /ASIA[0-9A-Z]{16}/g,
447
561
  /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
448
- /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
449
- /-----BEGIN OPENSSH PRIVATE KEY-----/g,
562
+ /-----BEGIN [A-Z0-9 ]*PRIVAT[E] KEY-----/g,
563
+ /-----BEGIN OPENSSH PRIVAT[E] KEY-----/g,
450
564
  /\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
451
565
  /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
452
566
  /\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
@@ -4419,289 +4533,10 @@ var init_zod = __esm(() => {
4419
4533
  init_external();
4420
4534
  });
4421
4535
 
4422
- // ../contracts/dist/client/transport.js
4423
- import { readFileSync as readFileSync2, statSync } from "fs";
4424
- import { join as join3 } from "path";
4425
- function envToken(name) {
4426
- return name.toUpperCase().replace(/-/g, "_");
4427
- }
4428
- function clientTransportEnvKeys(name) {
4429
- const envSegment = envToken(name);
4430
- return {
4431
- apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
4432
- apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
4433
- };
4434
- }
4435
- function credentialOverrideEnvKey(name) {
4436
- return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
4437
- }
4438
- function homeDir(env) {
4439
- const home = env.HOME?.trim();
4440
- return home ? home : null;
4441
- }
4442
- function credentialDiskSources(name, env) {
4443
- return profileDiskSources(name, env, null);
4444
- }
4445
- function profileDiskSources(name, env, profile) {
4446
- const home = homeDir(env);
4447
- if (!home || !SAFE_APP_SLUG.test(name))
4448
- return [];
4449
- const stem = profile ? `${name}.${profile}` : name;
4450
- const configStem = profile ? `${name}-${profile}` : name;
4451
- return [
4452
- join3(home, HASNA_STATE_DIR, FLEET_CREDENTIAL_DIR, `${stem}.env`),
4453
- join3(home, CONFIG_DIR, CONFIG_NAMESPACE, `${configStem}-cloud.env`)
4454
- ];
4455
- }
4456
- function parseEnvFile(text) {
4457
- const values = new Map;
4458
- for (const rawLine of text.split(/\r?\n/)) {
4459
- const line = rawLine.trim();
4460
- if (line.length === 0 || line.startsWith("#"))
4461
- continue;
4462
- const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
4463
- const equals = withoutExport.indexOf("=");
4464
- if (equals <= 0)
4465
- continue;
4466
- const key = withoutExport.slice(0, equals).trim();
4467
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
4468
- continue;
4469
- let value = withoutExport.slice(equals + 1).trim();
4470
- const quote = value[0];
4471
- if (quote === '"' || quote === "'") {
4472
- if (value.length < 2 || !value.endsWith(quote))
4473
- continue;
4474
- value = value.slice(1, -1);
4475
- }
4476
- if (value.length === 0)
4477
- continue;
4478
- values.set(key, value);
4479
- }
4480
- return values;
4481
- }
4482
- function readAppConfigFile(path) {
4483
- let text;
4484
- try {
4485
- const stats = statSync(path);
4486
- if (!stats.isFile() || stats.size > MAX_CREDENTIAL_FILE_BYTES)
4487
- return null;
4488
- text = readFileSync2(path, "utf8");
4489
- } catch {
4490
- return null;
4491
- }
4492
- return parseEnvFile(text);
4493
- }
4494
- function readCredentialFile(path, apiKeyKeys) {
4495
- const values = readAppConfigFile(path);
4496
- if (!values)
4497
- return null;
4498
- for (const key of apiKeyKeys) {
4499
- const value = values.get(key)?.trim();
4500
- if (value)
4501
- return value;
4502
- }
4503
- return null;
4504
- }
4505
- function assertUsableCredential(appName, source, value) {
4506
- if (!ILLEGAL_IN_HEADER_VALUE.test(value))
4507
- return;
4508
- 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]);
4509
- }
4510
- function sealCredential(fields) {
4511
- const { apiKey } = fields;
4512
- const visible = {
4513
- tier: fields.tier,
4514
- source: fields.source,
4515
- deliberate: fields.deliberate,
4516
- deprecated: fields.deprecated,
4517
- diskCandidates: Object.freeze([...fields.diskCandidates]),
4518
- warning: fields.warning
4519
- };
4520
- const sealed = { ...visible };
4521
- Object.defineProperty(sealed, "apiKey", {
4522
- value: apiKey,
4523
- enumerable: false,
4524
- writable: false,
4525
- configurable: false
4526
- });
4527
- Object.defineProperty(sealed, INSPECT_CUSTOM, {
4528
- value: () => ({ ...visible, apiKey: "[redacted]" }),
4529
- enumerable: false,
4530
- writable: false,
4531
- configurable: false
4532
- });
4533
- Object.defineProperty(sealed, CREDENTIAL_SEAL, {
4534
- value: true,
4535
- enumerable: false,
4536
- writable: false,
4537
- configurable: false
4538
- });
4539
- return Object.freeze(sealed);
4540
- }
4541
- function firstEnvValue(env, keys) {
4542
- for (const key of keys) {
4543
- const value = env[key]?.trim();
4544
- if (value)
4545
- return { key, value };
4546
- }
4547
- return null;
4548
- }
4549
- function deprecationNotified() {
4550
- const host = globalThis;
4551
- const existing = host[DEPRECATION_REGISTRY];
4552
- if (existing instanceof Set)
4553
- return existing;
4554
- const created = new Set;
4555
- host[DEPRECATION_REGISTRY] = created;
4556
- return created;
4557
- }
4558
- function defaultDeprecationSink(message) {
4559
- if (typeof process !== "undefined" && process.stderr) {
4560
- process.stderr.write(`${message}
4561
- `);
4562
- }
4563
- }
4564
- function resolveCredential(name, env, options = {}) {
4565
- const { apiKeyKeys } = clientTransportEnvKeys(name);
4566
- const diskPaths = credentialDiskSources(name, env);
4567
- const explicitKey = options.apiKey?.trim();
4568
- if (explicitKey) {
4569
- assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
4570
- return sealCredential({
4571
- apiKey: explicitKey,
4572
- tier: "argument",
4573
- source: "explicit apiKey argument",
4574
- deliberate: true,
4575
- deprecated: false,
4576
- diskCandidates: diskPaths,
4577
- warning: null
4578
- });
4579
- }
4580
- const overrideKeyName = credentialOverrideEnvKey(name);
4581
- const overrideRaw = env[overrideKeyName];
4582
- if (overrideRaw !== undefined) {
4583
- const override = overrideRaw.trim();
4584
- if (!override) {
4585
- 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]);
4586
- }
4587
- assertUsableCredential(name, overrideKeyName, override);
4588
- return sealCredential({
4589
- apiKey: override,
4590
- tier: "override",
4591
- source: overrideKeyName,
4592
- deliberate: true,
4593
- deprecated: false,
4594
- diskCandidates: diskPaths,
4595
- warning: null
4596
- });
4597
- }
4598
- const profile = options.profile?.trim() || env[CREDENTIAL_PROFILE_ENV_KEY]?.trim();
4599
- if (profile) {
4600
- const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
4601
- if (!SAFE_PROFILE.test(profile)) {
4602
- throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
4603
- }
4604
- const paths = profileDiskSources(name, env, profile);
4605
- for (const path of paths) {
4606
- const value = readCredentialFile(path, apiKeyKeys);
4607
- if (value) {
4608
- assertUsableCredential(name, path, value);
4609
- return sealCredential({
4610
- apiKey: value,
4611
- tier: "profile",
4612
- source: path,
4613
- deliberate: true,
4614
- deprecated: false,
4615
- diskCandidates: paths,
4616
- warning: null
4617
- });
4618
- }
4619
- }
4620
- 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);
4621
- }
4622
- const diskHits = diskPaths.map((path) => ({ path, value: readCredentialFile(path, apiKeyKeys) })).filter((hit) => hit.value !== null);
4623
- if (diskHits.length > 0) {
4624
- const winner = diskHits[0];
4625
- assertUsableCredential(name, winner.path, winner.value);
4626
- const divergentSources = [
4627
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.path),
4628
- ...(() => {
4629
- const legacyHit = firstEnvValue(env, apiKeyKeys);
4630
- return legacyHit && legacyHit.value !== winner.value ? [legacyHit.key] : [];
4631
- })()
4632
- ];
4633
- const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
4634
- return sealCredential({
4635
- apiKey: winner.value,
4636
- tier: "disk",
4637
- source: winner.path,
4638
- deliberate: false,
4639
- deprecated: false,
4640
- diskCandidates: diskPaths,
4641
- warning
4642
- });
4643
- }
4644
- const legacy = firstEnvValue(env, apiKeyKeys);
4645
- if (legacy) {
4646
- assertUsableCredential(name, legacy.key, legacy.value);
4647
- 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.`;
4648
- 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}`;
4649
- const sink = options.onDeprecation ?? defaultDeprecationSink;
4650
- const notified = deprecationNotified();
4651
- if (!notified.has(name)) {
4652
- notified.add(name);
4653
- sink(message);
4654
- }
4655
- return sealCredential({
4656
- apiKey: legacy.value,
4657
- tier: "legacy-env",
4658
- source: legacy.key,
4659
- deliberate: false,
4660
- deprecated: true,
4661
- diskCandidates: diskPaths,
4662
- warning: message
4663
- });
4664
- }
4665
- return null;
4666
- }
4667
- var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, HASNA_STATE_DIR = ".hasna", FLEET_CREDENTIAL_DIR = "cloud", CONFIG_DIR = ".config", CONFIG_NAMESPACE = "hasna", MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, INSPECT_CUSTOM, CREDENTIAL_SEAL, DEPRECATION_REGISTRY, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
4668
- var init_transport = __esm(() => {
4669
- CredentialResolutionError = class CredentialResolutionError extends Error {
4670
- appName;
4671
- attempted;
4672
- constructor(appName, message, attempted) {
4673
- super(message);
4674
- this.name = "CredentialResolutionError";
4675
- this.appName = appName;
4676
- this.attempted = attempted;
4677
- }
4678
- };
4679
- MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
4680
- SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
4681
- SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
4682
- ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
4683
- INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
4684
- CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
4685
- DEPRECATION_REGISTRY = Symbol.for("hasna:contracts:credentialDeprecationNotices");
4686
- IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
4687
- AUTHORITY_OVERRIDE_HEADERS = new Set([
4688
- "host",
4689
- ":authority",
4690
- "forwarded",
4691
- "x-forwarded-host",
4692
- "x-original-host"
4693
- ]);
4694
- });
4695
-
4696
4536
  // src/config.ts
4697
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync } from "fs";
4698
4537
  import { join as join4 } from "path";
4699
- import { homedir as homedir3 } from "os";
4700
4538
  function getHooksDataDir() {
4701
- const explicit = process.env.HASNA_HOOKS_DATA_DIR ?? process.env.HOOKS_DATA_DIR;
4702
- if (explicit)
4703
- return explicit;
4704
- return join4(homedir3(), ".hasna", "hooks");
4539
+ return getEffectiveDataRoot();
4705
4540
  }
4706
4541
  function getCustomHooksDir() {
4707
4542
  return join4(getHooksDataDir(), "hooks");
@@ -4712,41 +4547,8 @@ function getLockPath() {
4712
4547
  return explicit;
4713
4548
  return join4(getHooksDataDir(), "hooks.lock");
4714
4549
  }
4715
- function getConfigPath() {
4716
- const explicit = process.env.HASNA_HOOKS_CONFIG_PATH ?? process.env.HOOKS_CONFIG_PATH;
4717
- if (explicit)
4718
- return explicit;
4719
- return join4(getHooksDataDir(), "config.json");
4720
- }
4721
- function readConfig() {
4722
- try {
4723
- const path = getConfigPath();
4724
- if (!existsSync3(path))
4725
- return {};
4726
- return JSON.parse(readFileSync3(path, "utf-8"));
4727
- } catch {
4728
- return {};
4729
- }
4730
- }
4731
- function writeConfig(config) {
4732
- const path = getConfigPath();
4733
- mkdirSync2(join4(path, ".."), { recursive: true });
4734
- writeFileSync(path, JSON.stringify(config, null, 2) + `
4735
- `, "utf-8");
4736
- return path;
4737
- }
4738
- function resolveApiUrl() {
4739
- const envUrl = process.env.HASNA_HOOKS_API_URL ?? process.env.HOOKS_API_URL ?? process.env.HASNA_HOOKS_REGISTRY_URL ?? process.env.HOOKS_REGISTRY_URL;
4740
- if (envUrl)
4741
- return envUrl.replace(/\/+$/, "");
4742
- const cfg = readConfig();
4743
- return cfg.api_url ? cfg.api_url.replace(/\/+$/, "") : undefined;
4744
- }
4745
- function resolveApiKey() {
4746
- return resolveCredential("hooks", process.env)?.apiKey;
4747
- }
4748
4550
  var init_config = __esm(() => {
4749
- init_transport();
4551
+ init_app_home();
4750
4552
  });
4751
4553
 
4752
4554
  // src/lib/semver.ts
@@ -4773,8 +4575,8 @@ __export(exports_manifest, {
4773
4575
  ScriptContainmentError: () => ScriptContainmentError,
4774
4576
  HOOK_NAME_RE: () => HOOK_NAME_RE
4775
4577
  });
4776
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync, writeFileSync as writeFileSync2 } from "fs";
4777
- import { basename, dirname, isAbsolute, join as join5, normalize, resolve, sep } from "path";
4578
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync, writeFileSync } from "fs";
4579
+ import { basename, dirname, isAbsolute, join as join5, normalize, resolve as resolve2, sep } from "path";
4778
4580
  function parseManifest(json) {
4779
4581
  const parsed = JSON.parse(json);
4780
4582
  return manifestSchema.parse(parsed);
@@ -4789,8 +4591,8 @@ function isWithin(child, parent) {
4789
4591
  return child === parent || child.startsWith(parent + sep);
4790
4592
  }
4791
4593
  function assertContainedLexical(target, root, raw) {
4792
- const lexical = resolve(target);
4793
- const rootLexical = resolve(root);
4594
+ const lexical = resolve2(target);
4595
+ const rootLexical = resolve2(root);
4794
4596
  if (!isWithin(lexical, rootLexical)) {
4795
4597
  throw new ScriptContainmentError(target, root, raw);
4796
4598
  }
@@ -4843,7 +4645,7 @@ function resolveScript(manifest, manifestDir) {
4843
4645
  if (!existsSync4(resolved)) {
4844
4646
  throw new Error(`manifest script file not found: ${resolved}`);
4845
4647
  }
4846
- return { path: rel, content: readFileSync4(resolved, "utf-8") };
4648
+ return { path: rel, content: readFileSync2(resolved, "utf-8") };
4847
4649
  }
4848
4650
  function customHookDir(name) {
4849
4651
  return join5(getCustomHooksDir(), shortManifestName(name));
@@ -4854,7 +4656,7 @@ function readCustomManifest(name) {
4854
4656
  if (!existsSync4(manifestPath))
4855
4657
  return;
4856
4658
  try {
4857
- const manifest = parseManifest(readFileSync4(manifestPath, "utf-8"));
4659
+ const manifest = parseManifest(readFileSync2(manifestPath, "utf-8"));
4858
4660
  const script = resolveScript(manifest, dir);
4859
4661
  const scriptIsInline = isInlineScript(manifest);
4860
4662
  return { manifest, scriptPath: join5(dir, script.path), scriptContent: script.content, scriptIsInline };
@@ -4883,13 +4685,13 @@ function writeCustomHook(name, manifest, scriptContent, scriptRelPath) {
4883
4685
  }
4884
4686
  const scriptPath = scriptRelPath.includes("/") ? join5(dir, scriptRelPath) : join5(dir, scriptRelPath);
4885
4687
  assertContainedLexical(scriptPath, dir, scriptRelPath);
4886
- mkdirSync3(dir, { recursive: true });
4688
+ mkdirSync2(dir, { recursive: true });
4887
4689
  assertContainedReal(dir, scriptPath, scriptRelPath);
4888
4690
  const scriptDir = dirname(scriptPath);
4889
4691
  if (scriptDir !== dir)
4890
- mkdirSync3(scriptDir, { recursive: true });
4891
- writeFileSync2(scriptPath, scriptContent, "utf-8");
4892
- writeFileSync2(join5(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
4692
+ mkdirSync2(scriptDir, { recursive: true });
4693
+ writeFileSync(scriptPath, scriptContent, "utf-8");
4694
+ writeFileSync(join5(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + `
4893
4695
  `, "utf-8");
4894
4696
  return { dir, scriptPath };
4895
4697
  }
@@ -4947,7 +4749,7 @@ __export(exports_store, {
4947
4749
  LockFileError: () => LockFileError
4948
4750
  });
4949
4751
  import { createHash } from "crypto";
4950
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3, renameSync } from "fs";
4752
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
4951
4753
  import { readFile } from "fs/promises";
4952
4754
  import { randomBytes } from "crypto";
4953
4755
  function sha256Of(input) {
@@ -5013,7 +4815,7 @@ function readLock() {
5013
4815
  return { hooks: {} };
5014
4816
  let raw;
5015
4817
  try {
5016
- raw = readFileSync5(path, "utf-8");
4818
+ raw = readFileSync3(path, "utf-8");
5017
4819
  } catch {
5018
4820
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
5019
4821
  }
@@ -5028,13 +4830,13 @@ function readLock() {
5028
4830
  }
5029
4831
  function writeLock(lock) {
5030
4832
  const path = getLockPath();
5031
- mkdirSync4(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
4833
+ mkdirSync3(path.substring(0, path.lastIndexOf("/")) || ".", { recursive: true });
5032
4834
  const sorted = {};
5033
4835
  for (const name of Object.keys(lock.hooks).sort()) {
5034
4836
  sorted[name] = lock.hooks[name];
5035
4837
  }
5036
4838
  const tmp = `${path}.tmp-${randomBytes(8).toString("hex")}`;
5037
- writeFileSync3(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
4839
+ writeFileSync2(tmp, JSON.stringify({ hooks: sorted }, null, 2) + `
5038
4840
  `, "utf-8");
5039
4841
  renameSync(tmp, path);
5040
4842
  return path;
@@ -5110,7 +4912,7 @@ async function verifyScriptHash(name, scriptPath) {
5110
4912
  return checkScriptHash(name, await sha256File(scriptPath));
5111
4913
  }
5112
4914
  function retrustHook(name, scriptPath, version, source) {
5113
- const actual = sha256Of(readFileSync5(scriptPath));
4915
+ const actual = sha256Of(readFileSync3(scriptPath));
5114
4916
  const db = getDb();
5115
4917
  upsertHookRecord(db, {
5116
4918
  name,
@@ -5147,32 +4949,29 @@ __export(exports_profiles, {
5147
4949
  deleteProfile: () => deleteProfile,
5148
4950
  createProfile: () => createProfile
5149
4951
  });
5150
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync3, rmSync as rmSync2, cpSync as cpSync2 } from "fs";
4952
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
5151
4953
  import { join as join8 } from "path";
5152
- import { homedir as homedir5 } from "os";
5153
- function resolveProfilesDir() {
5154
- const newDir = join8(homedir5(), ".hasna", "hooks", "profiles");
5155
- const oldDir = join8(homedir5(), ".hooks", "profiles");
4954
+ function getProfilesDir() {
4955
+ const newDir = join8(getEffectiveDataRoot(), "profiles");
4956
+ const oldDir = join8(getHomeDir(), ".hooks", "profiles");
5156
4957
  if (!existsSync8(newDir) && existsSync8(oldDir)) {
5157
- mkdirSync6(join8(homedir5(), ".hasna", "hooks"), { recursive: true });
4958
+ mkdirSync5(newDir, { recursive: true });
5158
4959
  cpSync2(oldDir, newDir, { recursive: true });
5159
4960
  }
5160
4961
  return newDir;
5161
4962
  }
5162
4963
  function ensureProfilesDir() {
5163
- if (!existsSync8(PROFILES_DIR)) {
5164
- mkdirSync6(PROFILES_DIR, { recursive: true });
4964
+ const dir = getProfilesDir();
4965
+ if (!existsSync8(dir)) {
4966
+ mkdirSync5(dir, { recursive: true });
5165
4967
  }
5166
4968
  }
5167
4969
  function profilePath(id) {
5168
- return join8(PROFILES_DIR, `${id}.json`);
4970
+ return join8(getProfilesDir(), `${id}.json`);
5169
4971
  }
5170
4972
  function shortUuid() {
5171
4973
  return crypto.randomUUID().slice(0, 8);
5172
4974
  }
5173
- function getProfilesDir() {
5174
- return PROFILES_DIR;
5175
- }
5176
4975
  function createProfile(input) {
5177
4976
  ensureProfilesDir();
5178
4977
  const id = shortUuid();
@@ -5187,7 +4986,7 @@ function createProfile(input) {
5187
4986
  if (input.name) {
5188
4987
  profile.name = input.name;
5189
4988
  }
5190
- writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
4989
+ writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5191
4990
  `);
5192
4991
  return profile;
5193
4992
  }
@@ -5196,20 +4995,21 @@ function getProfile(id) {
5196
4995
  try {
5197
4996
  if (!existsSync8(path))
5198
4997
  return null;
5199
- return JSON.parse(readFileSync7(path, "utf-8"));
4998
+ return JSON.parse(readFileSync5(path, "utf-8"));
5200
4999
  } catch {
5201
5000
  return null;
5202
5001
  }
5203
5002
  }
5204
5003
  function listProfiles() {
5205
- if (!existsSync8(PROFILES_DIR))
5004
+ const dir = getProfilesDir();
5005
+ if (!existsSync8(dir))
5206
5006
  return [];
5207
5007
  try {
5208
- const files = readdirSync3(PROFILES_DIR).filter((f) => f.endsWith(".json"));
5008
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
5209
5009
  const profiles = [];
5210
5010
  for (const file of files) {
5211
5011
  try {
5212
- const content = readFileSync7(join8(PROFILES_DIR, file), "utf-8");
5012
+ const content = readFileSync5(join8(dir, file), "utf-8");
5213
5013
  profiles.push(JSON.parse(content));
5214
5014
  } catch {}
5215
5015
  }
@@ -5226,7 +5026,7 @@ function updateProfile(id, data) {
5226
5026
  profile.name = data.name;
5227
5027
  if (data.preferences !== undefined)
5228
5028
  profile.preferences = data.preferences;
5229
- writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
5029
+ writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5230
5030
  `);
5231
5031
  return profile;
5232
5032
  }
@@ -5235,7 +5035,7 @@ function deleteProfile(id) {
5235
5035
  if (!existsSync8(path))
5236
5036
  return false;
5237
5037
  try {
5238
- rmSync2(path);
5038
+ rmSync(path);
5239
5039
  return true;
5240
5040
  } catch {
5241
5041
  return false;
@@ -5246,7 +5046,7 @@ function touchProfile(id) {
5246
5046
  if (!profile)
5247
5047
  return;
5248
5048
  profile.last_seen_at = new Date().toISOString();
5249
- writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
5049
+ writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5250
5050
  `);
5251
5051
  }
5252
5052
  function exportProfiles() {
@@ -5266,20 +5066,19 @@ function importProfiles(profiles) {
5266
5066
  skipped++;
5267
5067
  continue;
5268
5068
  }
5269
- writeFileSync5(path, JSON.stringify(profile, null, 2) + `
5069
+ writeFileSync4(path, JSON.stringify(profile, null, 2) + `
5270
5070
  `);
5271
5071
  imported++;
5272
5072
  }
5273
5073
  return { imported, skipped };
5274
5074
  }
5275
- var PROFILES_DIR;
5276
5075
  var init_profiles = __esm(() => {
5277
- PROFILES_DIR = resolveProfilesDir();
5076
+ init_app_home();
5278
5077
  });
5279
5078
 
5280
5079
  // src/lib/hook-env.ts
5281
- import { dirname as dirname5, isAbsolute as isAbsolute2 } from "path";
5282
- import { existsSync as existsSync10, statSync as statSync2 } from "fs";
5080
+ import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
5081
+ import { existsSync as existsSync10, statSync } from "fs";
5283
5082
  function isDeniedEnvName(name) {
5284
5083
  const upper = name.toUpperCase();
5285
5084
  if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
@@ -5297,7 +5096,7 @@ function isBashFunctionEnvName(name) {
5297
5096
  return name.startsWith("BASH_FUNC_");
5298
5097
  }
5299
5098
  function isUnsafePathEntry(entry, home) {
5300
- if (entry === "" || !isAbsolute2(entry))
5099
+ if (entry === "" || !isAbsolute3(entry))
5301
5100
  return true;
5302
5101
  if (home && (entry === home || entry.startsWith(home + "/")))
5303
5102
  return true;
@@ -5306,7 +5105,7 @@ function isUnsafePathEntry(entry, home) {
5306
5105
  if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
5307
5106
  return true;
5308
5107
  try {
5309
- if ((statSync2(entry).mode & 2) !== 0)
5108
+ if ((statSync(entry).mode & 2) !== 0)
5310
5109
  return true;
5311
5110
  } catch {}
5312
5111
  return false;
@@ -5438,8 +5237,8 @@ __export(exports_run, {
5438
5237
  HookTimeoutError: () => HookTimeoutError
5439
5238
  });
5440
5239
  import { randomBytes as randomBytes2 } from "crypto";
5441
- import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
5442
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute3, join as join11 } from "path";
5240
+ import { rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
5241
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join12 } from "path";
5443
5242
  function extensionOf(scriptPath) {
5444
5243
  const base = basename2(scriptPath);
5445
5244
  const dot = base.lastIndexOf(".");
@@ -5510,7 +5309,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
5510
5309
  async function executeVerifiedScript(options) {
5511
5310
  const scriptDir = dirname6(options.scriptPath);
5512
5311
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
5513
- const tempPath = join11(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5312
+ const tempPath = join12(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5514
5313
  let proc = null;
5515
5314
  let timedOut = false;
5516
5315
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -5518,8 +5317,8 @@ async function executeVerifiedScript(options) {
5518
5317
  killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
5519
5318
  }, options.timeout) : null;
5520
5319
  try {
5521
- writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
5522
- proc = Bun.spawn([...interpreter.command, isAbsolute3(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5320
+ writeFileSync5(tempPath, options.content, { flag: "wx", mode: 384 });
5321
+ proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5523
5322
  stdin: new Response(options.stdin),
5524
5323
  stdout: "pipe",
5525
5324
  stderr: "pipe",
@@ -5540,7 +5339,7 @@ async function executeVerifiedScript(options) {
5540
5339
  if (timeoutTimer)
5541
5340
  clearTimeout(timeoutTimer);
5542
5341
  try {
5543
- rmSync4(tempPath, { force: true });
5342
+ rmSync3(tempPath, { force: true });
5544
5343
  } catch {}
5545
5344
  }
5546
5345
  }
@@ -6125,6 +5924,7 @@ function coerceForSqlite(value) {
6125
5924
  return String(value);
6126
5925
  }
6127
5926
  // src/lib/registry.ts
5927
+ init_app_home();
6128
5928
  var HOOK_EVENTS = [
6129
5929
  "PreToolUse",
6130
5930
  "PostToolUse",
@@ -6462,7 +6262,7 @@ var HOOKS = [
6462
6262
  {
6463
6263
  name: "sessionlog",
6464
6264
  displayName: "Session Log",
6465
- description: "Logs every tool call to SQLite (~/.hasna/hooks/hooks.db)",
6265
+ description: `Logs every tool call to SQLite (${getReportedDbPath()})`,
6466
6266
  version: "0.1.0",
6467
6267
  category: "Observability",
6468
6268
  event: "PostToolUse",
@@ -6472,7 +6272,7 @@ var HOOKS = [
6472
6272
  {
6473
6273
  name: "commandlog",
6474
6274
  displayName: "Command Log",
6475
- description: "Logs every Bash command to SQLite (~/.hasna/hooks/hooks.db)",
6275
+ description: `Logs every Bash command to SQLite (${getReportedDbPath()})`,
6476
6276
  version: "0.1.0",
6477
6277
  category: "Observability",
6478
6278
  event: "PostToolUse",
@@ -6492,7 +6292,7 @@ var HOOKS = [
6492
6292
  {
6493
6293
  name: "errornotify",
6494
6294
  displayName: "Error Notify",
6495
- description: "Detects tool failures and logs errors to SQLite (~/.hasna/hooks/hooks.db)",
6295
+ description: `Detects tool failures and logs errors to SQLite (${getReportedDbPath()})`,
6496
6296
  version: "0.1.0",
6497
6297
  category: "Observability",
6498
6298
  event: "PostToolUse",
@@ -6674,9 +6474,9 @@ function getHook(name) {
6674
6474
  return HOOKS.find((h) => h.name === name);
6675
6475
  }
6676
6476
  // src/lib/installer.ts
6677
- import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync, writeFileSync as writeFileSync4 } from "fs";
6477
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
6678
6478
  import { join as join7, dirname as dirname3 } from "path";
6679
- import { homedir as homedir4 } from "os";
6479
+ import { homedir as homedir3 } from "os";
6680
6480
  import { fileURLToPath as fileURLToPath2 } from "url";
6681
6481
 
6682
6482
  // src/lib/resolve.ts
@@ -6840,7 +6640,7 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
6840
6640
  if (scope === "project") {
6841
6641
  return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
6842
6642
  }
6843
- return target === "codewith" ? join7(homedir4(), dir, "config.toml") : join7(homedir4(), dir, "settings.json");
6643
+ return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
6844
6644
  }
6845
6645
  function getHookPath(name) {
6846
6646
  const shortName = shortHookName(name);
@@ -6856,7 +6656,7 @@ function readSettings(scope = "global", target = "claude") {
6856
6656
  const path = getSettingsPath(scope, target);
6857
6657
  try {
6858
6658
  if (existsSync7(path)) {
6859
- return JSON.parse(readFileSync6(path, "utf-8"));
6659
+ return JSON.parse(readFileSync4(path, "utf-8"));
6860
6660
  }
6861
6661
  } catch (error) {
6862
6662
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -6867,9 +6667,9 @@ function writeSettings(settings, scope = "global", target = "claude") {
6867
6667
  const path = getSettingsPath(scope, target);
6868
6668
  const dir = dirname3(path);
6869
6669
  if (!existsSync7(dir)) {
6870
- mkdirSync5(dir, { recursive: true });
6670
+ mkdirSync4(dir, { recursive: true });
6871
6671
  }
6872
- writeFileSync4(path, JSON.stringify(settings, null, 2) + `
6672
+ writeFileSync3(path, JSON.stringify(settings, null, 2) + `
6873
6673
  `);
6874
6674
  }
6875
6675
  function getTargetEventName(internalEvent, target) {
@@ -6955,7 +6755,7 @@ function buildCodewithTomlFragment(name, profile) {
6955
6755
  function readCodewithConfig(scope, configPath) {
6956
6756
  const path = getSettingsPath(scope, "codewith", configPath);
6957
6757
  try {
6958
- return existsSync7(path) ? readFileSync6(path, "utf-8") : "";
6758
+ return existsSync7(path) ? readFileSync4(path, "utf-8") : "";
6959
6759
  } catch {
6960
6760
  return "";
6961
6761
  }
@@ -6964,12 +6764,12 @@ function appendCodewithFragment(fragment, scope, configPath) {
6964
6764
  const path = getSettingsPath(scope, "codewith", configPath);
6965
6765
  const dir = dirname3(path);
6966
6766
  if (!existsSync7(dir))
6967
- mkdirSync5(dir, { recursive: true });
6968
- const existing = existsSync7(path) ? readFileSync6(path, "utf-8") : "";
6767
+ mkdirSync4(dir, { recursive: true });
6768
+ const existing = existsSync7(path) ? readFileSync4(path, "utf-8") : "";
6969
6769
  const sep2 = existing.trim() ? `
6970
6770
 
6971
6771
  ` : "";
6972
- writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
6772
+ writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
6973
6773
  return path;
6974
6774
  }
6975
6775
  function detectConflict(name, scope, target) {
@@ -7269,11 +7069,25 @@ function codewithHasHookEntry(name, scope = "global") {
7269
7069
  if (!existsSync7(path))
7270
7070
  return false;
7271
7071
  try {
7272
- return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
7072
+ return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
7273
7073
  } catch {
7274
7074
  return false;
7275
7075
  }
7276
7076
  }
7077
+ function removeStoreDirFailClosed(dir) {
7078
+ const entries = readdirSync3(dir, { withFileTypes: true });
7079
+ for (const entry of entries) {
7080
+ if (entry.isDirectory()) {
7081
+ removeStoreDirFailClosed(join7(dir, entry.name));
7082
+ }
7083
+ }
7084
+ for (const entry of entries) {
7085
+ if (!entry.isDirectory()) {
7086
+ unlinkSync(join7(dir, entry.name));
7087
+ }
7088
+ }
7089
+ rmdirSync(dir);
7090
+ }
7277
7091
  function uninstallHook(name, scope = "global", target = "claude") {
7278
7092
  const shortName = shortHookName(name);
7279
7093
  const custom2 = readCustomManifest(shortName);
@@ -7301,10 +7115,10 @@ function uninstallHook(name, scope = "global", target = "claude") {
7301
7115
  if (target === "codewith" || target === "all") {
7302
7116
  const codewithPath = getSettingsPath(scope, "codewith");
7303
7117
  if (existsSync7(codewithPath)) {
7304
- const before = readFileSync6(codewithPath, "utf-8");
7118
+ const before = readFileSync4(codewithPath, "utf-8");
7305
7119
  const after = removeCodewithHookEntry(before, shortName);
7306
7120
  if (after.removed) {
7307
- writeFileSync4(codewithPath, after.text, "utf-8");
7121
+ writeFileSync3(codewithPath, after.text, "utf-8");
7308
7122
  }
7309
7123
  if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
7310
7124
  registrationsRemaining.push("codewith");
@@ -7313,7 +7127,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
7313
7127
  }
7314
7128
  if (custom2) {
7315
7129
  try {
7316
- rmSync(customHookDir(shortName), { recursive: true, force: true });
7130
+ removeStoreDirFailClosed(customHookDir(shortName));
7317
7131
  } catch {
7318
7132
  return {
7319
7133
  name: shortName,
@@ -7343,27 +7157,896 @@ function uninstallHook(name, scope = "global", target = "claude") {
7343
7157
  // src/index.ts
7344
7158
  init_profiles();
7345
7159
  init_config();
7346
- init_store();
7347
- init_manifest();
7348
- import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
7160
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
7349
7161
 
7350
- // src/lib/custom-install.ts
7351
- init_config();
7352
- init_manifest();
7353
- import { existsSync as existsSync9, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
7354
- import { join as join9 } from "path";
7355
- import { tmpdir } from "os";
7356
- function manifestAt(dir) {
7357
- const path = join9(dir, "manifest.json");
7358
- if (!existsSync9(path))
7359
- return;
7360
- try {
7361
- return parseManifest(readFileSync8(path, "utf-8"));
7362
- } catch {
7363
- return;
7364
- }
7365
- }
7366
- function copyManifestDir(srcDir, source, kind) {
7162
+ // ../contracts/dist/client/transport.js
7163
+ import { isIP } from "net";
7164
+ import { spawnSync } from "child_process";
7165
+ import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
7166
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
7167
+ import { createRequire } from "module";
7168
+ import { hostname as osHostname } from "os";
7169
+ import { isAbsolute as isAbsolute2, join as join9 } from "path";
7170
+ function envToken(name) {
7171
+ return name.toUpperCase().replace(/-/g, "_");
7172
+ }
7173
+ function clientTransportEnvKeys(name) {
7174
+ const envSegment = envToken(name);
7175
+ return {
7176
+ apiUrlKeys: [`HASNA_${envSegment}_API_URL`, `${envSegment}_API_URL`],
7177
+ apiKeyKeys: [`HASNA_${envSegment}_API_KEY`, `${envSegment}_API_KEY`]
7178
+ };
7179
+ }
7180
+ function credentialOverrideEnvKey(name) {
7181
+ return `HASNA_${envToken(name)}_API_KEY_OVERRIDE`;
7182
+ }
7183
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE";
7184
+ function credentialPointerEnvKey(name) {
7185
+ return `HASNA_${envToken(name)}_API_KEY_REF`;
7186
+ }
7187
+
7188
+ class CredentialResolutionError extends Error {
7189
+ appName;
7190
+ attempted;
7191
+ constructor(appName, message, attempted) {
7192
+ super(message);
7193
+ this.name = "CredentialResolutionError";
7194
+ this.appName = appName;
7195
+ this.attempted = attempted;
7196
+ }
7197
+ }
7198
+
7199
+ class CredentialFileUnsafeError extends Error {
7200
+ path;
7201
+ constructor(path, reason) {
7202
+ super(`Refusing unsafe credential/config file ${path}: ${reason}.`);
7203
+ this.name = "CredentialFileUnsafeError";
7204
+ this.path = path;
7205
+ }
7206
+ }
7207
+ var HASNA_HOME_ENV_KEY = "HASNA_HOME";
7208
+ var HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME";
7209
+ var KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION";
7210
+ var HASNA_HOME_DIR = ".hasna";
7211
+ var CONFIG_SUBDIR = "config";
7212
+ var CREDENTIALS_FILE = "credentials";
7213
+ var KEYCHAIN_SECURITY_BIN = "/usr/bin/security";
7214
+ var KEYCHAIN_SERVICE_PREFIX = "hasna.credentials";
7215
+ var KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44;
7216
+ var KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4;
7217
+ var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
7218
+ var SAFE_APP_SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7219
+ var SAFE_PROFILE = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
7220
+ var ILLEGAL_IN_HEADER_VALUE = /[^\t\x20-\x7e]/;
7221
+ var VAULT_POINTER_SHAPE = /^[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-_.]*){2,}$/;
7222
+ function homeDir(env) {
7223
+ const home = env.HOME?.trim();
7224
+ return home ? home : null;
7225
+ }
7226
+ function absoluteOverride(env, key) {
7227
+ const value = env[key]?.trim();
7228
+ return value && isAbsolute2(value) ? value : null;
7229
+ }
7230
+ function hasnaHomeDir(env) {
7231
+ const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
7232
+ if (override)
7233
+ return override;
7234
+ const home = homeDir(env);
7235
+ return home ? join9(home, HASNA_HOME_DIR) : null;
7236
+ }
7237
+ function appConfigDir(name, env) {
7238
+ const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
7239
+ if (configRoot)
7240
+ return join9(configRoot, name);
7241
+ const root = hasnaHomeDir(env);
7242
+ return root ? join9(root, name, CONFIG_SUBDIR) : null;
7243
+ }
7244
+ function credentialDiskSourceList(name, env, profile = null) {
7245
+ if (!SAFE_APP_SLUG.test(name))
7246
+ return [];
7247
+ const directory = appConfigDir(name, env);
7248
+ if (!directory)
7249
+ return [];
7250
+ const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
7251
+ return [{ path: join9(directory, file), tier: "disk" }];
7252
+ }
7253
+ function credentialDiskSources(name, env) {
7254
+ return credentialDiskSourceList(name, env, null).map((s) => s.path);
7255
+ }
7256
+ function profileDiskSources(name, env, profile) {
7257
+ return credentialDiskSourceList(name, env, profile).map((s) => s.path);
7258
+ }
7259
+ function parseEnvFile(text) {
7260
+ const values = new Map;
7261
+ const unusable = new Set;
7262
+ for (const rawLine of text.split(/\r?\n/)) {
7263
+ const line = rawLine.trim();
7264
+ if (line.length === 0 || line.startsWith("#"))
7265
+ continue;
7266
+ const withoutExport = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
7267
+ const equals = withoutExport.indexOf("=");
7268
+ if (equals <= 0)
7269
+ continue;
7270
+ const key = withoutExport.slice(0, equals).trim();
7271
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
7272
+ continue;
7273
+ let value = withoutExport.slice(equals + 1).trim();
7274
+ const quote = value[0];
7275
+ if (quote === '"' || quote === "'") {
7276
+ if (value.length < 2 || !value.endsWith(quote)) {
7277
+ unusable.add(key);
7278
+ continue;
7279
+ }
7280
+ value = value.slice(1, -1);
7281
+ }
7282
+ if (value.trim().length === 0) {
7283
+ unusable.add(key);
7284
+ continue;
7285
+ }
7286
+ if (values.has(key) && values.get(key) !== value)
7287
+ unusable.add(key);
7288
+ values.set(key, value);
7289
+ }
7290
+ return { values, unusable };
7291
+ }
7292
+ function configFileModeAllowed(mode) {
7293
+ const permissions = mode & 4095;
7294
+ return permissions === 256 || permissions === 384;
7295
+ }
7296
+ function configFileReadsCoherent(before, after) {
7297
+ return before.dev === after.dev && before.ino === after.ino && before.size === after.size && before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs;
7298
+ }
7299
+ function readAppConfigFile(path) {
7300
+ const unsafe = (reason) => {
7301
+ throw new CredentialFileUnsafeError(path, reason);
7302
+ };
7303
+ let fd = -1;
7304
+ try {
7305
+ fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
7306
+ } catch (error) {
7307
+ const code = error.code;
7308
+ if (code === "ENOENT" || code === "ENOTDIR")
7309
+ return null;
7310
+ if (code === "ELOOP")
7311
+ unsafe("the path is a symlink");
7312
+ unsafe(`the path could not be opened (${code ?? "unknown error"})`);
7313
+ }
7314
+ try {
7315
+ const before = fstatSync(fd);
7316
+ if (!before.isFile())
7317
+ unsafe("the path is not a regular file");
7318
+ if (!configFileModeAllowed(before.mode)) {
7319
+ unsafe(`permission mode ${(before.mode & 4095).toString(8).padStart(4, "0")} is not owner-only 0400 or 0600`);
7320
+ }
7321
+ const uid = process.getuid?.() ?? process.geteuid?.();
7322
+ if (uid !== undefined && before.uid !== uid)
7323
+ unsafe("the file is not owned by the current user");
7324
+ if (before.size > MAX_CREDENTIAL_FILE_BYTES)
7325
+ unsafe("the file exceeds the size limit");
7326
+ const bytes = readFileSync6(fd);
7327
+ const after = fstatSync(fd);
7328
+ if (!configFileReadsCoherent(before, after)) {
7329
+ unsafe("the file changed while being read");
7330
+ }
7331
+ return parseEnvFile(bytes.toString("utf8"));
7332
+ } finally {
7333
+ if (fd !== -1)
7334
+ closeSync(fd);
7335
+ }
7336
+ }
7337
+ function readCredentialFile(path, apiKeyKeys) {
7338
+ const parsed = readAppConfigFile(path);
7339
+ if (!parsed)
7340
+ return null;
7341
+ for (const key of apiKeyKeys) {
7342
+ if (parsed.unusable.has(key)) {
7343
+ throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
7344
+ }
7345
+ }
7346
+ const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
7347
+ if (new Set(values).size > 1) {
7348
+ throw new CredentialFileUnsafeError(path, "credential aliases disagree");
7349
+ }
7350
+ return values[0] ?? null;
7351
+ }
7352
+ var CREDENTIAL_SHAPED_KEY = /(?:^|_)(?:API_KEY|KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH)(?:_|$)/;
7353
+ function appConfigDiskValue(name, env, keys) {
7354
+ const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
7355
+ if (wanted.length === 0)
7356
+ return null;
7357
+ for (const path of credentialDiskSources(name, env)) {
7358
+ const parsed = readAppConfigFile(path);
7359
+ if (!parsed)
7360
+ continue;
7361
+ if (wanted.some((key) => parsed.unusable.has(key))) {
7362
+ return { key: wanted.find((key) => parsed.unusable.has(key)), value: "", path, unusable: true };
7363
+ }
7364
+ const values = wanted.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
7365
+ if (new Set(values).size > 1)
7366
+ throw new CredentialFileUnsafeError(path, "configuration aliases disagree");
7367
+ for (const key of wanted) {
7368
+ if (parsed.unusable.has(key))
7369
+ return { key, value: "", path, unusable: true };
7370
+ const value = parsed.values.get(key)?.trim();
7371
+ if (value)
7372
+ return { key, value, path };
7373
+ }
7374
+ }
7375
+ return null;
7376
+ }
7377
+ function assertUsableCredential(appName, source, value) {
7378
+ if (VAULT_POINTER_SHAPE.test(value)) {
7379
+ throw new CredentialResolutionError(appName, `The credential from ${source} looks like a secrets-vault pointer (a path-shaped reference like ` + `'namespace/app/live/api_key'). A vault path is NEVER accepted as a literal API key. ` + `Use ${credentialPointerEnvKey(appName)} to resolve the key through the vault, or provide the actual key value.`, [source]);
7380
+ }
7381
+ if (!ILLEGAL_IN_HEADER_VALUE.test(value))
7382
+ return;
7383
+ 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]);
7384
+ }
7385
+ var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
7386
+ var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
7387
+ function sealCredential(fields) {
7388
+ const { apiKey } = fields;
7389
+ const visible = {
7390
+ tier: fields.tier,
7391
+ source: fields.source,
7392
+ deliberate: fields.deliberate,
7393
+ diskCandidates: Object.freeze([...fields.diskCandidates]),
7394
+ warning: fields.warning
7395
+ };
7396
+ const sealed = { ...visible };
7397
+ Object.defineProperty(sealed, "apiKey", {
7398
+ value: apiKey,
7399
+ enumerable: false,
7400
+ writable: false,
7401
+ configurable: false
7402
+ });
7403
+ if (fields.pointerVaultKey !== undefined) {
7404
+ Object.defineProperty(sealed, "pointerVaultKey", {
7405
+ value: fields.pointerVaultKey,
7406
+ enumerable: false,
7407
+ writable: false,
7408
+ configurable: false
7409
+ });
7410
+ }
7411
+ Object.defineProperty(sealed, INSPECT_CUSTOM, {
7412
+ value: () => ({ ...visible, apiKey: "[redacted]" }),
7413
+ enumerable: false,
7414
+ writable: false,
7415
+ configurable: false
7416
+ });
7417
+ Object.defineProperty(sealed, CREDENTIAL_SEAL, {
7418
+ value: true,
7419
+ enumerable: false,
7420
+ writable: false,
7421
+ configurable: false
7422
+ });
7423
+ return Object.freeze(sealed);
7424
+ }
7425
+ function firstEnvValue(env, keys) {
7426
+ for (const key of keys) {
7427
+ if (!Object.prototype.hasOwnProperty.call(env, key))
7428
+ continue;
7429
+ const value = env[key]?.trim();
7430
+ if (value)
7431
+ return { key, value };
7432
+ }
7433
+ return null;
7434
+ }
7435
+ var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
7436
+ function isAmbientEnvironment(env) {
7437
+ return env === process.env || env[AMBIENT_ENVIRONMENT] === true;
7438
+ }
7439
+ function defaultKeychainRunner(argv) {
7440
+ const result = spawnSync(KEYCHAIN_SECURITY_BIN, [...argv], {
7441
+ encoding: "utf8",
7442
+ stdio: ["ignore", "pipe", "pipe"],
7443
+ timeout: KEYCHAIN_SPAWN_TIMEOUT_MS
7444
+ });
7445
+ return {
7446
+ status: result.status,
7447
+ stdout: result.stdout ?? "",
7448
+ stderr: result.error ? result.error.message : result.stderr ?? ""
7449
+ };
7450
+ }
7451
+ function keychainTierEnabled(env, options) {
7452
+ if ((options.platform ?? process.platform) !== "darwin")
7453
+ return false;
7454
+ if (options.enabled !== undefined)
7455
+ return options.enabled;
7456
+ return options.run !== undefined || isAmbientEnvironment(env);
7457
+ }
7458
+ function keychainAccount(env, options) {
7459
+ const station = env[KEYCHAIN_STATION_ENV_KEY]?.trim();
7460
+ if (station)
7461
+ return station;
7462
+ const host = (options.hostname ?? osHostname)().split(".")[0]?.trim() ?? "";
7463
+ if (host)
7464
+ return host;
7465
+ const user = env.USER?.trim();
7466
+ return user || null;
7467
+ }
7468
+ function keychainFailureHint(text) {
7469
+ const line = text.split(/\r?\n/).find((entry) => entry.trim().length > 0)?.trim() ?? "";
7470
+ const clean = line.replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 200);
7471
+ return clean ? `: ${clean}` : "";
7472
+ }
7473
+ function readKeychainItem(name, env, kind, options) {
7474
+ if (!SAFE_APP_SLUG.test(name) || !keychainTierEnabled(env, options))
7475
+ return null;
7476
+ const account = keychainAccount(env, options);
7477
+ if (!account)
7478
+ return null;
7479
+ const service = `${KEYCHAIN_SERVICE_PREFIX}.${name}.${kind}`;
7480
+ const source = `keychain:${service}@${account}`;
7481
+ const run = options.run ?? defaultKeychainRunner;
7482
+ let result;
7483
+ try {
7484
+ result = run(["find-generic-password", "-a", account, "-s", service, "-w"]);
7485
+ } catch (error) {
7486
+ const reason = keychainFailureHint(error instanceof Error ? error.message : String(error));
7487
+ 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]);
7488
+ }
7489
+ if (result.status === KEYCHAIN_ITEM_NOT_FOUND_STATUS)
7490
+ return null;
7491
+ if (result.status !== 0) {
7492
+ 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]);
7493
+ }
7494
+ const value = result.stdout.trim();
7495
+ if (!value) {
7496
+ 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]);
7497
+ }
7498
+ return { value, source };
7499
+ }
7500
+ function keychainConfigValue(name, env, options = {}) {
7501
+ return readKeychainItem(name, env, "api-url", options);
7502
+ }
7503
+ function snapshotClientEnvironment(name, env) {
7504
+ const keys = clientTransportEnvKeys(name);
7505
+ const ambient = isAmbientEnvironment(env);
7506
+ const snapshot = Object.create(null);
7507
+ for (const key of [
7508
+ ...keys.apiUrlKeys,
7509
+ ...keys.apiKeyKeys,
7510
+ credentialOverrideEnvKey(name),
7511
+ credentialPointerEnvKey(name),
7512
+ CREDENTIAL_PROFILE_ENV_KEY,
7513
+ "HOME",
7514
+ HASNA_HOME_ENV_KEY,
7515
+ HASNA_CONFIG_HOME_ENV_KEY,
7516
+ KEYCHAIN_STATION_ENV_KEY,
7517
+ "USER"
7518
+ ]) {
7519
+ const descriptor = Object.getOwnPropertyDescriptor(env, key);
7520
+ if (!descriptor)
7521
+ continue;
7522
+ if (!("value" in descriptor)) {
7523
+ throw new CredentialResolutionError(name, `${key} is accessor-backed; client configuration requires own data properties.`, [key]);
7524
+ }
7525
+ if (descriptor.value !== undefined && typeof descriptor.value !== "string") {
7526
+ throw new CredentialResolutionError(name, `${key} must be a string data property.`, [key]);
7527
+ }
7528
+ snapshot[key] = descriptor.value;
7529
+ }
7530
+ if (ambient) {
7531
+ Object.defineProperty(snapshot, AMBIENT_ENVIRONMENT, {
7532
+ value: true,
7533
+ enumerable: false,
7534
+ writable: false,
7535
+ configurable: false
7536
+ });
7537
+ }
7538
+ return Object.freeze(snapshot);
7539
+ }
7540
+ function resolveCredential(name, env, options = {}) {
7541
+ env = snapshotClientEnvironment(name, env);
7542
+ const { apiKeyKeys } = clientTransportEnvKeys(name);
7543
+ const diskPaths = credentialDiskSources(name, env);
7544
+ if (options.apiKey !== undefined) {
7545
+ const explicitKey = options.apiKey.trim();
7546
+ if (!explicitKey) {
7547
+ throw new CredentialResolutionError(name, "The explicit apiKey argument is blank; an explicit credential never falls through to another identity.", ["explicit apiKey argument"]);
7548
+ }
7549
+ assertUsableCredential(name, "the explicit apiKey argument", explicitKey);
7550
+ return sealCredential({
7551
+ apiKey: explicitKey,
7552
+ tier: "argument",
7553
+ source: "explicit apiKey argument",
7554
+ deliberate: true,
7555
+ diskCandidates: diskPaths,
7556
+ warning: null
7557
+ });
7558
+ }
7559
+ const overrideKeyName = credentialOverrideEnvKey(name);
7560
+ const overrideRaw = Object.prototype.hasOwnProperty.call(env, overrideKeyName) ? env[overrideKeyName] : undefined;
7561
+ if (overrideRaw !== undefined) {
7562
+ const override = overrideRaw.trim();
7563
+ if (!override) {
7564
+ 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]);
7565
+ }
7566
+ assertUsableCredential(name, overrideKeyName, override);
7567
+ return sealCredential({
7568
+ apiKey: override,
7569
+ tier: "override",
7570
+ source: overrideKeyName,
7571
+ deliberate: true,
7572
+ diskCandidates: diskPaths,
7573
+ warning: null
7574
+ });
7575
+ }
7576
+ const pointerKeyName = credentialPointerEnvKey(name);
7577
+ const pointerRaw = Object.prototype.hasOwnProperty.call(env, pointerKeyName) ? env[pointerKeyName] : undefined;
7578
+ if (pointerRaw !== undefined) {
7579
+ const pointer = pointerRaw.trim();
7580
+ if (!pointer) {
7581
+ 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]);
7582
+ }
7583
+ if (!VAULT_POINTER_SHAPE.test(pointer)) {
7584
+ 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]);
7585
+ }
7586
+ return sealCredential({
7587
+ apiKey: "",
7588
+ pointerVaultKey: pointer,
7589
+ tier: "pointer",
7590
+ source: pointerKeyName,
7591
+ deliberate: true,
7592
+ diskCandidates: diskPaths,
7593
+ warning: null
7594
+ });
7595
+ }
7596
+ if (options.profile !== undefined && !options.profile.trim()) {
7597
+ throw new CredentialResolutionError(name, "The explicit profile argument is blank; an explicit identity selection never falls through.", ["explicit profile argument"]);
7598
+ }
7599
+ const profileRaw = Object.prototype.hasOwnProperty.call(env, CREDENTIAL_PROFILE_ENV_KEY) ? env[CREDENTIAL_PROFILE_ENV_KEY] : undefined;
7600
+ if (profileRaw !== undefined && !profileRaw.trim()) {
7601
+ throw new CredentialResolutionError(name, `${CREDENTIAL_PROFILE_ENV_KEY} is set but blank.`, [CREDENTIAL_PROFILE_ENV_KEY]);
7602
+ }
7603
+ const profile = options.profile?.trim() || profileRaw?.trim();
7604
+ if (profile) {
7605
+ const profileSource = options.profile?.trim() ? "explicit profile argument" : CREDENTIAL_PROFILE_ENV_KEY;
7606
+ if (!SAFE_PROFILE.test(profile)) {
7607
+ throw new CredentialResolutionError(name, `Profile name from ${profileSource} is not usable in a path. ` + `Use letters, digits, dot, dash, or underscore.`, [profileSource]);
7608
+ }
7609
+ const paths = profileDiskSources(name, env, profile);
7610
+ for (const path of paths) {
7611
+ const value = readCredentialFile(path, apiKeyKeys);
7612
+ if (value) {
7613
+ assertUsableCredential(name, path, value);
7614
+ return sealCredential({
7615
+ apiKey: value,
7616
+ tier: "profile",
7617
+ source: path,
7618
+ deliberate: true,
7619
+ diskCandidates: paths,
7620
+ warning: null
7621
+ });
7622
+ }
7623
+ }
7624
+ 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);
7625
+ }
7626
+ const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
7627
+ const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
7628
+ if (blankEnv) {
7629
+ throw new CredentialResolutionError(name, `${blankEnv.key} is set but blank; a declared credential never falls through to another alias or identity.`, [blankEnv.key]);
7630
+ }
7631
+ if (definedEnvEntries.length > 1 && new Set(definedEnvEntries.map((entry) => entry.value)).size > 1) {
7632
+ 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));
7633
+ }
7634
+ const envHit = firstEnvValue(env, apiKeyKeys);
7635
+ const keychainHit = readKeychainItem(name, env, "api-key", options.keychain ?? {});
7636
+ if (keychainHit) {
7637
+ assertUsableCredential(name, keychainHit.source, keychainHit.value);
7638
+ 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;
7639
+ return sealCredential({
7640
+ apiKey: keychainHit.value,
7641
+ tier: "keychain",
7642
+ source: keychainHit.source,
7643
+ deliberate: false,
7644
+ diskCandidates: diskPaths,
7645
+ warning
7646
+ });
7647
+ }
7648
+ const diskSourceList = credentialDiskSourceList(name, env, null);
7649
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
7650
+ if (diskHits.length > 0) {
7651
+ const winner = diskHits[0];
7652
+ assertUsableCredential(name, winner.src.path, winner.value);
7653
+ const divergentSources = [
7654
+ ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
7655
+ ...envHit && envHit.value !== winner.value ? [envHit.key] : []
7656
+ ];
7657
+ 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;
7658
+ return sealCredential({
7659
+ apiKey: winner.value,
7660
+ tier: winner.src.tier,
7661
+ source: winner.src.path,
7662
+ deliberate: false,
7663
+ diskCandidates: diskPaths,
7664
+ warning
7665
+ });
7666
+ }
7667
+ if (envHit) {
7668
+ assertUsableCredential(name, envHit.key, envHit.value);
7669
+ return sealCredential({
7670
+ apiKey: envHit.value,
7671
+ tier: "env",
7672
+ source: envHit.key,
7673
+ deliberate: false,
7674
+ diskCandidates: diskPaths,
7675
+ warning: null
7676
+ });
7677
+ }
7678
+ return null;
7679
+ }
7680
+ var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
7681
+ var requireSecretsSdk = createRequire(import.meta.url);
7682
+ var DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com";
7683
+ var DEFAULT_AUTHORITY_SOURCE = "default";
7684
+ function defaultFleetGatewayBaseUrl(name) {
7685
+ return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
7686
+ }
7687
+ var ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
7688
+ var DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
7689
+ function isValidDnsDomain(value) {
7690
+ if (value.length === 0 || value.length > 253 || ASCII_CONTROL_PATTERN.test(value) || /[^\x00-\x7f]/.test(value)) {
7691
+ return false;
7692
+ }
7693
+ return value.split(".").every((label) => label.length <= 63 && !label.startsWith("xn--") && DNS_LABEL_PATTERN.test(label));
7694
+ }
7695
+ function validateAppSlug(name) {
7696
+ if (name.length > 63 || !DNS_LABEL_PATTERN.test(name)) {
7697
+ throw new Error("App name must be one lowercase DNS label.");
7698
+ }
7699
+ return name;
7700
+ }
7701
+ function rawAuthority(value) {
7702
+ const match = /^[a-z][a-z0-9+.-]*:\/\//i.exec(value);
7703
+ if (!match)
7704
+ throw new Error("API URL must be absolute.");
7705
+ const afterScheme = value.slice(match[0].length);
7706
+ const boundary = afterScheme.search(/[/?#]/);
7707
+ const authority = boundary === -1 ? afterScheme : afterScheme.slice(0, boundary);
7708
+ if (!authority)
7709
+ throw new Error("API URL must include a hostname.");
7710
+ return authority;
7711
+ }
7712
+ function assertCanonicalPort(port) {
7713
+ if (!/^[0-9]+$/.test(port) || port.length > 1 && port.startsWith("0")) {
7714
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
7715
+ }
7716
+ const numericPort = Number(port);
7717
+ if (!Number.isSafeInteger(numericPort) || numericPort < 1 || numericPort > 65535) {
7718
+ throw new Error("API URL authority must contain a canonical port between 1 and 65535.");
7719
+ }
7720
+ }
7721
+ function canonicalAuthorityHostname(authority) {
7722
+ let rawHostname;
7723
+ if (authority.startsWith("[")) {
7724
+ const closingBracket = authority.indexOf("]");
7725
+ if (closingBracket === -1) {
7726
+ throw new Error("API URL authority must contain a canonical hostname.");
7727
+ }
7728
+ rawHostname = authority.slice(0, closingBracket + 1);
7729
+ const portSuffix = authority.slice(closingBracket + 1);
7730
+ if (portSuffix) {
7731
+ if (!portSuffix.startsWith(":")) {
7732
+ throw new Error("API URL authority must contain a canonical hostname and port.");
7733
+ }
7734
+ assertCanonicalPort(portSuffix.slice(1));
7735
+ }
7736
+ if (isIP(rawHostname.slice(1, -1)) !== 6) {
7737
+ throw new Error("API URL authority must contain a canonical IPv6 literal.");
7738
+ }
7739
+ } else {
7740
+ const firstColon = authority.indexOf(":");
7741
+ const lastColon = authority.lastIndexOf(":");
7742
+ if (firstColon !== lastColon) {
7743
+ throw new Error("IPv6 API URL authorities must use brackets.");
7744
+ }
7745
+ if (lastColon !== -1) {
7746
+ const port = authority.slice(lastColon + 1);
7747
+ assertCanonicalPort(port);
7748
+ rawHostname = authority.slice(0, lastColon);
7749
+ } else {
7750
+ rawHostname = authority;
7751
+ }
7752
+ const ipVersion = isIP(rawHostname);
7753
+ const numericAddressParts = rawHostname.split(".");
7754
+ const looksLikeNonCanonicalIpv4 = numericAddressParts.every((part) => /^(?:0x[0-9a-f]+|[0-9]+)$/i.test(part));
7755
+ if (ipVersion !== 4 && looksLikeNonCanonicalIpv4 || ipVersion !== 4 && !isValidDnsDomain(rawHostname.toLowerCase())) {
7756
+ throw new Error("API URL authority must contain a canonical ASCII hostname.");
7757
+ }
7758
+ }
7759
+ return rawHostname.toLowerCase();
7760
+ }
7761
+ function isDeliberateLoopbackHttpAuthority(authority) {
7762
+ return /^(?:localhost|127\.0\.0\.1|\[::1\])(?::[0-9]+)?$/i.test(authority);
7763
+ }
7764
+ function toV1BaseUrl(apiUrl) {
7765
+ if (ASCII_CONTROL_PATTERN.test(apiUrl)) {
7766
+ throw new Error("API URL must not contain ASCII control characters.");
7767
+ }
7768
+ const input = apiUrl.trim();
7769
+ const authority = rawAuthority(input);
7770
+ if (authority.includes("@") || authority.includes("\\") || authority.includes("%") || /[^\x00-\x7f]/.test(authority)) {
7771
+ throw new Error("API URL authority must be canonical ASCII without credentials.");
7772
+ }
7773
+ const canonicalHostname = canonicalAuthorityHostname(authority);
7774
+ const url = new URL(input);
7775
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
7776
+ throw new Error("API URL must use http or https.");
7777
+ }
7778
+ if (url.username || url.password) {
7779
+ throw new Error("API URL must not include credentials.");
7780
+ }
7781
+ if (!url.hostname || url.hostname.endsWith(".")) {
7782
+ throw new Error("API URL must include a canonical hostname.");
7783
+ }
7784
+ if (url.hostname.toLowerCase() !== canonicalHostname) {
7785
+ throw new Error("API URL authority must not rely on parser hostname normalization.");
7786
+ }
7787
+ if (url.hostname.split(".").some((label) => label.toLowerCase().startsWith("xn--"))) {
7788
+ throw new Error("API URL must not use IDN or punycode hostnames.");
7789
+ }
7790
+ if (url.protocol === "http:" && !isDeliberateLoopbackHttpAuthority(authority)) {
7791
+ throw new Error("API URL may use http only for an exact loopback authority.");
7792
+ }
7793
+ if (url.search || url.hash) {
7794
+ throw new Error("API URL must not include a query string or fragment.");
7795
+ }
7796
+ let path = url.pathname.replace(/\/+$/, "");
7797
+ if (path.endsWith("/v1"))
7798
+ path = path.slice(0, -"/v1".length);
7799
+ url.pathname = `${path}/v1`;
7800
+ return url.toString().replace(/\/+$/, "");
7801
+ }
7802
+ class ClientTransportConfigurationError extends Error {
7803
+ appName;
7804
+ sources;
7805
+ constructor(appName, message, sources = []) {
7806
+ super(message);
7807
+ this.name = "ClientTransportConfigurationError";
7808
+ this.appName = appName;
7809
+ this.sources = Object.freeze([...sources]);
7810
+ }
7811
+ }
7812
+ function resolveClientTransportSnapshot(name, env = process.env, options = {}) {
7813
+ env = snapshotClientEnvironment(name, env);
7814
+ const keys = clientTransportEnvKeys(name);
7815
+ const definedUrlEntries = keys.apiUrlKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, raw: String(env[key]) }));
7816
+ const blankUrl = definedUrlEntries.find((entry) => entry.raw.trim().length === 0);
7817
+ if (blankUrl) {
7818
+ 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]);
7819
+ }
7820
+ const controlledUrl = definedUrlEntries.find((entry) => ASCII_CONTROL_PATTERN.test(entry.raw));
7821
+ if (controlledUrl) {
7822
+ throw new ClientTransportConfigurationError(name, `${controlledUrl.key} contains ASCII control characters.`, [controlledUrl.key]);
7823
+ }
7824
+ const usableUrlEntries = definedUrlEntries.map((entry) => ({ key: entry.key, value: entry.raw.trim() }));
7825
+ if (usableUrlEntries.length > 1 && new Set(usableUrlEntries.map((entry) => entry.value)).size > 1) {
7826
+ 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));
7827
+ }
7828
+ const envUrlHit = usableUrlEntries[0] ?? null;
7829
+ const keychainUrlHit = keychainConfigValue(name, env, options.credentials?.keychain);
7830
+ const diskConfigUrlHit = appConfigDiskValue(name, env, keys.apiUrlKeys);
7831
+ if (diskConfigUrlHit?.unusable) {
7832
+ 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]);
7833
+ }
7834
+ const urlCandidates = [
7835
+ ...envUrlHit ? [envUrlHit] : [],
7836
+ ...keychainUrlHit ? [{ key: keychainUrlHit.source, value: keychainUrlHit.value }] : [],
7837
+ ...diskConfigUrlHit ? [{ key: diskConfigUrlHit.path, value: diskConfigUrlHit.value.trim() }] : []
7838
+ ];
7839
+ const configuredUrl = urlCandidates[0] ?? null;
7840
+ const divergentUrls = urlCandidates.filter((candidate) => candidate.value !== configuredUrl?.value);
7841
+ if (configuredUrl && divergentUrls.length > 0) {
7842
+ 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));
7843
+ }
7844
+ const warnings = [];
7845
+ if (configuredUrl && !envUrlHit) {
7846
+ 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.`);
7847
+ }
7848
+ const credential = resolveCredential(name, env, options.credentials);
7849
+ if (!credential) {
7850
+ const diskHint = credentialDiskSourcesForMessage(name, env);
7851
+ 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`;
7852
+ 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.`);
7853
+ throw new ClientTransportConfigurationError(name, warnings.join(" "), [configuredUrl?.key ?? keys.apiUrlKeys[0]]);
7854
+ }
7855
+ if (credential.warning)
7856
+ warnings.push(credential.warning);
7857
+ let urlHit;
7858
+ if (configuredUrl) {
7859
+ urlHit = configuredUrl;
7860
+ } else {
7861
+ try {
7862
+ urlHit = { key: DEFAULT_AUTHORITY_SOURCE, value: defaultFleetGatewayBaseUrl(name) };
7863
+ } catch (error) {
7864
+ const message = error instanceof Error ? error.message : String(error);
7865
+ 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]]);
7866
+ }
7867
+ }
7868
+ const apiUrlSource = urlHit.key;
7869
+ let baseUrl;
7870
+ try {
7871
+ baseUrl = toV1BaseUrl(urlHit.value);
7872
+ } catch (error) {
7873
+ const message = error instanceof Error ? error.message : String(error);
7874
+ throw new ClientTransportConfigurationError(name, `Invalid API URL from ${apiUrlSource}: ${message}`, [apiUrlSource]);
7875
+ }
7876
+ return {
7877
+ resolution: {
7878
+ transport: "http",
7879
+ transportSource: urlHit.key,
7880
+ baseUrl,
7881
+ apiUrlSource,
7882
+ apiKeyPresent: true,
7883
+ apiKeySource: credential.source,
7884
+ apiKeyTier: credential.tier,
7885
+ misconfigured: false,
7886
+ warning: warnings.length > 0 ? warnings.join(" ") : null
7887
+ },
7888
+ credential
7889
+ };
7890
+ }
7891
+ function resolveClientTransport(name, env = process.env, options = {}) {
7892
+ return resolveClientTransportSnapshot(name, env, options).resolution;
7893
+ }
7894
+ function credentialDiskSourcesForMessage(name, env) {
7895
+ const paths = credentialDiskSources(name, env);
7896
+ return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
7897
+ }
7898
+ var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
7899
+ var AUTHORITY_OVERRIDE_HEADERS = new Set([
7900
+ "host",
7901
+ ":authority",
7902
+ "forwarded",
7903
+ "x-forwarded-host",
7904
+ "x-original-host"
7905
+ ]);
7906
+
7907
+ // src/lib/transport.ts
7908
+ init_config();
7909
+
7910
+ // src/lib/local-opt-in.ts
7911
+ var HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
7912
+ function isHooksLocalOptIn(env = process.env) {
7913
+ return HOOKS_LOCAL_OPT_IN_ENV_KEYS.some((key) => (env[key] ?? "").trim() !== "");
7914
+ }
7915
+ function hooksAuthorityEnvKeys() {
7916
+ const keys = clientTransportEnvKeys("hooks");
7917
+ return [
7918
+ ...keys.apiUrlKeys,
7919
+ ...keys.apiKeyKeys,
7920
+ credentialOverrideEnvKey("hooks"),
7921
+ credentialPointerEnvKey("hooks"),
7922
+ CREDENTIAL_PROFILE_ENV_KEY
7923
+ ];
7924
+ }
7925
+ function hasHooksEnvAuthorityIntent(env = process.env) {
7926
+ return hooksAuthorityEnvKeys().some((key) => (env[key] ?? "").trim() !== "");
7927
+ }
7928
+ function selectsHooksLocalStore(env = process.env) {
7929
+ return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
7930
+ }
7931
+ function hooksResolverEnv(env) {
7932
+ const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
7933
+ if (blanks.length === 0)
7934
+ return env;
7935
+ const next = { ...env };
7936
+ for (const key of blanks)
7937
+ delete next[key];
7938
+ return next;
7939
+ }
7940
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
7941
+ function isAmbientHooksEnv(env) {
7942
+ if (typeof process !== "undefined" && env === process.env)
7943
+ return true;
7944
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
7945
+ }
7946
+ function hooksResolverInputs(env, credentials = {}) {
7947
+ const normalised = hooksResolverEnv(env);
7948
+ if (normalised === env)
7949
+ return { env: normalised, credentials };
7950
+ const keychain = { ...credentials.keychain };
7951
+ if (keychain.enabled === undefined && keychain.run === undefined) {
7952
+ keychain.enabled = isAmbientHooksEnv(env);
7953
+ }
7954
+ return { env: normalised, credentials: { ...credentials, keychain } };
7955
+ }
7956
+
7957
+ // src/lib/transport.ts
7958
+ var localNoticePrinted = false;
7959
+ function announceLocal(notice, reason) {
7960
+ if (localNoticePrinted)
7961
+ return;
7962
+ localNoticePrinted = true;
7963
+ const line = `hooks: LOCAL mode \u2014 ${reason}; using the bundled registry and the local store at ` + `${getHooksDataDir()}, not a remote registry. Set HASNA_HOOKS_API_KEY (or the Keychain item ` + `hasna.credentials.hooks.api-key / ~/.hasna/hooks/config/credentials) to go remote.`;
7964
+ if (notice)
7965
+ notice(line);
7966
+ else if (typeof process !== "undefined")
7967
+ process.stderr.write(`${line}
7968
+ `);
7969
+ }
7970
+ function hooksRegistryOrigin(v1BaseUrl) {
7971
+ return v1BaseUrl.replace(/\/+$/, "").replace(/\/v1$/, "");
7972
+ }
7973
+ function rethrowHooksAuthorityFailure(error) {
7974
+ const message = error instanceof Error ? error.message : String(error);
7975
+ const name = error instanceof Error ? error.name : "";
7976
+ if (name === "CredentialResolutionError" || name === "CredentialFileUnsafeError") {
7977
+ throw new Error(`REMOTE_API_CREDENTIAL_INVALID: ${message} There is no local fallback: ` + "local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
7978
+ }
7979
+ if (/no API key could be resolved/.test(message)) {
7980
+ if (/is not set and no API key could be resolved/.test(message)) {
7981
+ throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved from the Keychain item " + `hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1, alias HOOKS_LOCAL=1) " + "and is disabled by default \u2014 failing closed instead of serving the local store", { cause: error });
7982
+ }
7983
+ throw new Error("REMOTE_API_KEY_MISSING: the remote hooks registry requires HASNA_HOOKS_API_KEY, the Keychain item " + `hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials. ${message} ` + "There is no local fallback: local mode is opt-in only (HASNA_HOOKS_LOCAL=1) and is disabled by default \u2014 failing closed", { cause: error });
7984
+ }
7985
+ throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
7986
+ }
7987
+ function resolveHooksTransport(env = process.env, options = {}) {
7988
+ if (selectsHooksLocalStore(env)) {
7989
+ announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
7990
+ return { mode: "local", source: "local-opt-in", authority: null };
7991
+ }
7992
+ const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
7993
+ const tier1 = options.credentials?.apiKey;
7994
+ const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
7995
+ let resolution;
7996
+ let credential;
7997
+ try {
7998
+ credential = resolveCredential("hooks", resolverEnv, credentialOptions);
7999
+ resolution = resolveClientTransport("hooks", resolverEnv, {
8000
+ credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
8001
+ });
8002
+ } catch (error) {
8003
+ rethrowHooksAuthorityFailure(error);
8004
+ }
8005
+ if (!credential) {
8006
+ throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
8007
+ }
8008
+ return {
8009
+ mode: "remote",
8010
+ source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
8011
+ authority: {
8012
+ origin: hooksRegistryOrigin(resolution.baseUrl),
8013
+ apiKey: credential.apiKey,
8014
+ apiUrlSource: resolution.apiUrlSource,
8015
+ apiKeySource: credential.source,
8016
+ apiKeyTier: credential.tier,
8017
+ v1BaseUrl: resolution.baseUrl,
8018
+ warning: resolution.warning
8019
+ }
8020
+ };
8021
+ }
8022
+ function resolveHooksServePublishKey(env = process.env, options = {}) {
8023
+ const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
8024
+ const tier1 = options.credentials?.apiKey;
8025
+ const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
8026
+ return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
8027
+ }
8028
+
8029
+ // src/index.ts
8030
+ init_store();
8031
+ init_manifest();
8032
+
8033
+ // src/lib/custom-install.ts
8034
+ init_config();
8035
+ init_manifest();
8036
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
8037
+ import { join as join10 } from "path";
8038
+ import { tmpdir } from "os";
8039
+ function manifestAt(dir) {
8040
+ const path = join10(dir, "manifest.json");
8041
+ if (!existsSync9(path))
8042
+ return;
8043
+ try {
8044
+ return parseManifest(readFileSync7(path, "utf-8"));
8045
+ } catch {
8046
+ return;
8047
+ }
8048
+ }
8049
+ function copyManifestDir(srcDir, source, kind) {
7367
8050
  const manifest = manifestAt(srcDir);
7368
8051
  if (!manifest) {
7369
8052
  throw new Error(`No valid manifest.json found at ${srcDir}`);
@@ -7377,8 +8060,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
7377
8060
  return { name, version: manifest.version, kind, source, dir, scriptPath };
7378
8061
  }
7379
8062
  async function cloneToTemp(gitUrl) {
7380
- const tmp = mkdtempSync(join9(tmpdir(), "hooks-install-"));
7381
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join9(tmp, "repo")], {
8063
+ const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
8064
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
7382
8065
  stdout: "pipe",
7383
8066
  stderr: "pipe"
7384
8067
  });
@@ -7388,10 +8071,10 @@ async function cloneToTemp(gitUrl) {
7388
8071
  proc.exited
7389
8072
  ]);
7390
8073
  if (exitCode !== 0) {
7391
- rmSync3(tmp, { recursive: true, force: true });
8074
+ rmSync2(tmp, { recursive: true, force: true });
7392
8075
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
7393
8076
  }
7394
- return join9(tmp, "repo");
8077
+ return join10(tmp, "repo");
7395
8078
  }
7396
8079
  function isGitUrl(value) {
7397
8080
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -7409,7 +8092,7 @@ async function installCustomSource(source) {
7409
8092
  try {
7410
8093
  return copyManifestDir(repoDir, source, "git");
7411
8094
  } finally {
7412
- rmSync3(repoDir, { recursive: true, force: true });
8095
+ rmSync2(repoDir, { recursive: true, force: true });
7413
8096
  }
7414
8097
  }
7415
8098
  if (source.startsWith("http://") || source.startsWith("https://")) {
@@ -7419,7 +8102,7 @@ async function installCustomSource(source) {
7419
8102
  throw new Error(`No such local path: ${source}`);
7420
8103
  }
7421
8104
  if (source.endsWith("manifest.json")) {
7422
- const manifest = parseManifest(readFileSync8(source, "utf-8"));
8105
+ const manifest = parseManifest(readFileSync7(source, "utf-8"));
7423
8106
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
7424
8107
  }
7425
8108
  return copyManifestDir(source, source, "local");
@@ -7450,8 +8133,7 @@ async function installManifestUrl(url) {
7450
8133
  }
7451
8134
  // src/lib/sync.ts
7452
8135
  import { createHash as createHash2 } from "crypto";
7453
- import { readFileSync as readFileSync9 } from "fs";
7454
- init_config();
8136
+ import { readFileSync as readFileSync8 } from "fs";
7455
8137
  init_db();
7456
8138
  init_store();
7457
8139
  init_manifest();
@@ -7459,7 +8141,7 @@ function sha256OfText(text) {
7459
8141
  return createHash2("sha256").update(text).digest("hex");
7460
8142
  }
7461
8143
  function sha256OfPath(path) {
7462
- return createHash2("sha256").update(readFileSync9(path)).digest("hex");
8144
+ return createHash2("sha256").update(readFileSync8(path)).digest("hex");
7463
8145
  }
7464
8146
  function collectBundledCatalog() {
7465
8147
  const out = [];
@@ -7475,9 +8157,8 @@ function collectBundledCatalog() {
7475
8157
  }
7476
8158
  return out;
7477
8159
  }
7478
- async function fetchJson(base, path, opts) {
8160
+ async function fetchJson(base, path, apiKey, opts) {
7479
8161
  const headers = { accept: "application/json" };
7480
- const apiKey = resolveApiKey();
7481
8162
  if (apiKey)
7482
8163
  headers["x-api-key"] = apiKey;
7483
8164
  const res = await fetch(`${base}${path}`, {
@@ -7486,7 +8167,7 @@ async function fetchJson(base, path, opts) {
7486
8167
  signal: AbortSignal.timeout(15000)
7487
8168
  });
7488
8169
  if (res.status === 401) {
7489
- throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY or HOOKS_API_KEY (vault-delivered, never stored)");
8170
+ throw new Error("registry requires API key \u2014 set HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, " + "or ~/.hasna/hooks/config/credentials (resolved per call, never stored)");
7490
8171
  }
7491
8172
  if (!res.ok) {
7492
8173
  throw new Error(`GET ${path} failed with status ${res.status}`);
@@ -7497,9 +8178,9 @@ async function fetchJson(base, path, opts) {
7497
8178
  }
7498
8179
  return body;
7499
8180
  }
7500
- async function fetchRemoteState(apiUrl) {
7501
- const catalog = await fetchJson(apiUrl, "/api/v1/catalog");
7502
- const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
8181
+ async function fetchRemoteState(apiUrl, apiKey) {
8182
+ const catalog = await fetchJson(apiUrl, "/api/v1/catalog", apiKey);
8183
+ const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
7503
8184
  if (!catalog.hooks || !Array.isArray(catalog.hooks)) {
7504
8185
  throw new Error("remote catalog is malformed: missing hooks array");
7505
8186
  }
@@ -7542,20 +8223,25 @@ function computeDiff(catalog, remoteLock) {
7542
8223
  return diff;
7543
8224
  }
7544
8225
  async function planSync(options = {}) {
7545
- const apiUrl = resolveApiUrl();
7546
- if (apiUrl) {
7547
- const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
7548
- return { apiUrl, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
8226
+ const transport = resolveHooksTransport(options.env ?? process.env, {
8227
+ credentials: options.credentials
8228
+ });
8229
+ if (transport.mode === "remote" && transport.authority) {
8230
+ const { origin, apiKey } = transport.authority;
8231
+ const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
8232
+ return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
7549
8233
  }
7550
8234
  const diff = computeDiff(collectBundledCatalog(), null);
7551
8235
  return { apiUrl: null, dryRun: options.dryRun ?? false, diff };
7552
8236
  }
7553
8237
  async function syncHooks(options = {}) {
7554
- const apiUrl = resolveApiUrl();
7555
- const plan = await planSync();
8238
+ const transport = resolveHooksTransport(options.env ?? process.env, {
8239
+ credentials: options.credentials
8240
+ });
8241
+ const plan = await planSync(options);
7556
8242
  if (options.dryRun)
7557
8243
  return { ...plan, dryRun: true };
7558
- if (!apiUrl) {
8244
+ if (transport.mode !== "remote" || !transport.authority) {
7559
8245
  const db = getDb();
7560
8246
  for (const entry of collectBundledCatalog()) {
7561
8247
  setPinnedHook(entry.name, { version: entry.version, sha256: entry.sha256, source: "bundled" });
@@ -7570,19 +8256,20 @@ async function syncHooks(options = {}) {
7570
8256
  }
7571
8257
  return plan;
7572
8258
  }
7573
- const { catalog, remoteLock } = await fetchRemoteState(apiUrl);
8259
+ const { origin, apiKey } = transport.authority;
8260
+ const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
7574
8261
  const diff = computeDiff(catalog, remoteLock);
7575
- const staged = await stageSyncArtifacts(apiUrl, [...diff.added, ...diff.updated], remoteLock);
7576
- await commitSyncArtifacts(staged, apiUrl, remoteLock);
7577
- return { apiUrl, dryRun: false, diff };
8262
+ const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
8263
+ await commitSyncArtifacts(staged, origin, remoteLock);
8264
+ return { apiUrl: origin, dryRun: false, diff };
7578
8265
  }
7579
- async function stageSyncArtifacts(apiUrl, names, remoteLock) {
8266
+ async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
7580
8267
  const staged = [];
7581
8268
  for (const name of names) {
7582
8269
  const entry = remoteLock.hooks[name];
7583
8270
  if (!entry)
7584
8271
  continue;
7585
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`);
8272
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(entry.version)}`, apiKey);
7586
8273
  if (!artifact.manifest || typeof artifact.script !== "string") {
7587
8274
  throw new Error(`artifact for '${name}@${entry.version}' is malformed`);
7588
8275
  }
@@ -7649,8 +8336,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
7649
8336
  throw err;
7650
8337
  }
7651
8338
  }
7652
- async function fetchPinnedHook(name, version, apiUrl) {
7653
- const remoteLock = await fetchJson(apiUrl, "/api/v1/lock");
8339
+ async function fetchPinnedHook(name, version, apiUrl, apiKey) {
8340
+ const remoteLock = await fetchJson(apiUrl, "/api/v1/lock", apiKey);
7654
8341
  const entry = remoteLock.hooks?.[name];
7655
8342
  if (!entry) {
7656
8343
  throw new Error(`Hook '${name}' is not in the remote registry lock`);
@@ -7659,7 +8346,7 @@ async function fetchPinnedHook(name, version, apiUrl) {
7659
8346
  if (!isLatest && !(entry.versions ?? []).includes(version)) {
7660
8347
  throw new Error(`Hook '${name}' version ${version} is not in the remote registry (available: ${[entry.version, ...entry.versions ?? []].join(", ")})`);
7661
8348
  }
7662
- const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, { withHeader: "_headerSha" });
8349
+ const artifact = await fetchJson(apiUrl, `/api/v1/hooks/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, apiKey, { withHeader: "_headerSha" });
7663
8350
  if (!artifact.manifest || typeof artifact.script !== "string") {
7664
8351
  throw new Error(`artifact for '${name}@${version}' is malformed`);
7665
8352
  }
@@ -7713,9 +8400,8 @@ async function fetchPinnedHook(name, version, apiUrl) {
7713
8400
  // src/serve.ts
7714
8401
  init_manifest();
7715
8402
  init_store();
7716
- init_config();
7717
- import { readFileSync as readFileSync10 } from "fs";
7718
- import { dirname as dirname4, join as join10 } from "path";
8403
+ import { readFileSync as readFileSync9 } from "fs";
8404
+ import { dirname as dirname4, join as join11 } from "path";
7719
8405
  import { fileURLToPath as fileURLToPath3 } from "url";
7720
8406
 
7721
8407
  // src/lib/secure-compare.ts
@@ -7876,7 +8562,7 @@ var openApiDocument = {
7876
8562
  apiKey: {
7877
8563
  type: "http",
7878
8564
  scheme: "bearer",
7879
- description: "The registry API key (HASNA_HOOKS_API_KEY / HOOKS_API_KEY)."
8565
+ description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
7880
8566
  }
7881
8567
  }
7882
8568
  }
@@ -7889,8 +8575,8 @@ var SERVE_HOST = "127.0.0.1";
7889
8575
  var SERVE_SERVICE_NAME = "hooks-registry";
7890
8576
  function packageVersion() {
7891
8577
  try {
7892
- const pkgPath = join10(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
7893
- return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
8578
+ const pkgPath = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
8579
+ return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
7894
8580
  } catch {
7895
8581
  return "0.0.0";
7896
8582
  }
@@ -8038,15 +8724,20 @@ function handleServeRequest(req, apiKey) {
8038
8724
  }
8039
8725
  return Promise.resolve(json({ error: "Not Found" }, 404));
8040
8726
  }
8727
+ function resolveServeOptions(options) {
8728
+ const envPort = process.env.PORT?.trim();
8729
+ const envHost = process.env.HOST?.trim();
8730
+ const port = options.port ?? (envPort && /^[0-9]+$/.test(envPort) ? parseInt(envPort, 10) : DEFAULT_SERVE_PORT);
8731
+ const host = options.host ?? (envHost && envHost.length > 0 ? envHost : SERVE_HOST);
8732
+ return { port, host };
8733
+ }
8041
8734
  function startServeServer(options) {
8042
- const port = options.port ?? DEFAULT_SERVE_PORT;
8043
- const host = options.host ?? SERVE_HOST;
8044
- const apiKey = resolveApiKey();
8735
+ const { port, host } = resolveServeOptions(options);
8045
8736
  const server = Bun.serve({
8046
8737
  hostname: host,
8047
8738
  port,
8048
8739
  async fetch(req) {
8049
- return handleServeRequest(req, apiKey);
8740
+ return handleServeRequest(req, resolveHooksServePublishKey());
8050
8741
  }
8051
8742
  });
8052
8743
  console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
@@ -8155,7 +8846,7 @@ async function runHook(name, input, options = {}) {
8155
8846
  if (!script)
8156
8847
  throw new Error(`Hook script not found: ${name}`);
8157
8848
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
8158
- const content = readFileSync11(script);
8849
+ const content = readFileSync10(script);
8159
8850
  const check = checkScriptHash2(name, sha256Of2(content));
8160
8851
  if (!check.ok) {
8161
8852
  throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);
@@ -8246,7 +8937,6 @@ async function runHook(name, input, options = {}) {
8246
8937
  export {
8247
8938
  writeLock,
8248
8939
  writeCustomHook,
8249
- writeConfig,
8250
8940
  verifyScriptHash,
8251
8941
  upsertHookRecord,
8252
8942
  updateProfile,
@@ -8267,11 +8957,11 @@ export {
8267
8957
  resolveTables,
8268
8958
  resolveScriptPath,
8269
8959
  resolveScript,
8960
+ resolveHooksTransport,
8961
+ resolveHooksServePublishKey,
8270
8962
  resolveHookMeta,
8271
8963
  resolveHookDir,
8272
8964
  resolveHook,
8273
- resolveApiUrl,
8274
- resolveApiKey,
8275
8965
  removeProjectHook,
8276
8966
  removePinnedHook,
8277
8967
  removeHookRecord,
@@ -8279,7 +8969,6 @@ export {
8279
8969
  removeHook,
8280
8970
  readLock,
8281
8971
  readCustomManifest,
8282
- readConfig,
8283
8972
  provisionCloudflareResources,
8284
8973
  planSync,
8285
8974
  pinInstalledHook,
@@ -8297,6 +8986,7 @@ export {
8297
8986
  installHook,
8298
8987
  installCustomSource,
8299
8988
  importProfiles,
8989
+ hooksRegistryOrigin,
8300
8990
  hookExists,
8301
8991
  handleServeRequest,
8302
8992
  getSyncMetaAll,
@@ -8321,7 +9011,6 @@ export {
8321
9011
  getHookEvents,
8322
9012
  getHook,
8323
9013
  getCustomHooksDir,
8324
- getConfigPath,
8325
9014
  fetchPinnedHook,
8326
9015
  exportProfiles,
8327
9016
  deleteProfile,